Creating a Custom Constraint
Creating a Custom Constraint
Creating custom constraints enables you to validate objects against criteria specific to your needs. For example, you can require all passwords to include an uppercase letter, a lower case letter, and a digit.
To create a custom constraint, you need to define three items in your extension project:
Item | Definition |
---|---|
Validator | The validator contains the logic that describes an entity's validity criteria. |
Annotations | The custom annotation declares the annotation that calls the validator. |
Validation Messages | The ValidationMessages.properties file contains constraint violation messages. Bean Validation uses the first ValidationMessages.properties file it finds in the classpath. To ensure the out-of-the-box validation messages remain accessible, you must copy the original file located in ep-core\src\main\resources to your extension project's src/main/resources directory and add your new messages in the copy. |
General Information
Custom Annotations
To define a new Bean Validation annotation, create an @interface file with four annotations, @Target, @Retention, @Constraint, @Documented, and three attributes, message(), group(), payload():
package com.example.validation.constraints; ... import com.elasticpath.domain.validators.PersonAvailableValidator; @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = PersonAvailableValidator.class) @Documented public @interface PersonAvailable { String message() default "{com.example.validation.validators.impl.PersonAvailableValidator}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Standard Annotations:
- @Target - Defines the annotatable elements. The above example sets the @PersonAvailable annotation for type declarations, methods, fields, and other annotations.
- @Retention - Defines when the annotation applies. For Bean Validation annotations, this must be RUNTIME.
- @Constraint - Defines the annotation's validator. This example uses a custom validator named PersonAvailableValidator.
- @Documented - Includes the @PersonAvailable annotation on the Javadoc for all elements annotated with @PersonAvailable.
Standard attributes:
- message() - The message to display if this constraint is violated.
- groups() - The validation group associated with the constraint. According to the Bean Validation specification, you must give an empty array of type Class<?> as the default value.
- payload() - The constraint's payload. A payload is any additional metadata that should be associated with the constraint, such as the violation's severity rating. According to the Bean Validation specification, you must give an empty array of type Class<? extends Payload> as the default value.
To declare an attribute's default values, use the default keyword as shown in the example.
You assign groups or payload to the constraint by passing in a group or payload parameter when you apply the constraint to an element.
Validators
The validator determines an element's validity. To define a validator, create a class that implements the ConstraintValidator interface, which requires two parameters and two method implementations:
package com.example.validation.validators.impl; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; ... public class PersonAvailableValidator implements ConstraintValidator<PersonAvailable, Person> { ... public void initialize(PersonAvailable constraintAnnotation) { // do nothing } public boolean isValid(Person value, ConstraintValidatorContext context) { if(personService.isPersonOldEnough(value)) { return true; } final String message = "{com.example.validation.validators.impl.PersonAvailableValidator.age.message}"; context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(message).addNode("personAvailable").addConstraintViolation(); return false; } }
ConstraintValidator parameters:
- PersonAvailable - The custom annotation described in the preceding section.
- Person - The entity the custom validator verifies.
ConstraintValidator method implementations:
- initialize() - Sets the validator's fields with additional data from the annotation. If the annotation has no parameters, this method does nothing.
- isValid() - Validates the entity and returns TRUE or FALSE depending on the entity's validity.
A single validator's isValid() method can contain multiple conditions. You can return a different violation message for each condition by specifying custom messages. As you can see inside the example isValid() method, three tasks are done to specify a custom message:
- Define a string for the ValidationMessages.properties message.
- Stop the ConstraintValidator from returning the default message.
- Add the ValidationMessages.properties message to the constraint violations set. The constraint violations set is what the validate(object)method returns at the end of the validation process.
You don't have to specify a custom message if your validator returns only a single message. You can use the default message you define in the custom annotation.
ValidationMessages.properties
The ValidationMessages.properties file contains the constraint violation messages that Bean Validation returns when custom constraints are violated. In ValidationMessages.properties, define messages in key-value pairs. For example, the ValidationMessages.properties file for @PersonAvailable looks like this:
com.example.validation.validators.impl.PersonAvailableValidator.age.message=This person is not old enough.
You can create locale specific validation messages by appending an underscore and the locale prefix to the file name.
For example, to create French locale validation messages you create a file named ValidationMessages_fr.properties. For information on how the system determines which locale to use, see Section 4.3.1.1 of the Bean Validation Specification.
Bean Validation's current locale is dependent on the server's system locale.
Creating a Custom Constraint Tutorial
This tutorial gives you a closer look at the three components you need to define when creating a custom constraint.
Scenario
You want to customize the out of the box password constraints.
Out of the Box Password Constraints | Additional Password Constraints |
---|---|
8 characters minimum | Include an uppercase letter |
255 characters maximum | Include a lowercase letter |
no whitespaces | Include a digit |
not null for registered customers |
To add these additional constraints, you can create a custom Bean Validation constraint that matches customer passwords with regular expressions.
Code Organization
The tutorial source code is in an Elastic Path extension project:
- <extension_project_root>/src/main
- java
- com.example.validation
- constraints
- validators
- com.example.validation
- resources
- java
The Custom Annotation
The following @interface creates a Bean Validation annotation named @RegisteredCustomerPasswordWithRegex:
package com.example.validation.constraints; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; import com.example.validation.validators.impl.RegisteredCustomerPasswordWithRegexValidator; /** * * Additional validation on the OOTB CustomerPasswordCheck annotation. */ @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = RegisteredCustomerPasswordWithRegexValidator.class) @Documented public @interface RegisteredCustomerPasswordWithRegex { /** Regular expression to validate against on the password field. */ String regex(); /** Constraint violation message. */ String message() default "{com.example.validation.constraints.RegisteredCustomerPasswordWithRegex.message}"; /** Groups associated to this constraint. */ Class<?>[] groups() default { }; /** Payload for the constraint. */ Class<? extends Payload>[] payload() default { }; }
As you can see in RegisteredCustomerPasswordWithRegex, in addition to the three required attributes (message(), groups(), payload()), which are described in detail in the preceding Annotations section, we declare a regex() attribute to receive regular expressions. The regex() attribute is not given a default value. Instead, the value is passed in as a parameter when calling the annotation, which is shown later in Applying a Custom Constraint.
The Validator
The validator applies regular expressions to a customer password:
package com.example.validation.validators.impl; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import com.elasticpath.domain.customer.Customer; import com.example.validation.constraints.RegisteredCustomerPasswordWithRegex; /** * Add some extra validation to the OOTB CustomerPasswordValidator. */ public class RegisteredCustomerPasswordWithRegexValidator implements ConstraintValidator<RegisteredCustomerPasswordWithRegex, Customer> { private String regex; @Override public void initialize(final RegisteredCustomerPasswordWithRegex constraintAnnotation) { regex = constraintAnnotation.regex(); }; /** * {@inheritDoc} <br/> * Validation check for password matching against the given regular expression. */ @Override public boolean isValid(final Customer customer, final ConstraintValidatorContext context) { if (customer.isAnonymous()) { return true; } String password = customer.getClearTextPassword(); if (password == null) { return true; } if (password.matches(regex)) { return true; } else { addConstraintViolation("{com.example.validation.validators.impl.RegisteredCustomerPasswordWithRegexValidator.regex.message}", context); return false; } } private void addConstraintViolation(final String message, final ConstraintValidatorContext context) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(message).addNode("person").addConstraintViolation(); } }
As you can see, initialize() sets the validator's regex field to the custom annotation's regex attribute, while the logic inside the isValid() method adds in additional password requirements.
isValid() validates the Customer's password if:
- The customer is anonymous. Anonymous customers don't have passwords, so they automatically pass validation in this case. In this example, it's only registered customers that require password validation.
- The password is null. In this example, matching a null password with the regex causes the validation to fail. Since Customer passwords already include a null check out-of-the-box, a null password returns two violations. To remove the redundant violation, this constraint automatically passes null passwords.
- The password matches the regular expression that was set during initialize().
isValid() invalidates the Customer's password if the password doesn't match the given regular expression. If a password is invalid, addPasswordConstraintViolation() is called to insert the com.example.validation.validators.impl.RegisteredCustomerPasswordWithRegexValidator.regex.message into the constraint validation set.
ValidationMessages.properties file
The @RegisteredCustomerPasswordWithRegex's ValidationMessages.properties file is shown below:
com.elasticpath.validation.constraints.requiredAttribute=attribute is required com.elasticpath.validation.constraints.notBlank=must not be blank com.elasticpath.validation.constraints.CustomerUsernameUserIdModeEmail.message=Failed username validation com.elasticpath.validation.constraints.RegisteredCustomerPasswordNotBlankWithSize.message=Failed password validation com.elasticpath.validation.validators.impl.RegisteredCustomerPasswordNotBlankWithSizeValidator.blank.message=Password must not be blank com.elasticpath.validation.validators.impl.RegisteredCustomerPasswordNotBlankWithSizeValidator.size.message=Password must be between {min} to {max} characters inclusive com.elasticpath.validation.constraints.validCountry=does not exist in list of supported codes com.elasticpath.validation.constraints.validSubCountry=does not exist in list of supported codes com.elasticpath.validation.constraints.subCountry.missing=must not be blank com.elasticpath.validation.constraints.emailPattern=not a well-formed email address com.example.validation.constraints.RegisteredCustomerPasswordWithRegex.message=Failed password regex check
The com.example.validation.validators.impl.RegisteredCustomerPasswordWithRegexValidator.regex.message property shown above defines the message Bean Validation returns when @RegisteredCustomerPasswordWithRegex is violated.
Applying a Custom Constraint
You can apply a custom constraint to elements in either one of two ways:
- AnnotationsApply a custom constraint through an annotation. For example, apply our custom customer password constraint by inserting a @RegisteredCustomerPasswordWithRegex with the regular expression "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).*" in the Customer domain class:
@RegisteredCustomerPasswordNotBlankWithSize(min = Customer.MINIMUM_PASSWORD_LENGTH, max = GlobalConstants.SHORT_TEXT_MAX_LENGTH) @RegisteredCustomerPasswordWithRegex(regex = "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).*") public interface Customer extends Entity, UserDetails, DatabaseLastModifiedDate { ... }
- XML
- Using XML enables you to apply the custom constraint without having to change the out-of-the-box Customer class. For details on how to apply constraints in XML, refer to Applying Constraints with XML.