Spring 3 Custom Editor field replacement - spring

Having my ValueObject
UserVO {
long id;
String username;
}
I created custom editor for parsing this object from string id#username
public class UserVOEditor extends PropertyEditorSupport {
#Override
public void setAsText(String text) throws IllegalArgumentException {
Preconditions.checkArgument(text != null,"Null argument supplied when parsing UserVO");
String[] txtArray = text.split("\\#");
Preconditions.checkArgument(txtArray.length == 2, "Error parsing UserVO. Expected: id#username");
long parsedId = Long.valueOf(txtArray[0]);
String username = txtArray[1];
UserVO uvo = new UserVO();
uvo.setUsername(username);
uvo.setId(parsedId);
this.setValue(uvo);
}
#Override
public String getAsText() {
UserVO uvo = (UserVO) getValue();
return uvo.getId()+'#'+uvo.getUsername();
}
in my controller i register
#InitBinder
public void initBinder(ServletRequestDataBinder binder) {
binder.registerCustomEditor(UserVO.class, new UserVOEditor());
}
having in my model object ModelVO
ModelVO {
Set<UserVO> users = new HashSet<UserVO>();
}
after custom editor is invoked all you can see after form submission is
ModelVO {
Set<String> users (linkedHashSet)
}
so when trying to iterate
for(UserVO uvo : myModel.getUser()){ .. }
Im having classCastException .. cannot cast 1234#username (String) to UserVO ..
HOW THIS MAGIC IS POSSIBLE ?

It is not magic, it is because of Generics will be only proved at compile time. So you can put every thing in a Set at runtime, no one will check if you put the correct type in the Set.
What you can try, to make spring a bit more clever, is to put the ModelVO in your command object.
<form:form action="whatEver" method="GET" modelAttribute="modelVO">
#RequestMapping(method = RequestMethod.GET)
public ModelAndView whatEver(#Valid ModelVO modelVO){
...
}

Related

Custom Object as a #RequestParam

I have a paginated endpoint that looks like this /api/clients?range=0-25.
I'd like the getClients() method in my ClientController to directly receive an instance of a custom Range object rather than having to validate a "0-25" String but I'm having trouble figuring this out.
#Getter
final class Range {
#Min(0)
private Integer offset = 0;
#Min(1)
private Integer limit = 25;
}
#ResponseBody
#GetMapping(params = { "range" })
public ResponseEntity<?> getAllClients(#RequestParam(value = "range", required = false) QueryRange queryRange, final HttpServletResponse response) {
...
}
I'm not sure how to instruct the Controller to correctly deserialize the "0-25" string into the Range...
You can use a Converter<S, T>, as shown below:
#Component
public class RangeConverter implements Converter<String, Range> {
#Override
public Range convert(String source) {
String[] values = source.split("-");
return new Range(Integer.valueOf(values[0]), Integer.valueOf(values[1]));
}
}
You also could handle invalid values according to your needs. If you use the above converter as is, the attempt to convert an invalid value such as 1-x will result in a ConversionFailedException.
You can also do the following it seems :
public class QueryRangeEditor extends PropertyEditorSupport {
private static final Pattern PATTERN = Pattern.compile("^([1-9]\\d*|0)-([1-9]\\d*|0)$");
#Override
public void setAsText(String text) throws IllegalArgumentException {
final QueryRange range = new QueryRange();
Matcher matcher = PATTERN.matcher(text);
if (matcher.find()) {
range.setOffset(Integer.valueOf(matcher.group(1)));
range.setLimit(Integer.valueOf(matcher.group(2)));
} else {
throw new IllegalArgumentException("OI"); // todo - replace
}
setValue(range);
}
}
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(QueryRange.class, new QueryRangeEditor());
}
But #cassiomolin's looks cleaner...

How to force Jackson deserialize field values to lower case

I have spring application which expose REST endpoint, lets name it "doAction". As the request it consumes object:
class Person{
private String name;
private String email;
}
Some clients can call this endpoint by passing data with different practice of writing words, like:
Peter_1
name = Peter
email = peter#gmail.com (lower case)
Mark_2
name = mark
email = MARK#gmail.com (upper case)
Julia_3
name = julia
email = JuliaToward#gmail.com (camel case)
Is there some approach to force all income data be parsed to lowercase(lets assume all fields are Strings)?
So as a result I desire to have:
Peter_1
name = peter
email = peter#gmail.com
Mark_2
name = mark
email = mark#gmail.com
Julia_3
name = julia
email = juliatoward#gmail.com
Solution for Jackson is appreciated.
Short answer Call toLower in the setter
Here is an example:
class Animal
{
private String name;
public void setName(final String newValue)
{
StringUtils.trimToNull(StringUtils.lowerCase(newValue));
}
}
I also recommend either trimToNUll or trimToEmpty.
If you are using Spring Data Rest with spring mvc and you want all incoming string data to be in lower case then define following
public class StringSerializer extends StdDeserializer<String>{
public StringSerializer() {
this(null);
}
public StringSerializer(Class<String> vc) {
super(vc);
}
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonToken t = p.getCurrentToken();
if (t==JsonToken.VALUE_STRING){
String receivedValue = p.getText();
if (receivedValue == null)
return null;
else
return receivedValue.toLowerCase();
}else{
return null;
}
}
}
And following:
#Configuration
public class RestDataConfig extends RepositoryRestMvcConfiguration {
#Override
#Bean
public ObjectMapper halObjectMapper() {
ObjectMapper mapper = super.halObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(String.class, new StringSerializer());
mapper.registerModule(module);
return mapper;
}
}

Spring Boot String Deserializer for every form field

In our web app create and update forms have a size validation. For instance:
#Size(min = 4, max = 20)
private String mobile;
As seen the field is not required. But at the front-end user wants to clear field. Then form validation fails because of length restriction. Incoming data is an empty string instead of null. So minimum length validation restricts the input.
Therefore I start to search a solution to convert empty strings to null values. I found a #InitBinder and StringTrimmerEditor solution but our system uses #ResponseBody approach. So It doesn't fit.
Adding #JsonDeserialize(using = CustomTrimDeserializer.class) annotation or writing a custom setter for every string field is not DRY solution.
I just want to add app wide custom deserializer for String fields.
I finally examine the JsonComponentModule class and noticed spring is looking for the JsonComponent annotation for deserializer registration.
This is a one file spring boot project for solution
#RestController
#SpringBootApplication
public class CheckNullApplication {
public static void main(String[] args) {
SpringApplication.run(CheckNullApplication.class, args);
}
#PostMapping("/check-null")
public boolean checkNull(#RequestBody final HelloForm form) {
return form.getName() == null;
}
public static class HelloForm {
private String name;
public String getName() { return name; }
public void setName(final String name) { this.name = name;}
}
#JsonComponent
public static class StringTrimmerDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(final JsonParser p, final DeserializationContext ctxt)
throws IOException, JsonProcessingException {
String result = StringDeserializer.instance.deserialize(p, ctxt);
if (result != null) {
result = result.trim();
if (StringUtils.isEmpty(result)) {
return null;
}
}
return result;
}
}
}
Instead of adding #JsonDeserialize annotation you may want to just register your custom deserializer via Module (for example, SimpleModule), and it will apply to all String valued properties. Something like:
SimpleModule module = new SimpleModule(...);
module.addDeserializer(String.class, new CustomTrimDeserializer());
mapper.registerModule(module);
Create a class as following and annotate with #JsonComponent. Spring boot will pick that up as a component.
import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
#JsonComponent
public class WhitSpaceTrimmerDeserializer extends StringDeserializer {
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
final String value = super.deserialize(p, ctxt);
return value!=null?value.trim():null;
}

How to validate Spring MVC #PathVariable values?

For a simple RESTful JSON api implemented in Spring MVC, can I use Bean Validation (JSR-303) to validate the path variables passed into the handler method?
For example:
#RequestMapping(value = "/number/{customerNumber}")
#ResponseBody
public ResponseObject searchByNumber(#PathVariable("customerNumber") String customerNumber) {
...
}
Here, I need to validate the customerNumber variables's length using Bean validation. Is this possible with Spring MVC v3.x.x? If not, what's the best approach for this type of validations?
Thanks.
Spring does not support #javax.validation.Valid on #PathVariable annotated parameters in handler methods. There was an Improvement request, but it is still unresolved.
Your best bet is to just do your custom validation in the handler method body or consider using org.springframework.validation.annotation.Validated as suggested in other answers.
You can use like this:
use org.springframework.validation.annotation.Validated to valid RequestParam or PathVariable.
*
* Variant of JSR-303's {#link javax.validation.Valid}, supporting the
* specification of validation groups. Designed for convenient use with
* Spring's JSR-303 support but not JSR-303 specific.
*
step.1 init ValidationConfig
#Configuration
public class ValidationConfig {
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
return processor;
}
}
step.2 Add #Validated to your controller handler class, Like:
#RequestMapping(value = "poo/foo")
#Validated
public class FooController {
...
}
step.3 Add validators to your handler method:
#RequestMapping(value = "{id}", method = RequestMethod.DELETE)
public ResponseEntity<Foo> delete(
#PathVariable("id") #Size(min = 1) #CustomerValidator int id) throws RestException {
// do something
return new ResponseEntity(HttpStatus.OK);
}
final step. Add exception resolver to your context:
#Component
public class BindExceptionResolver implements HandlerExceptionResolver {
#Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
if (ex.getClass().equals(BindException.class)) {
BindException exception = (BindException) ex;
List<FieldError> fieldErrors = exception.getFieldErrors();
return new ModelAndView(new MappingJackson2JsonView(), buildErrorModel(request, response, fieldErrors));
}
}
}
The solution is simple:
#GetMapping(value = {"/", "/{hash:[a-fA-F0-9]{40}}"})
public String request(#PathVariable(value = "hash", required = false) String historyHash)
{
// Accepted requests: either "/" or "/{40 character long hash}"
}
And yes, PathVariables are ment to be validated, like any user input.
Instead of using #PathVariable, you can take advantage of Spring MVC ability to map path variables into a bean:
#RestController
#RequestMapping("/user")
public class UserController {
#GetMapping("/{id}")
public void get(#Valid GetDto dto) {
// dto.getId() is the path variable
}
}
And the bean contains the actual validation rules:
#Data
public class GetDto {
#Min(1) #Max(99)
private long id;
}
Make sure that your path variables ({id}) correspond to the bean fields (id);
#PathVariable is not meant to be validated in order to send back a readable message to the user. As principle a pathVariable should never be invalid. If a pathVariable is invalid the reason can be:
a bug generated a bad url (an href in jsp for example). No #Valid is
needed and no message is needed, just fix the code;
"the user" is manipulating the url.
Again, no #Valid is needed, no meaningful message to the user should
be given.
In both cases just leave an exception bubble up until it is catched by
the usual Spring ExceptionHandlers in order to generate a nice
error page or a meaningful json response indicating the error. In
order to get this result you can do some validation using custom editors.
Create a CustomerNumber class, possibly as immutable (implementing a CharSequence is not needed but allows you to use it basically as if it were a String)
public class CustomerNumber implements CharSequence {
private String customerNumber;
public CustomerNumber(String customerNumber) {
this.customerNumber = customerNumber;
}
#Override
public String toString() {
return customerNumber == null ? null : customerNumber.toString();
}
#Override
public int length() {
return customerNumber.length();
}
#Override
public char charAt(int index) {
return customerNumber.charAt(index);
}
#Override
public CharSequence subSequence(int start, int end) {
return customerNumber.subSequence(start, end);
}
#Override
public boolean equals(Object obj) {
return customerNumber.equals(obj);
}
#Override
public int hashCode() {
return customerNumber.hashCode();
}
}
Create an editor implementing your validation logic (in this case no whitespaces and fixed length, just as an example)
public class CustomerNumberEditor extends PropertyEditorSupport {
#Override
public void setAsText(String text) throws IllegalArgumentException {
if (StringUtils.hasText(text) && !StringUtils.containsWhitespace(text) && text.length() == YOUR_LENGTH) {
setValue(new CustomerNumber(text));
} else {
throw new IllegalArgumentException();
// you could also subclass and throw IllegalArgumentException
// in order to manage a more detailed error message
}
}
#Override
public String getAsText() {
return ((CustomerNumber) this.getValue()).toString();
}
}
Register the editor in the Controller
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(CustomerNumber.class, new CustomerNumberEditor());
// ... other editors
}
Change the signature of your controller method accepting CustomerNumber instead of String (whatever your ResponseObject is ...)
#RequestMapping(value = "/number/{customerNumber}")
#ResponseBody
public ResponseObject searchByNumber(#PathVariable("customerNumber") CustomerNumber customerNumber) {
...
}
You can create the answer you want by using the fields in the ConstraintViolationException with the following method;
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handlePathVariableError(final ConstraintViolationException exception) {
log.error(exception.getMessage(), exception);
final List<SisSubError> subErrors = new ArrayList<>();
exception.getConstraintViolations().forEach(constraintViolation -> subErrors.add(generateSubError(constraintViolation)));
final SisError error = generateErrorWithSubErrors(VALIDATION_ERROR, HttpStatus.BAD_REQUEST, subErrors);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
You need to added an #Validated annotation to Controller class and any validation annotation before path variable field
Path variable may not be linked with any bean in your system. What do you want to annotate with JSR-303 annotations?
To validate path variable you should use this approach Problem validating #PathVariable url on spring 3 mvc
Actually there is a very simple solution to this. Add or override the same controller method with its request mapping not having the placeholder for the path variable and throw ResponseStatusException from it. Code given below
#RequestMapping(value = "/number")
#ResponseBody
public ResponseObject searchByNumber() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"customer number missing")
}

Server side Validation not working properly

I am configuring server side validation for my form.My problem is that when the control comes in the Areavalidator class
#Override
public boolean supports(Class<?> clazz) {
return Area.class.isAssignableFrom(clazz);
}
from the above method the control again back to the controller class and in the error set it shows zero error.My question is that why it is not entering in the method where I am doing my validation stuff.
#Override
public void validate(Object target, Errors errors) {
Area object = (Area)target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "areaName",
"label.areaNameRequired");
if(object.getAreaCode().length()==0)
{
{
errors.rejectValue("areaCode", "label.areaCode", null);
}
}
}
The code in my controller class for validation
#Autowired
private AreaValidator areaValidator;
#InitBinder("area")
protected void initBinder(WebDataBinder binder) {
binder.setValidator(areaValidator);
}
#RequestMapping(value = "/saveGridArea", method = RequestMethod.POST)
public String saveCountry(#ModelAttribute #Valid Area area,ModelMap map,BindingResult error) {
if (error.hasErrors()) {
return "area";
}
It's because when you do #Valid, it is expected to have the corresponding BindingResult right next to the modelAttribute:
Here, your ModelMap is in between, making it impossible for the framework to associate the linked/associated errors to the modelAttribute .
You just need to change the order of your variable of the method.
Try this and it should work:
#RequestMapping(value = "/saveGridArea", method = RequestMethod.POST)
public String saveCountry(#ModelAttribute #Valid Area area,BindingResult error, ModelMap map){
...
}

Resources