Wednesday, January 2, 2013

Jersey @Provider example

I’ve found many examples of writing a Jersey @Provider to implement data marshalling, or mapping exceptions into a Response, but I've found very little on how to inject your own data types into your Jersey resource methods.

My code used to look something like this:

@POST
@Path("/create")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createThing(@Context HttpServletRequest request, String arg) {
    UserSession us = /* code to extract UserSession from cookie in request */
    /* more code here */
}

But I wanted it to look like this:

@POST
@Path("/create")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createThing(@Context UserSession us, String arg) {
    /* more code here */
}

Doing it this way cleans up the code more than the above snippets suggest. I eliminate some exception handling from all the resource functions that need the UserSession (which are many), plus I get a good separation of concerns. It’s just dependency injection.

So the @Provider class to support the new code looks like this:

import java.lang.reflect.Type;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sun.jersey.api.core.HttpContext;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;
@Provider
public class UserSessionProvider extends AbstractHttpContextInjectable<UserSession> implements InjectableProvider<Context, Type> {
    final static private Logger logger = LoggerFactory.getLogger(UserSessionProvider.class);
    @Override
    public Injectable<UserSession> getInjectable(ComponentContext ic, Context a, Type c) {
        if (c.equals(UserSession.class)) {  
            return this;  
        }  
        return null;
    }
    @Override
    public ComponentScope getScope() {
        return ComponentScope.PerRequest;
    }
    @Override
    public UserSession getValue(HttpContext context) {
        try {
            // CookieUserSession takes care of the marshalling/validation of data in the cookie
            CookieUserSession cookie = CookieUserSession.checkForCookie(context.getRequest().getCookies());
            return cookie.recoverAllFields(); // returns a UserSession, clearly
        }
        catch (CookieTamperedException e) {
            logger.error(e.getMessage(), e);
            throw new RsException(e); // my subclass of WebApplicationException
        }
    }
}

It took me a while to sort out which class I needed to extend (and interface to implement). A number of trials and errors gave me Jersey exceptions complaining that it could not find an injector for my resource function. Messages like this:

INFO: Initiating Jersey application, version 'Jersey: 1.14 09/09/2012 07:21 PM'
Jan 02, 2013 10:20:12 PM com.sun.jersey.spi.inject.Errors processErrorMessages
SEVERE: The following errors and warnings have been detected with resource and/or provider classes:
  SEVERE: Missing dependency for method public javax.ws.rs.core.Response com.example.rs.MyService.createThing(com.example.stateful.UserSession,java.lang.String) at parameter at index 0
  SEVERE: Method, public javax.ws.rs.core.Response com.example.rs.MyService.createThing(com.example.stateful.UserSession,java.lang.String), annotated with POST of resource, class com.example.rs.MyService, is not recognized as valid resource method.

Hat tip to Antoine Vianey’s blog post where one of his examples demonstrated this.