Saturday, November 9, 2013

Resource versioning with Spring MVC

Since 3.0.4.RELEASE, Spring MVC provides help in managing web page static resources (images, CSS, javascripts). The most important for me was to make browser cache resources for a long time while loading the new version when the content changes. This is a best practice recommendation by YSlow and others. You just need to set a far future Expires header and change the resource URL each time the content changes.

The feature description in Spring documentation looks simple, but in reality it requires some additional work:
  • in web.xml, you should map the DispatcherServlet to root path (<url-pattern>/</url-pattern>). It was mapped to *.do previously (as usual in Spring MVC projects)
  • the location attributes relates to the root category (same level as WEB-INF). If you used to have images directory there and want to create mapping using mvc:resource, it should be like this: <mvc:resources mapping="/images/**" location="/images/" />
  • don't forget the trailing slash in the location attribute (<mvc:resources mapping="/images/**" location="/images/" />)
Now, after images are mapped everything is working just as did before. The only difference is that images are now served by Spring DispatcherServlet instead of (Tomcat) default servlet. This will incur some perfomance penaly (measure and decide if it's ok in your case!).

To introduce versioning, we need to add some string into image URLs. It could be a (Maven) version number or just whatever unique identifier. I decided to use timestamp - it's better in development environment where I have the same Maven version (1.2.3-SNAPSHOT) but the content changes all the time.

<mvc:resources mapping="/assets/#{properties['buildTime']}/**" location="/" cache-period="31556926" />

<util:map id="properties">
     <entry key="buildTime" value="${buildTimestamp}"/>
</util:map>

in pom.xml:

<properties>
     <buildTimestamp>${maven.build.timestamp}</buildTimestamp>
     <maven.build.timestamp.format>yyyyMMddHHmmss</maven.build.timestamp.format>
</properties>

make sure your Spring configuration file is filtered by Maven, so that ${buildTimestamp} gets replaced by the corresponding Maven property:

<resources>
     <resource>
         <directory>src/main/resources</directory>
     </resource>
     <resource>
         <filtering>true</filtering>
         <directory>src/main/resources</directory>
         <includes>
             <include>*.xml</include>
         </includes>
     </resource>
</resources>

The only thing left is to replace all (or some) image URLs. One possibility (as suggested by the Spring doc) is to use <spring:eval> and <spring:url> tags on each JSP page. But I very much dislike such duplicating boilerplate. So I introduced an interceptor for all pages I need:

<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
     <property name="mappings">
         <props>
             <!-- You servlet mappings go here ...-->
         </props>
     </property>
     <property name="interceptors" >
         <list>
             <ref bean="resourceUrlInterceptor"/>
         </list>
     </property>
</bean>

<bean id="resourceUrlInterceptor" class="com.example.interceptor.ResourceUrlInterceptor">
     <property name="properties" ref="properties" />
</bean>

And the interceptor itself is very simple:

public class ResourceUrlInterceptor extends HandlerInterceptorAdapter {

    private Map<String, String> properties;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        request.setAttribute("resourcesPath", response.encodeURL(request.getContextPath() + "/resources/" + properties.get("buildTime")));
        return true;
    }

    public void setProperties(Map<String, String> properties) {
        this.properties = properties;
    }
}

Finally, we can use image links like:

<img src="${resourcesPath}/images/icons/myicon.png" />

which will translate to something like:

<img src="/myapp/resources/20110725101003/images/icons/myicon.png" />

Being an old fan of Tapestry, I cannot restrain myself from demonstrating its superiority :)
In Tapestry, all you need to do is:

<img src="${context:images/icons/myicon.png}" />

Gotcha: Error handling in JSP

Struggled a lot with error handling in JSP (yeah, JSP is not my first choice).

The problem I've encountered: if an exception occurs directly in some JSP tag then:
  •  the rendered HTML appear broken at some random point
  •  the error message bypasses the application log and ends up in application server's log (localhost.log in case of Tomcat)
Example problem in JSP:

<%@ page pageEncoding="UTF-8" contentType="text/html; charset=utf-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>

<!-- some amount of markup here -->

<fmt:formatNumber value="100" type="currency" currencyCode="${myMissingAttribute}"/>

<!-- some more markup -->


What I've found while investigating:
  1. JSP default buffer is 8k and auto flush is on. That means after rendering of each 8k chunk the result is sent back to browser. And if your error occurs later in the JSP, browser will end up with incomplete HTML.
  2. To avoid having broken HTML, you should set up big enough buffer for each page and switch auto flush off, for example: <%@ page buffer="512kb" autoFlush="false" %>
  3. And last but not least in the JSP misery list: <jsp:include> fails silently when the page path is incorrect, no errors whatsoever in ANY log !

How to easily create local SVN repository on Mac

Version control is addictive - once you get used to it, it's hard to go without, even on small temporary "pet" projects. You want to have a history of working versions to revert to if you get something totally messed up.
But what if you don't want to commit to your usual "official" repo and also don't bother to set up Assembla or something? Local repository comes to rescue!

If you are lucky to be on Mac it's a piece of cake to set up:

mkdir -p ~/repositories/svn
svnadmin create ~/repositories/svn/local

And then you can use file:///Users/yourname/repositories/svn/local as the SVN root for your projects. e.g. file:///Users/yourname/repositories/svn/local/MyProject

Win!