Does Spring MVC 4 consider Jackson annotation of parameters? - java

There is a class as follow:
class A {
#JsonProperty("first_name")
String firstName;
}
And a MVC controller:
public A createNewA(A a){
...
}
I expect the following REST create an instance of A with first name:
POST: /path/to/resouce
Form data
first_name: Ali
But the first name is null. By the way, the following request works fine:
POST: /path/to/resouce
Form data
firstName: Ali
As I debug, ServletModelAttributeMethodProcessor is used to resolve parameter and where objects are Considered as bean.
Is there any parameter resolver to check jackson annotation?

Jackson is used to parse and serialize JSON. You're sending x-www-form-urlencoded data. So Jackson is irrelevant.
If you send a JSON request body, and thus annotate the a parameter with #RequestBody, then Jackson will be used, and will honor the annotation.

Default resolver of spring-mvc to convert method arguments to bean objects uses exactly name of fields in the bean to search in key-values sent in the request. If you want another behavior you should implement yours resolver. To do that you should implement HandlerMethodArgumentResolver which has two main method.
In your case which you want resolver behaves similar to jackson deserializer you could implement such resolver as follow. This resolver create an map of key-values from parameters of sent request then uses jackson mapper to fill a bean object by given key-values:
public class BeanObjectParameterResolver implements HandlerMethodArgumentResolver {
#Inject
ObjectMapper mapper;
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Map<String, Object> params = new HashMap<>();
Iterator<String> enumName = webRequest.getParameterNames();
while(enumName.hasNext()){
String name = enumName.next();
params.put(name, webRequest.getParameter(name));
}
// jackson 1.9 and before
// mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// or jackson 2.0
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Object obj = mapper.convertValue(params, parameter.getParameterType());
return obj;
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return !BeanUtils.isSimpleProperty(parameter.getParameterType());
}
}

Related

Spring Boot: Optional mapping of request parameters to POJO

I'm trying to map request parameters of a controller method into a POJO object, but only if any of its fields are present. However, I can't seem to find a way to achieve this. I have the following POJO:
public class TimeWindowModel {
#NotNull
public Date from;
#NotNull
public Date to;
}
If none of the fields are specified, I'd like to get an empty Optional, otherwise I'd get an Optional with a validated instance of the POJO. Spring supports mapping request parameter into POJO objects by leaving them unannotated in the handler:
#GetMapping("/shop/{shopId}/slot")
public Slice<Slot> getSlots(#RequestAttribute("staff") Staff staff,
#PathVariable("shopId") Long shopId, #Valid TimeWindowModel timeWindow) {
// controller code
}
With this, Spring will map request parameters "from" and "to" to an instance of TimeWindowModel. However, I want to make this mapping optional. For POST requests you can use #RequestBody #Valid Optional<T>, which will give you an Optional<T> containing an instance of T, but only if a request body was provided, otherwise it will be empty. This makes #Valid work as expected.
When not annotated, Optional<T> doesn't appear to do anything. You always get an Optional<T> with an instance of the POJO. This is problematic when combined with #Valid because it will complain that "from" and "to" are not set.
The goal is to get either (a) an instance of the POJO where both "from" and "to" are not null or (b) nothing at all. If only one of them is specified, then #Valid should fail and report that the other is missing.
I came up with a solution with a custom HandlerMethodArgumentResolver, Jackson and Jackson Databind.
The annotation:
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface RequestParamBind {
}
The resolver:
public class RequestParamBindResolver implements HandlerMethodArgumentResolver {
private final ObjectMapper mapper;
public RequestParamBindResolver(ObjectMapper mapper) {
this.mapper = mapper.copy();
this.mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(RequestParamBind.class) != null;
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mav, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// take the first instance of each request parameter
Map<String, String> requestParameters = webRequest.getParameterMap()
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()[0]));
// perform the actual resolution
Object resolved = doResolveArgument(parameter, requestParameters);
// *sigh*
// see: https://stackoverflow.com/questions/18091936/spring-mvc-valid-validation-with-custom-handlermethodargumentresolver
if (parameter.hasParameterAnnotation(Valid.class)) {
String parameterName = Conventions.getVariableNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, resolved, parameterName);
// DataBinder constructor unwraps Optional, so the target could be null
if (binder.getTarget() != null) {
binder.validate();
BindingResult bindingResult = binder.getBindingResult();
if (bindingResult.getErrorCount() > 0)
throw new MethodArgumentNotValidException(parameter, bindingResult);
}
}
return resolved;
}
private Object doResolveArgument(MethodParameter parameter, Map<String, String> requestParameters) {
Class<?> clazz = parameter.getParameterType();
if (clazz != Optional.class)
return mapper.convertValue(requestParameters, clazz);
// special case for Optional<T>
Type type = parameter.getGenericParameterType();
Class<?> optionalType = (Class<?>)((ParameterizedType)type).getActualTypeArguments()[0];
Object obj = mapper.convertValue(requestParameters, optionalType);
// convert back to a map to find if any fields were set
// TODO: how can we tell null from not set?
if (mapper.convertValue(obj, new TypeReference<Map<String, String>>() {})
.values().stream().anyMatch(Objects::nonNull))
return Optional.of(obj);
return Optional.empty();
}
}
Then, we register it:
#Configuration
public class WebConfig implements WebMvcConfigurer {
//...
#Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new RequestParamBindResolver(new ObjectMapper()));
}
}
Finally, we can use it like so:
#GetMapping("/shop/{shopId}/slot")
public Slice<Slot> getSlots(#RequestAttribute("staff") Staff staff,
#PathVariable("shopId") Long shopId,
#RequestParamBind #Valid Optional<TimeWindowModel> timeWindow) {
// controller code
}
Which works exactly as you'd expect.
I'm sure it's possible to accomplish this by using Spring's own DataBind classes in the resolver. However, Jackson Databind seemed like the most straight-forward solution. That said, it's not able to distinguish between fields that are set to null and fields that just not set. This is not really an issue for my use-case, but it's something that should be noted.
To implement logic (a) both not null or (b) both are nulls you need to implement custom validation.
Examples are here:
https://blog.clairvoyantsoft.com/spring-boot-creating-a-custom-annotation-for-validation-edafbf9a97a4
https://www.baeldung.com/spring-mvc-custom-validator
Generally, you create a new annotation, it's just a stub, and then you create a validator which implements ConstraintValidator where you provide your logic and then you put your new annotation to your POJO.

ObjectMapper in spring boot can read string, but doesn't work when parsing headers

I am trying to give an enum value as a header parameter to my rest endpoint in a spring boot #RestController. To that end I put the jackson libraries in my build.gradle file since the autogenerated enum used jackson annotations. I cannot change the enum code (it is autogenerated from a openapi specification). It looks like this:
public enum DocumentTypes {
APPLICATION_PDF("application/pdf"),
APPLICATION_RTF("application/rtf"),
APPLICATION_VND_OASIS_OPENDOCUMENT_TEXT("application/vnd.oasis.opendocument.text"),
APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_WORDPROCESSINGML_DOCUMENT("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
APPLICATION_VND_MS_WORD("application/vnd.ms-word"),
TEXT_HTML("text/html"),
TEXT_PLAIN("text/plain");
private String value;
DocumentTypes(String value) {
this.value = value;
}
#Override
#JsonValue
public String toString() {
return String.valueOf(value);
}
#JsonCreator
public static DocumentTypes fromValue(String text) {
for (DocumentTypes b : DocumentTypes.values()) {
if (String.valueOf(b.value).equals(text)) {
return b;
}
}
throw new IllegalArgumentException("Unexpected value '" + text + "'");
}
}
The restcontroller I am using to test looks like this:
#RestController
#RequestMapping("/test")
public class TestController {
#Autowired
private ObjectMapper objectMapper;
#RequestMapping(path = "", method = RequestMethod.GET)
public void test(#RequestHeader(value = "Accept", required = false) DocumentTypes targetFormat) throws IOException {
DocumentTypes value = objectMapper.readValue("\"application/pdf\"", DocumentTypes.class);
}
}
If I don't supply the Accept header and just let break inside the code I can see that the first line of the code works fine, the application/pdf String is transformed into value so the ObjectMapper did its job using the #JsonCreator method.
However if I pass Accept=application/pdf header along with the request I get an error:
Failed to convert value of type 'java.lang.String' to required type 'de.some.namespace.model.DocumentTypes';
nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [#org.springframework.web.bind.annotation.RequestHeader de.some.namespace.model.DocumentTypes] for value 'application/pdf';
nested exception is java.lang.IllegalArgumentException: No enum constant de.some.namespace.model.DocumentTypes.application/pdf"
This looks to me as if spring is not using the Jackson provided ObjectMapper, thus ignoring the #JsonCreator method and just trying to resolve the enum by default by looking if there is a key with that provided name.
This to me does not make sense, becuase I also only #Autowire the ObjectMapper,... isn't that the one that spring should also use, how can I force spring to use the correct one for parsing the arguments? I tried putting it into a #Configuration and making it a #Bean and #Primary with the same results.
I have a workaround by implementing a converter:
#Component
public class StringToDocumentTypesConverter implements Converter<String, DocumentTypes> {
#Autowired
private ObjectMapper mapper;
#Override
public DocumentTypes convert(String s) {
try {
return mapper.readValue(String.format("\"%s\"", s), DocumentTypes.class);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
But I don't understand why this would be necessary, normally spring automatically puts arguments through the ObjectMapper.
I think this is working as designed. Spring only uses the Jackson ObjectMapper for conversion of message bodies (using a registered HttpMessageConverter, specifically the MappingJackson2HttpMessageConverter).
This is documented at https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-typeconversion:
Some annotated controller method arguments that represent String-based request input (such as #RequestParam, #RequestHeader, #PathVariable, #MatrixVariable, and #CookieValue) can require type conversion if the argument is declared as something other than String.
For such cases, type conversion is automatically applied based on the configured converters
And https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-requestbody:
You can use the #RequestBody annotation to have the request body read and deserialized into an Object through an HttpMessageConverter

How to get the payload of REST api using ContainerRequestContext in filter method

I've a filter through which POST REST api goes with and i want to extract the below part of my payload in the filter.
{
"foo": "bar",
"hello": "world"
}
Filter code :-
public class PostContextFilter implements ContainerRequestFilter {
#Override
public void filter(ContainerRequestContext requestContext)
throws IOException {
String transactionId = requestContext.getHeaderString("id");
// Here how to get the key value corresponding to the foo.
String fooKeyVal = requestContext. ??
}
}
I don't see any easy method to get the payload to the api using the ContainerRequestContext object.
So my question is how do i get the key value corresponding to the foo key in my payload.
Whereas filters are primarily intended to manipulate request and response parameters like HTTP headers, URIs and/or HTTP methods, interceptors are intended to manipulate entities, via manipulating entity input/output streams.
A ReaderInterceptor allows you to manipulate inbound entity streams, that is, the streams coming from the "wire". Using Jackson to parse the inbound entity stream, your interceptor could be like:
#Provider
public class CustomReaderInterceptor implements ReaderInterceptor {
// Create a Jackson ObjectMapper instance (it can be injected instead)
private ObjectMapper mapper = new ObjectMapper();
#Override
public Object aroundReadFrom(ReaderInterceptorContext context)
throws IOException, WebApplicationException {
// Parse the request entity into the Jackson tree model
JsonNode tree = mapper.readTree(context.getInputStream());
// Extract the values you need from the tree
// Proceed to the next interceptor in the chain
return context.proceed();
}
}
This answer and this answer may also be related to your question.

Grab a specific property from JSON payload as MVC method argument

I have asked a similar question before: this one
Now I have a similar but different issue.
My Spring MVC controller model is a JSON payload with a defined set of attributes that, unfortunately, are not part of a class in my project.
E.g.
{
"userId" : "john",
"role" : "admin"
}
I need to treat userId and role as separate Strings.
I currently have two ways to declare the controller method
public ResponseObject mvc(#RequestBody MyCustomDTO dto){
String userId = dto.getUserId();
String role = dto.getRole();
}
public ResponseObject mvc(#RequestBody ModelMap map){
String userId = (String)map.get("userId");
String role = (String)map.get("role");
}
I have been asked to find a different implementation because 1) requires to create a custom DTO class for each combination of parameters (most cases need 1 named parameter, e.g. delete(productId)) and 2) involves an entity that is not strictly defined. Especially when dealing with lists, it can contain arbitrary values that need to be checked at runtime.
Spring MVC, as I have found, does not support resolving #ModelAttribute from a JSON request body. Am I doing something wrong or is it just Spring not doing it? Can I grab a specific property, be it a plain primitive or an entire POJO, from the Request Body into a method argument?
In the second case it would be better to request a useful feature to Spring developers.
Spring version is 4.2.x.
This question is related with the previously linked but differs in the fact that now I will be encapsulating the single property into a Javascript object, so the object that Jackson needs to deserialize won't be a primitive but a POJO.
You won't be able to get individual members as easily, simply because Spring MVC doesn't have any builtin tools to do that. One option is to write your own annotation that describes a parameter at the root of an excepted JSON object body. Then write and register a new HandlerMethodArgumentResolver implementation which processes that annotation on a handler method parameter.
This is not a simple task. Since you can't consume the request content multiple times, you have to save it somehow, in a Filter, for example. For now, let's ignore this restriction and assume we only wanted one parameter. You'd define an annotation
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
#interface JsonObjectProperty {
String name();
}
And the HandlerMethodArgumentResolver
class JsonObjectPropertyResolver implements HandlerMethodArgumentResolver {
/**
* Configured as appropriate for the JSON you expect.
*/
private final ObjectMapper objectMapper = new ObjectMapper();
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(JsonObjectProperty.class);
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Class<?> parameterType = parameter.getParameterType();
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
MediaType contentType = inputMessage.getHeaders().getContentType();
if (!contentType.equals(MediaType.APPLICATION_JSON_UTF8)) {
throw new HttpMessageNotReadableException(
"Could not read document. Expected Content-Type " + MediaType.APPLICATION_JSON_UTF8 + ", was " + contentType + ".");
}
// handle potential exceptions from this as well
ObjectNode rootObject = objectMapper.readValue(inputMessage.getBody(), ObjectNode.class);
if (parameterType == String.class) {
JsonObjectProperty annotation = parameter.getParameterAnnotation(JsonObjectProperty.class);
return rootObject.get(annotation.name()).asText();
}
// handle more
throw new HttpMessageNotReadableException("Could not read document. Parameter type " + parameterType + " not parseable.");
}
}
and finally the handler method
#RequestMapping(value = "/json-new", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public String handleJsonProperty(#JsonObjectProperty(name = "userId") String userId) {
String result = userId;
System.out.println(result);
return result;
}
You'll have to register the JsonObjectPropertyResolver appropriately. Once you do, it will be able to extract that JSON property directly into the parameter.
You can use some JSON inline parsers (similar to XML Xpath) where you can provide your JSON string and ask your parser to retrieve some subdocument as String, List or Map. One of the examples is OGNL. It's quite powerful tool, although it is not the only one and not the most performance efficient, but still mature and stable Apache product. So, in your case you would be able feed your JSON string to OGNL and tell it to retrieve properties "userId" and "role" as separate strings. See the OGNL documentation at Apache OGNL page

Spring MVC ModelAttribute as Interface

Using a Spring MVC #Controller, how do I have a #RequestMapping endpoint have a #ModelAttribute declared as an interface?
I want to have three different forms being posted to the mapping that the underlying classes are all of an interface type.
So for example, I can have three different form objects with the action to the following:
#RequestMapping(path="/doSomething", method=RequestMethod.POST)
public String doSomething(ObjectInterface formInfo) {
...
}
(Where ObjectInterface is an interface, not a concrete class.)
It can be done using request level model attributes as follows:
Suppose There is ObjectInterace is an interface with three implementation classes as ObjectA, ObjectB and ObjectC. then Controller method declaration is:
#RequestMapping(path="/doSomething", method=RequestMethod.POST)
public String doSomething(#ModelAttribute("object") ObjectInterface formInfo) {
...
}
Add Method to populate modelattribute in the controller class:
#ModelAttribute("object")
public ObjectInterface getModelObject(HttpServletRequest request) {
ObjectInterface object = null;
String type = request.getParameter("type");
if (StringUtils.equals("A", type)) {
object= new objectA();
} else if (StringUtils.equals("B", type)){
object= new objectB();
}else if (StringUtils.equals("C", type)){
object= new objectC();
}else{
//object=any default object.
}
return object ;
}
the value returned by getModelObject is added to the Model and it will be populated with the values from the view to the controller method.
Before invoking the handler method, Spring invokes all the methods that have #ModelAttribute annotation. It adds the data returned by these methods to a temporary Map object. The data from this Map would be added to the final Model after the execution of the handler method.
Figured it out. It is to write and register a custom HandlerMethodArgumentResolver. Below is the core code. You just need to figure out which concrete bean class to pass into the webDataBinderFactory. Your controller can then be written to accept an interface and you will be provided the concrete implementing bean behind the interface.
public class MessageResolverTest implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().equals(<Interface>.class);
}
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
String name = ModelFactory.getNameForParameter(methodParameter);
WebDataBinder webDataBinder = webDataBinderFactory.createBinder(nativeWebRequest, new <ConcreteBean>(), name);
MutablePropertyValues mutablePropertyValues = new MutablePropertyValues(nativeWebRequest.getParameterMap());
webDataBinder.bind(mutablePropertyValues);
return webDataBinder.getBindingResult().getTarget();
}
}

Categories

Resources