Spring MVC Binding: How to bind ArrayList<...>? - spring

I've got a DTO (bean) with ArrayList field:
public MyDTO {
...
private List<MyThing> things;
...
... getters, setters and so on
}
In my initBinder I have:
#InitBinder
public void initBinder(WebDataBinder binder) {
...
binder.registerCustomEditor(List.class, "things", new PropertyEditorSupport() {
#Override
public void setAsText(String text) throws IllegalArgumentException {
List<MyThing> things = new ArrayList<MyThings>;
// fill things array with data from text
...
// On that stage things value is correct!
super.setValue(things);
}
});
}
And in my controller request method:
#RequestMapping({"save"})
public ModelAndView doSaveMyDTO(#ModelAttribute MyDTO myDTO) {
// very strange myDTO comes here=(
}
The problem is that while I'm in registerCustomEditor staff the things array is ok.
But when I get to the doSaveMyDTO method - MyDTO.things looks like Array of one element arrays of actual values:
Expected (things in initBinder):
[value1, value2, value3]
Get in doSaveMyDTO (myDTO.getThings()):
[[value1], [value2], [value3]]
Why? Please explain...

If the request is correctly formed (things=v1&things=v2&things=v3 or things=v1,v2,v3), spring's built-in converters should properly convert it to a List - no need to register your own.
If your input is JSON, then you'd need #RequestBody instead of #ModelAttribute

Related

FormUrlEncoded POST request, I need to convert snake case values into camelCase with SpringBoot and Jackson

I am integrating with a third-party's vendor API.
I have a SpringBoot and Jackson setup
They are sending me a POST request that is of type formUrlEncoded and with the params in snake_case
(over 10 params in total and no body)
e.g.
POST www.example.com?player_id=somePlayerId&product_id=someProductId&total_amount=totalAmount...
There are many out of the box helpers for JSON but I cannot find any for formUrlEncoded (I hope I am missing something obvious).
I have tried #ModelAttribute and #RequestParam but had no luck.
I am trying to avoid the #RequestParam MultiValueMap<String, String> params + custom mapper option
#RequestParam is the simplest way which allows you to define the exact name of the query parameter something like:
#PostMapping
public String foo(#RequestParam("player_id") String playerId){
}
If you want to bind all the query parameters to an object , you have to use #ModelAttribute. It is based on the DataBinder and is nothing to do with Jackson. By default it only supports binding the query parameter to an object which fields have the same name as the query parameter. So you can consider to bind the query paramater to the following object :
public class Request {
private String player_id;
private String product_id;
private Long total_amount;
}
If you really want to bind to the object that follow traditional java naming convention (i.e lower camel case) from the query parameter that has snake case values , you have to cusomtize WebDataBinder.
The idea is to override its addBindValues() and check if the query parameter name is in snake case format , convert it the lower camel case format and also add it as the bind values for the request. Something like :
public class MyServletRequestDataBinder extends ExtendedServletRequestDataBinder {
private static Converter<String, String> snakeCaseToLowerCamelConverter = CaseFormat.LOWER_UNDERSCORE
.converterTo(CaseFormat.LOWER_CAMEL);
public MyServletRequestDataBinder(Object target) {
super(target);
}
public MyServletRequestDataBinder(Object target, String objectName) {
super(target, objectName);
}
#Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
Enumeration<String> paramNames = request.getParameterNames();
while (paramNames != null && paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
if(paramName.contains("_")) {
String[] values = request.getParameterValues(paramName);
if (values == null || values.length == 0) {
// Do nothing, no values found at all.
} else if (values.length > 1) {
mpvs.addPropertyValue(snakeCaseToLowerCamelConverter.convert(paramName), values);
} else {
mpvs.addPropertyValue(snakeCaseToLowerCamelConverter.convert(paramName), values[0]);
}
}
}
}
}
P.S I am using Guava for helping me to convert snake case to lowerCamelCase.
But in order to use the customized WebDataBinder , you have to in turn customize WebDataBinderFactory and RequestMappingHandlerAdapter because :
customize WebDataBinderFactory in order to create the customised WebDataBinder
customize RequestMappingHandlerAdapter in order to create the WebDataBinderFactory
Something like:
public class MyServletRequestDataBinderFactory extends ServletRequestDataBinderFactory {
public MyServletRequestDataBinderFactory(List<InvocableHandlerMethod> binderMethods,
WebBindingInitializer initializer) {
super(binderMethods, initializer);
}
#Override
protected ServletRequestDataBinder createBinderInstance(Object target, String objectName,
NativeWebRequest request) throws Exception {
return new MyServletRequestDataBinder(target, objectName);
}
}
and
public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
#Override
protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
throws Exception {
return new MyServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}
}
And finally register to use the customised RequestMappingHandlerAdapter in your configuration :
#Configuration
public class Config extends DelegatingWebMvcConfiguration {
#Override
protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
return new MyRequestMappingHandlerAdapter();
}
}
I don't think you are missing anything. Looking at the RequestParamMethodArgumentResolver#resolveName source I do no see a way to customize how a request parameter is matched. So it looks either you have to implement your own resolver or just annotate each parameter with #RequestParam and provide the name, e.g. #RequestParam("product_id") String productId
EDIT:
As for ModelAttribute, ModelAttributeMethodProcessor uses WebDataBinder. Again you can customize it with your custom DataBinder but I didn't found any that out of the box supports aliases as Jackson does.

How to write a custom convertor for recieving data annotated with #RequestBody

I'm trying to write a custom convertor for receiving data POSTed to a REST application. The object I want to populate already has its own builder that accepts a string JSON so I have to use that instead of the Jackson deserializer Spring would normally use.
I've tried a number of different things but I keep getting the following exception:
org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class xxx.yyy.zzz.MyType]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `xxx.yyy.zzz.MyType` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (PushbackInputStream); line: 1, column: 1]
at at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:281) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:250) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
My convertor looks like:
public class MyConverter extends AbstractHttpMessageConverter<MyType> {
public MyConverter() {
super(/*MediaType.TEXT_PLAIN*/ MediaType.APPLICATION_JSON);
}
#Override
protected boolean supports(Class<?> type) {
return MyType.class.isAssignableFrom(type);
}
#Override
protected MyType readInternal(Class<? extends MyType> type, HttpInputMessage inputMessage) throws IOException {
String str = ..... read data from inputMessage
return MyType.build(str);
}
#Override
protected void writeInternal(MyType s, HttpOutputMessage outputMessage) {
}
}
and the controller is:
#RequestMapping(method = RequestMethod.POST)
public void add(#RequestBody MyType data) {
System.out.println("add:" + data.toString());
}
Even if change the MediaType in the constructor for MyConverter to 'MediaType.ALL' it will still fail. Curiously if I change it to TEXT_PLAIN and POST with Content-Type set to 'text/plain' it works!
Answering my own question. The problem turned out to be the order the HttpMessageConverter objects are processed. The inbuilt converters were being processed, and failing, before my converter had chance.
This isn't the best code, but it works:
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void extendMessageConverters(#Nonnull List<HttpMessageConverter<?>> converters) {
List<HttpMessageConverter<?>> temp = new ArrayList<>();
temp.add(new MyConverter());
temp.addAll(converters);
converters.clear();
converters.addAll(temp);
}
}
I can't quite believe this is the best answer for what, I think, should be a fairly standard problem. If somebody can suggest a better answer I'll happily accept that instead

Spring MVC data binding diff in javabean and #RequestParam boolean request parameter

Spring MVC support request parameter to javabean. But when I want to bind boolean,
It is still not work in javabean.
If the code in controller is:
public void test(#RequestParam(value="isCheck") boolean isCheck)
I can get the isCheck boolean value.
But when the code in contoller is:
public void test(TestVO testVO)
TestVO javabean:
public class TestVO {
private boolean isPrecheck;
public boolean isPrecheck() {
return isPrecheck;
}
public void setPrecheck(boolean isPrecheck) {
this.isPrecheck = isPrecheck;
}
}
I can not set the request parameter to this Javabean.
Anyone has ideas?
You can you the PropertiesEditor of the Spring.
http://static.springsource.org/spring/docs/3.2.x/javadoc-api/org/springframework/beans/propertyeditors/CustomBooleanEditor.html
Inside your controller create a method to set the editor
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Boolean.class, new CustomBooleanEditor(allowEmpty???));
}
This is due to boolean naming convention and associated ambiguity with its getter methods.
Rename your boolean from isPrecheck to precheck and it will work.

Adding multiple validators using initBinder

I'm adding a user validator using the initBinder method:
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new UserValidator());
}
Here is the UserValidator
public class UserValidator implements Validator {
public boolean supports(Class clazz) {
return User.class.equals(clazz);
}
public void validate(Object target, Errors errors) {
User u = (User) target;
// more code here
}
}
The validate method is getting properly called during the controller method call.
#RequestMapping(value = "/makePayment", method = RequestMethod.POST)
public String saveUserInformation(#Valid User user, BindingResult result, Model model){
// saving User here
// Preparing CustomerPayment object for the payment page.
CustomerPayment customerPayment = new CustomerPayment();
customerPayment.setPackageTb(packageTb);
model.addAttribute(customerPayment);
logger.debug("Redirecting to Payment page.");
return "registration/payment";
}
But while returning to the payment screen I'm getting this error:
java.lang.IllegalStateException: Invalid target for Validator [com.validator.UserValidator#710db357]: com.domain.CustomerPayment[ customerPaymentId=null ]
org.springframework.validation.DataBinder.setValidator(DataBinder.java:476)
com.web.UserRegistrationController.initBinder(UserRegistrationController.java:43)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
java.lang.reflect.Method.invoke(Method.java:597)
org.springframework.web.bind.annotation.support.HandlerMethodInvoker.initBinder(HandlerMethodInvoker.java:393)
org.springframework.web.bind.annotation.support.HandlerMethodInvoker.updateModelAttributes(HandlerMethodInvoker.java:222)
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.invokeHandlerMethod(AnnotationMethodHandlerAdapter.java:429)
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(AnnotationMethodHandlerAdapter.java:414)
This might be because I'm returning a CustomerPayment and there is not validator defined for that.
I'm also not able to add multiple validators in initBinder method.
How can I fix this?
You need to set the value of the #InitBinder annotation to the name of the command you want it to validate. This tells Spring what to apply the binder to; without it, Spring will try to apply it to everything. This is why you're seeing that exception: Spring is trying to apply the binder - with your UserValidator - to a parameter of type CustomerPayment.
In your specific case, it looks like you need something like:
#InitBinder("user")
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new UserValidator());
}
To your second question, as Rigg802 explained, Spring does not support attaching multiple validators to a single command. You can, however, define multiple #InitBinder methods for different commands. So, for example, you could put the following in a single controller and validate your user and payment parameters:
#InitBinder("user")
protected void initUserBinder(WebDataBinder binder) {
binder.setValidator(new UserValidator());
}
#InitBinder("customerPayment")
protected void initPaymentBinder(WebDataBinder binder) {
binder.setValidator(new CustomerPaymentValidator());
}
It's a bit tricky to do, 1 controller has only 1 validator on 1 command object.
you need to create a "Composite Validator" that will get all the validators and run them seperately.
Here is a tutorial that explains how to do it: using multiple validators
You can add multiple validators by iterating over all org.springframework.validation.Validator in an ApplicationContext and set up suitable ones in #InitBinder for each request.
#InitBinder
public void setUpValidators(WebDataBinder webDataBinder) {
for (Validator validator : validators) {
if (validator.supports(webDataBinder.getTarget().getClass())
&& !validator.getClass().getName().contains("org.springframework"))
webDataBinder.addValidators(validator);
}
}
See my project for examples and simple benchmarks. https://github.com/LyashenkoGS/spring-mvc-and-jms-validation-POC/tree/benchamark
I do not see a reason why Spring does not filter out all validators which are not applicable to the current entity by default which forces to use things like CompoundValidator described by #Rigg802.
InitBinder allows you to specify name only which give you some control but not full control over how and when to apply your custom validator. Which from my perspective is not enough.
Another thing you can do is to perform check yourself and add validator to binder only if it is actually necessary, since binder itself has binding context information.
For example if you want to add a new validator which will work with your User object in addition to built-in validators you can write something like this:
#InitBinder
protected void initBinder(WebDataBinder binder) {
Optional.ofNullable(binder.getTarget())
.filter((notNullBinder) -> User.class.equals(notNullBinder.getClass()))
.ifPresent(o -> binder.addValidators(new UserValidator()));
}
There is a simple hack, always return true in supports method, and delegate the class checking to validate. Then basically you can add multiple validator in the initBinder without issue.
#Component
public class MerchantRegisterValidator implements Validator {
#Autowired
private MerchantUserService merchantUserService;
#Autowired
private MerchantCompanyService merchantCompanyService;
#Override
public boolean supports(Class<?> clazz) {
return true; // always true
}
#Override
public void validate(Object target, Errors errors) {
if (!RegisterForm.getClass().equals(target.getClass()))
return; // do checking here.
RegisterForm registerForm = (RegisterForm) target;
MerchantUser merchantUser = merchantUserService.getUserByEmail(registerForm.getUserEmail());
if (merchantUser != null) {
errors.reject("xxx");
}
MerchantCompany merchantCompany = merchantCompanyService.getByRegno(registerForm.getRegno());
if (merchantCompany != null) {
errors.reject("xxx");
}
}
}
Multiple validator on one command is supported with Spring MVC 4.x now. You could use this snippet code:
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new UserValidator(), new CustomerPaymentValidator());
}
The safest way is to add a generic validator handling that Controller:
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.setValidator(new GenericControllerOneValidator());
}
Then, in the generic validator you can support multiple request body models and based of the instance of the object, you can invoke the appropriate validator:
public class GenericValidator implements Validator {
#Override
public boolean supports(Class<?> aClass) {
return ModelRequestOne.class.equals(aClass)
|| ModelRequestTwo.class.equals(aClass);
}
#Override
public void validate(Object body, Errors errors) {
if (body instanceof ModelRequestOne) {
ValidationUtils.invokeValidator(new ModelRequestOneValidator(), body, errors);
}
if (body instanceof ModelRequestTwo) {
ValidationUtils.invokeValidator(new ModelRequestTwoValidator(), body, errors);
}
}
}
Then you add your custom validations inside for each model validator implementatios. ModeRequestOneValidator and ModeRequestTwoValidator still need to implement the Validator interface of org.springframework.validation
Also, do not forget to use #Valid ModeRequestOne and #Valid ModeRequestTwo inside the controllers method call.
One addition to Annabelle's answer:
If controller has this method parameter and you want to validate that one specifically
#RequestMapping(value = "/users", method = RequestMethod.POST)
public String findUsers(UserRequest request){..}
Then the binding should be lower case of the class name (but just the first letter, and not everything else)
#InitBinder("userRequest")
protected void initUserBinder(WebDataBinder binder) {
binder.setValidator(new YourValidator());
}
Declare request as
(... , Model model,HttpServletRequest request)
and change
model.addAttribute(customerPayment);
to
request.setAttribute("customerPayment",customerPayment);

How do you handle deserializing empty string into an Enum?

I am trying to submit a form from Ext JS 4 to a Spring 3 Controller using JSON. I am using Jackson 1.9.8 for the serialization/deserialization using Spring's built-in Jackson JSON support.
I have a status field that is initially null in the Domain object for a new record. When the form is submitted it generates the following json (scaled down to a few fields)
{"id":0,"name":"someName","status":""}
After submitted the following is seen in the server log
"nested exception is org.codehaus.jackson.map.JsonMappingException: Can not construct instance of com.blah.domain.StatusEnum from String value '': value not one of the declared Enum instance names"
So it appears that Jackson is expecting a valid Enum value or no value at all including an empty string. How do I fix this whether it is in Ext JS, Jackson or Spring?
I tried to create my own ObjectMapper such as
public class MyObjectMapper extends Object Mapper {
public MyObjectMapper() {
configure(DeserializationConfig.Feature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
}
}
and send this as a property to MappingJacksonMappingView but this didn't work. I also tried sending it in to MappingJacksonHttpMessageConverter but that didn't work. Side question: Which one should I be sending in my own ObjectMapper?
Suggestions?
The other thing you could do is create a specialized deserializer (extends org.codehaus.jackson.map.JsonDeserializer) for your particular enum, that has default values for things that don't match. What I've done is to create an abstract deserializer for enums that takes the class it deserializes, and it speeds this process along when I run into the issue.
public abstract class EnumDeserializer<T extends Enum<T>> extends JsonDeserializer<T> {
private Class<T> enumClass;
public EnumDeserializer(final Class<T> iEnumClass) {
super();
enumClass = iEnumClass;
}
#Override
public T deserialize(final JsonParser jp,
final DeserializationContext ctxt) throws IOException, JsonProcessingException {
final String value = jp.getText();
for (final T enumValue : enumClass.getEnumConstants()) {
if (enumValue.name().equals(value)) {
return enumValue;
}
}
return null;
}
}
That's the generic class, basically just takes an enum class, iterates over the values of the enum and checks the next token to match any name. If they do it returns it otherwise return null;
Then If you have an enum MyEnum you'd make a subclass of EnumDeserializer like this:
public class MyEnumDeserializer extends EnumDeserializer<MyEnum> {
public MyEnumDeserializer() {
super(MyEnum.class);
}
}
Then wherever you declare MyEnum:
#JsonDeserialize(using = MyEnumDeserializer.class)
public enum MyEnum {
...
}
I'm not familiar with Spring, but just in case, it may be easier to handle that on the client side:
Ext.define('My.form.Field', {
extend: 'Ext.form.field.Text',
getSubmitValue: function() {
var me = this,
value;
value = me.getRawValue();
if ( value === '' ) {
return ...;
}
}
});
You can also disallow submitting empty fields by setting their allowBlank property to false.
Ended up adding defaults in the EXT JS Model so there is always a value. Was hoping that I didn't have to this but it's not that big of a deal.
I have the same issue. I am reading a JSON stream with some empty strings. I am not in control of the JSON stream, because it is from a foreign service. And I am always getting the same error message. I tried this here:
mapper.getDeserializationConfig().with(DeserializationConfig.Feature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
But without any effect. Looks like a Bug.

Resources