Nota:
Hay que tener puesto <mvc:annotation-driven /> para que funcione el @Valid.
Item 1 - javax.validation
Añadiendo anotaciones de javax.validation a un objeto y recibiéndolo como un @ModelAttribute en el controller, al poner @Valid se lanza automáticamente la validación y se rellena el objeto BindingResult. @RequestMapping(value="/nuevo", params="accion=crear", method=RequestMethod.POST)
public ModelAndView formNuevoCrear(@Valid @ModelAttribute("usuarioForm") UsuarioForm usuarioForm, BindingResult result, RedirectAttributes redirectAttributes) {
//Anotando el model attribute con @Valid se fuerza a que se lanze su validación automática antes de entrar en el método.
//En caso de detectarse errores se añaden al objeto errors.
logger.debug("Acción de crear usuario");
if (result.hasErrors()) {
ModelAndView mav = new ModelAndView();
mav.addObject("tiposUsuario", tipoUsuarioService.getAll());
mav.addObject("roles", roleService.getAll());
mav.addObject("usuarioForm", usuarioForm);
mav.setViewName("app/usuarios/nuevoUsuario");
return mav;
}
Usuario usuario = usuarioForm.getUsuario();
usuario = usuarioService.save(usuario);
//Mostrar un aviso en el listado indicando que el usuario se creó correctamente
redirectAttributes.addFlashAttribute("usuarioCreado", true);
ModelAndView mav = new ModelAndView();
mav.setViewName("redirect:/usuarios");
return mav;
}
Item 2 - Validaciones anidadas
Si lo que se recibe de la request es un objeto Form (Bean o POJO adaptador para no usar directamente el objeto de negocio) que, a su vez, contiene un objeto de negocio con anotaciones de validación, hay que anotar con @Valid esa propiedad. De este modo se lanzan las validaciones anidadas. public class UsuarioForm {
@Valid
private Usuario usuario = new Usuario();
public Usuario getUsuario() {
return usuario;
}
public void setUsuario(Usuario usuario) {
this.usuario = usuario;
}
}
@Entity
@Table(name="USUARIO")
public class Usuario {
@Id @GeneratedValue
private Long id;
@NotEmpty
private String nombre;
...
}
Item 3 - Crear un validador de spring
Se puede usar el objeto de validación propio de spring mvc org.springframework.validation.Validator.En este caso hay varias opciones para lanzarlo:
- Se puede registrar dentro del métod intBinder del controller, con lo que aplicaría a todos los métodos del mismo. Esta suele ser una opción no deseada, porque obliga a tener un validador para cada tipo de objeto recibido como parámetro.
- Se puede registrar dentro de un método propio, también con el initBinder. En este caso sólo aplicará a aquellos métodos del controller que tengan un parámetro como el especificado.
/** * Se restringe el initBinder para que únicamente aplique a los métodos del controller que tenga * un atributo de ese nombre * @param binder */ @InitBinder("usuarioForm") protected void initBinderUsuarioForm(WebDataBinder binder) { //Registro el/los validadores manuales aplicables a usuarioForm binder.setValidator(new UsuarioFormValidator()); }
- Se puede no declarar en ningún lado y ejecutarlo manualmente al entrar en el controller.
Item 4 - Validaciones combinadas
Se pueden combinar validaciones de spring con las de javax, pero dentro de objeto validador de spring.
La idea es invocar manualmente las validaciones javax, y realizar luego el resto de validaciones de negocio más complejas.
public class UsuarioFormValidator implements Validator {
/**
* Logger
*/
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
public boolean supports(Class<?> clazz) {
return UsuarioForm.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
logger.debug("validando usuarioForm");
UsuarioForm usuarioForm = (UsuarioForm) target;
Usuario usuario = usuarioForm.getUsuario();
realizarValidacionesJavax(usuarioForm, errors);
realizarValidacionesComplejasNegocio(usuario, errors);
logger.debug("UsuarioForm valdiado con {} errores detectados", errors.getErrorCount());
}
/**
* Validaciones generales de javax validation
* @param usuarioForm
* @param errors
*/
private void realizarValidacionesJavax(UsuarioForm usuarioForm, Errors errors) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
javax.validation.Validator validator = factory.getValidator();
Set<ConstraintViolation<UsuarioForm>> constraintViolations = validator.validate(usuarioForm);
for (ConstraintViolation<UsuarioForm> violation : constraintViolations) {
errors.rejectValue(violation.getPropertyPath().toString(),
violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(),
violation.getConstraintDescriptor().getAttributes().values().toArray(),
violation.getMessage());
}
}
/**
* Validaciones más específicas relacionadas con el negocio
* @param usuario
* @param errors
*/
private void realizarValidacionesComplejasNegocio(Usuario usuario, Errors errors) {
if (usuario.getRoles() != null && usuario.getRoles().size() > 2) {
errors.rejectValue("usuario.roles", "error.ususarios.maxRoles", "Sólo se permiten dos roles por usuario");
}
}
}
Item 5 - De ConstraintViolation a Errors
Al hacer las validaciones de javax manuales, hay que hacer una transformación entre las constraint violation de javax y el objeto errors de spring.Para ello se recorren los errores detectados y se extrare la información necesaria para meter un rejectValue.
Item 6 - Prevalencia
Si se tiene un @Valid en el controller y un validador registrado de spring, sólo se ejecutarán las validaciones de spring. Las de javax no se ejecutarán.
Item 7 - Internacionalización
Para internacionalizar los mensajes de spring se necesita un messageSource que referencie a los ficheros properties con los códigos de error.
Para internacionalizar los mensajes de javax.validation se necesita un messageInterpolator. El funcionamiento por defecto es buscar en el classpath un fichero ValidationMessages.properties con todos los textos, pero esto presenta el problema de cómo reconocer los cambios de idioma.
Para solucionarlo, spring proporciona una clase LocalValidatorFactoryBean a la que se le puede setear un messageSource, que es el que contendrá todos los códigos de error para los diferentes idiomas.
La configuración resultante será la siguiente (también se ponen los beans para detectar los cambios de idioma y setearlos):
<!-- ***************************** -->
<!-- Mensajes y etiquetas de spring-->
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="cacheSeconds" value="60"/>
<property name="basenames">
<list>
<value>classpath:bundles/errores</value>
</list>
</property>
<property name="defaultEncoding" value="UTF-8" />
</bean>
<!-- Para la internacionalización de los mensajes de javax.validation se emplea un inter -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="validationMessageSource" ref="messageSource"/>
</bean>
<!-- ***************************** -->
<!-- ***************************** -->
<!-- Locale resolver e interceptor -->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<property name="defaultLocale" value="gl" />
</bean>
<mvc:interceptors>
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="idioma" />
</bean>
</mvc:interceptors>
<!-- ***************************** -->
También hay que tener en cuenta que, en vez de crear el validator a mano, ahora hay que recibir el inyectado por spring en las clases de validación.
@Component("usuarioFormValidator")
public class UsuarioFormValidator implements Validator {
/**
* Logger
*/
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private javax.validation.Validator validator;
...
}