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>

7 comments:

nobody said...

Thanks a lot! After countless hours of searching, finally found this working solution :)

If we are using only annotation based spring config, test cases won't load properly because current version of jersey spring expects xml config. We overcame that with putting an application context only for testing and enabling context config from that: ""

Anonymous said...

Hi, in your example how would you set the Servelt Context with the ResourceConfig? This is all new to me.

Example:

@Path(value = "/service")
public class Foo{

@Context ServletContext ctx;

@GET
@Path(value="/list")
public String list() {
Controller ctrl = new Controller();
ctx.setAttribute("controller", ctrl);
return ctrl.getList();
}

}

public class FooUnitTest extends JerseyTest
{
@Test
public void testService()
{

//set/how to configure the context?
}
}

Thanks,
Derek

Russ Jackson said...

Hi Derek,

It appears that the Grizzly2 test framework provider, which I am using, doesn't support injection of the ServletContext.

I found an article on StackOverflow that addresses this and offers a solution. See: http://stackoverflow.com/questions/17973277/problems-running-jerseytest-when-dealing-with-httpservletresponse/17989236#17989236

I'll see if I can adapt the example to my use case. I'll need to tinker with the way they're injecting the service provider packages into the TestContainer so it's not hard-coded.

Might be a couple days before I can get to it.

Russ

Anonymous said...

Hi Russ,

Thanks for the response. I've actually decided to do a direct @Inject instead of a servlet context. This is the code I used:

---------- Resource ----------------
@Path("myresource")
public class MyResource {

@Inject Controller ctrl;

@GET
@Produces(MediaType.TEXT_PLAIN)
public String getIt() {
System.out.println(ctrl.getStatement());
return "Got it!";
}
}

--------- Servlet Container -------------
public class TempServletContainer extends ServletContainer
{
@Override
public void init() throws ServletException
{
// TODO Auto-generated method stub
super.init();
ResourceConfig rc = this.getConfiguration();
ResourceConfig rc_new = new ResourceConfig(rc);

rc_new.register(new AbstractBinder(){
@Override
protected void configure() {
final Controller ctrl = new Controller ();
bind(ms_ctrl).to(Controller.class);
}
});

this.reload(rc_new);
}
}


-------------------------------------------------

The interesting part was that there seems to be a bug in the Jersey Code where it locks up the resource during the initialization. It's specified here:

http://jersey.576304.n2.nabble.com/Container-reload-ResourceConfig-is-locked-td7580847.html

However, I'm still curious to see how to do it with the ServletContext. I've actually seen the link you sent me, but this is all new to me so I couldn't get it to work with the TestContainerFactory/TestContainer.

In addition, it's still not clear to me what the difference is between using @Context vs. @Inject and using the ServletContext vs. a ResourceConfig. Do you have an insights?

Thanks,
Derek

Anonymous said...

Opps forgot the JerseyTest part for completion:

---------------- JerseyTest 2.x -------------------
@Override
protected Application configure() {

ResourceConfig rc = new ResourceConfig(MyResource .class);
Controller ctrl = Mockito.mock(Controller .class);
rc.register(new AbstractBinder(){
@Override
protected void configure() {
bind(ctrl).to(Controller .class);
}
});
rc.register(ms_ctrl);
return rc;
}

----------------------------------------------------

In terms of the servletContext, it wasn't clear to me how that was set in JerseyTest 2.x.

Thanks,
Derek

Anonymous said...

Also an interesting side question came up.

In the case of doing a custom injection, how do you distinguish between to instances of the same class. In my example, I register a bind an instance of a Controller to the Controller class. What happens in the case if I want to two to create two different instances of the Controller. How would that work in terms of the register.

Trenton D. Adams said...

I don't use spring. And jersey configured as a filter in web.xml doesn't seem to work (it doesn't respond to my REST requests). I rename the filter to the servlet based configuration, and it starts working immediately. Any thoughts?