Here's the basic steps:
- A custom Apache CXF RequestHandler filter inspects each Request to look for a 'fields' Query Parameter.
- When the 'fields' parameter is found and has a value, we make sure the requested fields are initialized by Hibernate.
- 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.
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
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);
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// apply the Jackson filter and return the Response | |
return applyFieldsFilter(filterProperties, entity, response); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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()); | |
} | |
} |
Any reader comments, questions, and suggestions are appreciated. Thanks!
Hi justin, the code your pointing at on github looks obsolete (404). I can't find it anymore.
ReplyDeleteRespect and I have a nifty proposal: Where To Buy Houses That Need Renovation split level home exterior remodel
ReplyDelete