Java REST deserialize list - java

I'm building a Java REST application with JAX-RS.
In the context of the application, I have the following bean classes:
public class ContentView {
// Field definitions, getters/setters
}
public class ArticleDetailView extends ContentView {
// more definitions, getters/setters
}
public class UpsellDetailView extends ContentView {
// more definitions, getters/setters
}
UpsellDetailView and ArticleDetailView have more attributes and they both extend ContentView. I've got all the mappers correctly wired up, so all the instances of each respective class gets all its properties set correctly. I don't use any extra mappers - object properties get deserialized based on the public getters in the respective bean classes.
SeriesDetailView is similar:
public class SeriesDetailView extends SeriesLiteView {
private List<Content> content;
#JsonIgnore //We don't want content appear on the Series detail page
public List<Content> getContent() {
return content;
}
public void setContent(List<Content> content) {
this.content = content;
}
}
Now, I have a REST resource class as follows:
#Produces({MediaType.APPLICATION_JSON})
#Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED})
#Path("/")
public class ApiSiteResource extends AbstractContentResource {
...
#GET
#Path("/series/{uuid}/contents")
public List<ContentView> getSessionsForSeries(#Context HttpServletRequest request, #Context HttpServletResponse response, #BeanParam ApiParams params) {
final SeriesDetailView series = seriesService.findByUuid(params.getUuid());
if (series == null) {
response.setStatus(SC_NOT_FOUND);
return null;
}
List<ContentView> contentViewList = contentMapper.map(series.getContent());
List<ContentView> results = contentViewList.stream().map(e -> mapContent(e, e.getUuid())).collect(Collectors.toList());
return results;
}
#GET
#Path("/contents/{uuid}")
public ContentView uniqueContent(#Context HttpServletRequest request, #Context HttpServletResponse response, #BeanParam ApiParams params) {
ContentView beanView = contentService.findByUuid(params.getUuid());
if (beanView == null) {
response.setStatus(SC_NOT_FOUND);
return null;
}
beanView = mapContent(beanView, params.getUuid());
return beanView;
}
private ContentView mapContent(ContentView beanView, String uuid){
if (ArticleType.TypeGroup.upsell.toString().equals(beanView.getType())) {
UpSell upsell = ((UpSell)contentService.findByUuidRaw(uuid));
beanView = (UpsellDetailView)upsellMapper.map(upsell);
rewriteBodyHtml(beanView, upsell.getBody());
}
else if (ArticleType.TypeGroup.article.toString().equals(beanView.getType()) ||
ArticleType.TypeGroup.audio.toString().equals(beanView.getType()) ||
ArticleType.TypeGroup.video.toString().equals(beanView.getType())) {
Article article = ((Article)contentService.findByUuidRaw(uuid));
beanView = (ArticleDetailView)articleMapper.map(article);
rewriteBodyHtml(beanView, article.getBody());
}
return beanView;
}
}
Now, here's the problem:
when I invoke /contents/{uuid} in the browser, I get fully deserialized correct content type json (Article or UpsellDetailView), but
when I invoke /series/{uuid}/contents, I get a list of short-form json elements corresponding to the ContentView structure.
I do confirm that the List results in getSessionsForSeries() is a list of correct types (Article or UpsellDetailView).
But I can't find for the life of mine why these don't get properly deserialized when in the List. What am I missing?

Here's the answer:
For some reason, List's serializer was messed up and was when serializing was defaulting to the declared content class (ContentView).
If anyone has an idea why I'd appreciate enlightment.
So, I had to use brute force and provide my own serialization (do note the method's response type is changed to String):
#GET
#Path("/series/{uuid}/contents")
public String getSessionsForSeries(#Context HttpServletRequest request, #Context HttpServletResponse response, #BeanParam ApiParams params) {
final SeriesDetailView series = seriesService.findByUuid(params.getUuid());
if (series == null) {
response.setStatus(SC_NOT_FOUND);
return null;
}
List<ContentView> contentViewList = contentMapper.map(series.getContent());
List<ContentView> results = contentViewList.stream()
.map(e -> mapContent(e, e.getUuid())).collect(Collectors.toList());
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(results);
} catch (IOException e) {
response.setStatus(SC_INTERNAL_SERVER_ERROR);
return null;
}
}
Thanks to the tutorial here: http://www.davismol.net/2015/03/05/jackson-json-deserialize-a-list-of-objects-of-subclasses-of-an-abstract-class/

Related

How to mock array of class using Mockito

I want to mock Order array class Order[].class. below mockMapper reads Order[].class and need to return Order[].class.
Service class
public class OrdersConnectorImpl {
public Order getOrderById(String Id, OrderType orderType) throws Exception {
Response response = null;
response = orderServiceTarget.queryParam("ID", Id).queryParam(ORDER_TYPE, orderType).request().accept(MediaType.APPLICATION_JSON)
.get();
final StatusType responseStatus = response.getStatusInfo();
final String serverResponseStr = response.readEntity(String.class);
if (responseStatus.equals(Response.Status.OK)) {
objectMapper = getObjectMapper(); // we have a private method in this class
Order[] orders = objectMapper.readValue(serverResponseStr, Order[].class);
if(orders.length>0) {
return orders[0];
}
}
}
}
Test class
public class OrdersConnectorImplTest {
private ObjectMapper mockMapper;
private class MockOrdersConnectorImpl extends OrdersConnectorImpl {
#Override
protected ObjectMapper getObjectMapper() {
return mockMapper;
}
}
}
#Test
public void test_getRenewalOrderForContract() throws Exception {
Response mockResponse = mock(javax.ws.rs.core.Response.class);
Order mockOrder = mock(Order.class);
when(mockResponse.getStatusInfo()).thenReturn(Status.OK);
when(mockResponse.readEntity(String.class)).thenReturn("{}");
when(mockBuilder.get()).thenReturn(mockResponse);
when(mockMapper.readValue("{}", Order[].class)).thenReturn(mockOrder); // this is the problem line
orderConnector.getOrderById("id", OrderType.NEW);
}
}
Please correct me here how to return expected.
You have autowired mockMapper which means, the actual object of ObjectMapper will be injected.
And in the when part, you setting up the behavior of mockMapper, which shouldn't be the case.
Edit 1
In your case, there is no need for mocking the Order class, you have to return the array Order which you can create in the tests.
For your information, with Junit there is no way, you can mock the object which is been created inside the method.

Jersey: hardcode POST/PUT ObjectMapper, without needing Content-Type header

I have a Jersey 1.19.1 resource that implements a #PUT and #POST method. The #PUT method expects a JSON string as the input/request body, while the #POST method accepts plain text.
For the JSON mapping I am using Jackson 2.8.
Since the resource is defined to work this way, I don't want the client to be required to specify a Content-Type request header, just because Jersey needs it to figure out which ObjectMapper to use on the request body.
What I want instead, is to tell Jersey "Use this ObjectMapper for this #PUT input", or "Always assume this input will have an application/json Content-Type on this method."
#Produces(MediaType.APPLICATION_JSON)
#Path("/some/endpoint/{id}")
public class MyResource {
#PUT
public JsonResult put(
#PathParam("id") String id,
// this should always be deserialized by Jackson, regardless of the `Content-Type` request header.
JsonInput input
) {
log.trace("PUT {}, {}, {}", id, input.foo, input.bar);
return new JsonResult("PUT result");
}
#POST
public JsonResult post(
#PathParam("id") String id,
// this should always be treated as plain text, regardless of the `Content-Type` request header.
String input
) {
log.trace("POST {}, {}", id, input);
return new JsonResult("POST result");
}
}
I only found this answer, but that's not what I'm looking for, as the solution there seems to be that the client should be required to add the correct Content-Type header, or otherwise do the object mapping manually.
I managed to come up with a workaround. Instead of declaring which ObjectMapper to use on a Jersey resource method, I decided to create a ResourceFilter, corresponding ResourceFilterFactory, and an annotation type. Whenever a resource class or method is annotated with this type, the ResourceFilter will override the request's Content-Type to whatever is declared in the annotation's parameter.
Here's my code:
OverrideInputType annotation:
#Target({ElementType.TYPE, ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
public #interface OverrideInputType {
// What the Content-Type request header value should be replaced by
String value();
// which Content-Type request header values should not be replaced
String[] except() default {};
}
OverrideInputTypeResourceFilter:
public class OverrideInputTypeResourceFilter implements ResourceFilter, ContainerRequestFilter {
private MediaType targetType;
private Set<MediaType> exemptTypes;
OverrideInputTypeResourceFilter(
#Nonnull String targetType,
#Nonnull String[] exemptTypes
) {
this.targetType = MediaType.valueOf(targetType);
this.exemptTypes = new HashSet<MediaType>(Lists.transform(
Arrays.asList(exemptTypes),
exemptType -> MediaType.valueOf(exemptType)
));
}
#Override
public ContainerRequest filter(ContainerRequest request) {
MediaType inputType = request.getMediaType();
if (targetType.equals(inputType) || exemptTypes.contains(inputType)) {
// unmodified
return request;
}
MultivaluedMap<String, String> headers = request.getRequestHeaders();
if (headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
headers.putSingle(HttpHeaders.CONTENT_TYPE, targetType.toString());
request.setHeaders((InBoundHeaders)headers);
}
return request;
}
#Override
public final ContainerRequestFilter getRequestFilter() {
return this;
}
#Override
public final ContainerResponseFilter getResponseFilter() {
// don't filter responses
return null;
}
}
OverrideInputTypeResourceFilterFactory:
public class OverrideInputTypeResourceFilterFactory implements ResourceFilterFactory {
#Override
public List<ResourceFilter> create(AbstractMethod am) {
// documented to only be AbstractSubResourceLocator, AbstractResourceMethod, or AbstractSubResourceMethod
if (am instanceof AbstractSubResourceLocator) {
// not actually invoked per request, nothing to do
log.debug("Ignoring AbstractSubResourceLocator {}", am);
return null;
} else if (am instanceof AbstractResourceMethod) {
OverrideInputType annotation = am.getAnnotation(OverrideInputType.class);
if (annotation == null) {
annotation = am.getResource().getAnnotation(OverrideInputType.class);
}
if (annotation != null) {
return Lists.<ResourceFilter>newArrayList(
new OverrideInputTypeResourceFilter(annotation.value(), annotation.except()));
}
} else {
log.warn("Got an unexpected instance of {}: {}", am.getClass().getName(), am);
}
return null;
}
}
Example MyResource demonstrating its use:
#Produces(MediaType.APPLICATION_JSON)
#Path(/objects/{id}")
public class MyResource {
#PUT
// #Consumes(MediaType.APPLICATION_JSON)
#OverrideInputType(MediaType.APPLICATION_JSON)
public StatusResult put(#PathParam("id") int id, JsonObject obj) {
log.trace("PUT {}", id);
// do something with obj
return new StatusResult(true);
}
#GET
public JsonObject get(#PathParam("id") int id) {
return new JsonObject(id);
}
}
In Jersey 2, you could do this with a post-matching ContainerRequestFilters

Custom Jackson HttpMessageConverter no longer works in Spring 4.2

I am updating an application from Spring Platform version 1.1.3.RELEASE to 2.0.1.RELEASE, which bumps the Spring Framework version from 4.1.7 to 4.2.4, and Jackson from 2.4.6 to 2.6.4. There does not seem to have been any significant changes in Spring or Jackson's handling of custom HttpMessageConverter implementations, but my custom JSON serialization is failing to occur, and I have not been able to determine why. The following works fine in the previous Spring Platform release:
Model
#JsonFilter("fieldFilter")
public class MyModel {
/*model fields and methods*/
}
Model wrapper
public class ResponseEnvelope {
private Set<String> fieldSet;
private Set<String> exclude;
private Object entity;
public ResponseEnvelope(Object entity) {
this.entity = entity;
}
public ResponseEnvelope(Object entity, Set<String> fieldSet, Set<String> exclude) {
this.fieldSet = fieldSet;
this.exclude = exclude;
this.entity = entity;
}
public Object getEntity() {
return entity;
}
#JsonIgnore
public Set<String> getFieldSet() {
return fieldSet;
}
#JsonIgnore
public Set<String> getExclude() {
return exclude;
}
public void setExclude(Set<String> exclude) {
this.exclude = exclude;
}
public void setFieldSet(Set<String> fieldSet) {
this.fieldSet = fieldSet;
}
public void setFields(String fields) {
Set<String> fieldSet = new HashSet<String>();
if (fields != null) {
for (String field : fields.split(",")) {
fieldSet.add(field);
}
}
this.fieldSet = fieldSet;
}
}
Controller
#Controller
public class MyModelController {
#Autowired MyModelRepository myModelRepository;
#RequestMapping(value = "/model", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
public HttpEntity find(#RequestParam(required=false) Set<String> fields, #RequestParam(required=false) Set<String> exclude){
List<MyModel> objects = myModelRepository.findAll();
ResponseEnvelope envelope = new ResponseEnvelope(objects, fields, exclude);
return new ResponseEntity<>(envelope, HttpStatus.OK);
}
}
Custom HttpMessageConverter
public class FilteringJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
private boolean prefixJson = false;
#Override
public void setPrefixJson(boolean prefixJson) {
this.prefixJson = prefixJson;
super.setPrefixJson(prefixJson);
}
#Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
ObjectMapper objectMapper = getObjectMapper();
JsonGenerator jsonGenerator = objectMapper.getFactory().createGenerator(outputMessage.getBody());
try {
if (this.prefixJson) {
jsonGenerator.writeRaw(")]}', ");
}
if (object instanceof ResponseEnvelope) {
ResponseEnvelope envelope = (ResponseEnvelope) object;
Object entity = envelope.getEntity();
Set<String> fieldSet = envelope.getFieldSet();
Set<String> exclude = envelope.getExclude();
FilterProvider filters = null;
if (fieldSet != null && !fieldSet.isEmpty()) {
filters = new SimpleFilterProvider()
.addFilter("fieldFilter", SimpleBeanPropertyFilter.filterOutAllExcept(fieldSet))
.setFailOnUnknownId(false);
} else if (exclude != null && !exclude.isEmpty()) {
filters = new SimpleFilterProvider()
.addFilter("fieldFilter", SimpleBeanPropertyFilter.serializeAllExcept(exclude))
.setFailOnUnknownId(false);
} else {
filters = new SimpleFilterProvider()
.addFilter("fieldFilter", SimpleBeanPropertyFilter.serializeAllExcept())
.setFailOnUnknownId(false);
}
objectMapper.setFilterProvider(filters);
objectMapper.writeValue(jsonGenerator, entity);
} else if (object == null){
jsonGenerator.writeNull();
} else {
FilterProvider filters = new SimpleFilterProvider().setFailOnUnknownId(false);
objectMapper.setFilterProvider(filters);
objectMapper.writeValue(jsonGenerator, object);
}
} catch (JsonProcessingException e){
e.printStackTrace();
throw new HttpMessageNotWritableException("Could not write JSON: " + e.getMessage());
}
}
}
Configuration
#Configuration
#EnableWebMvc
public class WebServicesConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FilteringJackson2HttpMessageConverter jsonConverter = new FilteringJackson2HttpMessageConverter();
jsonConverter.setSupportedMediaTypes(MediaTypes.APPLICATION_JSON);
converters.add(jsonConverter);
}
// Other configurations
}
Now I am getting this exception (which is caught by Spring and logged) and a 500 error when making any sort of request:
[main] WARN o.s.w.s.m.s.DefaultHandlerExceptionResolver - Failed to write HTTP message:
org.springframework.http.converter.HttpMessageNotWritableException: Could not write content:
Can not resolve PropertyFilter with id 'fieldFilter';
no FilterProvider configured (through reference chain:
org.oncoblocks.centromere.web.controller.ResponseEnvelope["entity"]->java.util.ArrayList[0]);
nested exception is com.fasterxml.jackson.databind.JsonMappingException:
Can not resolve PropertyFilter with id 'fieldFilter';
no FilterProvider configured (through reference chain:
org.oncoblocks.centromere.web.controller.ResponseEnvelope["entity"]->java.util.ArrayList[0])
The configureMessageConverters method executes, but it does not look like custom converter is ever utilized during requests. Is it possible that another message converter could be preventing this one from reaching my response? My understanding was that overriding configureMessageConverters would prevent converters other than the manually registered ones from being used.
No changes have been made between the working and non-working versions of this code, besides updating dependency versions via the Spring Platform. Has there been any change in the JSON serialization that I am just missing in the documentation?
Edit
Further testing yields strange results. I wanted to test to check the following things:
Is my custom HttpMessageConverter actually being registered?
Is another converter overriding/superseding it?
Is this a problem with my test setup only?
So, I added an extra test and took a look at the output:
#Autowired WebApplicationContext webApplicationContext;
#Before
public void setup(){
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
#Test
public void test() throws Exception {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) webApplicationContext.getBean("requestMappingHandlerAdapter");
List<EntrezGene> genes = EntrezGene.createDummyData();
Set<String> exclude = new HashSet<>();
exclude.add("entrezGeneId");
ResponseEnvelope envelope = new ResponseEnvelope(genes, new HashSet<String>(), exclude);
for (HttpMessageConverter converter: adapter.getMessageConverters()){
System.out.println(converter.getClass().getName());
if (converter.canWrite(ResponseEnvelope.class, MediaType.APPLICATION_JSON)){
MockHttpOutputMessage message = new MockHttpOutputMessage();
converter.write((Object) envelope, MediaType.APPLICATION_JSON, message);
System.out.println(message.getBodyAsString());
}
}
}
...and it works fine. My the envelope object and its contents are serialized and filtered correctly. So either there is an issue with the request handling before it reaches the message converters, or there has been a change in how MockMvc is testing requests.
Your configuration is ok. The reason why writeInternal() is not called from your custom converter is because you are overriding the wrong method.
Looking at the source code of 4.2.4.RELEASE
AbstractMessageConverterMethodProcessor#writeWithMessageConverters
protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
...
((GenericHttpMessageConverter<T>) messageConverter).write(returnValue, returnValueType, selectedMediaType, outputMessage);
...
}
AbstractGenericHttpMessageConverter#write
public final void write(final T t, final Type type, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
...
writeInternal(t, type, outputMessage);
...
}
The method writeInternal(...) called from within AbstractGenericHttpMessageConverter#write(...) has three arguments - (T t, Type type, HttpOutputMessage outputMessage). You are overriding the overloaded version of writeInternal(...) that has only 2 arguments - (T t, HttpOutputMessage outputMessage).
However, in version 4.1.7.RELEASE, it is not the case, hence the root cause of your problem. The writeInternal(...) used in this version is the other overloaded method (the method with 2 arguments) that you have overriden. This explains why it is working fine in 4.1.7.RELEASE.
#Override
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
...
writeInternal(t, outputMessage);
...
}
So, to solve your problem, instead of overriding writeInternal(Object object, HttpOutputMessage outputMessage), override writeInternal(Object object, Type type, HttpOutputMessage outputMessage)

Mapping URL parameters with dashes to object in Spring Web MVC

Mapping URL request parameters with Spring MVC to an object is fairly straightforward if you're using camelCase parameters in your request, but when presented with hyphen delimited values, how do you map these to an object?
Example for reference:
Controller:
#RestController
public class MyController {
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<String> search(RequestParams requestParams) {
return new ResponseEntity<>("my-val-1: " + requestParams.getMyVal1() + " my-val-2: " + requestParams.getMyVal2(), HttpStatus.OK);
}
}
Object to hold parameters:
public class RequestParams {
private String myVal1;
private String myVal2;
public RequestParams() {}
public String getMyVal1() {
return myVal1;
}
public void setMyVal1(String myVal1) {
this.myVal1 = myVal1;
}
public String getMyVal2() {
return myVal2;
}
public void setMyVal2(String myVal2) {
this.myVal2 = myVal2;
}
}
A request made like this works fine:
GET http://localhost:8080/search?myVal1=foo&myVal2=bar
But, what I want is for a request with hyphens to map to the object, like so:
GET http://localhost:8080/search?my-val-1=foo&my-val-2=bar
What do I need to configure in Spring to map url request parameters with hyphens to fields in an object? Bear in mind that we may have many parameters, so using a #RequestParam annotation for each field is not ideal.
I extended ServletRequestDataBinder and ServletModelAttributeMethodProcessor to solve the problem.
Consider that your domain object may already be annotated with #JsonProperty or #XmlElement for serialization. This example assumes this is the case. But you could also create your own custom annotation for this purpose e.g. #MyParamMapping.
An example of your annotated domain class is:
public class RequestParams {
#XmlElement(name = "my-val-1" )
#JsonProperty(value = "my-val-1")
private String myVal1;
#XmlElement(name = "my-val-2")
#JsonProperty(value = "my-val-2")
private String myVal2;
public RequestParams() {
}
public String getMyVal1() {
return myVal1;
}
public void setMyVal1(String myVal1) {
this.myVal1 = myVal1;
}
public String getMyVal2() {
return myVal2;
}
public void setMyVal2(String myVal2) {
this.myVal2 = myVal2;
}
}
You will need a SerletModelAttributeMethodProcessor to analyze the target class, generate a mapping, invoke your ServletRequestDataBinder.
public class KebabCaseProcessor extends ServletModelAttributeMethodProcessor {
public KebabCaseProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<Class<?>, Map<String, String>>();
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Class<?> targetClass = target.getClass();
if (!replaceMap.containsKey(targetClass)) {
Map<String, String> mapping = analyzeClass(targetClass);
replaceMap.put(targetClass, mapping);
}
Map<String, String> mapping = replaceMap.get(targetClass);
ServletRequestDataBinder kebabCaseDataBinder = new KebabCaseRequestDataBinder(target, binder.getObjectName(), mapping);
requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(kebabCaseDataBinder, nativeWebRequest);
super.bindRequestParameters(kebabCaseDataBinder, nativeWebRequest);
}
private static Map<String, String> analyzeClass(Class<?> targetClass) {
Field[] fields = targetClass.getDeclaredFields();
Map<String, String> renameMap = new HashMap<String, String>();
for (Field field : fields) {
XmlElement xmlElementAnnotation = field.getAnnotation(XmlElement.class);
JsonProperty jsonPropertyAnnotation = field.getAnnotation(JsonProperty.class);
if (xmlElementAnnotation != null && !xmlElementAnnotation.name().isEmpty()) {
renameMap.put(xmlElementAnnotation.name(), field.getName());
} else if (jsonPropertyAnnotation != null && !jsonPropertyAnnotation.value().isEmpty()) {
renameMap.put(jsonPropertyAnnotation.value(), field.getName());
}
}
if (renameMap.isEmpty())
return Collections.emptyMap();
return renameMap;
}
}
This KebabCaseProcessor will use reflection to get a list of mappings for your request object. It will then invoke the KebabCaseDataBinder - passing in the mappings.
#Configuration
public class KebabCaseRequestDataBinder extends ExtendedServletRequestDataBinder {
private final Map<String, String> renameMapping;
public KebabCaseRequestDataBinder(Object target, String objectName, Map<String, String> mapping) {
super(target, objectName);
this.renameMapping = mapping;
}
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
for (Map.Entry<String, String> entry : renameMapping.entrySet()) {
String from = entry.getKey();
String to = entry.getValue();
if (mpvs.contains(from)) {
mpvs.add(to, mpvs.getPropertyValue(from).getValue());
}
}
}
}
All that remains now is to add this behavior to your configuration. The following configuration overrides the default configuration that the #EnableWebMVC delivers and adds this behavior to your request processing.
#Configuration
public static class WebContextConfiguration extends WebMvcConfigurationSupport {
#Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(kebabCaseProcessor());
}
#Bean
protected KebabCaseProcessor kebabCaseProcessor() {
return new KebabCaseProcessor(true);
}
}
Credit should be given to #Jkee. This solution is derivative of an example he posted here: How to customize parameter names when binding spring mvc command objects.
One way I can think of getting around the hyphens is to use HttpServletRequestWrapper class to wrap the original request.
Parse all the request parameters in this class and convert all hyphenated parameters into camelcase. After this, spring will be able to automatically map those parameters to your POJO classes.
public class CustomRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String> camelCasedParams = new Hashmap();
public CustomRequestWrapper(HttpServletRequest req){
//Get all params from request.
//Transform each param name from hyphenated to camel case
//Put them in camelCasedParams;
}
public String getParameter(String name){
return camelCasedParams.get(name);
}
//Similarly, override other methods related to request parameters
}
Inject this request wrapper from J2EE filter. You can refer to below link for a tutorial on injecting request wrappers using filter.
http://www.programcreek.com/java-api-examples/javax.servlet.http.HttpServletRequestWrapper
Update your web xml to include filter and its filter mapping.

How to get resource method matched to URI before Jersey invokes it?

I'm trying to implement a ContainerRequestFilter that does custom validation of a request's parameters. I need to look up the resource method that will be matched to the URI so that I can scrape custom annotations from the method's parameters.
Based on this answer I should be able to inject ExtendedUriInfo and then use it to match the method:
public final class MyRequestFilter implements ContainerRequestFilter {
#Context private ExtendedUriInfo uriInfo;
#Override
public ContainerRequest filter(ContainerRequest containerRequest) {
System.out.println(uriInfo.getMatchedMethod());
return containerRequest;
}
}
But getMatchedMethod apparently returns null, all the way up until the method is actually invoked (at which point it's too late for me to do validation).
How can I retrieve the Method that will be matched to a given URI, before the resource method is invoked?
For those interested, I'm trying to roll my own required parameter validation, as described in JERSEY-351.
Actually, you should try to inject ResourceInfo into your custom request filter. I have tried it with RESTEasy and it works there. The advantage is that you code against the JSR interfaces and not the Jersey implementation.
public class MyFilter implements ContainerRequestFilter
{
#Context
private ResourceInfo resourceInfo;
#Override
public void filter(ContainerRequestContext requestContext)
throws IOException
{
Method theMethod = resourceInfo.getResourceMethod();
return;
}
}
I figured out how to solve my problem using only Jersey. There's apparently no way to match a request's URI to the method that will be matched before that method is invoked, at least in Jersey 1.x. However, I was able to use a ResourceFilterFactory to create a ResourceFilter for each individual resource method - that way these filters can know about the destination method ahead of time.
Here's my solution, including the validation for required query params (uses Guava and JSR 305):
public final class ValidationFilterFactory implements ResourceFilterFactory {
#Override
public List<ResourceFilter> create(AbstractMethod abstractMethod) {
//keep track of required query param names
final ImmutableSet.Builder<String> requiredQueryParamsBuilder =
ImmutableSet.builder();
//get the list of params from the resource method
final ImmutableList<Parameter> params =
Invokable.from(abstractMethod.getMethod()).getParameters();
for (Parameter param : params) {
//if the param isn't marked as #Nullable,
if (!param.isAnnotationPresent(Nullable.class)) {
//try getting the #QueryParam value
#Nullable final QueryParam queryParam =
param.getAnnotation(QueryParam.class);
//if it's present, add its value to the set
if (queryParam != null) {
requiredQueryParamsBuilder.add(queryParam.value());
}
}
}
//return the new validation filter for this resource method
return Collections.<ResourceFilter>singletonList(
new ValidationFilter(requiredQueryParamsBuilder.build())
);
}
private static final class ValidationFilter implements ResourceFilter {
final ImmutableSet<String> requiredQueryParams;
private ValidationFilter(ImmutableSet<String> requiredQueryParams) {
this.requiredQueryParams = requiredQueryParams;
}
#Override
public ContainerRequestFilter getRequestFilter() {
return new ContainerRequestFilter() {
#Override
public ContainerRequest filter(ContainerRequest request) {
final Collection<String> missingRequiredParams =
Sets.difference(
requiredQueryParams,
request.getQueryParameters().keySet()
);
if (!missingRequiredParams.isEmpty()) {
final String message =
"Required query params missing: " +
Joiner.on(", ").join(missingRequiredParams);
final Response response = Response
.status(Status.BAD_REQUEST)
.entity(message)
.build();
throw new WebApplicationException(response);
}
return request;
}
};
}
#Override
public ContainerResponseFilter getResponseFilter() {
return null;
}
}
}
And the ResourceFilterFactory is registered with Jersey as an init param of the servlet in web.xml:
<init-param>
<param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
<param-value>my.package.name.ValidationFilterFactory</param-value>
</init-param>
At startup, ValidationFilterFactory.create gets called for each resource method detected by Jersey.
Credit goes to this post for getting me on the right track: How can I get resource annotations in a Jersey ContainerResponseFilter
I know you're looking for a Jersey only solution but here's a Guice approach that should get things working:
public class Config extends GuiceServletContextListener {
#Override
protected Injector getInjector() {
return Guice.createInjector(
new JerseyServletModule() {
#Override
protected void configureServlets() {
bindInterceptor(Matchers.inSubpackage("org.example"), Matchers.any(), new ValidationInterceptor());
bind(Service.class);
Map<String, String> params = Maps.newHashMap();
params.put(PackagesResourceConfig.PROPERTY_PACKAGES, "org.example");
serve("/*").with(GuiceContainer.class, params);
}
});
}
public static class ValidationInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation method) throws Throwable {
System.out.println("Validating: " + method.getMethod());
return method.proceed();
}
}
}
#Path("/")
public class Service {
#GET
#Path("service")
#Produces({MediaType.TEXT_PLAIN})
public String service(#QueryParam("name") String name) {
return "Service " + name;
}
}
EDIT: A performance comparison:
public class AopPerformanceTest {
#Test
public void testAopPerformance() {
Service service = Guice.createInjector(
new AbstractModule() {
#Override
protected void configure() { bindInterceptor(Matchers.inSubpackage("org.example"), Matchers.any(), new ValidationInterceptor()); }
}).getInstance(Service.class);
System.out.println("Total time with AOP: " + timeService(service) + "ns");
}
#Test
public void testNonAopPerformance() {
System.out.println("Total time without AOP: " + timeService(new Service()) + "ns");
}
public long timeService(Service service) {
long sum = 0L;
long iterations = 1000000L;
for (int i = 0; i < iterations; i++) {
long start = System.nanoTime();
service.service(null);
sum += (System.nanoTime() - start);
}
return sum / iterations;
}
}
In resteasy-jaxrs-3.0.5, you can retrieve a ResourceMethodInvoker representing the matched resource method from ContainerRequestContext.getProperty() inside a ContainerRequestFilter:
import org.jboss.resteasy.core.ResourceMethodInvoker;
public class MyRequestFilter implements ContainerRequestFilter
{
public void filter(ContainerRequestContext request) throws IOException
{
String propName = "org.jboss.resteasy.core.ResourceMethodInvoker";
ResourceMethodInvoker invoker = (ResourceMethodInvoker)request.getProperty();
invoker.getMethod().getParameterTypes()....
}
}

Categories