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<String, String> 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<Class<?>, 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.