Announcement: You can find the guides for Commerce 7.5 and later on the new Elastic Path Documentation site. The Developer Center continues to support Commerce 6.13.0 through 7.4.1.Visit new site

Constraint Violations

Constraint Violations

Elastic Path performs validation on all JavaBeans using the Apache BVal implementation of the JSR-349 specification, 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. For 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 presentation.
  • debugMessage is a text description of the error as a debugging convenience.
  • data 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 to 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

  1. Add a new field to the bean (for example, an age field in com.elasticpath.domain.Customer) and corresponding mutator methods, (i.e. firstName).
  2. Add a new entry to the structuredErrorMessageToMessageFieldname map, defined in structuredMessage.xml (optional, if the constraint is not a part of a complex graph like CustomerProfile)
  3. Add a new message key in the messages properties file (client specific).

Creating a New Constraint

  1. Create a new constraint.
  2. Add a new field to the bean.
  3. In the interface class, annotate the getter method with new constraint.
  4. Add a new entry to the validationConstraintsToMessageId map, defined in >structuredErrorMessage.xml
  5. Add a new entry to the structuredErrorMessageToMessageFieldname map, defined in structuredMessage.xml (optional, if the constraint is not a part of a complex graph like CustomerProfile)
  6. 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.