users@jersey.java.net

[Jersey] Accessing the Entity inside ExceptionMapper

From: Gili <cowwoc_at_bbs.darktech.org>
Date: Mon, 28 Mar 2011 19:52:31 -0700 (PDT)

Gili wrote:
>
> Hi,
>
> It would be extremely useful to be able to use EntityHolder inside an
> ExceptionMapper. If a parsing error occurs converting the entity from
> String to (say) JSON, I could do something like this:
>
> @Provider
> public class JsonMappingMapper implements ExceptionMapper
> {
> private final String entity;
>
> /**
> * Creates a new JsonMappingMapper.
> *
> * @param entity the entity associated with the request
> */
> @Inject
> public JsonMappingMapper(EntityHolder entity)
> {
> this.entity = entity.getEntity();
> }
>
> @Override
> public Response toResponse(JsonMappingException e)
> {
> return Response.status(Status.BAD_REQUEST).entity(e.getMessage() +
> "\nEntity:\n" + entity).
> type("text/plain").build();
> }
> }
>
> In other words, I would like the log the entity body as a raw String.
> Should I file a feature request for this? Is there another way of
> accomplishing this?
>
> Thanks,
> Gili
>

Here is how I ended up implementing this:

1. Register a filter for caching the entity.

Map&lt;String, String&gt; jerseyParams = Maps.newHashMap();
jerseyParams.put(ResourceConfig.PROPERTY_CONTAINER_REQUEST_FILTERS,
CachedEntityFilter.class.getName());
serve("/*").with(GuiceContainer.class, jerseyParams);

2. Define a mutable provider for setting/getting a HttpRequestContext.

/**
 * Provides a HttpRequestContext.
 *
 * @author Gili Tzabari
 */
@RequestScoped
public class HttpRequestContextProvider implements MutableProvider
{
        private HttpRequestContext request;

        @Override
        public HttpRequestContext get()
        {
                return request;
        }

        @Override
        public void set(HttpRequestContext request)
        {
                this.request = request;
        }
}

3. Bind the HttpRequestContextProvider to @Named("entityFilter") to
differentiate it with the default HttpRequestContext provider:

        
binder.bind(HttpRequestContext.class).annotatedWith(Names.named("entityFilter")).
                        toProvider(HttpRequestContextProvider.class).in(ServletScopes.REQUEST);

4. Implement a filter for caching the entity:

/**
 * Caches the request entity.
 *
 * @author Gili Tzabari
 */
public class CachedEntityFilter implements ContainerRequestFilter
{
        private final Injector injector;
        private final Logger log =
LoggerFactory.getLogger(CachedEntityFilter.class);

        @Inject
        public CachedEntityFilter(Injector injector)
        {
                this.injector = injector;
        }

        @Override
        public ContainerRequest filter(ContainerRequest request)
        {
                HttpRequestContextProvider requestContextProvider =
                        injector.getInstance(HttpRequestContextProvider.class);
                CachedEntityContainerRequest result;
                try
                {
                        result = new CachedEntityContainerRequest(request);
                }
                catch (IOException e)
                {
                        log.error("An error occured reading the entity", e);
                        return request;
                }
                requestContextProvider.set(result);
                return result;
        }

        /**
         * An in-bound HTTP request that caches the entity body
         * obtained from the adapted container request.
         *

         * A filter may utilize this class if it requires an entity of a specific
type
         * and a different type will be utilized by a resource method.
         *
         * @author Gili Tzabari
         */
        private static class CachedEntityContainerRequest extends
AdaptingContainerRequest
        {
                private final byte[] in;
                private final Map&lt;Class&lt;?&gt;, Object> entityCache =
Maps.newHashMap();

                /**
                 * Creates a new CachedEntityContainerRequest.
                 *
                 * @param acr the adapted container request
                 * @throws IOException if an error occurs reading the entity
                 */
                public CachedEntityContainerRequest(ContainerRequest acr) throws
IOException
                {
                        super(acr);
                        this.in = ByteStreams.toByteArray(acr.getEntityInputStream());
                }

                /**
                 * Get the entity or a cached instance.
                 *
                 * @param the type of the entity
                 * @return If called for the first time obtain the entity from the
adapting
                 * container request and cache the instance. If called for second
or
                 * subsequent time return the cached instance.
                 * @throws ClassCastException if the cached entity cannot be cast to the
                 * type requested.
                 */
                @Override
                public T getEntity(Class type) throws ClassCastException
                {
                        if (!entityCache.containsKey(type))
                        {
                                // reset entity stream
                                acr.setEntityInputStream(new ByteArrayInputStream(in));

                                T t = acr.getEntity(type);
                                entityCache.put(type, t);
                                return t;
                        }
                        else
                        {
                                @SuppressWarnings("unchecked")
                                T t = (T) entityCache.get(type);
                                return t;
                        }
                }

                /**
                 * Get the entity or a cached instance.
                 *
                 * @param the type of the entity
                 * @return If called for the first time obtain the entity from the
adapting
                 * container request and cache the instance. If called for second
or
                 * subsequent time return the cached instance.
                 * @throws ClassCastException if the cached entity cannot be cast to the
                 * type requested.
                 */
                @Override
                public T getEntity(Class type, Type genericType, Annotation[] as)
                        throws ClassCastException
                {
                        if (!entityCache.containsKey(type))
                        {
                                // reset entity stream
                                acr.setEntityInputStream(new ByteArrayInputStream(in));

                                T t = acr.getEntity(type, genericType, as);
                                entityCache.put(type, t);
                                return t;
                        }
                        else
                        {
                                @SuppressWarnings("unchecked")
                                T t = (T) entityCache.get(type);
                                return t;
                        }
                }
        }
}

5. Retrieve the Entity inside the ExceptionMapper:

/**
 * @author Gili Tzabari
 */
@Provider
public class JsonParseMapper implements ExceptionMapper
{
        private final com.google.inject.Provider requestContext;

        /**
         * Creates a new JsonParseMapper.
         *
         * @param requestContext the entity associated with the request
         */
        @Inject
        public JsonParseMapper(
                @Named("entityFilter") com.google.inject.Provider requestContext)
        {
                this.requestContext = requestContext;
        }

        @Override
        public Response toResponse(JsonParseException e)
        {
                HttpRequestContext provider = requestContext.get();
                String entity = provider.getEntity(String.class);
                int sourceStart = e.getMessage().indexOf("Source:");
                int sourceEnd = e.getMessage().indexOf("line: ", sourceStart);
                StringBuilder messageWithoutSource = new StringBuilder(e.getMessage());
                messageWithoutSource.delete(sourceStart, sourceEnd);
                messageWithoutSource.insert(messageWithoutSource.length(), " Source: \n" +
entity);
                return Response.status(Status.BAD_REQUEST).entity(e.getClass().getName() +
": " + messageWithoutSource.toString()).type("text/plain").build();
        }
}

You will notice that my CachedEntityContainerRequest caches the entity body
as opposed to the entity instance. This is because we must be able to
re-parse the entity body (as a different type) in case of a parsing error.

Here is what the output looks like:

org.codehaus.jackson.map.JsonMappingException: Expected VALUE_STRING. Got:
VALUE_NUMBER_INT
 at [line: 4, column: 20] Source:
{"name": "First",
"address":
{
"streetNumber": 132
}
}

Paul, do you think it makes sense to integrate some of this functionality
into Jersey-proper?

Thanks,
Gili

--
View this message in context: http://jersey.576304.n2.nabble.com/Using-EntityHolder-with-ExceptionMapper-tp6205881p6217509.html
Sent from the Jersey mailing list archive at Nabble.com.