I'm handling a MethodArgumentNotValidException thrown after a failed validation of a request object. All the usual stuff is in place: #Valid, #ControllerAdvice, and an extended ResponseEntityExceptionHandler, in which I override handleMethodArgumentNotValid().
As it happens, I need to access that same request object in order to form a customized error response. One way would be to intercept the request before it hits the controller and create a #RequestScope bean with the needed fields in case validation fails later.
Is there a better way?
Thanks to a suggestion from a colleague, I've found that the BindingResult within MethodArgumentNotValidException has a method named getTarget() that returns the validated object. As seen from the method signature (Object getTarget()), the return value needs a cast.
You should have the error fields in the MethodArgumentNotValidException class. Your handleMethodArgumentNotValid function might look like something as follows.
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
#ResponseBody
public CustomInputErrorResponse handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
String message = "Invalid inputs";
ArrayList<String> fieldNames = new ArrayList<String>();
for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
fieldNames.add(fieldError.getField());
}
return new CustomInputErrorResponse(message, fieldNames);
}
Considering you have a CustomInputErrorResponse class that takes two arguments for a custom message and error field names.
Related
I have a Map that I receive from a browser redirection back from a third party to my Spring Controller as below -
#RequestMapping(value = "/capture", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void capture(#RequestParam
final Map<String, String> response)
{
// TODO : perform validations first.
captureResponse(response);
}
Before using this payload, I need to do non-trivial validation, involving first checking for non-null values of a map, and then using those values in a checksum validation. So, I would like to validate my payload programmatically using the Spring Validator interface. However, I could not find any validator example for validating a Map.
For validating a Java Object, I understand how a Validator is invoked by passing the object and a BeanPropertyBindingResult to contain the errors to the Validator as below -
final Errors errors = new BeanPropertyBindingResult(object, objectName);
myValidator.validate(object, errors);
if (errors.hasErrors())
{
throw new MyWebserviceValidationException(errors);
}
For a Map, I can see that there is a MapBindingResult class that extends AbstractBindingResult. Should I simply use it, and pass my map in the Object object and in the validator cast it back to a Map? Also, how would the Validator method of supports(final Class<?> clazz) be implemented in my validator? Would it simply be like below code snippet where there can only be one validator supporting this generic class of HashMap? Somehow doesn't feel right. (Although this does not matter to me as I will be injecting my validator and use it directly and not through a validator registry, but still curious.)
#Override
public boolean supports(final Class<?> clazz)
{
return HashMap.class.equals(clazz);
}
Since, there is a MapBindingResult, I'm positive that Spring must be supporting Maps for validation, would like to know how. So would like to know if this is the way to go, or am I heading in the wrong direction and there is a better way of doing this.
Please note I would like to do this programmatically and not via annotations.
Just like I thought, Spring Validator org.springframework.validation.Validator does support validation of a Map. I tried it out myself, and it works!
I created a org.springframework.validation.MapBindingResult by passing in the map I need to validate and an identifier name for that map (for global/root-level error messages). This Errors object is passed in the validator, along with the map to be validated as shown in the snippet below.
final Errors errors = new MapBindingResult(responseMap, "responseMap");
myValidator.validate(responseMap, errors);
if (errors.hasErrors())
{
throw new MyWebserviceValidationException(errors);
}
The MapBindingResult extends AbstractBindingResult and overrides the method getActualFieldValue to give it's own implementation to get field from a map being validated.
private final Map<?, ?> target;
#Override
protected Object getActualFieldValue(String field) {
return this.target.get(field);
}
So, inside the Validator I was able to use all the useful utility methods provided in org.springframework.validation.ValidationUtils just like we use in a standard object bean validator. For example -
ValidationUtils.rejectIfEmpty(errors, "checksum", "field.required");
where "checksum" is a key in my map. Ah, the beauty of inheritance! :)
For the other non-trivial validations, I simply cast the Object to Map and wrote my custom validation code.
So the validator looks something like -
#Override
public boolean supports(final Class<?> clazz)
{
return HashMap.class.equals(clazz);
}
#Override
public void validate(final Object target, final Errors errors)
{
ValidationUtils.rejectIfEmpty(errors, "transactionId", "field.required");
ValidationUtils.rejectIfEmpty(errors, "checksum", "field.required");
final Map<String, String> response = (HashMap<String, String>) target;
// do custom validations with the map's attributes
// ....
// if validation fails, reject the whole map -
errors.reject("response.map.invalid");
}
Validate the parameters inside the map
For the validation of your Map following a specific mapping you will need a custom validator.
As this may be the usecase for some, validation of #RequestParam can be done using org.springframework.validation annotations, e.g.
#GetMapping(value = "/search")
public ResponseEntity<T> search(#RequestParam
Map<#NotBlank String,#NotBlank String> searchParams,
While #NotBlank checks if your string is not "",
#NotNull can validate non-null parameters which I guess was something you needed.
An alternative is to create your custom constraint annotation for a Map.
You can take a look the following link:
https://www.baeldung.com/spring-mvc-custom-validator
I think in terms of REST, the ID should be placed into the URL, something like:
https://example.com/module/[ID]
and then I call GET, PUT, DELETE on that URL. That's kind of clear I think. In Spring MVC controllers, I'd get the ID with #PathVariable. Works.
Now, my practical problem with Spring MVC is, that if I do this, I have to NOT include the ID as part of the form (as well), Spring emits warnings of type
Skipping URI variable 'id' since the request contains a bind value with the same name.
otherwise. And it also makes kind of sense to only send it once, right? What would you do if they don't match??
That would be fine, but I do have a custom validator for my form backing bean, that needs to know the ID! (It needs to check if a certain unique name is already being used for a different entity instance, but cannot without knowing the ID of the submitted form).
I haven't found a good way to tell the validator that ID from #PathVariable, since the validation happens even before code in my controller method is executed.
How would you solve this dilemma?
This is my Controller (modified):
#Controller
#RequestMapping("/channels")
#RoleRestricted(resource = RoleResource.CHANNEL_ADMIN)
public class ChannelAdminController
{
protected ChannelService channelService;
protected ChannelEditFormValidator formValidator;
#Autowired
public ChannelAdminController(ChannelService channelService, ChannelEditFormValidator formValidator)
{
this.channelService = channelService;
this.formValidator = formValidator;
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.GET)
public String editChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") ChannelEditForm channelEditForm, Model model)
{
if (channelId > 0)
{
// Populate from persistent entity
}
else
{
// Prepare form with default values
}
return "channel/admin/channel-edit";
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
try
{
// Has to validate in controller if the name is already used by another channel, since in the validator, we don't know the channelId
Long nameChannelId = channelService.getChannelIdByName(channelEditForm.getName());
if (nameChannelId != null && !nameChannelId.equals(channelId))
result.rejectValue("name", "channel:admin.f1.error.name");
}
catch (EmptyResultDataAccessException e)
{
// That's fine, new valid unique name (not so fine using an exception for this, but you know...)
}
if (result.hasErrors())
{
return "channel/admin/channel-edit";
}
// Copy properties from form to ChannelEditRequest DTO
// ...
// Save
// ...
redirectAttributes.addFlashAttribute("successMessage", new SuccessMessage.Builder("channel:admin.f1.success", "Success!").build());
// POST-REDIRECT-GET
return "redirect:/channels/" + channelId + "/admin";
}
#InitBinder("channelForm")
protected void initBinder(WebDataBinder binder)
{
binder.setValidator(formValidator);
}
}
I think I finally found the solution.
As it turns out Spring binds path variables to form beans, too! I haven't found this documented anywhere, and wouldn't have expected it, but when trying to rename the path variable, like #DavidW suggested (which I would have expected to only have a local effect in my controller method), I realized that some things got broken, because of the before-mentioned.
So, basically, the solution is to have the ID property on the form-backing object, too, BUT not including a hidden input field in the HTML form. This way Spring will use the path variable and populate it on the form. The local #PathVariable parameter in the controller method can even be skipped.
The cleanest way to solve this, I think, is to let the database handle the duplicates: Add a unique constraint to the database column. (or JPA by adding a #UniqueConstraint)
But you still have to catch the database exception and transform it to a user friendly message.
This way you can keep the spring MVC validator simple: only validate fields, without needing to query the database.
Could you not simply disambiguate the 2 (URI template variables vs. parameters) by using a different name for your URI template variable?
#RequestMapping(value = "/{chanId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long chanId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
[...]
What ever you said is correct the correct way to design rest api is to mention the resource id in path variable if you look at some examples from the swagger now as open api you could find similar examples there
for you the correct solution would be to use a custom validator like this
import javax.validation.Validator;`
import org.apache.commons.lang3.StringUtils;`
import org.springframework.validation.Errors;`
importorg.springframework.validation.beanvalidation.CustomValidatorBean;`
public class MyValidator extends CustomValidatorBean {`
public void myvalidate(Object target,Errors errors,String flag,Profile profile){
super.validate(target,errors);
if(StringUtils.isEmpty(profile.name())){
errors.rejectValue("name", "NotBlank.profilereg.name", new Object[] { "name" }, "Missing Required Fields");
}
}
}
This would make sure all the fields are validated and you dont need to pass the id in the form.
Is there any way to get inside to the exception object when using annotation #ExceptionHandler?
this is my code:
#ExceptionHandler(DataInputException.class)
public ResponseEntity handleException(){
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body("Entity contains null or forbidden values");
}
I'd like to have returned message to contain customized info about perticular fields. (That's why I need error object).
Pass in the exception
#ExceptionHandler(DataInputException.class)
public ResponseEntity handleException(DataInputException exception) {
Hi i'm trying to build my own parameter extractor + validator by using Spring AOP.
First of all, i defined a request mapping like this:
#RequestMapping(value = "/hello", method = RequestMethod.GET)
public void ttt(#MyParam(method = CheckMethod.LONG_TIMESTAMP) Long mparam) {
// do something here
System.out.println(mparam);
}
And the AOP:
#Around(value = "xxx")
public Object extractAndValidate(ProceedingJoinPoint pjp) throws Throwable {
Object[] arguments = pjp.getArgs();
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
Class[] classes = method.getParameterTypes();
Annotation[][] annotations = method.getParameterAnnotations();
// get arguments whose annotation type is MyParam and get the argument name and extract it from GET request parameter or POST/PUT body
// validate all params appointed by MyParam.method, if something wrong, return user the detailed error explanation.
// if all validation has passed, then convert all parameters into appointed type, and then send them to pjp to proceed.
return pjp.proceed(arguments);
}
The big problem is Spring automatically identifies the parameter name from RequestMapping method, and trying to map(and type converting) GET parameter into corresponding parameter, even if i did not declare any Spring annotations for the parameter. For example, "/hello?mparam=jwoijf" will simply return a default tomcat 400 error describing "The request sent by the client was syntactically incorrect.", and there are WARN level debug logs which says spring cannot convert String "jwijf" into Long value. So how do I avoid this kind of Spring feature?
The reason why i don't want to use #ModelAttribute and #Valid way to extract params and validate them is:
Using ModelAttribute will make me create too many models(every request for a model).
Some model attributes are identical, some are not even if they have the same name.
Teammates will have to know how each attribute is restricted in order to create a new ModelAttribute.
I have a use case where I need to return a PDF to a user which is generated for us. It seems that what I need to do is utilize the ResponseEntity in this case, but I have a couple of things which are not very clear.
How can I redirect the user -- let's pretend they don't have the permissions to access this page? How can I redirect them to a separate controller?
Am I able to set the response encoding?
Can I achieve either of these two without bringing in the HttpResponse as a parameter to my RequestMapping?
I'm using Spring 3.0.5. Example code below:
#Controller
#RequestMapping("/generate/data/pdf.xhtml")
public class PdfController {
#RequestMapping
public ResponseEntity<byte []> generatePdf(#RequestAttribute("key") Key itemKey) {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.valueOf("application/pdf"));
if (itemKey == null || !allowedToViewPdf(itemKey)) {
//How can I redirect here?
}
//How can I set the response content type to UTF_8 -- I need this
//for a separate controller
return new ResponseEntity<byte []>(PdfGenerator.generateFromKey(itemKey),
responseHeaders,
HttpStatus.CREATED);
}
I'd really like to not pull in the Response... None of my controllers have done so thus far, and I'd hate to have to bring it in at all.
Note, this works in Spring 3.1, not sure about spring 3.0.5 as asked in the original question.
In your return ResponseEntity statement where you want to handle the redirect, just add in a "Location" header to the ResponseEntity, set the body to null and set the HttpStatus to FOUND (302).
HttpHeaders headers = new HttpHeaders();
headers.add("Location", "http://stackoverflow.com");
return new ResponseEntity<byte []>(null,headers,HttpStatus.FOUND);
This will keep you from having to change the return type of the controller method.
Regarding the redirect, all you need to do is change the return type to Object:
#Controller
#RequestMapping("/generate/data/pdf.xhtml")
public class PdfController {
#RequestMapping
public Object generatePdf(#RequestAttribute("key") Key itemKey) {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.valueOf("application/pdf"));
if (itemKey == null || !allowedToViewPdf(itemKey)) {
return "redirect:/some/path/to/redirect"
}
//How can I set the response content type to UTF_8 -- I need this
//for a separate controller
return new ResponseEntity<byte []>(PdfGenerator.generateFromKey(itemKey),
responseHeaders,
HttpStatus.CREATED);
}
Redirects are easy - for your handler method's return String, just prepend with redirect:, as in return "redirect:somewhere else".
Not sure why you're objecting to the Response object. Is there a reason? Otherwise, if you just stream the PDF as an OutputStream on the HttpServletResponse object, then you don't actually need to return the PDF from your handler method - you just need to set the PDF stream on the response, which you can add to your handler method's signature. See http://www.exampledepot.com/egs/javax.servlet/GetImage.html for an example.
Instead of dealing with redirecting (these are instances which we open in new windows / tabs) anyhow we decided to just display the error message they would have received.
This likely won't work for all, but with the way we add error / status messages we were unable to get those messages to persist on the view upon exception occurring.