Mattan
Mattan

Reputation: 753

Jersey ContextResolver GetContext() called only once

I have the following ContextResolver<ObjectMapper> implementation, which based on the query params should return the corresponding JSON mapper (pretty/DateToUtc/Both):

import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

@Provider
@Produces(MediaType.APPLICATION_JSON)
public class JsonMapper implements ContextResolver<ObjectMapper> {

    private ObjectMapper prettyPrintObjectMapper;
    private ObjectMapper dateToUtcMapper;
    private ObjectMapper bothMapper;
    private UriInfo uriInfoContext;

    public JsonMapper(@Context UriInfo uriInfoContext) throws Exception {
        this.uriInfoContext = uriInfoContext;

        this.prettyPrintObjectMapper = new ObjectMapper();
        this.prettyPrintObjectMapper.enable(SerializationFeature.INDENT_OUTPUT);

        this.dateToUtcMapper = new ObjectMapper();
        this.dateToUtcMapper.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        this.bothMapper = new ObjectMapper();
        this.bothMapper.enable(SerializationFeature.INDENT_OUTPUT);
        this.bothMapper.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }

    @Override
    public ObjectMapper getContext(Class<?> objectType) {
        System.out.println("hi");
        try {
            MultivaluedMap<String, String> queryParameters = uriInfoContext.getQueryParameters();
            Boolean containsPretty = queryParameters.containsKey("pretty");
            Boolean containsDate   = queryParameters.containsKey("date_to_utc");
            Boolean containsBoth   = containsPretty && containsDate;

            if (containsBoth) {
                System.out.println("Returning containsBoth");
                return bothMapper;
            }

            if (containsDate) {
                System.out.println("Returning containsDate");
                return dateToUtcMapper;
            }

            if (containsPretty) {
                System.out.println("Returning pretty");
                return prettyPrintObjectMapper;
            }

        } catch(Exception e) {
            // protect from invalid access to uriInfoContext.getQueryParameters()
        }

        System.out.println("Returning null");
        return null; // use default mapper
    }
}

And the following Main Application:

 private Server configureServer() {
        ObjectMapper mapper = new ObjectMapper();

        ResourceConfig resourceConfig = new ResourceConfig();
        resourceConfig.packages(Calculator.class.getPackage().getName());
        resourceConfig.property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
        // @ValidateOnExecution annotations on subclasses won't cause errors.
        resourceConfig.property(ServerProperties.BV_DISABLE_VALIDATE_ON_EXECUTABLE_OVERRIDE_CHECK, true);
        resourceConfig.register(JacksonFeature.class);
        resourceConfig.register(JsonMapper.class);
        resourceConfig.register(AuthFilter.class);
        ServletContainer servletContainer = new ServletContainer(resourceConfig);
        ServletHolder sh = new ServletHolder(servletContainer);
        Server server = new Server(serverPort);
        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
        context.setContextPath("/");
        context.addServlet(sh, "/*");
        server.setHandler(context);
        return server;
    }

However, getContext() function is only called once for the entire server lifetime, only on the first request. The whole idea of this class is to determine on runtime what is the mapper based on the url parameters.

UPDATE

getContext() is called once for each uri path. For example, http://server/path1?pretty=true will yield pretty output for all request to /path1, regardless of thier future pretty queryParam. A call to path2 will call getContext again, but not to future path2 calls.

UPDATE2

Well, it seems like the GetContext is called for each class once, and caches it for that specific class. This is why it expects a class as parameter. So it seems like @LouisF is right, and the objectMapper isn't suited for conditional serialization. However, the ContainerResponseFilter alternative is partially working, but not exposing ObjectMapper features, such as converting dates to UTC. So I'm quite puzzled right now on what is the most appropriate solution for conditional serialization.

SOLVED

With the help of @LoisF, I've managed to have conditional serialization, using ContainerResponseFilter. I havn't use ContextResolver. Below is the working example:

import java.io.IOException;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.jaxrs.cfg.EndpointConfigBase;
import com.fasterxml.jackson.jaxrs.cfg.ObjectWriterInjector;
import com.fasterxml.jackson.jaxrs.cfg.ObjectWriterModifier;

/**
 * Created by matt on 17/01/2016.
 */
@Provider
public class ResultTransformer implements ContainerResponseFilter {


    public static final String OUTPUT_FORMAT_HEADER = "X-Output-Format";
    public static final ObjectMapper MAPPER         = new ObjectMapper();

    public static class OutputFormat {
        Boolean pretty              = true;
        Boolean dateAsTimestamp     = false;

        public Boolean getPretty() {
            return pretty;
        }

        public void setPretty(Boolean pretty) {
            this.pretty = pretty;
        }

        @JsonProperty("date_as_timestamp")
        public Boolean getDateAsTimestamp() {
            return dateAsTimestamp;
        }

        public void setDateAsTimestamp(Boolean dateAsTimestamp) {
            this.dateAsTimestamp = dateAsTimestamp;
        }
    }

    @Override
    public void filter(ContainerRequestContext reqCtx, ContainerResponseContext respCtx) throws IOException {

        String outputFormatStr = reqCtx.getHeaderString(OUTPUT_FORMAT_HEADER);
        OutputFormat outputFormat;
        if (outputFormatStr == null) {
            outputFormat = new OutputFormat();
        } else {
            try {
                outputFormat = MAPPER.readValue(outputFormatStr, OutputFormat.class);
                ObjectWriterInjector.set(new IndentingModifier(outputFormat));
            } catch (Exception e) {
                e.printStackTrace();
                ObjectWriterInjector.set(new IndentingModifier(new OutputFormat()));
            }
        }
    }

    public static class IndentingModifier extends ObjectWriterModifier {

       private OutputFormat outputFormat;

        public IndentingModifier(OutputFormat outputFormat) {
            this.outputFormat = outputFormat;

        }


        @Override
        public ObjectWriter modify(EndpointConfigBase<?> endpointConfigBase, MultivaluedMap<String, Object> multivaluedMap, Object o, ObjectWriter objectWriter, JsonGenerator jsonGenerator) throws IOException {
            if(outputFormat.getPretty())      jsonGenerator.useDefaultPrettyPrinter();
            if (outputFormat.dateAsTimestamp)  {
                objectWriter = objectWriter.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
            } else {
                objectWriter = objectWriter.without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
            }
            return objectWriter;
        }
    }

}

Upvotes: 2

Views: 2014

Answers (2)

You should consider performance. With your solution, you are creating a new ObjectMapper instance with each request. This is quite heavy!!! I found ObjectMapper creation as main performance stopper during a JProfile measurement.

Not sure if just having 2 static members for pretty / non-pretty is a sufficient solution regarding thread-safety. You need to take care of the mechanism used by the JAX-RS framework in order to cache the ObjectMapper, in order to not have any side-effects.

Upvotes: 2

Louis F.
Louis F.

Reputation: 2048

If you want it by request, you need it to be evaluated for each call. What I would suggest here is to move this logic in a dedicated component and do something as follow :

@GET
public Response demo(@Context final UriInfo uriInfoContext, final String requestBody) {
    final ObjectMapper objectMapper = objectMapperResolver.resolve(uriInfoContext.getQueryParameters());
    objectMapper.readValue(requestBody, MyClass.class);
    ...
}

where objectMapperResolver encapsulates the logic of choosing the right ObjectMapper depending on the query parameters

Upvotes: 0

Related Questions