Thursday, December 26, 2013

Jersey & JerseyTest migration from 1.x to 2.5 with Spring, JSP, Tomcat 7 and FreeMarker


I looked at upgrading to Jersey 2 a while ago but it didn't include important functionality I needed from the 1.x versions, like support for JSP templates, so I decided to wait (although I would have expected that a 2.0 release would have included all the capability from 1.x).

I recently went back and discovered that Jersey 2.5 now supports templates so I decided to take the plunge.  Just let me say that the experience has been very painful and end my rant there.  The high-level documentation is pretty good, and there are some useful working examples, but I had to dig into the Jersey source code to try to figure some things out and the low level documentation is not what I had hoped for.

Thus I am writing this article in hopes of sparing other poor souls from the pain I experienced in upgrading to Jersey 2 and getting the following combination of technologies integrated and working:

Jersey 2 + Jersey Test Framework + Spring + templates + Tomcat 7

If you're looking for information on Jersey 1.x please see my previous article on the Jersey Test Framework.

I had intended for JSP to be the template provider but I couldn't get it to work with the Jersey Test Framework (Grizzly2 container), which caused me to look at other options.  After much difficulty I was able to get FreeMarker working as the template provider, but without being able to include the Spring macro library (will explain alternative below).

First, let's look at the JerseyTest class.  Notice on line 8 the forward slash '/' in front of the folder name where I am putting the FreeMarker templates.  Please don't forget that.

At first I put my templates folder (call it whatever you want) at 'src/main/webapp/templates', which worked fine when the app was deployed to Tomcat but failed when the unit tests were being run under Grizzly2.  I then noticed in the Jersey source code for the FreeMarker examples and tests that they were putting the template files in the resources folder ('src/main/resources').  When I moved my .ftl files to that location FreeMarker could find them under both Tomcat and Grizzly.

As you can see from this snippet below, I've created my own abstract test class on top of JerseyTest so that I could have a shared configuration for all my web resource tests and include some other helper methods (not depicted) that help simplify my REST service tests.

public abstract class AbstractSpringEnabledWebServiceTest extends JerseyTest {

    @Override
    protected Application configure() {
        ResourceConfig rc = new ResourceConfig()
            .register(SpringLifecycleListener.class)
            .register(RequestContextFilter.class)
            .property(FreemarkerMvcFeature.TEMPLATES_BASE_PATH, "/templates")
            .register(FreemarkerMvcFeature.class)
            ;

        enable(TestProperties.LOG_TRAFFIC);
        enable(TestProperties.DUMP_ENTITY);

        return configure(rc);
    } // configure()

    protected abstract ResourceConfig configure(ResourceConfig rc);

    protected abstract String getResourcePath();

If you've used JerseyTest in Jersey 1.x you will notice some significant changes to how the tests are configured.  I'd like to say it's an improvement but I think you will agree it's much less intuitive.  In Jersey 1.x it was obvious we were building up a web.xml equivalent.  Not so in Jersey 2.  You'll have to rely more on the documentation, source code, blogs, and StackOverflow to to figure out how to set up your test web app correctly for your scenario.

Next is the concrete test class where we (a) provide the Jersey resource classes to load, (b) the location of the Spring context file to use, if using Spring, and (c) the root resource path, which should match the filter mapping from web.xml.

public class ResourceATest extends AbstractSpringEnabledWebServiceTest {

    @Override
    protected ResourceConfig configure(ResourceConfig rc) {
        rc.register(ResourceA.class)
            .property(
                "contextConfigLocation",
                "classpath:**/my-web-test-context.xml"
            );
        return rc;
    } // configure()

    @Override
    protected String getResourcePath() {
        return "/my/resource";
    } // getResourcePath()


Next, here's my web.xml:

<web-app version="2.4" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xsi:schemalocation="http://java.sun.com/xml/ns/j2ee
                        http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:/META-INF/spring/my-web-context.xml</param-value>
    </context-param>

    <context-param>
        <param-name>spring.profiles.default</param-name>
        <param-value>prod</param-value>
    </context-param>

    <!-- Spring -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <listener>
        <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
    </listener>

    <filter>
        <filter-name>My Jersey Services</filter-name>
        <filter-class>org.glassfish.jersey.servlet.ServletContainer</filter-class>

        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>com.abc.resources.widget</param-value>
        </init-param>

        <init-param>
            <param-name>jersey.config.server.mvc.templateBasePath.jsp</param-name>
            <param-value>/WEB-INF/jsp</param-value>
        </init-param>

        <init-param>
            <param-name>jersey.config.server.mvc.templateBasePath.freemarker</param-name>
            <param-value>/templates</param-value>
        </init-param>
  
        <init-param>
            <param-name>jersey.config.server.mvc.templateBasePath.freemarker</param-name>
            <param-value>/templates</param-value>
        <init-param>
  
        <init-param>
            <param-name>jersey.config.server.provider.classnames</param-name>
            <param-value>org.glassfish.jersey.server.mvc.freemarker.FreemarkerMvcFeature</param-value>
        </init-param>
  
        <init-param>
            <param-name>jersey.config.server.tracing</param-name>
            <param-value>ALL</param-value>
        </init-param>

        <init-param>
            <param-name>jersey.config.servlet.filter.staticContentRegex</param-name>
            <param-value>(/index.jsp)|(/(content|(WEB-INF/jsp))/.*)</param-value>
        </init-param>

    </filter>

    <filter-mapping>
        <filter-name>My Jersey Services</filter-name>
        <url-pattern>/my/resource/*</url-pattern>
    </filter-mapping>

</web-app>

Here's my Jersey resource class.  Not much to call out here, except what I mentioned earlier about not being able to load the Spring FreeMarker macros.  In my case I wanted to use the spring.url macro as a replacement for c:url in JSP.  What I ended up doing in the short term is simply injecting the base url into my data map so I could then use it in my template.

@Service
@Path("/my/resource")
public class ResourceA{

    @Context
    private UriInfo _uriInfo;
    ...

    @Path("/resourceA")
    @Produces(MediaType.TEXT_HTML)
    @GET
    public Response getResourceA(@Context SecurityContext sc) {
        // fetch data for resource A

        // put data in map if it isn't already
        Map data = new HashMap<>();
        data.put("myData", data);
        data.put("baseUrl", _uriInfo.getBaseUri().toString());

        Viewable view = new Viewable("/myTemplate.ftl", data);

        return Response.ok().entity(view).build();
    } // getResourceA()

Finally is a snippet from my FreeMarker template file.  You can see the usage of 'baseUrl' that I included in the data model above.  One thing you might easily overlook is that I'm not using a 'model' prefix nor an 'it' prefix for the data elements.  In Jersey 1.x 'it' was required and the documentation for 2.5 states that the model will be passed in to the view as either 'model' or 'it'.  However, that didn't work and when I dropped the model prefix it started working.  Something to keep in mind as you troubleshoot any issues you may be having referencing your model data elements.

<head>
    <title>Web Resource A</title>
    <link href="${baseUrl}content/font-awesome/4.0.3/css/font-awesome.css"></link>        
         rel="stylesheet">
    <link href="${baseUrl}content/bootstrap/2.3.2/css/bootstrap.css"></link>        
         rel="stylesheet">
</head>

Oops - almost forgot the maven dependencies: (note: my spring dependencies are declared in my parent pom)

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <servlet-api.version>2.4</servlet-api.version>
        <jersey.version>2.5</jersey.version>
        <jersey.scope>compile</jersey.scope>
        <jettison.version>1.3.3</jettison.version>
        <freemarker.version>2.3.20</freemarker.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>${freemarker.version}</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>${servlet-api.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.codehaus.jettison</groupId>
            <artifactId>jettison</artifactId>
            <version>${jettison.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.glassfish.jersey.test-framework</groupId>
            <artifactId>jersey-test-framework-core</artifactId>
            <version>${jersey.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
            <version>${jersey.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- Required only when you are using JAX-RS Client -->
        <dependency>
            <groupId>org.glassfish.jersey.core</groupId>
            <artifactId>jersey-client</artifactId>
            <version>${jersey.version}</version>
            <scope>${jersey.scope}</scope>
        </dependency>

        <dependency>
            <groupId>org.glassfish.jersey.ext</groupId>
            <artifactId>jersey-mvc-freemarker</artifactId>
            <version>${jersey.version}</version>
            <scope>${jersey.scope}</scope>
        </dependency>

        <dependency>
            <groupId>org.glassfish.jersey.ext</groupId>
            <artifactId>jersey-spring3</artifactId>
            <version>${jersey.version}</version>
            <scope>${jersey.scope}</scope>
        </dependency>

    </dependencies>