Validation and Error Messages
When a user takes an action that violates a business state or provides invalid input, an error is generated in Commerce Engine.
Types of Errors
Errors generated by Commerce Engine produce one of two results:
A collection of of
com.elasticpath.common.dto.StructuredErrorMessage
objectsAn
InvalidBusinessStateException
orEpValidationException
exception, from which a collection ofStructuredErrorMessage
objects can be extracted
StructuredErrorMessage
objects are consumable by Cortex API resources. This allows you to present a localized version of the error message to the user if needed, allowing the user to resolve the cause of the error.
How Errors are Generated
Errors generated by Commerce Engine fall into two categories:
Constraint violations
These errors are typically a result of bad user input.
For example, attempting to fill in an email into a phone number field. Constraint violations generate an
EpValidationException
exception, from which a collection ofcom.elasticpath.common.dto.StructuredErrorMessage
objects can be extracted.Invalid business state errors
These errors are typically a result of a business state that is incompatible with the user’s intent, or impermissible by the business logic within Commerce Engine.
For example, attempting to purchase an item that is out of stock. Business state errors generate an
InvalidBusinessStateException
, from which a collection ofStructuredErrorMessage
objects can be extracted
Add-to-cart and purchase validation
When you update or check out your cart, Commerce Engine performs a number of checks to ensure that the action is valid and the system can complete the action. If the action is invalid, Commerce Engine returns StructuredErrorMessage
objects, with an optional StructuredErrorResolution
embedded in them through the call stack to the user.
The com.elasticpath.service.shoppingcart.validation
package provides a generic interface, Validator
, as well as interfaces intended for more specific purposes, such as ProductSkuValidator
. These interfaces are implemented for specific validation cases and aggregated by a set of service classes, which are called at specific parts of the purchasing workflow.
The service classes are:
AddProductSkuToCartValidationService
Determines if a product SKU can be added to a cart. Invoked when the add to cart form is retrieved.
AddOrUpdateShoppingItemDtoToCartValidationService
Determines if a
ShoppingItemDto
orShoppingItem
object can be added to a cart, or whether an existingShoppingItemDto
orShoppingItem
can be updated. AShoppingItem
object represents a specific quantity of a product SKU. Invoked when a client sends aPOST
request to the add to cart action link or aPUT
request to a cart line item to update it.PurchaseCartValidationService
Determines if a shopping cart can be purchased. Invoked when the purchase form is retrieved and when a client sends a
POST
request to the purchase cart action link
ProductSku
validators
The AddProductSkuToCartValidationService
class aggregates validators that implement the ProductSkuValidator
interface and validate whether a product SKU can be added to a cart. The validators are:
AutoSelectableBundleConstituentDelegateValidatorImpl
Runs all product SKU validators except
SoldSeparatelyProductSkuValidatorImpl
on bundle constituents that are automatically added to the cart.A
StructuredErrorMessage
ID returned by aProductSkuValidator
implementation, depending on the validation failed.ProductSkuDatesValidatorImpl
Validates product SKU start and end dates.
StructuredErrorMessage
ID:item.no.longer.available
item.not.yet.available
SoldSeparatelyProductSkuValidatorImpl
Validates that a product SKU can be sold separately.
StructuredErrorMessage
ID:item.not.sold.separately
PriceExistsProductSkuValidatorImpl
Validates that a product SKU has a price assigned to it.
StructuredErrorMessage
ID:item.missing.price
VisibleProductSkuValidatorImpl
Validates that a product SKU is visible in the store.
StructuredErrorMessage
ID:item.not.visible
InventoryProductSkuValidatorImpl
Validates that the product SKU has unallocated inventory which can be added to a cart.
StructuredErrorMessage
ID:item.insufficient.inventory
InCatalogProductSkuValidatorImpl
Validates that the product SKU is in the store’s catalog.
StructuredErrorMessage
ID:item.not.in.store.catalog
ShoppingItemDto
validators
The AddOrUpdateShoppingItemDtoToCartValidationService
class aggregate validators that implement the ShoppingItemDtoValidator
or ShoppingItemValidator
interfaces. Validators that implement ShoppingItemDtoValidator
validate a specific quantity of a product SKU before it is added to the cart. The validators are:
BundleMaxSelectionRulesShoppingItemDtoValidatorImpl
Validates whether the maximum allowed number of constituents for the bundle is exceeded by adding an item to the cart.
StructuredErrorMessage
ID:bundle.exceeds.max.constituents
BundleStructureShoppingItemDtoValidatorImpl
Validates whether the cart item contains dependent line items that are invalid for the bundle’s defined structure.
StructuredErrorMessage
ID:item.invalid.bundle.structure
CartItemModifierShoppingItemDtoValidatorImpl
Validates that the cart item modifiers are correct for a
ShoppingItemDto
object.InventoryShoppingItemDtoValidatorImpl
Validates that there is sufficient inventory to add a specific quantity of an item specified in a ShoppingItemDto object to a cart.
StructuredErrorMessage
ID:item.insufficient.inventory
QuantityShoppingItemDtoValidatorImpl
Validates that the minimum allowable quantity for a product is added. When adding an item to a cart, the minimum quantity is 1. When updating an existing item in a cart, the minimum quantity is 0.
StructuredErrorMessage
ID:field.invalid. minimum.value
ShoppingItem
validators
The AddOrUpdateShoppingItemDtoToCartValidationService
class aggregate validators that implement the ShoppingItemDtoValidator
or ShoppingItemValidator
interfaces. Validators that implement ShoppingItemValidator
validate a specific quantity of a product SKU after it is added to the cart. The validators are:
BundleMinSelectionRulesShoppingItemValidatorImpl
Validates that the bundle in a cart has the minimum required constituents.
StructuredErrorMessage
ID:bundle.does.not.contain.min.constituents
CartItemModifierShoppingItemValidatorImpl
Validates that the cart item modifiers are correct for a ShoppingItem object.
InventoryShoppingItemValidatorImpl
Validates that sufficient inventory to add a specific quantity of an item specified in a ShoppingItem object to a cart is available.
StructuredErrorMessage
ID:item.insufficient.inventory
ProductSkuDelegateFromShoppingItemValidatorImpl
Runs all product SKU validators for the product SKU in the ShoppingItem object.
A
StructuredErrorMessage
ID returned by aProductSkuValidator
implementation, depending on the validation failed
ShoppingCart
validators
The PurchaseCartValidationService class aggregates the following validators that implement the ShoppingCartValidator interface and validate whether a shopping cart and its constituent shopping items can be purchased. The validators are:
BillingAddressShoppingCartValidatorImpl
Validates that a billing address is specified for the cart.
StructuredErrorMessage
ID:need.billing.address
EmailAddressShoppingCartValidatorImpl
Validates that an email address is specified for the cart.
StructuredErrorMessage
ID:need.email
EmptyShoppingCartValidatorImpl
Validates that the cart is not empty.
StructuredErrorMessage
ID:cart.empty
GiftCertificateBalanceShoppingCartValidatorImpl
Validates that gift certificates specified as a payment method exist and have a sufficient balance to complete the purchase.
StructuredErrorMessage
ID:cart.gift.certificate.not.found
cart.gift.certificate.insufficient.balance
PaymentMethodShoppingCartValidatorImpl
Validates that a payment method is specified for the cart.
StructuredErrorMessage
ID:need.payment.method
ShippingAddressShoppingCartValidatorImpl
Validates that a valid shipping address is specified for the cart if the cart contains physical SKUs.
StructuredErrorMessage
ID:need.shipping.address
ShippingOptionShoppingCartValidatorImpl
Validates that a valid shipping option is specified for the cart if the cars contains physical SKUs.
StructuredErrorMessage
ID:need.shipping.option
invalid.shipping.option
shipping.options.unavailable
ShoppingItemDelegateFromShoppingCartValidatorImpl
Runs all shopping item validators on all items in the cart. A StructuredErrorMessage ID returned by a ShoppingItemValidator implementation, depending on the validation failed.
ShoppingItemNotAutoSelectedValidatorImpl
Prevents the deletion of cart line items that are added as a result of auto-selected bundle constituents. However, you can delete the complete bundle.
StructuredErrorMessage
ID:cart.item.auto.selected.in.bundle
Creating or overriding a cart or purchase validation
To customize the validation strategies or features do the following:
- Extend the appropriate validator in the
com.elasticpath.service.shoppingcart.validation
package and override thevalidate()
method. - Add the validator’s bean definition to
/ep-commerce/commerce-engine/core/ep-core/src/main/resources/spring/service/validation-strategies.xml
file. - Add the validator to the appropriate validation list in the
validation-strategies.xml
file.
Invalid Business State Errors
An invalid business state error is an error thrown during a state change operation where a business condition is not fulfilled. Invalid business state error types prevent a state change operation from happening. Often the client can solve the underlying business error and then retry the state change operation successfully.
For example, when purchasing an order, the following scenarios could generate invalid business state errors:
- An item in the cart isn’t available in the requested quantity
- The shopping cart is empty
- A shipping address is missing
Invalid business state error is mapped to the HTTP status code 409
, which indicates the presence of a conflict. To help the client localize error messages and recover from this error type, the body of the HTTP response contains structured error messages.
Constructing Invalid Business State Errors
Unlike validation errors, which are generated during validation, invalid business state errors must be constructed manually.
To construct an invalid business state error:
- Create a class which extends the
InvalidBusinessStateException
interface
InvalidBusinessStateException
objects return a collection of StructuredErrorMessage
objects indicating why the failure state occurred. Optionally, the StructuredErrorMessage
object can contain a StructuredErrorResolution
object, which contains a link which can help the user resolve the error.
For example, AvalabilityException
is a fully constructed invalid business state error:
public class AvailabilityException extends EpSystemException implements InvalidBusinessStateException {
private final List<StructuredErrorMessage> structuredErrorMessages;
public AvailabilityException(final String message, final Collection<StructuredErrorMessage> structuredErrorMessages) {
super(message);
this.structuredErrorMessages = structuredErrorMessages == null ? emptyList() : ImmutableList.copyOf(structuredErrorMessages);
}
@Override
public List<StructuredErrorMessage< getStructuredErrorMessages() {
return structuredErrorMessages;
}
@Override
public String getExceptionMessage() {
return getMessage();
}
}
Constraint Violations
Elastic Path performs validation on all JavaBeans using the Apache BVal implementation of the JSR-349 (Java Specification Requests), and the Hibernate Validator implementation of the JSR-303 specification. Both frameworks define a set of standard annotations representing required constraints: ApacheBVal for static validation, and Hibernate Validator for static and dynamic validation of JavaBeans. When a JavaBean validation fails, a constraint violation is produced.
Understanding Constraints
Constraints are applied to JavaBeans as either static constraints or dynamic constraints.
Static constraints are an integral part of any JavaBean. For example, com.elasticpath.domain.customer.Customer
has the following constraints:
@NotNull
@NotBlank
@Size(max = GlobalConstants.SHORT_TEXT_MAX_LENGTH)
String getUsername();
Dynamic constraints are added conditionally at runtime. As an example, the constraints for com.elasticpath.domain.cartmodifier.CartItemModifierField
are specified in the database and applied at the runtime during the validation process.
Every constraint has a default set of fields required by its specification. For example the javax.validation.constraints.NotNull
constraint has the following fields:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {}
)
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
NotNull[] value();
}
}
Generally, every constraint has a message
field where a plain text or a message property key is specified. The validation frameworks will try to interpolate the message by looking at the ValidationMessages.properties
file, if it exists. If a key is found, the message value is returned. Otherwise, any value specified in the message
field is presented as is. The validation frameworks may also provide the means to override the messaging mechanism so that message key-value pairs can be specified in a file other than ValidationMessages.properties
.
Constraint Violations Through the Call Stack
Upon validation of a bean in Commerce Engine, the framework returns zero or more ConstraintViolation instances. The ConstraintViolation objects are then transformed into a collection com.elasticpath.common.dto.StructuredErrorMessage
objects which Cortex can consume.
If custom (non JSR-303/349) validation is performed, the implementer must create an instance of a StructuredErrorMessage
and provide required data:
public class StructuredErrorMessage {
private final String messageId;
private final String debugMessage;
private final Map<String, String> data;
}
Where:
messageId
contains the message property key used by the client developer for further message customization and presentationdebugMessage
is a text description of the error as a debugging conveniencedata
is a map that may contain additional data, like a field name and its invalid value as well as optional placeholders (for example, expected min/max values) that may provide a meaningful and understandable response
If validation fails, the application developer must call the transformer and throw a new com.elasticpath.commons.exception.EpValidationException
. They must then attach the set of validation errors.
For example, this might be an exception thrown by CustomerServiceImpl
:
Set<ConstraintViolation<Customer>> customerViolations = validator.validate(customer);
if (CollectionUtils.isNotEmpty(customerViolations)) {
// Attach the validation errors
List<StructuredErrorMessage> structuredErrorMessageList = constraintViolationTransformer.transform(customerViolations);
// Throw the exception
throw new EpValidationException("Customer validation failure.", structuredErrorMessageList);
}
Apart from conversion, the transformer is also responsible for proper mapping and normalization of message keys, that can be further localized by the application developer. The normalization is done using a map, defined in commerce-engine/core/ep-core/src/main/resources/spring/commons/structuredErrorMessage.xml
, which can be expanded further if needed.
The second transformation is from StructuredErrorMessage
to com.elasticpath.rest.advise.Message
which is wrapped in a ResourceOperationFailure
. Once the EpValidationException
is thrown, it has to be handled in the upper layer (Cortex) where another transformation takes place.
The following example shows how a com.elasticpath.commons.exception.EpValidationException
is handled by CustomerRepositoryImpl
. ReactiveAdapter methods handle EpValidationException
(HTTP 400) exceptions automatically, and return appropriate structured error messages.
return reactiveAdapter.fromServiceAsCompletable(() -> customerService.update(customer));
Under the covers a StructuredErrorMessageTransformer
is used. The transformer also performs additional normalization of message keys, by utilizing a map defined in structuredMessage.xml
. The map is created based on javax.validation.ConstraintViolation.propertyPath
property which captures the bean’s field name being validated.
The final transformation transforms com.elasticpath.rest.advise.Message
to a set of com.elasticpath.schema.StructuredMessage
. This step is transparent to the application developer and no further customization is required at this level.
Customizing Validation Errors
Standard validators cannot be changed in terms of their functionality, but can be extended.
Editing an Existing Validation Error
The validation functionality of standard constraints cannot be changed. If you need to display a different message, you can change the value of a constraint’s message property:
@NotNull(message="new.message.key")
public String getUsername();
This may introduce inconsistent display of messages for the same constraint but different beans. For each constraint, there should always be a single message value (plain text or a key) so its meaning is consistent throughout the application.
Custom constraints can be changed in both terms of validation functionality and messaging, although the same rules apply for messaging as for the standard constraints.
To change a constraint’s validation functionality, change the corresponding validator specified by @Constraint(validatedBy = {}
.
Similarly, if you need to use a different validator, change the validatedBy
property. Note that changing the validatedBy
property will change the validation for all beans using that constraint.
Creating a New Validation Error
To create a new validation error, first create a new annotation constraint that conforms to either the JSR-303 or the JSR-349 specification:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {pkg1.sub1.sub2.NewConstraintValidator.class}
)
public @interface NewConstraint {
String message() default "{validation.constraint.newconstraint}";
int length() default 0;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
If required, a constraint may have additional fields that may be used by a validator class. The allowed field return types are:
- String
- Class
- Primitive types
- Enum
- Another annotation, or an array of any of these types
These types are specified by Java, not Elastic Path.
In the example above, the NewConstraint
constraint has an additional int field called length.
The next step is to set a default message, either as a plain text message or a message key specified in the ep-core/src/main/resources/ValidationMessages.properties
file. If the validation framework provides an overriding mechanism, the message key may be specified in a different file.
The final step is to implement a validator, specified by @Constraint(validatedBy = {}
. The validator class must implement:
javax.validation.ConstraintValidator
- The void
initialize(A annotation)
method - The boolean
isValid(T beanToValidate, ConstraintValidatorContext context)
In the example constraint, the value of the length property is read from the constraint in the initialize method, and used later in the isValid
method.
If you require additional constraint violations are 'on-the-fly’, they can be added by using context
methods:
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message).addNode("length").addConstraintViolation();
See com.elasticpath.validation.validators.impl.RegisteredCustomerPasswordNotBlankWithSizeValidator
as well as other validators in the same package for more information.
Customizing Constraints
While you can customize a constraint’s existing properties like message
, or its custom properties, like min
or max
, you cannot extend the constraint itself.
In the event that you need more functionality, you can either add a new field to an existing constraint, create a new constraint (and thus a new validation error), or create a new method.
Adding a New Field to an Existing Constraint
- Add a new field to the bean (for example, an age field in
com.elasticpath.domain.Customer
) and corresponding mutator methods, (i.e. firstName). - Add a new entry to the
structuredErrorMessageToMessageFieldname
map, defined instructuredMessage.xml
(optional, if the constraint is not a part of a complex graph likeCustomerProfile
). - Add a new message key in the messages properties file (client specific).
Creating a New Constraint
- Create a new constraint.
- Add a new field to the bean.
- In the interface class, annotate the getter method with new constraint.
- Add a new entry to the
validationConstraintsToMessageId
map, defined instructuredErrorMessage.xml
. - Add a new entry to the
structuredErrorMessageToMessageFieldname
map, defined instructuredMessage.xml
(optional, if the constraint is not a part of a complex graph likeCustomerProfile
). - Add a new message key in the messages properties file (client specific).
Creating a New Method to be Validated
Depending on the constraint being used, you should either add a new field to an existing constraint or create a new constraint first. The usual pattern for implementing a method is to add it first in the Commerce Engine service class, and then in a repository class in the Cortex integration layer.
N Commerce Engine, the new method should then call a validator class (Apache BVal for static validation or CartItemModifierFieldValidationService
for dynamic validation) and check for constraint violations. In the case of dynamic validation, the return set will already containsStructuredErrorMessage
instances, while for static validation transformation into StructuredErrorMessage
is required.
On the Cortex integration level, in the new repository method, catch the EpValidationException
and transform the set of StructuredErrorMessage
to a set of Message
objects.