Java Bean Validation
Overview
Java Bean Validation, also known as JSR-303
(Java Specification Requests), is an annotation based validation specification for validating fields on objects. Java Bean Validation uses flexible annotations, which can be overridden or extended through the Java code or in an XML file, to apply validation constraints.
Elastic Path uses the Apache BVal implementation of JSR-303 in our Customer and Attribute domain objects to validate incoming information.
Apache BVal provides:
- Spring support
- Customizable constraints
- Extensible validation
- Constraint grouping
The Elastic Path platform uses BVal in the following locations:
- Customer Fields in
Customer.java
- Addresses Fields in
Address.java
- Attributes Fields (Short-Text, Long-Text, and Short-Text multivalue) in
AttributeValueWithType.java
To get started with Java Bean Validation, take a look at the following documentation:
This section gives an overview on how Elastic Path uses Java Bean Validation.
Validating an Entity
To validate entities in Elastic Path:
- Get the validator instance
- Pass the object to the validator through the
validate(object)
method
For example, this is how you validate a customer:
Validator validator = beanFactory.getBean("validator");
Set<ConstraintViolation<Customer>> violations = validator.validate(customer);
The validate(object)
method returns a set of constraint violations. If this set is empty, the object passes validation. Otherwise, you can pass the constraint violations set to loggers or UI widgets to display validation errors.
Constraint Annotations
Constraints specify the criteria the validation framework uses to determine if an object is valid. In Bean Validation, constraints are primarily provided in the form of annotations.
In Elastic Path, annotations are placed directly above an object’s interfaces, classes, fields, and getter methods as shown:
@RegisteredCustomerPasswordNotBlankWithSize(min = Customer.MINIMUM_PASSWORD_LENGTH, max = GlobalConstants.SHORT_TEXT_MAX_LENGTH)
public interface Customer extends Entity, UserDetails, DatabaseLastModifiedDate {
...
/**
* Get the username.
*
* @return the username
*/
@Override
@NotNull
@NotBlank
@Size(max = GlobalConstants.SHORT_TEXT_MAX_LENGTH)
@CustomerUsernameCheck
String getUsername();
Common Annotations
The following lists some common bean validation annotations in the Elastic Path code:
@NotNull
Verifies the value is not null.
@Email
Verifies the email address is valid.
@Size
Verifies a value’s length is between the minimum and maximum value.
@Valid
Verifies the value passed validation
In addition, there are custom Elastic Path Bean Validation annotations listed in the following:
@NotBlank
Verifies a field is not empty and containing only whitespace.
@EpEmail
Verifies an email has a "." in the domain name. For example:
Oliver@elasticpath.com
@AttributeRequired
Verifies an entity’s required attributes are defined.
@ValidCountry
Verifies a country and sub-country are valid.
@RegisteredCustomerPasswordNotBlankWithSize
Verifies a password is valid.
@BooleanConstraint
Verifies a String value is strictly true or false.
@DecimalConstraint
Verifies a value can be parsed as a decimal number.
@IntegerConstraint
Verifies a value can be parsed as an integer.
@ISO8601DateConstraint
Verifies a date conforms to the ISO 8601 date standard.
@ISO8601DateTimeConstraint
Verifies a date and time conforms to the ISO 8601 Date/Time standard.
@LengthConstraint
Verifies a String length is within a given range.
@LongTextValueSize
Verifies an attribute value is of the
LongText
type and that the length is within a given range.@MultiOptionConstraint
Verifies a comma-separated String of options is a part of a given set of valid options.
@ShortTextMultiValuesElementSize
Verifies an attribute value is of the
MultiValue
type and that the length is within a given range.@ShortTextValueSize
Verifies an attribute value is of the
ShortText
type and that the length is within a given range.@SingleOptionConstraint
Verifies a String contains one of a set of valid options
Custom Annotations
In addition to the out-of-the-box annotations, you can create custom constraints to suit your needs. For more information on how to create custom constraints, see Creating a Custom Constraint.
Validation Groups
Bean Validation supports group validation, which allows you to group constraints into subsets that validate input at different times.
By default, constraints do not validate in a particular order. However, you can specify a validation order by using validation groups. For example, you can create two groups to validate a country / sub-country relationship. In the first group, you verify that the country exists. In the second group, you verify the sub-country exists within the country.
XML Constraints
In Bean Validation, constraints can be specified in an XML file. This allows you to overwrite or extend an entity’s validation without modifying the entity’s source code. For more information on how to use XML constraints, refer to Applying Constraints with XML.
Applying Constraints with XML
Bean Validation allows you to apply constraints though XML files. This lets you override or extend validation constraints on Elastic Path domain objects, methods, and properties without having to replace or modify your code.
To use XML constraints, you must create these files in your extension project’s src/main/resources
directory:
constraints.xml
Describes your validation constraints. You can have as many constraints.xml files on your classpath as required.
META-INF/validation.xml
Specifies your
constraints.xml
file’s location. Each extension project can have only onevalidation.xml
file, which is located in theMETA-INF
directory
Adding an XML Constraint
This tutorial teaches you how to add existing validation constraints through XML files.
Scenario
You want to add the following password constraints:
- Include an uppercase letter
- Include a lowercase letter
- Include a number
To add these additional constraints, you can use the regular expression (?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).*
You have a custom validation constraint, named @RegisteredCustomerPasswordWithRegex
, that matches passwords with regular expressions. To add @RegisteredCustomerPasswordWithRegex
without modifying the out-of-the-box code, you can use XML.
constraints.xml
To add @RegisteredCustomerPasswordWithRegex
as a customer password constraint:
Add the following XML code in a blank constraints.xml
file:
<?xml version='1.0' encoding='UTF-8'?>
<constraint-mappings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/mapping http://www.jboss.org/xml/ns/javax/validation/mapping/validation-mapping-1.0.xsd"
xmlns="http://jboss.org/xml/ns/javax/validation/mapping">
<bean class="com.elasticpath.domain.customer.Customer" ignore-annotations="false">
<class>
<constraint annotation="com.example.validation.constraints.RegisteredCustomerPasswordWithRegex" >
<!-- contains at least one digit, one lower case, and one upper case character -->
<element name="regex">(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).*</element>
</constraint>
</class>
</bean>
</constraint-mappings>
Five key points for how the above code works:
The bean class specifies the class whose validation you are modifying, which in this case is customer
By setting ignore-annotations to false, Bean Validation validates using both annotations and XML constraints
By setting ignore-annotations to true, Bean validation ignores annotations and applies only XML constraints.
Constraints applied at the class level are defined in the
<class>
tagThe
@RegisteredCustomerPasswordWithRegex
constraint is applied by the<constraint annotation>
tagThe regular expression
(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).*
, which applies the additional restrictions on passwords, is passed into the annotation by using the<element name>
tag
note
Standard JSR-303 constraints are defined in the javax.validation.constraints
package.
validation.xml
To add the constraints.xml
to your classpath, add the <constraint-mapping>
tag in the <validation>
element as shown below:
<?xml version='1.0' encoding='UTF-8'?>
<validation-config xmlns="http://jboss.org/xml/ns/javax/validation/configuration" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration http://jboss.org/xml/ns/javax/validation/configuration/validation-configuration-1.0.xsd">
<constraint-mapping>/constraints.xml</constraint-mapping>
</validation-config>
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:
-
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 inep-core\src\main\resources
to your extension project’ssrc/main/resources
directory and add your new messages in the copy
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.
Required Parameters:
A extends Annotation
The custom annotation to validate.
T
The object type to validate
Required 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 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;
}
}
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
messageStop the
ConstraintValidator
from returning the default messageAdd the
ValidationMessages.properties
message to the constraint violations setThe 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.
Defining a Custom Bean Validation Annotation
To define a new Bean Validation annotation, create an @interface
file with four annotations:
@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:
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.
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 {};
}
Validation Message
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.
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.
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:
- Minimum 8 characters
- Maximum 255 characters
- No whitespaces
- Not null for registered customers
Additional password constraint:
- Include an uppercase letter
- Include a lowercase letter
- Include a digit
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 RegisteredCustomerPasswordWithRegex.java validators RegisteredCustomerPasswordWithRegexValidator.java resources ValidationMessages.properties
The Custom Annotation
The following @interface
creates a Bean Validation annotation named @RegisteredCustomerPasswordWithRegex
. The java file is located in <extension_project_root>/src/main/java/com/example/validation/constraint/RegisteredCustomerPasswordWithRegex.java
:
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()
), we declare a regex()
attribute to receive regular expressions. Those required attributes are described in detail in the preceding Annotations section. 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. The java file is located in <extension_project_root>/src/main/java/com/example/validation/validators/RegisteredCustomerPasswordWithRegexValidator.java
:
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 the above 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.
Validation Messages
The <extension_project_root>/src/main/java/resources/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:
Apply 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 { ... }
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