Las validaciones de campos cruzados se pueden realizar creando restricciones personalizadas.
Ejemplo: - Compare la contraseña y confirme los campos Contraseña de la instancia de Usuario.
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;
}
}
Restricción Validador Ayudante
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;
}
}
Usuario
@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; }
}
Prueba
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);
}
Salida Message:- [Password, ConfirmPassword] must be equal.
Al usar la restricción de validación CompareStrings, también podemos comparar más de dos propiedades y podemos mezclar cualquiera de los cuatro métodos de comparación de cadenas.
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;
......
}
Prueba
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());
}
Salida Message:- Please choose three different colors.
Del mismo modo, podemos tener restricciones de validación de campos cruzados CompareNumbers, CompareDates, etc.
PD: No he probado este código en un entorno de producción (aunque lo probé en un entorno de desarrollo), así que considere este código como lanzamiento de Milestone. Si encuentra un error, por favor escriba un buen comentario. :)