Elastic Path Commerce Development

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

Defining a Custom Bean Validation Annotation

To define a new Bean Validation annotation, create an @interface file with four annotations:

Table 1. Required Annotations
Annotation Description
@Target Defines the annotatable elements.
@Retention Defines when the annotation applies. For Bean Validation annotations, this must be RUNTIME.
@Constraint Defines the annotation's validator.
@Documented Includes the annotation on the Javadocs for all elements the annotation is present on.

The @interface file should also contain three attributes:

Table 2. Required Attributes
Attributes Description
message() The message to display if this constraint is violated.
group() 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.

The following example defines a @PersonAvailable annotation which uses a Pers onAvailableValidator validator.

Note:

To declare an attribute's default values, use the default keyword as shown in the example.

Note:

You assign groups or payload to the constraint by passing in a group or payload parameter when you apply the constraint to an element.

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 {};
}

Defining a Validator

The validator determines an element's validity. To define a validator, create a class that implements the ConstraintValidator<A extends Annotation, T> interface. The interface accepts two parameters and requires two method implementations.

Table 3. Required Parameters
Parameter Description
A extends Annotation The custom annotation to validate.
T The object type to validate.
Table 4. Required Method Implementation
Method Description
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 a boolean depending on the entity's validity. In the event of a false, this method should also return a constraint violation message.
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;
	}
}

Implementing isValid()

A single validator's isValid() method can contain multiple conditions. You can return a different constraint violation message for each condition by specifying custom messages. The isValid() method does three things 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.
Note:

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 is as follows:

com.example.validation.validators.impl.PersonAvailableValidator.age.message=This person is not old enough.
Tip: Locale Specific Messages

You can create locale specific validation messages by appending an underscore and the locale prefix to the file name. Bean Validation's current locale is dependent on the server's system locale.

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.

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:

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.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.