3
votes

I have an entity with a field of type String on which I defined a @Pattern annotation enforcing the content of the field matching a given regular expression. Now I wonder, whether it is possible to parameterize the constraint such as that the first character not matching the regular expression is shown in the defined validation error message.

To make it even worse, the error message to be shown for the constraint violation is not defined in the annotation directly, but within a properties file, as in the example shown below:

Example class:

public class Address {
  @Pattern(regexp="[a-zA-Z]*", message="paremeterizedMessage")
  private String street;
}

Example properties file:

parameterizedMessage = Invalid character {0}. Only characters a-z, A-Z allowed.

Is it possible to do such a thing with javax.validation? I assume that the @Pattern annotation is not able to parameterize error messages. But maybe it is possible to define my own ConstraintValidator with parameterized validation messages?

1

1 Answers

0
votes

Considering the fact that some validation annotations such as @Size(min=1,max=5) allow for parameterized error messages with their annotation parameters such as The String must have a length of {min} to {max} characters., I found a solution for parameterized error messages in general with place holders such as {0}, {1}, ...:

Define an own validation constraint such as:

@Constraint(validatedBy=OnlyCharactersAllowedValidator.class)
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OnlyCharactersAllowed {
    String message() default "parameterizedMessage";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Within the validator class, reflection can be used to extend a map that stores all the annotation parameters (such as min and max for @Size) and additional parameters can be added for the keys 0, 1 etc.:

public class OnlyCharactersAllowedValidator implements ConstraintValidator<OnlyCharactersAllowed,String>
{
    private static final String validCharacters = "abcdefghijklmnopqrstuvwxyz";
    @Override
    public boolean isValid(String text, ConstraintValidatorContext constraintContext)
    {
        for (int index = 0; index < text.length; index++) {
            String aCharacter = text.substring(index, index+1);
            if (validCharacters.indexOf(aCharacter) < 0) {
                /* Within this message call the magic happens: {0} is mapped to the invalid character. */
                addMessageParameter(constraintContext, aCharacter);
                /* Create violation message manually and suppress the default message. */   constraintContext.buildConstraintViolationWithTemplate(constraintContext.getDefaultConstraintMessageTemplate()).addConstraintViolation();
                constraintContext.disableDefaultConstraintViolation();
                /* No further validation: show only one error message for invalid characters. */
                break;
            }
        }
    }

    private void addMessageParameter(ConstraintValidatorContext constraintContext, Object... parameter)
    {
        try
        {
            /* Get map for parameters (reflection necessary). */
            Field descriptorField = ConstraintValidatorContextImpl.class.getDeclaredField("constraintDescriptor");
            descriptorField.setAccessible(true);
            @SuppressWarnings("unchecked")
            ConstraintDescriptorImpl<OnlyCharactersAllowed> descriptor = (ConstraintDescriptorImpl<OnlyCharactersAllowed>) descriptorField.get(constraintContext);

            Map<String,Object> attributes = descriptor.getAttributes();

            /* Copy immutable Map to new Map. */
            Map<String,Object> newAttributes = new HashMap<String,Object>(attributes.size() + parameter.length);
            for (String key : attributes.keySet())
            {
                newAttributes.put(key, attributes.get(key));
            }

            /* Add given parameters to attributes. */
            Integer parameterCounter = 0;
            for (Object param : parameter)
            {
                newAttributes.put(parameterCounter.toString(), param);
                parameterCounter++;
            }

            /* Set new Map in Descriptor (reflection necessary). */
            Field attributesField = ConstraintDescriptorImpl.class.getDeclaredField("attributes");
            attributesField.setAccessible(true);
            attributesField.set(descriptor, newAttributes);
        }
        catch (NoSuchFieldException | IllegalAccessException | SecurityException | ClassCastException e1)
        {
            /* Do nothing in case of exception. Unparameterized message is shown. */
        }
    }
}

This solution even works if your validation message is defined in a properties file such as.

parameterizedMessage = Invalid character: Do not use the {0} character.