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.StructuredErrorMessageobjectsAn
InvalidBusinessStateExceptionorEpValidationExceptionexception, from which a collection ofStructuredErrorMessageobjects 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
EpValidationExceptionexception, from which a collection ofcom.elasticpath.common.dto.StructuredErrorMessageobjects 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 ofStructuredErrorMessageobjects 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:
AddProductSkuToCartValidationServiceDetermines if a product SKU can be added to a cart. Invoked when the add to cart form is retrieved.
AddOrUpdateShoppingItemDtoToCartValidationServiceDetermines if a
ShoppingItemDtoorShoppingItemobject can be added to a cart, or whether an existingShoppingItemDtoorShoppingItemcan be updated. AShoppingItemobject represents a specific quantity of a product SKU. Invoked when a client sends aPOSTrequest to the add to cart action link or aPUTrequest to a cart line item to update it.PurchaseCartValidationServiceDetermines if a shopping cart can be purchased. Invoked when the purchase form is retrieved and when a client sends a
POSTrequest 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:
AutoSelectableBundleConstituentDelegateValidatorImplRuns all product SKU validators except
SoldSeparatelyProductSkuValidatorImplon bundle constituents that are automatically added to the cart.A
StructuredErrorMessageID returned by aProductSkuValidatorimplementation, depending on the validation failed.ProductSkuDatesValidatorImplValidates product SKU start and end dates.
StructuredErrorMessageID:item.no.longer.availableitem.not.yet.available
SoldSeparatelyProductSkuValidatorImplValidates that a product SKU can be sold separately.
StructuredErrorMessageID:item.not.sold.separately
PriceExistsProductSkuValidatorImplValidates that a product SKU has a price assigned to it.
StructuredErrorMessageID:item.missing.priceVisibleProductSkuValidatorImplValidates that a product SKU is visible in the store.
StructuredErrorMessageID:item.not.visible
InventoryProductSkuValidatorImplValidates that the product SKU has unallocated inventory which can be added to a cart.
StructuredErrorMessageID:item.insufficient.inventory
InCatalogProductSkuValidatorImplValidates that the product SKU is in the store’s catalog.
StructuredErrorMessageID: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:
BundleMaxSelectionRulesShoppingItemDtoValidatorImplValidates whether the maximum allowed number of constituents for the bundle is exceeded by adding an item to the cart.
StructuredErrorMessageID:bundle.exceeds.max.constituents
BundleStructureShoppingItemDtoValidatorImplValidates whether the cart item contains dependent line items that are invalid for the bundle’s defined structure.
StructuredErrorMessageID:item.invalid.bundle.structure
CartItemModifierShoppingItemDtoValidatorImplValidates that the cart item modifiers are correct for a
ShoppingItemDtoobject.InventoryShoppingItemDtoValidatorImplValidates that there is sufficient inventory to add a specific quantity of an item specified in a ShoppingItemDto object to a cart.
StructuredErrorMessageID:item.insufficient.inventory
QuantityShoppingItemDtoValidatorImplValidates 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.
StructuredErrorMessageID: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:
BundleMinSelectionRulesShoppingItemValidatorImplValidates that the bundle in a cart has the minimum required constituents.
StructuredErrorMessageID:bundle.does.not.contain.min.constituents
CartItemModifierShoppingItemValidatorImplValidates that the cart item modifiers are correct for a ShoppingItem object.
InventoryShoppingItemValidatorImplValidates that sufficient inventory to add a specific quantity of an item specified in a ShoppingItem object to a cart is available.
StructuredErrorMessageID:item.insufficient.inventory
ProductSkuDelegateFromShoppingItemValidatorImplRuns all product SKU validators for the product SKU in the ShoppingItem object.
A
StructuredErrorMessageID returned by aProductSkuValidatorimplementation, 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:
BillingAddressShoppingCartValidatorImplValidates that a billing address is specified for the cart.
StructuredErrorMessageID:need.billing.address
EmailAddressShoppingCartValidatorImplValidates that an email address is specified for the cart.
StructuredErrorMessageID:need.email
EmptyShoppingCartValidatorImplValidates that the cart is not empty.
StructuredErrorMessageID:cart.empty
GiftCertificateBalanceShoppingCartValidatorImplValidates that gift certificates specified as a payment method exist and have a sufficient balance to complete the purchase.
StructuredErrorMessageID:cart.gift.certificate.not.foundcart.gift.certificate.insufficient.balance
PaymentMethodShoppingCartValidatorImplValidates that a payment method is specified for the cart.
StructuredErrorMessageID:need.payment.method
ShippingAddressShoppingCartValidatorImplValidates that a valid shipping address is specified for the cart if the cart contains physical SKUs.
StructuredErrorMessageID:need.shipping.address
ShippingOptionShoppingCartValidatorImplValidates that a valid shipping option is specified for the cart if the cars contains physical SKUs.
StructuredErrorMessageID:need.shipping.optioninvalid.shipping.optionshipping.options.unavailable
ShoppingItemDelegateFromShoppingCartValidatorImplRuns all shopping item validators on all items in the cart. A StructuredErrorMessage ID returned by a ShoppingItemValidator implementation, depending on the validation failed.
ShoppingItemNotAutoSelectedValidatorImplPrevents the deletion of cart line items that are added as a result of auto-selected bundle constituents. However, you can delete the complete bundle.
StructuredErrorMessageID: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.validationpackage 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.xmlfile. - Add the validator to the appropriate validation list in the
validation-strategies.xmlfile.
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
InvalidBusinessStateExceptioninterface
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:
messageIdcontains the message property key used by the client developer for further message customization and presentationdebugMessageis a text description of the error as a debugging conveniencedatais 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
structuredErrorMessageToMessageFieldnamemap, 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
validationConstraintsToMessageIdmap, defined instructuredErrorMessage.xml. - Add a new entry to the
structuredErrorMessageToMessageFieldnamemap, 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.