Saturday, September 1, 2012

Returning a partial response in a JAX-RS web service - Part 2

See Part 1 of this blog post for background and reasoning for providing partial response in your JAX-RS API.

Here's the basic steps:
  1. A custom Apache CXF RequestHandler filter inspects each Request to look for a 'fields' Query Parameter.
  2. When the 'fields' parameter is found and has a value, we make sure the requested fields are initialized by Hibernate.
  3. Lastly, we use a Jackson ObjectWriter to apply a Jackson filter to the object and then build and return a Response with the resulting JSON.
Here's a link  to the ResponseFilter I'm using in my sample project in Github.

First we inspect the Request and make sure it is a Get, the request succeeded (200 OK), has a JSON response type, and indeed has 'fields' query parameter and corresponding value.

@Override
public Response handleResponse(Message m, OperationResourceInfo ori,
Response response) {
// exit now if not an http GET method
if (!ori.getHttpMethod().equals("GET"))
return null;
// exit now if not a 200 response, no sense in apply filtering if not a
// '200 OK'
if (response.getStatus() != 200)
return null;
// exit now if we are not returning json
if (!ori.getProduceTypes().get(0).toString()
.equals(MediaType.APPLICATION_JSON))
return null;
// get a reference to the response entity. the entity holds the payload
// of our response
Object entity = response.getEntity();
// try to get the 'fields' parameter from the QueryString
String fields = uriInfo.getQueryParameters().getFirst("fields");
// if the 'fields' QueryString is blank, then check to see if we have
// @DefaultValue for 'fields'
if (StringUtils.isBlank(fields)) {
// get the Parameters for this Resource
List<Parameter> parameters = ori.getParameters();
for (Parameter parm : parameters) {
// is the current Parameter named 'fields'?
if (parm.getName().equals("fields")) {
// get the default value for 'fields'
fields = parm.getDefaultValue();
// now that we found 'fields', there's no need to keep
// looping
break;
}
}
// If 'fields' is still blank then we don't have a value from either
// the QueryString or @DefaultValue
if (StringUtils.isBlank(fields)) {
logger.debug("Did not find 'fields' pararm for Resource '"
+ uriInfo.getPath() + "'");
return null;
}
}
view raw check response hosted with ❤ by GitHub
At this point let's make sure any requested lazy loaded fields get initialized.  I'm sure there's a better way of doing the next part.

Set<String> filterProperties = getFieldsAsSet(fields);
// is the entity a collection?
if (entity instanceof Collection<?>) {
initializeEntity((Collection<?>) entity, filterProperties);
}
// is the entity an array?
else if (entity instanceof Object[]) {
initializeEntity((Object[]) entity, filterProperties);
} else {
initializeEntity(entity, filterProperties);
}
// make sure each of the requested 'fields' are initialized by Hibernate
private void initializeEntity(Object entity, Set<String> fields) {
if (entity == null) return;
// loop through the values of the 'fields'
for (String field : fields) {
try {
// Initialize the current 'field'
// This is needed since Hibernate will not auto-initialize most
// collections
// Therefore, if we want to return the field in the response, we
// need to make sure it is loaded
fieldInitializer.initializeField(entity, field);
} catch (Exception e) {
e.printStackTrace();
throw new GenericWebServiceException(
Response.Status.INTERNAL_SERVER_ERROR, e.getMessage());
}
}
return;
}
view raw gistfile1.txt hosted with ❤ by GitHub

The code that initializes the fields is beyond scope of this post but here's a link to the class in my Github repo in case you are interested.

Lastly, let's apply the Jackson filter and build the response. Notice we are adding "apiFilter" to the SimpleFilterProvider. Your entity classes will need to be annotated with @JsonFilter("apiFilter") and your CustomObjectMapper should be configured with filterProvider.setFailOnUnknownId(false);

// apply the Jackson filter and return the Response
return applyFieldsFilter(filterProperties, entity, response);
// configure the Jackson filter for the speicified 'fields'
private Response applyFieldsFilter(Set<String> filterProperties,
Object object, Response response) {
SimpleFilterProvider filterProvider = new SimpleFilterProvider();
filterProvider.addFilter("apiFilter",
SimpleBeanPropertyFilter.filterOutAllExcept(filterProperties));
filterProvider.setFailOnUnknownId(false);
return applyWriter(object, filterProvider, response);
}
// Get a JSON string from Jackson using the provided filter.
// Note we are using the ObjectWriter rather than the ObjectMapper directly.
// According to the Jackson docs,
// "ObjectWriter instances are immutable and thread-safe: they are created by ObjectMapper"
// You should not change config settings directly on the ObjectMapper while
// using it (changing config of ObjectMapper is not thread-safe
private Response applyWriter(Object object,
SimpleFilterProvider filterProvider, Response response) {
try {
String jsonString = objectMapper.writer(filterProvider)
.writeValueAsString(object);
// replace the Response entity with our filtered JSON string
return Response.fromResponse(response).entity(jsonString).build();
} catch (Exception e) {
e.printStackTrace();
throw new GenericWebServiceException(
Response.Status.INTERNAL_SERVER_ERROR, e.getMessage());
}
}
view raw writer hosted with ❤ by GitHub

Any reader comments, questions, and suggestions are appreciated. Thanks!

2 comments:

  1. Hi justin, the code your pointing at on github looks obsolete (404). I can't find it anymore.

    ReplyDelete
  2. Respect and I have a nifty proposal: Where To Buy Houses That Need Renovation split level home exterior remodel

    ReplyDelete