I have a spring mvc controller like the following
#RequestMapping(value="/new", method=RequestMethod.POST)
public String createBooking(#Valid Booking booking, BindingResult bindingResult, Model model, Principal principal)
{
if(bindingResult.hasErrors()) {
return "booking/edit";
}
//Store Booking in db...
...
The problem is the Booking object i get from the POST is constructed by Spring, but one of the properties required by the validator cannot be populated, as the property is not present in the form. So my question is is there a way for me to intercept the Booking before it gets processed by the #Valid tag handler to add this required property?
Cheers!
NFV
There are 2 ways to modify the model attribute object before the #Valid will trigger:
Remove #Valid and autowire the validator and manually trigger the validator:
class MyController {
private final Validator validator;
class MyController(Validator validator) {
this.validator = validator;
}
#PostMapping("/new")
public String createBooking(Booking booking, BindingResult bindingResult, Model model, Principal principal) {
// edit booking here
validator.validate(booking, result)
// original method body here
}
}
Decorate the default validator and pre-process the booking object inside the decorated validator.
class MyController {
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.setValidator(new PreProcessBookingValidator(binder.getValidator()));
}
#PostMapping("/new")
public String createBooking(#Valid Booking booking, BindingResult bindingResult, Model model, Principal principal) {
...
}
private static class PreProcessBookingValidator implements Validator {
private final Validator validator;
public PreProcessBookingValidator(Validator validator) {
this.validator = validator;
}
#Override
public boolean supports(#Nonnull Class<?> clazz) {
return validator.supports(clazz);
}
#Override
public void validate(#Nonnull Object target, #Nonnull Errors errors) {
if (target instanceof Booking) {
Booking booking = (Booking) target;
// manipulate booking here
}
validator.validate(target, errors);
}
}
}
(This second tip is what I picked up from https://github.com/spring-projects/spring-framework/issues/11103)
I'm not sure I understand the question, but it sounds like you want to set a field in the command object to a value before your command is bound by the form submission. If so, you can add a method in your controller as follows...
#ModelAttribute
public Booking getBooking() {
Booking booking = new Booking();
booking.setMyRequiredProperty("some value");
return booking;
}
Hope that helps
Related
I have a controller where works fine, It can register and update an entity, just how the following about to create the forms to save and update an entity respectively
#RequestMapping(value="/registrar.htm", method=RequestMethod.GET)
public String crearRegistrarFormulario(Model model){
…
}
#RequestMapping(value="/{id}/actualizar.htm", method=RequestMethod.GET)
public String crearActualizarFormulario(#PathVariable("id") String id, Model model){
…
}
Until here I have no problems.
My problem is about the #InitBinder
I need work with the same entity Deportista (Sportsman), one special setting for save and update. For example
#InitBinder
public void registrarInitBinder(WebDataBinder binder) { // register or save
logger.info(">>>>>>>> registrarInitBinder >>>>>>>>>>>>>");
…
CustomDateEditor customDateEditor = new CustomDateEditor(...
…
}
#InitBinder
public void actualizarInitBinder(WebDataBinder binder) { // update
logger.info(">>>>>>>> actualizarInitBinder >>>>>>>>>>>>>");
…
CustomDateEditor customDateEditor = new CustomDateEditor(...
…
binder.setDisallowedFields(…) //I need this only for update
}
I have read the following:
How to define multiple initBinders
Addiing multiple validators using initBinder
The links mentioned work around different entities, for example User and Customer, it through the #InitBinder's value attribute, but I need work with the same entity, how I can configure the #InitBinder's value to indicate Spring use or discriminate each #InitBinder? one for save and update respectively.
Thanks
Edit: From the Serge Ballesta's answer, the following is mandatory too:
#Controller
#RequestMapping(value="/deportista")
#SessionAttributes(value={"deportistaRegistrar", "deportistaActualizar"})
public class DeportistaController {
…
#RequestMapping(value="/registrar.htm", method=RequestMethod.GET)
public String crearRegistrarFormulario(Model model){
Deportista deportista = new Deportista();
model.addAttribute("deportistaRegistrar", deportista);
return "deportista.formulario.registro";
}
#RequestMapping(value="/{id}/actualizar.htm", method=RequestMethod.GET)
public String crearActualizarFormulario(#PathVariable("id") String id, Model model){
Deportista deportista = this.fakeMultipleRepository.findDeportista(id);
model.addAttribute("deportistaActualizar", deportista);
return "deportista.formulario.actualizacion";
}
It to let work his answer:
// registrarInitBinder will be used here
#RequestMapping(value="/registrar.htm", method=RequestMethod.POST)
public String doCrearRegistrarFormulario(#ModelAttribute("deportistaRegistrar") XXX value,
BindingResult result, Model model){
…
}
// actualizarInitBinder will be used here
#RequestMapping(value="/{id}/actualizar.htm", method=RequestMethod.POST)
public String crearActualizarFormulario(#PathVariable("id") String id,
#ModelAttribute("deportistaActualizar") XXX value, BindingResult result, Model model){
…
}
According to the javadoc page for #InitBinder, you can use multiple init-binders in one single controllers, and specialize them with the name of the model attribute variable for which they will be applied. Example :
#InitBinder("saveValue")
public void registrarInitBinder(WebDataBinder binder) { // register or save
logger.info(">>>>>>>> registrarInitBinder >>>>>>>>>>>>>");
…
CustomDateEditor customDateEditor = new CustomDateEditor(...
…
}
#InitBinder("updateValue")
public void actualizarInitBinder(WebDataBinder binder) { // update
logger.info(">>>>>>>> actualizarInitBinder >>>>>>>>>>>>>");
…
CustomDateEditor customDateEditor = new CustomDateEditor(...
…
binder.setDisallowedFields(…) //I need this only for update
}
and then (XXX is the type of the form object that will be processed by the submit)
// registrarInitBinder will be used here
#RequestMapping(value="/registrar.htm", method=RequestMethod.POST)
public String doCrearRegistrarFormulario(#ModelAttribute("saveValue") XXX value,
BindingResult result, Model model){
…
}
// actualizarInitBinder will be used here
#RequestMapping(value="/{id}/actualizar.htm", method=RequestMethod.POST)
public String crearActualizarFormulario(#PathVariable("id") String id,
#ModelAttribute("updateValue") XXX value, BindingResult result, Model model){
…
}
When I don't use #RequestBody the #PathVariable id is automatically set at my Entity class. But if I use #RequestBody it's not. I need that the id of Entity is set before my GenericValidator executes validation. Why does it work without #RequestBody and not with it?
The Entity class:
public class Entity {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
//...
}
The controller class:
#Controller
#RequestMapping(value = "/entity")
public class EntityController {
#Autowired
private GenericValidator validator;
#InitBinder
private void initBinder(WebDataBinder binder) {
binder.addValidators(validator);
}
#RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public #ResponseBody Response update(
#PathVariable String id,
#Valid #RequestBody Entity entity)
{
//...
}
}
When used alone, #Valid works much like #ModelAttribute. The Entity method argument would be retrieved from the Model or instantiated, the WebDataBinder would handle the data binding process (this is when the id would be set), and then validation would occur.
#RequestBody arguments do not go through the data binding process like #ModelAttribute arguments. They're created via an HttpMessageConverter using the body of the request instead of matching the names of request parameters and path variables to the names of your object's fields. When combined with #Valid, the configured validator is run against the new object but #ModelAttribute style data binding still does not occur.
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")
}
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){
...
}
I have an application written in Spring 3.0 hooked up using Hibernate to a database. I have a controller to an update form. Whenever the form is submitted, I expect the object that is shown to be updated however a new object is created with a new ID value. I've looked over the "petclinic" sample and i can't see how it is different.
POJO
public class Person
{
private int id;
#NotNull
private String name;
//getter/setter for id
//getter/setter for name
}
Controller
public class PersonUpdateController
{
//injected
private PersonService personService;
#RequestMapping(value="/person/{personId}/form", method=RequestMethod.POST)
public String updateForm(ModelMap modelMap, #PathVariable personId)
{
Person person = personService.getById(personId);
modelMap.addAttribute(person);
return "person/update";
}
#RequestMapping(value="/person/{personId}", method=RequestMethod.POST)
public String update(ModelMap modelMap, #Valid Person person, BindingResult bindingResult)
{
if(bindingResult.hasErrors())
{
modelMap.addAttribute(person);
return "person/update";
}
personService.save(person);
return "redirect:person/" + person.getId() + "/success";
}
}
JSP
<spring:url value="/person/${person.id}" var="action_url" />
<spring:form action="${action_url}" modelAttribute="person" method="POST">
<spring:input name="name" path="name" />
<input type="submit" value="Save" />
</spring:form>
PersonService Implementation
public class HibernatePersonService
implements PersonService
{
//injected
private SessionFactory sessionFactory;
//other methods
public void save(Person person)
{
Session session = sessionFactory.getCurrentSession();
session.saveOrUpdate(person);
}
}
Spring MVC doesn't do any magic with HTML forms. Since your form contains only one field, you get only one field populated in update method. So, you have two options:
Pass id as a hidden field in the form: <spring:hidden path = "id" />. Note that in this case you need to check possible consequences for security (what happens if malicious person changes the id).
Store Person in the session so that data from the form is used to update the stored object (note that it may cause interference if several instances of the form is opened in one session). That's how it's done in Petclinic:
-
#SessionAttributes("person")
public class PersonUpdateController {
...
#RequestMapping(value="/person/{personId}", method=RequestMethod.POST)
public String update(ModelMap modelMap, #Valid Person person,
BindingResult bindingResult, SessionStatus status)
{
...
personService.save(person);
status.setComplete(); // Removes person from the session after successful submit
...
}
#InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields("id"); // For security
}
}