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
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, Self-Managed Commerce 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.
Validation is now done through extensions that implement the validate extension points. The default extensions for these extension points are described in the sections below.
Product SKU validation extensions
These validators are executed when viewing the add-to-cart form resource or during an actual add-to-cart operation. Many of them are also executed when viewing the checkout form resource or during checkout. These extensions implement the ProductSkuValidator interface.
AutoSelectableBundleConstituentDelegateValidatorImpl
This is a delegating extension. It executes all product SKU validation extensions on each auto-selectable bundle constituent.
InCatalogProductSkuValidatorImpl
This extension validates that the product SKU is in the store’s catalog. It may return these structured error message IDs:
item.not.in.store.catalog
InventoryProductSkuValidatorImpl
This extension validates that the product SKU has unallocated inventory which can be added to a cart. It may return these structured error message IDs:
item.insufficient.inventory
PriceExistsProductSkuValidatorImpl
This extension validates that a product SKU has a price assigned to it. It may return these structured error message IDs:
item.missing.price
ProductSkuDatesValidatorImpl
This extension validates product SKU start and end dates. It may return these structured error message IDs:
item.no.longer.availableitem.not.yet.available
SoldSeparatelyProductSkuValidatorImpl
This extension validates that a product SKU can be sold separately. It may return these structured error message IDs:
item.not.sold.separately
VisibleProductSkuValidatorImpl
This extension validates that a product SKU is visible in the store. It may return these structured error message IDs:
item.not.visible
Shopping Item validation extensions
These validators are executed when viewing the add-to-cart form resource or during an actual add-to-cart operation. Many of them are also executed when viewing the checkout form resource or during checkout. These extensions implement the ShoppingItemValidator interface.
BundleMaxSelectionRulesShoppingItemValidatorImpl
This extension validates that bundle constituents do not exceed the maximum selection limit. It may return these structured error message IDs:
bundle.exceeds.max.constituents
BundleMinSelectionRulesShoppingItemValidatorImpl
This extension validates that the bundle meets the minimum requirements for constituents. It may return these structured error message IDs:
bundle.does.not.contain.min.constituents
BundleStructureShoppingItemValidatorImpl
This extension validates that the bundle in the cart matches the bundle configuration structure. It may return these structured error message IDs:
item.invalid.bundle.structure
InventoryShoppingItemValidatorImpl
This extension validates that there is sufficient inventory for the quantity specified on the item in the cart. It may return these structured error message IDs:
item.insufficient.inventory
ModifierShoppingItemValidatorImpl
This extension validates that the cart item modifiers are correct for the shopping item's product type.
ProductSkuDelegateFromShoppingItemValidatorImpl
This is a delegating extension. It executes all product SKU validation extensions on each shopping item's product SKU.
QuantityShoppingItemValidatorImpl
This extension validates that at least the minimum quantity of product is being added. It may return these structured error message IDs:
field.invalid.minimum.value
ShoppingItemNotAutoSelectedValidatorImpl
This extension validates that an item being removed is not an auto-selected bundle constituent. It may return these structured error message IDs:
cart.item.auto.selected.in.bundle
Shopping Cart validation extensions
These validators are executed when viewing the checkout form resource or during checkout. These extensions implement the ShoppingCartValidator interface.
BillingAddressShoppingCartValidatorImpl
This extension validates that a billing address has been specified. It may return these structured error message IDs:
need.billing.address
CouponUsageLimitValidator
This extension validates that coupons applied to the cart are still available and have not reached their usage limit.
DuplicatePaymentInstrumentValidator
This extension validates that there are no duplicate payment instruments on the cart order. It may return these structured error message IDs:
invalid.paymentInstrument.configuration
EmailAddressShoppingCartValidatorImpl
This extension validates that a valid email address has been provided for the customer. It may return these structured error message IDs:
need.email
EmptyShoppingCartValidatorImpl
This extension validates that the shopping cart is not empty. It may return these structured error message IDs:
cart.empty
ModifierShoppingCartValidatorImpl
This extension validates that the cart modifiers are correct for the cart type. It may return these structured error message IDs:
cart.missing.data
PaymentInstrumentLimitAmountValidator
This extension validates that exactly one payment instrument is configured as unlimited. It may return these structured error message IDs:
invalid.paymentInstrument.configuration
PaymentMethodShoppingCartValidatorImpl
This extension validates that a valid payment method has been specified when payment is required. It may return these structured error message IDs:
need.payment.method
ShippingAddressShoppingCartValidatorImpl
This extension validates that a valid shipping address has been specified when the cart contains physical SKUs. It may return these structured error message IDs:
need.shipping.address
ShippingOptionShoppingCartValidatorImpl
This extension validates that a valid shipping option has been specified when the cart contains physical SKUs. It may return these structured error message IDs:
need.shipping.optioninvalid.shipping.optionshipping.options.unavailable
SuspendedAccountShoppingCartValidatorImpl
This extension validates that the customer account is not suspended. It may return these structured error message IDs:
account.suspended
UniqueCartDataValidatorImpl
This extension validates that the cart modifier data are unique across the customer's shopping carts. It may return these structured error message IDs:
cart.descriptor.not-unique
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 do one of the following:
- Add a new field to an existing constraint
- Create a new constraint and a new validation error
- 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.
Catalog Entity Codes Validation
Commerce Engine contains rules to ensure that catalog entity codes conform to specific restrictions, such as allowed characters and maximum length. These rules cover the following codes:
- Catalog
- Category
- Product
- SKU
- Brand
These restrictions are consistently applied when entering data through Commerce Manager or the CSV import utility. If a specified code violates these restrictions, an error message is returned and the entity is not saved until the code is updated to meet the criteria.
note
Currently, Import/Export does not validate these codes in the same way.
By default, the following rules are enforced for catalog entity codes:
- Codes can only contain the
A-Z,a-z,0-9, and underscore characters. - Product codes and SKU codes allow dashes and periods.
- Codes can have a maximum length of 64 characters.
- Codes cannot contain spaces.
Customizing catalog code validation
You can override these rules by creating a custom class that extends CatalogCodeUtilImpl. The custom class can override any of the following methods:
getCatalogCodeFormatgetCategoryCodeFormatgetProductCodeFormatgetSkuCodeFormatgetBrandCodeFormat
The ep-core-plugin.xml file registers a Spring bean named catalogCodeUtil that refers to this custom class.
You can customize the way that codes are validated by extending CatalogCodeUtilImpl.
Create a file named
CatalogCodeUtilCustomImpl.javainextensions/core/ext-core/src/main/java/com/elasticpath/extensions/commons/util/impl.Write an extension class that overrides the
CatalogCodeUtilImplmethods as required.For example:
public class CatalogCodeUtilCustomImpl extends CatalogCodeUtilImpl { private static final String REG_EXPRESSION_CUSTOM_CATALOG_CODE = "YOUR_REGEX_HERE"; private static final String REG_EXPRESSION_CUSTOM_CATEGORY_CODE = "YOUR_REGEX_HERE"; private static final String REG_EXPRESSION_CUSTOM_PRODUCT_CODE = "YOUR_REGEX_HERE"; private static final String REG_EXPRESSION_CUSTOM_SKU_CODE = "YOUR_REGEX_HERE"; private static final String REG_EXPRESSION_CUSTOM_BRAND_CODE = "YOUR_REGEX_HERE"; private static final CatalogCodeFormat CUSTOM_CATALOG_CODE_SPECS = new CatalogCodeFormatImpl .Builder(REG_EXPRESSION_CUSTOM_CATALOG_CODE) .setMaxLength(32) // If you do not set the default MaxLength is 255 .setSpacesAllowed(true) // If you do not set the default NoSpaces is 'true' .build(); private static final CatalogCodeFormat CUSTOM_CATEGORY_CODE_SPECS = new CatalogCodeFormatImpl .Builder(REG_EXPRESSION_CUSTOM_CATEGORY_CODE) .setMaxLength(32) .setSpacesAllowed(true) .build(); private static final CatalogCodeFormat CUSTOM_PRODUCT_CODE_SPECS = new CatalogCodeFormatImpl .Builder(REG_EXPRESSION_CUSTOM_PRODUCT_CODE) .setMaxLength(32) .setSpacesAllowed(true) .build(); private static final CatalogCodeFormat CUSTOM_SKU_CODE_SPECS = new CatalogCodeFormatImpl .Builder(REG_EXPRESSION_CUSTOM_SKU_CODE) .setMaxLength(32) .setSpacesAllowed(true) .build(); private static final CatalogCodeFormat CUSTOM_BRAND_CODE_SPECS = new CatalogCodeFormatImpl .Builder(REG_EXPRESSION_CUSTOM_BRAND_CODE) .setMaxLength(32) .setSpacesAllowed(true) .build(); @Override public CatalogCodeFormat getCatalogCodeFormat() { return CUSTOM_CATALOG_CODE_SPECS; } @Override public CatalogCodeFormat getCategoryCodeFormat() { return CUSTOM_CATEGORY_CODE_SPECS; } @Override public CatalogCodeFormat getProductCodeFormat() { return CUSTOM_PRODUCT_CODE_SPECS; } @Override public CatalogCodeFormat getSkuCodeFormat() { return CUSTOM_SKU_CODE_SPECS; } @Override public CatalogCodeFormat getBrandCodeFormat() { return CUSTOM_BRAND_CODE_SPECS; } }In the
ep-core-plugin.xmlfile and override thecatalogCodeUtilbean definition to point to the extension class.For example:
<bean id="catalogCodeUtil" class="com.elasticpath.extensions.commons.util.impl.CatalogCodeUtilCustomImpl" scope="singleton" />