Cross field validation with HibernateValidator displays no error messages - spring

I'm validating two fields, "password" and "confirmPassword" on the form for equality using HibernateValidator as specified in this answer. The following is the constraint descriptor (validator interface).
package constraintdescriptor;
import constraintvalidator.FieldMatchValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = FieldMatchValidator.class)
#Documented
public #interface FieldMatch
{
String message() default "{constraintdescriptor.fieldmatch}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* #return The first field
*/
String first();
/**
* #return The second field
*/
String second();
/**
* Defines several <code>#FieldMatch</code> annotations on the same element
*
* #see FieldMatch
*/
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Documented
public #interface List{
FieldMatch[] value();
}
}
The following is the constraint validator (the implementing class).
package constraintvalidator;
import constraintdescriptor.FieldMatch;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.commons.beanutils.BeanUtils;
public final class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object>
{
private String firstFieldName;
private String secondFieldName;
public void initialize(final FieldMatch constraintAnnotation) {
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
//System.out.println("firstFieldName = "+firstFieldName+" secondFieldName = "+secondFieldName);
}
public boolean isValid(final Object value, final ConstraintValidatorContext cvc) {
try {
final Object firstObj = BeanUtils.getProperty(value, firstFieldName );
final Object secondObj = BeanUtils.getProperty(value, secondFieldName );
//System.out.println("firstObj = "+firstObj+" secondObj = "+secondObj);
return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
}
catch (final Exception e) {
System.out.println(e.toString());
}
return true;
}
}
The following is the validator bean which is mapped with the JSP page (as specified commandName="tempBean" with the <form:form></form:form> tag).
package validatorbeans;
import constraintdescriptor.FieldMatch;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.NotEmpty;
#FieldMatch.List({
#FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match", groups={TempBean.ValidationGroup.class})
})
public final class TempBean
{
#NotEmpty(groups={ValidationGroup.class}, message="Might not be left blank.")
private String password;
#NotEmpty(groups={ValidationGroup.class}, message="Might not be left blank.")
private String confirmPassword;
public interface ValidationGroup {};
//Getters and setters
}
UPDATE
It's all working correctly and does the validation intended. Just one thing remains is to display the specified error message above the TempBean class within #FieldMatch is not being displayed i.e only one question : how to display error messages on the JSP page when validation violation occurs?
(the annotation #NotEmpty on both of the fields password and confirmPassword in the TempBean class works and displays the specified messages on violation, the thing is not happening with #FieldMatch).
I'm using validation group based on this question as specified in this blog and it works well causing no interruption in displaying error messages (as it might seem to be).
On the JSP page these two fields are specified as follows.
<form:form id="mainForm" name="mainForm" method="post" action="Temp.htm" commandName="tempBean">
<form:password path="password"/>
<font style="color: red"><form:errors path="password"/></font><br/>
<form:password path="confirmPassword"/>
<font style="color: red"><form:errors path="confirmPassword"/></font><br/>
</form:form>

Could you try your isValid method to be like this? (this is certainly working for me in live project):
public boolean isValid(final Object value, final ConstraintValidatorContext cvc){
boolean toReturn = false;
try{
final Object firstObj = BeanUtils.getProperty(value, firstFieldName );
final Object secondObj = BeanUtils.getProperty(value, secondFieldName );
//System.out.println("firstObj = "+firstObj+" secondObj = "+secondObj);
toReturn = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
}
catch (final Exception e){
System.out.println(e.toString());
}
//If the validation failed
if(!toReturn) {
cvc.disableDefaultConstraintViolation();
//In the initialiaze method you get the errorMessage: constraintAnnotation.message();
cvc.buildConstraintViolationWithTemplate(errorMessage).addNode(firstFieldName).addConstraintViolation();
}
return toReturn;
}
Also I see that you are implementing the ConstraintValidator interface with an Object, literally. It should be the backing object that you have from your form:
tempBean // the one that you specify in the commandName actually.
So you implementation should like this:
implements ConstraintValidator<FieldMatch, TempBean>
This is probably not the issue here, but as a future reference, this is how it should be.
UPDATE
Inside your FieldMatch interface/annotation you have two methods : first and second, add one more called errorMessage for example:
Class<? extends Payload>[] payload() default {};
/**
* #return The first field
*/
String first();
/**
* #return The second field
*/
String second();
/**
#return the Error Message
*/
String errorMessage
Look inside you method from the Validation class - you are getting the first and second field names there., so just add the errorMessage, like this for example:
private String firstFieldName;
private String secondFieldName;
//get the error message name
private String errorMessagename;
public void initialize(final FieldMatch constraintAnnotation)
{
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
errorMessageNAme = constraintAnnotation.errorMessage();
//System.out.println("firstFieldName = "+firstFieldName+" secondFieldName = "+secondFieldName);
}
Inside isValida get it, the same way you do for first and second field name and use it.

Related

Spring custom validator with dependencies on other fields

We are using spring custom validator for our request object used in our controller endpoint. We implemented it the same way as how its done in the link below:
https://www.baeldung.com/spring-mvc-custom-validator
The problem we are facing is, it can't work if the particular field has dependencies on other input fields as well. For example, we have the code below as the request object for our controller endpoint:
public class FundTransferRequest {
private String accountTo;
private String accountFrom;
private String amount;
#CustomValidator
private String reason;
private Metadata metadata;
}
public class Metadata {
private String channel; //e.g. mobile, web, etc.
}
Basically #CustomValidator is our custom validator class and the logic we want is, if the supplied channel from Metadata is "WEB". The field "reason" of the request won't be required. Else, it will be required.
Is there a way to do this? I've done additional research and can't see any that handles this type of scenario.
Obviously if you need access to multiple fields in your custom validator, you have to use a class-level annotation.
The same very article you mentioned has an example of that: https://www.baeldung.com/spring-mvc-custom-validator#custom-class-level-validation
In your case it might look something like this:
#Constraint(validatedBy = CustomValidator.class)
#Target({ ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
public #interface CustomValidation {
String message() default "Reason required";
String checkedField() default "metadata.channel";
String checkedValue() default "WEB";
String requiredField() default "reason";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package com.example.demo;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/*
If the supplied channel from Metadata is "WEB". The field "reason" of the request won't be required.
Else, it will be required.
*/
#Component
public class CustomValidator implements ConstraintValidator<CustomValidation, Object> {
private String checkedField;
private String checkedValue;
private String requiredField;
#Override
public void initialize(CustomValidation constraintAnnotation) {
this.checkedField = constraintAnnotation.checkedField();
this.checkedValue = constraintAnnotation.checkedValue();
this.requiredField = constraintAnnotation.requiredField();
}
#Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
Object checkedFieldValue = new BeanWrapperImpl(value)
.getPropertyValue(checkedField);
Object requiredFieldValue = new BeanWrapperImpl(value)
.getPropertyValue(requiredField);
return checkedFieldValue != null && checkedFieldValue.equals(checkedValue) || requiredFieldValue != null;
}
}
And the usage will be:
#CustomValidation
public class FundTransferRequest {
...
or with parameters specified:
#CustomValidation(checkedField = "metadata.channel",
checkedValue = "WEB",
requiredField = "reason",
message = "Reason required")
public class FundTransferRequest {
...

Spring Boot : Custom Validation in Request Params

I want to validate one of the request parameters in my controller . The request parameter should be from one of the list of given values , if not , an error should be thrown . In the below code , I want the request param orderBy to be from the list of values present in #ValuesAllowed.
#RestController
#RequestMapping("/api/opportunity")
#Api(value = "Opportunity APIs")
#ValuesAllowed(propName = "orderBy", values = { "OpportunityCount", "OpportunityPublishedCount", "ApplicationCount",
"ApplicationsApprovedCount" })
public class OpportunityController {
#GetMapping("/vendors/list")
#ApiOperation(value = "Get all vendors")
public ResultWrapperDTO getVendorpage(#RequestParam(required = false) String term,
#RequestParam(required = false) Integer page, #RequestParam(required = false) Integer size,
#RequestParam(required = false) String orderBy, #RequestParam(required = false) String sortDir) {
I have written a custom bean validator but somehow this is not working . Even if am passing any random values for the query param , its not validating and throwing an error.
#Repeatable(ValuesAllowedMultiple.class)
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {ValuesAllowedValidator.class})
public #interface ValuesAllowed {
String message() default "Field value should be from list of ";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String propName();
String[] values();
}
public class ValuesAllowedValidator implements ConstraintValidator<ValuesAllowed, Object> {
private String propName;
private String message;
private String[] values;
#Override
public void initialize(ValuesAllowed requiredIfChecked) {
propName = requiredIfChecked.propName();
message = requiredIfChecked.message();
values = requiredIfChecked.values();
}
#Override
public boolean isValid(Object object, ConstraintValidatorContext context) {
Boolean valid = true;
try {
Object checkedValue = BeanUtils.getProperty(object, propName);
if (checkedValue != null) {
valid = Arrays.asList(values).contains(checkedValue.toString().toLowerCase());
}
if (!valid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message.concat(Arrays.toString(values)))
.addPropertyNode(propName).addConstraintViolation();
}
} catch (IllegalAccessException e) {
log.error("Accessor method is not available for class : {}, exception : {}", object.getClass().getName(), e);
return false;
} catch (NoSuchMethodException e) {
log.error("Field or method is not present on class : {}, exception : {}", object.getClass().getName(), e);
return false;
} catch (InvocationTargetException e) {
log.error("An exception occurred while accessing class : {}, exception : {}", object.getClass().getName(), e);
return false;
}
return valid;
}
}
Case 1: If the annotation ValuesAllowed is not triggered at all, it could be because of not annotating the controller with #Validated.
#Validated
#ValuesAllowed(propName = "orderBy", values = { "OpportunityCount", "OpportunityPublishedCount", "ApplicationCount", "ApplicationsApprovedCount" })
public class OpportunityController {
#GetMapping("/vendors/list")
public String getVendorpage(#RequestParam(required = false) String term,..{
}
Case 2: If it is triggered and throwing an error, it could be because of the BeanUtils.getProperty not resolving the properties and throwing exceptions.
If the above solutions do not work, you can try moving the annotation to the method level and update the Validator to use the list of valid values for the OrderBy parameter. This worked for me. Below is the sample code.
#RestController
#RequestMapping("/api/opportunity")
#Validated
public class OpportunityController {
#GetMapping("/vendors/list")
public String getVendorpage(#RequestParam(required = false) String term,
#RequestParam(required = false) Integer page, #RequestParam(required = false) Integer size,
#ValuesAllowed(propName = "orderBy", values = { "OpportunityCount", "OpportunityPublishedCount", "ApplicationCount",
"ApplicationsApprovedCount" }) #RequestParam(required = false) String orderBy, #RequestParam(required = false) String sortDir) {
return "success";
}
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = { ValuesAllowed.Validator.class })
public #interface ValuesAllowed {
String message() default "Field value should be from list of ";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String propName();
String[] values();
class Validator implements ConstraintValidator<ValuesAllowed, String> {
private String propName;
private String message;
private List<String> allowable;
#Override
public void initialize(ValuesAllowed requiredIfChecked) {
this.propName = requiredIfChecked.propName();
this.message = requiredIfChecked.message();
this.allowable = Arrays.asList(requiredIfChecked.values());
}
public boolean isValid(String value, ConstraintValidatorContext context) {
Boolean valid = value == null || this.allowable.contains(value);
if (!valid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message.concat(this.allowable.toString()))
.addPropertyNode(this.propName).addConstraintViolation();
}
return valid;
}
}
}
You would have to change few things for this validation to work.
Controller should be annotated with #Validated and #ValuesAllowed should annotate the target parameter in method.
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
#Validated
#RestController
#RequestMapping("/api/opportunity")
public class OpportunityController {
#GetMapping("/vendors/list")
public String getVendorpage(
#RequestParam(required = false)
#ValuesAllowed(values = {
"OpportunityCount",
"OpportunityPublishedCount",
"ApplicationCount",
"ApplicationsApprovedCount"
}) String orderBy,
#RequestParam(required = false) String term,
#RequestParam(required = false) Integer page, #RequestParam(required = false) Integer size,
#RequestParam(required = false) String sortDir) {
return "OK";
}
}
#ValuesAllowed should target ElementType.PARAMETER and in this case you no longer need propName property because Spring will validate the desired param.
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target({ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {ValuesAllowedValidator.class})
public #interface ValuesAllowed {
String message() default "Field value should be from list of ";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] values();
}
Validator:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.List;
public class ValuesAllowedValidator implements ConstraintValidator<ValuesAllowed, String> {
private List<String> expectedValues;
private String returnMessage;
#Override
public void initialize(ValuesAllowed requiredIfChecked) {
expectedValues = Arrays.asList(requiredIfChecked.values());
returnMessage = requiredIfChecked.message().concat(expectedValues.toString());
}
#Override
public boolean isValid(String testValue, ConstraintValidatorContext context) {
boolean valid = expectedValues.contains(testValue);
if (!valid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(returnMessage)
.addConstraintViolation();
}
return valid;
}
}
But the code above returns HTTP 500 and pollutes logs with ugly stacktrace. To avoid it, you can put such #ExceptionHandler method in controller body (so it will be scoped only to this controller) and you gain control over HTTP status:
#ExceptionHandler(ConstraintViolationException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
String handleConstraintViolationException(ConstraintViolationException e) {
return "Validation error: " + e.getMessage();
}
... or you can put this method to the separate #ControllerAdvice class and have even more control over this validation like using it across all the controllers or only desired ones.
I found that I was missing this dependency after doing everything else. Regular validation steps were working but the custom validators didn't work until I added this to my pom.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Is there any way that we can use two custom error messages using spring-boot custom validation?

I'm using the below custom validation code to validate personName and it seems to be working fine, but the problem is when am passing an EMPTY string, it is giving same error message instead of the empty error message. Can someone please help me with this?
#Documented
#Constraint(validatedBy = {PersonNameValidator.class})
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
#Retention(RUNTIME)
#ReportAsSingleViolation
public #interface PersonName {
/**
* Default error message defined for the validator.
*
* #return message
*/
String message() default "invalid person name";
/**
* Method to define groups parameters for validation.
*
* #return groups
*/
Class[] groups() default {};
/**
* Method to load payload.
*
* #return payload
*/
Class<? extends Payload>[] payload() default {};
}
public class PersonNameValidator implements ConstraintValidator<PersonName, String> {
#Override
public boolean isValid(String name, ConstraintValidatorContext context) {
if (name.length() == 0) {
throw new IllegalArgumentException("must not be Empty");
} else if (!name.matches("(?=^(?!\\s*$).+)(^[^±!#£$%^&*_+§€#¢§¶•«\\\\/<>?:;|=]{1,256}$)")) {
throw new IllegalArgumentException("name should start with uppercase.");
}
return true;
}
}
#Data
public class NameDto {
#NotNull
#PersonName
private String family1Name;
#PersonName
private String family2Name;
#NotNull
#PersonName
private String givenName;
#PersonName
private String middleName;
}
Getting NullPointerException
#Name
#NotEmpty(message = "name cannot be empty")
String name;
should work
but if you want to join constraint you should use a custom ConstraintValidator add provide this validator via #Constraint(validatedBy = {YourCustomValidator.class}
see example below
full example
used dependency spring-boot-starter-validation (not needed if you use spring-boot-starter-web)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
application.properties
upper.name=dirk
application
package stackoverflow.demo;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
#SpringBootApplication
public class SoCustomValidationApplication {
public static void main(String[] args) {
SpringApplication.run(SoCustomValidationApplication.class, args);
}
}
#Component
class ConfigurationLoader{
final MyCustomValidatedProperties config;
ConfigurationLoader(MyCustomValidatedProperties config){
this.config = config;
}
#EventListener()
void showName() {
System.err.println("name is: " + config.getName());
}
}
#org.springframework.context.annotation.Configuration
#Validated
#ConfigurationProperties(prefix = "upper")
class MyCustomValidatedProperties {
#Uppercase
#NotEmpty(message = "name cannot be empty")
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
#Constraint(validatedBy = {ValidNameValidator.class})
#Target({ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
#ReportAsSingleViolation
#interface Uppercase {
String message() default "name should start with uppercase";
Class[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
class ValidNameValidator implements ConstraintValidator<Uppercase, String> {
#Override
public boolean isValid(String name, ConstraintValidatorContext context) {
if (null == name || 0 == name.length() ) {
throw new IllegalArgumentException("name cannot be empty.");
} else if(!name.matches("^([A-Z][a-z]+)")) {
throw new IllegalArgumentException("name should start with uppercase.");
}
return true;
}
}
APPLICATION FAILED TO START
Description:
Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'upper' to stackoverflow.demo.MyCustomValidatedProperties$$EnhancerBySpringCGLIB$$d0094cdb failed:
Property: upper.name
Value: dirk
Origin: class path resource [application.properties]:1:12
Reason: name should start with uppercase
and if you leave upper.name empty
upper.name=
APPLICATION FAILED TO START
Description:
Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'upper' to stackoverflow.demo.MyCustomValidatedProperties$$EnhancerBySpringCGLIB$$29925f50 failed:
Property: upper.name
Value:
Origin: class path resource [application.properties]:1:12
Reason: name cannot be empty
Property: upper.name
Value:
Origin: class path resource [application.properties]:1:12
Reason: name should start with uppercase

<form:checkbox> hidden value messing my form?

i have a field in a form and its bound to an user class, in the form its this field
<tr>
<td>AcceptTerms:</td>
<td><form:checkbox path="acceptTerms"/><td>
</tr>
but at the moment of submitting the form an error arises
The request sent by the client was syntactically incorrect.
i inspected it with firebug and got this
Parameters application/x-www-form-urlencoded
_acceptTerms on
email ZX
password ZXZX
rol 2
username ZxZ
Fuente
username=ZxZ&password=ZXZX&email=ZX&rol=2&_acceptTerms=on
the _acceptTerms its a hidden value for the checkbox that as i assume it a default of spring tag <form:checkbox>.
this arises 3 questions for me:
does this field (_acceptTerms) mess with my form submit?
why is _acceptTerms always set to on?
if this field does indeed mess up my form how can i get rid of it or how should i process it? add to the user model?
thanks in advance
Edit
package com.carloscortina.paidosSimple.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import com.carloscortina.paidosSimple.model.Categoria;
import com.carloscortina.paidosSimple.model.Personal;
import com.carloscortina.paidosSimple.model.Usuario;
import com.carloscortina.paidosSimple.service.CategoriaService;
import com.carloscortina.paidosSimple.service.PersonalService;
import com.carloscortina.paidosSimple.service.UsuarioService;
#Controller
public class PersonalController {
private static final Logger logger = LoggerFactory.getLogger(PersonalController.class);
#Autowired
private PersonalService personalService;
#Autowired
private UsuarioService usuarioService;
#Autowired
private CategoriaService categoriaService;
#RequestMapping(value="/usuario/add")
public ModelAndView addUsuarioPage(){
ModelAndView modelAndView = new ModelAndView("add-usuario-form");
modelAndView.addObject("categorias",categorias());
modelAndView.addObject("user", new Usuario());
return modelAndView;
}
#RequestMapping(value="/usuario/add/process",method=RequestMethod.POST)
public ModelAndView addingUsuario(#ModelAttribute Usuario user){
ModelAndView modelAndView = new ModelAndView("add-personal-form");
usuarioService.addUsuario(user);
logger.info(modelAndView.toString());
String message= "Usuario Agregado Correctamente.";
modelAndView.addObject("message",message);
modelAndView.addObject("staff",new Personal());
return modelAndView;
}
#RequestMapping(value="/personal/list")
public ModelAndView listOfPersonal(){
ModelAndView modelAndView = new ModelAndView("list-of-personal");
List<Personal> staffMembers = personalService.getAllPersonal();
logger.info(staffMembers.get(0).getpNombre());
modelAndView.addObject("staffMembers",staffMembers);
return modelAndView;
}
#RequestMapping(value="/personal/edit/{id}",method=RequestMethod.GET)
public ModelAndView editPersonalPage(#PathVariable int id){
ModelAndView modelAndView = new ModelAndView("edit-personal-form");
Personal staff = personalService.getPersonal(id);
logger.info(staff.getpNombre());
modelAndView.addObject("staff",staff);
return modelAndView;
}
#RequestMapping(value="/personal/edit/{id}", method=RequestMethod.POST)
public ModelAndView edditingPersonal(#ModelAttribute Personal staff, #PathVariable int id) {
ModelAndView modelAndView = new ModelAndView("home");
personalService.updatePersonal(staff);
String message = "Personal was successfully edited.";
modelAndView.addObject("message", message);
return modelAndView;
}
#RequestMapping(value="/personal/delete/{id}", method=RequestMethod.GET)
public ModelAndView deletePersonal(#PathVariable int id) {
ModelAndView modelAndView = new ModelAndView("home");
personalService.deletePersonal(id);
String message = "Personal was successfully deleted.";
modelAndView.addObject("message", message);
return modelAndView;
}
private Map<String,String> categorias(){
List<Categoria> lista = categoriaService.getCategorias();
Map<String,String> categorias = new HashMap<String, String>();
for (Categoria categoria : lista) {
categorias.put(Integer.toString(categoria.getId()), categoria.getCategoria());
}
return categorias;
}
Form.jsp
<%# taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<head>
<title>Add personal page</title>
</head>
<body>
<p>${message}<br>
<h1>Add User Page</h1>
<p>Here you can add a new staff member.</p>
<form:form method="POST" commandName="user" action="${pageContext.request.contextPath}/usuario/add/process" >
<table>
<tbody>
<tr>
<td>Nombre de Usuario:</td>
<td><form:input path="username"/><td>
</tr>
<tr>
<td>Contraseña:</td>
<td><form:password path="password"/><td>
</tr>
<tr>
<td>Email:</td>
<td><form:input path="email"/><td>
</tr>
<tr>
<td>Categoria:</td>
<td><form:select path="rol">
<form:options items="${categorias}" />
</form:select><td>
</tr>
<tr>
<td>AcceptTerms:</td>
<td><form:checkbox path="acceptTerms"/><td>
</tr>
<tr>
<td><input type="submit" value="Registrar"><td>
</tr>
</tbody>
</table>
</form:form>
<p>Home Page</p>
</body>
Log
DEBUG: org.springframework.web.servlet.DispatcherServlet - DispatcherServlet with name 'appServlet' processing POST request for [/paidosSimple/usuario/add/process]
DEBUG: org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Looking up handler method for path /usuario/add/process
DEBUG: org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - Returning handler method [public org.springframework.web.servlet.ModelAndView com.carloscortina.paidosSimple.controller.PersonalController.addingUsuario(com.carloscortina.paidosSimple.model.Usuario)]
DEBUG: org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'personalController'
DEBUG: org.springframework.beans.BeanUtils - No property editor [com.carloscortina.paidosSimple.model.CategoriaEditor] found for type com.carloscortina.paidosSimple.model.Categoria according to 'Editor' suffix convention
DEBUG: org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - Resolving exception from handler [public org.springframework.web.servlet.ModelAndView com.carloscortina.paidosSimple.controller.PersonalController.addingUsuario(com.carloscortina.paidosSimple.model.Usuario)]: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'usuario' on field 'rol': rejected value [2]; codes [typeMismatch.usuario.rol,typeMismatch.rol,typeMismatch.com.carloscortina.paidosSimple.model.Categoria,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [usuario.rol,rol]; arguments []; default message [rol]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'com.carloscortina.paidosSimple.model.Categoria' for property 'rol'; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [com.carloscortina.paidosSimple.model.Categoria] for property 'rol': no matching editors or conversion strategy found]
DEBUG: org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver - Resolving exception from handler [public org.springframework.web.servlet.ModelAndView com.carloscortina.paidosSimple.controller.PersonalController.addingUsuario(com.carloscortina.paidosSimple.model.Usuario)]: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'usuario' on field 'rol': rejected value [2]; codes [typeMismatch.usuario.rol,typeMismatch.rol,typeMismatch.com.carloscortina.paidosSimple.model.Categoria,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [usuario.rol,rol]; arguments []; default message [rol]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'com.carloscortina.paidosSimple.model.Categoria' for property 'rol'; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [com.carloscortina.paidosSimple.model.Categoria] for property 'rol': no matching editors or conversion strategy found]
DEBUG: org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver - Resolving exception from handler [public org.springframework.web.servlet.ModelAndView com.carloscortina.paidosSimple.controller.PersonalController.addingUsuario(com.carloscortina.paidosSimple.model.Usuario)]: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'usuario' on field 'rol': rejected value [2]; codes [typeMismatch.usuario.rol,typeMismatch.rol,typeMismatch.com.carloscortina.paidosSimple.model.Categoria,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [usuario.rol,rol]; arguments []; default message [rol]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'com.carloscortina.paidosSimple.model.Categoria' for property 'rol'; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [com.carloscortina.paidosSimple.model.Categoria] for property 'rol': no matching editors or conversion strategy found]
DEBUG: org.springframework.web.servlet.DispatcherServlet - Null ModelAndView returned to DispatcherServlet with name 'appServlet': assuming HandlerAdapter completed request handling
DEBUG: org.springframework.web.servlet.DispatcherServlet - Successfully completed request
Usuario.java
package com.carloscortina.paidosSimple.model;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.OneToOne;
import javax.persistence.Table;
#Entity
#Table(name="Usuario")
public class Usuario {
private Integer id;
private String username,password,email;
boolean acceptTerms = false,active=true;
private Categoria rol;
/**
* #return the id
*/
#Id
#GeneratedValue
#Column(name="id")
public Integer getId() {
return id;
}
/**
* #return the rol
*/
#OneToOne(cascade=CascadeType.ALL)
#JoinTable(name="usuario_rol",
joinColumns={#JoinColumn(name="Usuario",referencedColumnName="id")},
inverseJoinColumns={#JoinColumn(name="Rol",referencedColumnName="id")})
public Categoria getRol() {
return rol;
}
/**
* #param rol the rol to set
*/
public void setRol(Categoria rol) {
this.rol = rol;
}
/**
* #param id the id to set
*/
public void setId(Integer id) {
this.id = id;
}
/**
* #return the username
*/
#Column(name="Username")
public String getUsername() {
return username;
}
/**
* #param username the username to set
*/
public void setUsername(String username) {
this.username = username;
}
/**
* #return the password
*/
#Column(name="Password")
public String getPassword() {
return password;
}
/**
* #param password the password to set
*/
public void setPassword(String password) {
this.password = password;
}
/**
* #return the email
*/
#Column(name="Email")
public String getEmail() {
return email;
}
/**
* #param email the email to set
*/
public void setEmail(String email) {
this.email = email;
}
/**
* #return the acceptTerms
*/
#Column(name="acceptTerms")
public boolean isAcceptTerms() {
return acceptTerms;
}
/**
* #param acceptTerms the acceptTerms to set
*/
public void setAcceptTerms(boolean acceptTerms) {
this.acceptTerms = acceptTerms;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
/* (non-Javadoc)
* #see java.lang.Object#toString()
*/
#Override
public String toString() {
return "Usuario [id=" + id + ", username=" + username + ", password="
+ password + ", email=" + email + ", acceptTerms="
+ acceptTerms + "]";
}
}
Categoria.java
package com.carloscortina.paidosSimple.model;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.OneToMany;
import javax.persistence.Table;
#Entity
#Table(name="Categoria")
public class Categoria {
private int id;
private String categoria,descripcion;
private Set<Usuario> UserRoles;
/**
* #return the userRoles
*/
#OneToMany(cascade=CascadeType.ALL)
#JoinTable(name="usuario_rol",
joinColumns={#JoinColumn(name="Categoria",referencedColumnName="id")},
inverseJoinColumns={#JoinColumn(name="Usuario",referencedColumnName="id")})
public Set<Usuario> getUserRoles() {
return UserRoles;
}
/**
* #param userRoles the userRoles to set
*/
public void setUserRoles(Set<Usuario> userRoles) {
UserRoles = userRoles;
}
/**
* #return the id
*/
#Id
#GeneratedValue
public int getId() {
return id;
}
/**
* #param id the id to set
*/
public void setId(int id) {
this.id = id;
}
/**
* #return the categoria
*/
public String getCategoria() {
return categoria;
}
/**
* #param categoria the categoria to set
*/
public void setCategoria(String categoria) {
this.categoria = categoria;
}
/**
* #return the descripcion
*/
public String getDescripcion() {
return descripcion;
}
/**
* #param descripcion the descripcion to set
*/
public void setDescripcion(String descripcion) {
this.descripcion = descripcion;
}
Have you checked if your form submission works if you remove form:checbox, to ensure that indeed this checkbox is causing the issue
can you share debug logs of server
Also share your controller method that is hadnling this form submission including #requestMapping value
you can add a custom binder to convert rol into category
Create a custom property editor
public class CategoryEditor extends PropertyEditorSupport {
#Override
public void setAsText(String text) {
Category cat = new Category ();
//set rol vlaue in cat
setValue(cat);
}
now use following method in your controller
#InitBinder
public void initBinderAll(WebDataBinder binder) {
binder.registerCustomEditor(Category .class, new CategoryEditor ()); }

Cross field validation with Hibernate Validator (JSR 303)

Is there an implementation of (or third-party implementation for) cross field validation in Hibernate Validator 4.x? If not, what is the cleanest way to implement a cross field validator?
As an example, how can you use the API to validate two bean properties are equal (such as validating a password field matches the password verify field).
In annotations, I'd expect something like:
public class MyBean {
#Size(min=6, max=50)
private String pass;
#Equals(property="pass")
private String passVerify;
}
Each field constraint should be handled by a distinct validator annotation, or in other words it's not suggested practice to have one field's validation annotation checking against other fields; cross-field validation should be done at the class level. Additionally, the JSR-303 Section 2.2 preferred way to express multiple validations of the same type is via a list of annotations. This allows the error message to be specified per match.
For example, validating a common form:
#FieldMatch.List({
#FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
#FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")
})
public class UserRegistrationForm {
#NotNull
#Size(min=8, max=25)
private String password;
#NotNull
#Size(min=8, max=25)
private String confirmPassword;
#NotNull
#Email
private String email;
#NotNull
#Email
private String confirmEmail;
}
The Annotation:
package constraints;
import constraints.impl.FieldMatchValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
/**
* Validation annotation to validate that 2 fields have the same value.
* An array of fields and their matching confirmation fields can be supplied.
*
* Example, compare 1 pair of fields:
* #FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match")
*
* Example, compare more than 1 pair of fields:
* #FieldMatch.List({
* #FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
* #FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")})
*/
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = FieldMatchValidator.class)
#Documented
public #interface FieldMatch
{
String message() default "{constraints.fieldmatch}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* #return The first field
*/
String first();
/**
* #return The second field
*/
String second();
/**
* Defines several <code>#FieldMatch</code> annotations on the same element
*
* #see FieldMatch
*/
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Documented
#interface List
{
FieldMatch[] value();
}
}
The Validator:
package constraints.impl;
import constraints.FieldMatch;
import org.apache.commons.beanutils.BeanUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object>
{
private String firstFieldName;
private String secondFieldName;
#Override
public void initialize(final FieldMatch constraintAnnotation)
{
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
}
#Override
public boolean isValid(final Object value, final ConstraintValidatorContext context)
{
try
{
final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
final Object secondObj = BeanUtils.getProperty(value, secondFieldName);
return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
}
catch (final Exception ignore)
{
// ignore
}
return true;
}
}
I suggest you another possible solution. Perhaps less elegant, but easier!
public class MyBean {
#Size(min=6, max=50)
private String pass;
private String passVerify;
#NotNull
private LocalDate passExpiry;
#NotNull
private LocalDate dateOfJoining;
#AssertTrue(message = "Fields `pass` and `passVerify` should be equal")
// Any method name is ok als long it begins with `is`
private boolean isValidPass() {
//return pass == null && passVerify == null || pass.equals(passVerify);
// Since Java 7:
return Objects.equals(pass, passVerify);
}
#AssertTrue(message = "Field `passExpiry` should be later than `dateOfJoining`")
// Other rules can also be validated in other methods
private boolean isPassExpiryAfterDateOfJoining() {
return dateOfJoining.isBefore(passExpiry);
}
}
The isValid() and isPassExpiryAfterDateOfJoining() methods are invoked automatically by the validator. The property paths reported in the ConstraintViolations will be extracted from the method names: valid and passExpiryAfterDateOfJoining.
I'm surprised this isn't available out of the box. Anyway, here is a possible solution.
I've created a class level validator, not the field level as described in the original question.
Here is the annotation code:
package com.moa.podium.util.constraints;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = MatchesValidator.class)
#Documented
public #interface Matches {
String message() default "{com.moa.podium.util.constraints.matches}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String field();
String verifyField();
}
And the validator itself:
package com.moa.podium.util.constraints;
import org.mvel2.MVEL;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class MatchesValidator implements ConstraintValidator<Matches, Object> {
private String field;
private String verifyField;
public void initialize(Matches constraintAnnotation) {
this.field = constraintAnnotation.field();
this.verifyField = constraintAnnotation.verifyField();
}
public boolean isValid(Object value, ConstraintValidatorContext context) {
Object fieldObj = MVEL.getProperty(field, value);
Object verifyFieldObj = MVEL.getProperty(verifyField, value);
boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null);
if (neitherSet) {
return true;
}
boolean matches = (fieldObj != null) && fieldObj.equals(verifyFieldObj);
if (!matches) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("message")
.addNode(verifyField)
.addConstraintViolation();
}
return matches;
}
}
Note that I've used MVEL to inspect the properties of the object being validated. This could be replaced with the standard reflection APIs or if it is a specific class you are validating, the accessor methods themselves.
The #Matches annotation can then be used used on a bean as follows:
#Matches(field="pass", verifyField="passRepeat")
public class AccountCreateForm {
#Size(min=6, max=50)
private String pass;
private String passRepeat;
...
}
As a disclaimer, I wrote this in the last 5 minutes, so I probably haven't ironed out all the bugs yet. I'll update the answer if anything goes wrong.
With Hibernate Validator 4.1.0.Final I recommend using #ScriptAssert. Exceprt from its JavaDoc:
Script expressions can be written in any scripting or expression
language, for which a JSR 223 ("Scripting for the JavaTM Platform")
compatible engine can be found on the classpath.
Note: the evaluation is being performed by a scripting "engine" running in the Java VM, therefore on Java "server side", not on "client side" as stated in some comments.
Example:
#ScriptAssert(lang = "javascript", script = "_this.passVerify.equals(_this.pass)")
public class MyBean {
#Size(min=6, max=50)
private String pass;
private String passVerify;
}
or with shorter alias and null-safe:
#ScriptAssert(lang = "javascript", alias = "_",
script = "_.passVerify != null && _.passVerify.equals(_.pass)")
public class MyBean {
#Size(min=6, max=50)
private String pass;
private String passVerify;
}
or with Java 7+ null-safe Objects.equals():
#ScriptAssert(lang = "javascript", script = "Objects.equals(_this.passVerify, _this.pass)")
public class MyBean {
#Size(min=6, max=50)
private String pass;
private String passVerify;
}
Nevertheless, there is nothing wrong with a custom class level validator #Matches solution.
Cross fields validations can be done by creating custom constraints.
Example:- Compare password and confirmPassword fields of User instance.
CompareStrings
#Target({TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy=CompareStringsValidator.class)
#Documented
public #interface CompareStrings {
String[] propertyNames();
StringComparisonMode matchMode() default EQUAL;
boolean allowNull() default false;
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
StringComparisonMode
public enum StringComparisonMode {
EQUAL, EQUAL_IGNORE_CASE, NOT_EQUAL, NOT_EQUAL_IGNORE_CASE
}
CompareStringsValidator
public class CompareStringsValidator implements ConstraintValidator<CompareStrings, Object> {
private String[] propertyNames;
private StringComparisonMode comparisonMode;
private boolean allowNull;
#Override
public void initialize(CompareStrings constraintAnnotation) {
this.propertyNames = constraintAnnotation.propertyNames();
this.comparisonMode = constraintAnnotation.matchMode();
this.allowNull = constraintAnnotation.allowNull();
}
#Override
public boolean isValid(Object target, ConstraintValidatorContext context) {
boolean isValid = true;
List<String> propertyValues = new ArrayList<String> (propertyNames.length);
for(int i=0; i<propertyNames.length; i++) {
String propertyValue = ConstraintValidatorHelper.getPropertyValue(String.class, propertyNames[i], target);
if(propertyValue == null) {
if(!allowNull) {
isValid = false;
break;
}
} else {
propertyValues.add(propertyValue);
}
}
if(isValid) {
isValid = ConstraintValidatorHelper.isValid(propertyValues, comparisonMode);
}
if (!isValid) {
/*
* if custom message was provided, don't touch it, otherwise build the
* default message
*/
String message = context.getDefaultConstraintMessageTemplate();
message = (message.isEmpty()) ? ConstraintValidatorHelper.resolveMessage(propertyNames, comparisonMode) : message;
context.disableDefaultConstraintViolation();
ConstraintViolationBuilder violationBuilder = context.buildConstraintViolationWithTemplate(message);
for (String propertyName : propertyNames) {
NodeBuilderDefinedContext nbdc = violationBuilder.addNode(propertyName);
nbdc.addConstraintViolation();
}
}
return isValid;
}
}
ConstraintValidatorHelper
public abstract class ConstraintValidatorHelper {
public static <T> T getPropertyValue(Class<T> requiredType, String propertyName, Object instance) {
if(requiredType == null) {
throw new IllegalArgumentException("Invalid argument. requiredType must NOT be null!");
}
if(propertyName == null) {
throw new IllegalArgumentException("Invalid argument. PropertyName must NOT be null!");
}
if(instance == null) {
throw new IllegalArgumentException("Invalid argument. Object instance must NOT be null!");
}
T returnValue = null;
try {
PropertyDescriptor descriptor = new PropertyDescriptor(propertyName, instance.getClass());
Method readMethod = descriptor.getReadMethod();
if(readMethod == null) {
throw new IllegalStateException("Property '" + propertyName + "' of " + instance.getClass().getName() + " is NOT readable!");
}
if(requiredType.isAssignableFrom(readMethod.getReturnType())) {
try {
Object propertyValue = readMethod.invoke(instance);
returnValue = requiredType.cast(propertyValue);
} catch (Exception e) {
e.printStackTrace(); // unable to invoke readMethod
}
}
} catch (IntrospectionException e) {
throw new IllegalArgumentException("Property '" + propertyName + "' is NOT defined in " + instance.getClass().getName() + "!", e);
}
return returnValue;
}
public static boolean isValid(Collection<String> propertyValues, StringComparisonMode comparisonMode) {
boolean ignoreCase = false;
switch (comparisonMode) {
case EQUAL_IGNORE_CASE:
case NOT_EQUAL_IGNORE_CASE:
ignoreCase = true;
}
List<String> values = new ArrayList<String> (propertyValues.size());
for(String propertyValue : propertyValues) {
if(ignoreCase) {
values.add(propertyValue.toLowerCase());
} else {
values.add(propertyValue);
}
}
switch (comparisonMode) {
case EQUAL:
case EQUAL_IGNORE_CASE:
Set<String> uniqueValues = new HashSet<String> (values);
return uniqueValues.size() == 1 ? true : false;
case NOT_EQUAL:
case NOT_EQUAL_IGNORE_CASE:
Set<String> allValues = new HashSet<String> (values);
return allValues.size() == values.size() ? true : false;
}
return true;
}
public static String resolveMessage(String[] propertyNames, StringComparisonMode comparisonMode) {
StringBuffer buffer = concatPropertyNames(propertyNames);
buffer.append(" must");
switch(comparisonMode) {
case EQUAL:
case EQUAL_IGNORE_CASE:
buffer.append(" be equal");
break;
case NOT_EQUAL:
case NOT_EQUAL_IGNORE_CASE:
buffer.append(" not be equal");
break;
}
buffer.append('.');
return buffer.toString();
}
private static StringBuffer concatPropertyNames(String[] propertyNames) {
//TODO improve concating algorithm
StringBuffer buffer = new StringBuffer();
buffer.append('[');
for(String propertyName : propertyNames) {
char firstChar = Character.toUpperCase(propertyName.charAt(0));
buffer.append(firstChar);
buffer.append(propertyName.substring(1));
buffer.append(", ");
}
buffer.delete(buffer.length()-2, buffer.length());
buffer.append("]");
return buffer;
}
}
User
#CompareStrings(propertyNames={"password", "confirmPassword"})
public class User {
private String password;
private String confirmPassword;
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getConfirmPassword() { return confirmPassword; }
public void setConfirmPassword(String confirmPassword) { this.confirmPassword = confirmPassword; }
}
Test
public void test() {
User user = new User();
user.setPassword("password");
user.setConfirmPassword("paSSword");
Set<ConstraintViolation<User>> violations = beanValidator.validate(user);
for(ConstraintViolation<User> violation : violations) {
logger.debug("Message:- " + violation.getMessage());
}
Assert.assertEquals(violations.size(), 1);
}
Output Message:- [Password, ConfirmPassword] must be equal.
By using the CompareStrings validation constraint, we can also compare more than two properties and we can mix any of four string comparison methods.
ColorChoice
#CompareStrings(propertyNames={"color1", "color2", "color3"}, matchMode=StringComparisonMode.NOT_EQUAL, message="Please choose three different colors.")
public class ColorChoice {
private String color1;
private String color2;
private String color3;
......
}
Test
ColorChoice colorChoice = new ColorChoice();
colorChoice.setColor1("black");
colorChoice.setColor2("white");
colorChoice.setColor3("white");
Set<ConstraintViolation<ColorChoice>> colorChoiceviolations = beanValidator.validate(colorChoice);
for(ConstraintViolation<ColorChoice> violation : colorChoiceviolations) {
logger.debug("Message:- " + violation.getMessage());
}
Output Message:- Please choose three different colors.
Similarly, we can have CompareNumbers, CompareDates, etc cross-fields validation constraints.
P.S. I have not tested this code under production environment (though I tested it under dev environment), so consider this code as Milestone Release. If you find a bug, please write a nice comment. :)
If you’re using the Spring Framework then you can use the Spring Expression Language (SpEL) for that. I’ve wrote a small library that provides JSR-303 validator based on SpEL – it makes cross-field validations a breeze! Take a look at https://github.com/jirutka/validator-spring.
This will validate length and equality of the password fields.
#SpELAssert(value = "pass.equals(passVerify)",
message = "{validator.passwords_not_same}")
public class MyBean {
#Size(min = 6, max = 50)
private String pass;
private String passVerify;
}
You can also easily modify this to validate the password fields only when not both empty.
#SpELAssert(value = "pass.equals(passVerify)",
applyIf = "pass || passVerify",
message = "{validator.passwords_not_same}")
public class MyBean {
#Size(min = 6, max = 50)
private String pass;
private String passVerify;
}
I have tried Alberthoven's example (hibernate-validator 4.0.2.GA) and i get an ValidationException: „Annotated methods must follow the JavaBeans naming convention. match() does not.“ too. After I renamed the method from „match“ to "isValid" it works.
public class Password {
private String password;
private String retypedPassword;
public Password(String password, String retypedPassword) {
super();
this.password = password;
this.retypedPassword = retypedPassword;
}
#AssertTrue(message="password should match retyped password")
private boolean isValid(){
if (password == null) {
return retypedPassword == null;
} else {
return password.equals(retypedPassword);
}
}
public String getPassword() {
return password;
}
public String getRetypedPassword() {
return retypedPassword;
}
}
I like the idea from Jakub Jirutka to use Spring Expression Language. If you don't want to add another library/dependency (assuming that you already use Spring), here is a simplified implementation of his idea.
The constraint:
#Constraint(validatedBy=ExpressionAssertValidator.class)
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface ExpressionAssert {
String message() default "expression must evaluate to true";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String value();
}
The validator:
public class ExpressionAssertValidator implements ConstraintValidator<ExpressionAssert, Object> {
private Expression exp;
public void initialize(ExpressionAssert annotation) {
ExpressionParser parser = new SpelExpressionParser();
exp = parser.parseExpression(annotation.value());
}
public boolean isValid(Object value, ConstraintValidatorContext context) {
return exp.getValue(value, Boolean.class);
}
}
Apply like this:
#ExpressionAssert(value="pass == passVerify", message="passwords must be same")
public class MyBean {
#Size(min=6, max=50)
private String pass;
private String passVerify;
}
I made a small adaptation in Nicko's solution so that it is not necessary to use the Apache Commons BeanUtils library and replace it with the solution already available in spring, for those using it as I can be simpler:
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
private String firstFieldName;
private String secondFieldName;
#Override
public void initialize(final FieldMatch constraintAnnotation) {
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
}
#Override
public boolean isValid(final Object object, final ConstraintValidatorContext context) {
BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object);
final Object firstObj = beanWrapper.getPropertyValue(firstFieldName);
final Object secondObj = beanWrapper.getPropertyValue(secondFieldName);
boolean isValid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
if (!isValid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode(firstFieldName)
.addConstraintViolation();
}
return isValid;
}
}
I don't have the reputation for commenting on the first answer but wanted to add that I have added unit tests for the winning answer and have the following observations:
If you get the first or field names wrong then you get a validation error as though the values don't match. Don't get tripped up by spelling mistakes e.g.
#FieldMatch(first="invalidFieldName1", second="validFieldName2")
The validator will accept equivalent data types i.e. these will all pass with FieldMatch:
private String stringField = "1";
private Integer integerField = new Integer(1)
private int intField = 1;
If the fields are of an object type which does not implement equals, the validation will fail.
Very nice solution bradhouse. Is there any way to apply the #Matches annotation to more than one field?
EDIT:
Here's the solution I came up with to answer this question, I modified the Constraint to accept an array instead of a single value:
#Matches(fields={"password", "email"}, verifyFields={"confirmPassword", "confirmEmail"})
public class UserRegistrationForm {
#NotNull
#Size(min=8, max=25)
private String password;
#NotNull
#Size(min=8, max=25)
private String confirmPassword;
#NotNull
#Email
private String email;
#NotNull
#Email
private String confirmEmail;
}
The code for the annotation:
package springapp.util.constraints;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = MatchesValidator.class)
#Documented
public #interface Matches {
String message() default "{springapp.util.constraints.matches}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] fields();
String[] verifyFields();
}
And the implementation:
package springapp.util.constraints;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.commons.beanutils.BeanUtils;
public class MatchesValidator implements ConstraintValidator<Matches, Object> {
private String[] fields;
private String[] verifyFields;
public void initialize(Matches constraintAnnotation) {
fields = constraintAnnotation.fields();
verifyFields = constraintAnnotation.verifyFields();
}
public boolean isValid(Object value, ConstraintValidatorContext context) {
boolean matches = true;
for (int i=0; i<fields.length; i++) {
Object fieldObj, verifyFieldObj;
try {
fieldObj = BeanUtils.getProperty(value, fields[i]);
verifyFieldObj = BeanUtils.getProperty(value, verifyFields[i]);
} catch (Exception e) {
//ignore
continue;
}
boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null);
if (neitherSet) {
continue;
}
boolean tempMatches = (fieldObj != null) && fieldObj.equals(verifyFieldObj);
if (!tempMatches) {
addConstraintViolation(context, fields[i]+ " fields do not match", verifyFields[i]);
}
matches = matches?tempMatches:matches;
}
return matches;
}
private void addConstraintViolation(ConstraintValidatorContext context, String message, String field) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message).addNode(field).addConstraintViolation();
}
}
You need to call it explicitly. In the example above, bradhouse has given you all the steps to write a custom constraint.
Add this code in your caller class.
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
Set<ConstraintViolation<yourObjectClass>> constraintViolations = validator.validate(yourObject);
in the above case it would be
Set<ConstraintViolation<AccountCreateForm>> constraintViolations = validator.validate(objAccountCreateForm);
Why not try Oval: http://oval.sourceforge.net/
I looks like it supports OGNL so maybe you could do it by a more natural
#Assert(expr = "_value ==_this.pass").
You guys are awesome. Really amazing ideas. I like Alberthoven's and McGin's most, so I decided to combine both ideas. And develop some generic solution to cater all cases. Here is my proposed solution.
#Documented
#Constraint(validatedBy = NotFalseValidator.class)
#Target({ElementType.METHOD, ElementType.FIELD,ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface NotFalse {
String message() default "NotFalse";
String[] messages();
String[] properties();
String[] verifiers();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class NotFalseValidator implements ConstraintValidator<NotFalse, Object> {
private String[] properties;
private String[] messages;
private String[] verifiers;
#Override
public void initialize(NotFalse flag) {
properties = flag.properties();
messages = flag.messages();
verifiers = flag.verifiers();
}
#Override
public boolean isValid(Object bean, ConstraintValidatorContext cxt) {
if(bean == null) {
return true;
}
boolean valid = true;
BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
for(int i = 0; i< properties.length; i++) {
Boolean verified = (Boolean) beanWrapper.getPropertyValue(verifiers[i]);
valid &= isValidProperty(verified,messages[i],properties[i],cxt);
}
return valid;
}
boolean isValidProperty(Boolean flag,String message, String property, ConstraintValidatorContext cxt) {
if(flag == null || flag) {
return true;
} else {
cxt.disableDefaultConstraintViolation();
cxt.buildConstraintViolationWithTemplate(message)
.addPropertyNode(property)
.addConstraintViolation();
return false;
}
}
}
#NotFalse(
messages = {"End Date Before Start Date" , "Start Date Before End Date" } ,
properties={"endDateTime" , "startDateTime"},
verifiers = {"validDateRange" , "validDateRange"})
public class SyncSessionDTO implements ControllableNode {
#NotEmpty #NotPastDate
private Date startDateTime;
#NotEmpty
private Date endDateTime;
public Date getStartDateTime() {
return startDateTime;
}
public void setStartDateTime(Date startDateTime) {
this.startDateTime = startDateTime;
}
public Date getEndDateTime() {
return endDateTime;
}
public void setEndDateTime(Date endDateTime) {
this.endDateTime = endDateTime;
}
public Boolean getValidDateRange(){
if(startDateTime != null && endDateTime != null) {
return startDateTime.getTime() <= endDateTime.getTime();
}
return null;
}
}
Solution realated with question:
How to access a field which is described in annotation property
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Match {
String field();
String message() default "";
}
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = MatchValidator.class)
#Documented
public #interface EnableMatchConstraint {
String message() default "Fields must match!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class MatchValidator implements ConstraintValidator<EnableMatchConstraint, Object> {
#Override
public void initialize(final EnableMatchConstraint constraint) {}
#Override
public boolean isValid(final Object o, final ConstraintValidatorContext context) {
boolean result = true;
try {
String mainField, secondField, message;
Object firstObj, secondObj;
final Class<?> clazz = o.getClass();
final Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Match.class)) {
mainField = field.getName();
secondField = field.getAnnotation(Match.class).field();
message = field.getAnnotation(Match.class).message();
if (message == null || "".equals(message))
message = "Fields " + mainField + " and " + secondField + " must match!";
firstObj = BeanUtils.getProperty(o, mainField);
secondObj = BeanUtils.getProperty(o, secondField);
result = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
if (!result) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message).addPropertyNode(mainField).addConstraintViolation();
break;
}
}
}
} catch (final Exception e) {
// ignore
//e.printStackTrace();
}
return result;
}
}
And how to use it...? Like this:
#Entity
#EnableMatchConstraint
public class User {
#NotBlank
private String password;
#Match(field = "password")
private String passwordConfirmation;
}

Resources