Skip to content

dyakonoff/cuba-validation

Repository files navigation

Validation cookbook for CUBA applications

Content

Introduction

Input validation is one of the common tasks in everyday developer’s life. We need to check our data in many different situations: after getting data from UI, in API call handlers, before saving our model to the DB etc, etc.

Main goal of this cookbook is to summarize all validation methods that are common to CUBA platform, give detailed explanations with examples for all of them and talk a bit about pros and cons of each of these approaches. I hope that the article will be a good tutorial and reference for all the questions related to data validation in CUBA platform based applications.

The sample application for this article could be downloaded from here.

A list of additional examples and materials for further reading is in Appendix A.

Happy reading!

Tags cloud

Top

Model problem description

Before discussing different validation methods available for us in CUBA platform application, let's discuss what the application does first.

For this article I'm going to use an application that demonstrates all validation methods mentioned here and how they work from different perspectives: code, user interface, CUBA studio IDE, REST endpoints etc.

This application mimics a small order-management system for an US-based store that ships its goods over the US. That's why this system uses imperial measurement units and enforces US phone numbers and ZIP codes formats.

The application's entities structure is shown below:

Figure 1: Entities structure

Figure 1: Entities structure.

For more detailed description of the application's requirements and data constraints you can consult this page.

List of implemented validation methods with links is here.

Top

Sample application

The sample application's repository is here. I encourage you to clone it using the next command:

git clone git@github.com:dyakonoff/cuba-validation-examples.git

Or download sample app .zip archive from here.

Top

Bean validation

Let's start the review of validation methods with the simplest ones that we have in our toolbox: bean validations. Annotation-based validators provide uniform approach to data checking on the middleware, GUI and universal REST services. They are based on the JSR 349 - Bean Validation 1.1 and its reference implementation: Hibernate Validator.

Documentation says that this mechanism allows users to set limitations on entity fields, getters and classes. Most of the annotations are available from javax.validation.constraints namespace, although couple of them come from javax.persistence, javax.validation and org.hibernate.validator.constraints.

Also, it's not hard to create your own custom annotations to validate fields and entities, which we'll see in the later sections.

Top

JPA constraints

JPA annotations put constraints on DB level as table indexes or table column / multi-column constraints. Although there are only few of such annotations, they are only ones which act on DB server level.

  • @Column(..., unique=true) - sets SQL unique constraint on a table column for entity fields marked as Unique
  • @Column(..., nullable=false) - sets SQL not null constraint on a table column (entity field) for fields marked as Mandatory in studio. Acts together with @NotNull JPA annotation which works at UI and middleware levels.
  • @Column(..., length = 16) - sets length of a varchar column, and limits the maximum input field length at UI level.
  • @UniqueConstraint - sets a multi column index with unique constraint.

Top

Single column JPA constraints

Single column constraints could be applied from CUBA studio entity editor UI during entity field creation or modification:

Figure 2: Setting column level DB constraints from CUBA

Figure 2: Setting column level DB constraints from CUBA.

They are reflected in Java code in the next manner (which you can always edit manually, of course):

public class Order extends StandardEntity {
...
    @NotNull
    @Column(name = "NUMBER_", nullable = false, unique = true, length = 20)
    protected String number;
...
}

Order.java

These @Column annotation parameters are translated to the DDL like that:

create table ORDERMAN_ORDER (
    ...
    NUMBER_ varchar(20) not null,
    ...
)
...
-- unique indexes
create unique index IDX_ORDERMAN_ORDER_UNIQ_NUMBER_ on ORDERMAN_ORDER (NUMBER_)

Note that CUBA studio automatically adds @NotNull constraint to the field marked as Mandatory in Entity designer. Hence, this validations are made on UI and middleware level as well.

On the other hand, Unique constraint (reflected with @Column(..., unique=true) annotation) works only on DB level, because it can not be validated on the application level without reading whole SQL table which might be quite expensive operation.

Top

Multi-column JPA constraints

This type of data constraint / validation acts on DB level only and is represented by multi-column index with unique constraint. It could be created from Entity designer in CUBA studio:

Figure 3: Creating multi-column unique constraint

Figure 3: Creating multi-column unique constraint.

Or right in your Java code:

@NamePattern("%s|name")
@Table(name = "ORDERMAN_PRODUCT", uniqueConstraints = {
    @UniqueConstraint(name = "IDX_ORDERMAN_PRODUCT_UNQ", columnNames = {"NAME", "MEASURE"})
})
@Entity(name = "orderman$Product")
public class Product extends StandardEntity {
    ...
}

Product.java

Which adds create unique index IDX_ORDERMAN_PRODUCT_UNQ on ORDERMAN_PRODUCT (NAME, MEASURE) to generated DDL.

Top

Single field constraints

Let's go through other standard annotations that were designed for data validation. As I mentioned before most of them came from javax.validation.constraints namespace, although some are from javax.persistence, javax.validation and org.hibernate.validator.constraints. For details, you can take a look at CUBA documentation section about bean validation.

Some of these annotations can be configured from CUBA studio in VALIDATION section (list of constraints available varies for different entity field types).

Figure 4: Bean validation tab

Figure 4: Validation tab.

From the code perspective these annotations look like that:

@NamePattern("%s order#: %s|customer,number")
@Table(name = "ORDERMAN_ORDER")
@Entity(name = "orderman$Order")
public class Order extends StandardEntity {
    private static final long serialVersionUID = -5542761764517463640L;

    @NotNull
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "CUSTOMER_ID")
    protected Customer customer;

    @Temporal(TemporalType.DATE)
    @Past(message = "Order date can't be in the future")
    @NotNull
    @Column(name = "DATE_", nullable = false)
    protected Date date;

    @Pattern(message = "Number format should be yyyy-MM-dd-<sequential number>", regexp = "\\d{4}-\\d{2}-\\d{2}-\\d+")
    @NotNull
    @Column(name = "NUMBER_", nullable = false, unique = true, length = 20)
    protected String number;

    @NotNull
    @Column(name = "STATUS", nullable = false)
    protected Integer status;

    @Size(min = 1, max = 10)
    @Valid
    @Composition
    @OnDelete(DeletePolicy.CASCADE)
    @OneToMany(mappedBy = "order")
    protected List<OrderItem> items;

    @DecimalMin(message = "Price should be greater than 0", value = "0")
    @NotNull
    @Column(name = "PRICE", nullable = false)
    protected BigDecimal price;

    ...

}

Order.java

Here you can find a list of common annotations used in CUBA applications.

Top

Bean validation with custom annotations

There is no need to limit ourselves with the standard constraint annotations, if it's needed we can define our custom ones. Custom annotations could be defined not just for entities fields but also for Entity classes, POJOs and service methods. Custom annotations can help you to express your validation logic clearer or implement custom checks that can even do the cross-field checks of an entity object.

Let’s check out how to do that. In our sample application there at two custom constraints implemented:

  • @UsPhoneNumber - a constraint which is a descendant of @Pattern annotation and does the simple regexp check for the phone format.
  • @CustomerContactsCheck ensures that a customer entity has either phone number or email specified.

@UsPhoneNumber validator is quite primitive and implemented as a custom interface:

/**
 * Validates the US phone numbers format: `+1 (NXX) NXX-XXXX` , where: `N`=digits 2–9, `X`=digits 0–9
 */
@Pattern(regexp = "\\+1\\s\\([2-9](\\d){2}\\)\\s[2-9](\\d){2}-(\\d){4}")
@Documented
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
public @interface UsPhoneNumber {
    String message() default "{msg://com.haulmont.dyakonoff.orderman.entity.validator/PhoneNumberError.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

UsPhoneNumber

Going through the code we should note the next:

  • @Constraint(validatedBy = {}) says that there is no actual class implementing the validator.
  • @Retention(RetentionPolicy.RUNTIME) marks the annotation as a runtime one, which all validators should be.
  • @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) specifies targets that this annotation can be applied to. Although, in our case, it would be possible to limit the scope only with ElementType.FIELD.
  • And finally, @Pattern(...) part specifies the actual validator's behavior.

Usage of such annotation is simple:

public class Customer extends StandardEntity {
    ...

    @UsPhoneNumber
    @Column(name = "PHONE", length = 30)
    protected String phone;

    ...
}

Customer.java

You can also check the more complex example of a custom validator that comes with hibernate and uses a list of checks to validate data: org.hibernate.validator.constraints.br.CPF.

Now, it's time to take a look at more complex custom annotation that does cross-field check. @CustomerContactsCheck interface defines a constraint that could be applied to classes only:

/**
 * Checks that Customer has either phone or email
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CustomerContactsCheckValidator.class)
public @interface CustomerContactsCheck {
    String message() default "{msg://com.haulmont.dyakonoff.orderman.entity.validator/CustomerContactsCheck.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

CustomerContactsCheck

This annotation using @Constraint(validatedBy = CustomerContactsCheckValidator.class) refers to the next implementation:

public class CustomerContactsCheckValidator implements ConstraintValidator<CustomerContactsCheck, Customer> {
    @Override
    public void initialize(CustomerContactsCheck constraint) {
    }

    @Override
    public boolean isValid(Customer customer, ConstraintValidatorContext context) {
        if (customer == null)
            return false;

        return !((customer.getEmail() == null || customer.getEmail().length() == 0) &&
                 (customer.getPhone() == null || customer.getPhone().length() == 0));
    }
}

CustomerContactsCheckValidator.java

So far, we have seen nothing special, but if we want to get this annotation called and be able to do the cross-field check, we need to specify appropriate constraint group.

Editor screens perform validation against class-level constraints on commit if the constraint includes the UiCrossFieldChecks group and if all attribute-level checks are passed. You can turn off the validation of this kind using the crossFieldValidate property of the screen in the screen XML descriptor or in the controller.

@CustomerContactsCheck(groups = {Default.class, UiCrossFieldChecks.class})
@NamePattern("%s (%s)|name,email")
@Table(name = "ORDERMAN_CUSTOMER")
@Entity(name = "orderman$Customer")
public class Customer extends StandardEntity {
    ...
}

Customer.java

Top

Notes on bean validation

At what level bean annotation works

By default, constraint annotations works:

  • At UI level when method validateAll of the editor's controller is called automatically on the screen commit. (But you need to override postValidate method to do the custom validation in the screen controller, see later section).
  • At REST level when Universal REST endpoints are called.
  • At middleware layer when validating method marked with @Validated annotation.
  • At client or middleware levels when validation is called manually using BeanValidation interface.

Validation of related objects

For cascade validation of related objects, we need to mark the reference fields with @Valid:

public class Order extends StandardEntity {
    ...

    @Size(min = 1, max = 10)
    @Valid
    @Composition
    @OnDelete(DeletePolicy.CASCADE)
    @OneToMany(mappedBy = "order")
    protected List<OrderItem> items;

    ...
}

In the example above, when an instance of Order is validated, the list of items will be checked for the fact that it contains at least one instance (because of @Size(min = 1)), and all instances of Product in the list will be validated also.

Custom messages in bean validation constraints

All bean validation constraints can have custom messages (see documentation):

@Pattern(message = "Postal code should follow US ZIP codes format: 12345 or 12345-6789 or 12345 1234", regexp = "^\\d{5}(?:[-\\s]\\d{4})?$")
@NotNull
@Column(name = "POSTAL_CODE", nullable = false, length = 16)
protected String postalCode;

Messages can contain parameters and expressions. Parameters are enclosed in {} and represent either localized messages or annotation parameters, e.g. {min}, {max}, {value}. Expressions are enclosed in ${} and can include the validated value variable validatedValue, annotation parameters like value or min, and JSR-341 (EL 3.0) expressions:

@Column(name = "EMAIL")
@Email(message = "Invalid email format: ${validatedValue}", regexp = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$")
protected String email;

@Length(message = "Address line 1 should have length not less than {min}", min = 5)
@NotNull
@Column(name = "ADDRESS_LINE1", nullable = false)
protected String addressLine1;

Message packs in bean validation constraints

You can also place the message in a localized messages pack and use the following format to specify the message in an annotation: {msg://message_pack/message_key} or simply {msg://message_key} (for entities only). For example:

@Pattern(regexp = "\\S+@\\S+", message = "{msg://com.company.demo.entity/Customer.email.validationMsg}")
@Column(name = "EMAIL")
protected String email;

Top

Validation in REST

Universal REST

In CUBA application all your entities are available via REST by default. Protocol follows Swagger specification and available here: http://files.cuba-platform.com/swagger/ . This feature is called Universal REST API. Constraints defined for entities are applied to universal REST create and update actions automatically (see documentation for details).

The CRUD entities operation (universal REST) are defined here in swagger format.

Figure 5: Universal REST swagger specification

Figure 5: Universal REST swagger specification

Following this steps, let's get OAuth2 access token and try to add a new customer using universal REST. To test validation let's make this test customer having couple fields that shouldn't allow it to pass validation checks:

{
    "addressLine1": "Earth",
    "addressLine2": "1240 W Main str, Louisville, KY",
    "addressLine3": "USA",
    "email": "john.smith_invalid_email",
    "name": "John Smith",
    "phone": "+1 (000) 000-1000",
    "postalCode": "40203"
}

We can see that email is bad-formed and the phone number doesn't follow the requirements. So, after running the next command (don't forget to set your own access token as it shown here). We are getting expected error message in the format described in the later section:

$ http --json POST localhost:8080/app/rest/v2/entities/orderman\$Customer 'Authorization:Bearer 819fe70b-7881-43ca-98ef-b5749f417f49'  @rest/customer_john_smith.json
HTTP/1.1 400
Content-Type: application/json;charset=UTF-8

[
    {
        "invalidValue": "john.smith_invalid_email",
        "message": "Invalid email format: john.smith_invalid_email",
        "messageTemplate": "Invalid email format: ${validatedValue}",
        "path": "email"
    },
    {
        "invalidValue": "+1 (000) 000-1000",
        "message": "must match \"\\+1\\s\\([2-9](\\d){2}\\)\\s[2-9](\\d){2}-(\\d){4}\"",
        "messageTemplate": "{javax.validation.constraints.Pattern.message}",
        "path": "phone"
    }
]

Let's test if our custom validation java class works as well. After making this try we can see that it works just fine:

$ http --json POST localhost:8080/app/rest/v2/entities/orderman\$Customer 'Authorization:Bearer 819fe70b-7881-43ca-98ef-b5749f417f49'  @rest/customer_mary_smith.json
HTTP/1.1 400
Content-Type: application/json;charset=UTF-8

[
    {
        "invalidValue": null,
        "message": "Either 'name' or 'email' should be defined for a customer",
        "messageTemplate": "{msg://com.haulmont.dyakonoff.orderman.entity.validator/CustomerContactsCheck.message}",
        "path": ""
    }
]

Top

REST queries validation

Just for completeness, I need to say that because predefined JPQL Queries allow only SELECT methods, and so they do only read operations. Hence, they don't provide any methods to validate input parameters. But of course, they screen input data to protect the calls from SQL injection attack.

Top

Validation by contract

Let's make the custom REST service and specify limitations for method parameters and return values, in the way that is somewhat similar to contract programming approach.

We want our service to:

  1. List all products in stock.
  2. Get a Stock object by particular product's name (and throw exception if there are zero or more than one product with such name in stock).
  3. Add a new Product to Stock.
  4. Increase amount of existing product in stock.

To do that, let's create a new middleware service using CUBA studio and call it StockApiService.

Figure 6: Adding a middleware service

Figure 6: Adding a middleware service

The next step is opening StockApiService.java in IDE and creating appropriate methods:

List<Stock> getProductsInStock();
Stock getStockForProductByName(String productName);
Stock addNewProduct(Product product, BigDecimal inStock, BigDecimal optimalLevel);
Stock increaseQuantityByProductName(String productName, BigDecimal increaseAmount);

Next, we need to mark these methods in CUBA studio as REST methods:

Figure 7: Marking methods as REST available

Figure 7: Marking methods as REST available

Now, let's open StockApiService in our Java IDE again and annotate the methods properly.

  1. First, we need to know, that constraint validations will be applied only to the methods that are marked with @Validated annotation. (See documentation here). By default, @Validated uses the next constraint groups:
    • Default and ServiceParametersChecks - for method parameters
    • Default and ServiceResultChecks - for method return value
    • As for constraint group RestApiChecks, it could be used for those validations that must be checked only when instance is passed to REST-API.
    • It's also possible to mark the whole class / interface with @Validated annotation to say CUBA that it needs to run bean validations for all methods and their input parameters and return values.
  2. @RequiredView annotation could be used to ensure that input parameters of a method have fields that corresponding to the specified view.
    • @RequiredView validates that the validated Entity has at least those fields loaded that are required by the view. It doesn't fire an error if the Entity has extra fields loaded.
    • This annotation works with entity objects and their collections.
  3. All standard and custom constrauint validations can be applied either to:
    • methods - then the return value is checked.
    • method parameters - which makes these parameters to be validated
    • once again, this constraint validations used only if method is marked with @Validated annotation.
  4. Error messages can be provided with annotations either directly or by using messages packs. (see documentation for more details)

Here is a result of applying all our validations to the REST service interface:

@Validated
public interface StockApiService {
    String NAME = "orderman_StockApiService";

    @NotNull
    @RequiredView("stock-api-view")
    List<Stock> getProductsInStock();

    @NotNull
    @RequiredView("stock-api-view")
    Stock getStockForProductByName(@NotNull(message = "{msg://com.haulmont.dyakonoff.orderman.service/StockApiService.productNameMissing)")
                                   @Length(min = 1, max = 255, message = "{msg://com.haulmont.dyakonoff.orderman.service/StockApiService.productName}")
                                           String productName);

    @NotNull
    @RequiredView("_local")
    Stock addNewProduct(@RequiredView("_local")
                                Product product,
                        @NotNull
                        @DecimalMin("0")
                        @DecimalMax(value = "1000", message = "{msg://com.haulmont.dyakonoff.orderman.service/StockApiService.inStockLimit}")
                                BigDecimal inStock,
                        @Min(0)
                                BigDecimal optimalLevel);

    @NotNull
    @RequiredView("stock-api-view")
    Stock increaseQuantityByProductName(@NotNull(message = "{msg://com.haulmont.dyakonoff.orderman.service/StockApiService.productNameMissing)")
                                        @Length(min = 1, max = 255, message = "{msg://com.haulmont.dyakonoff.orderman.service/StockApiService.productName}")
                                                String productName,
                                        @NotNull
                                        @DecimalMin(value = "0", inclusive = false)
                                        @DecimalMax(value = "1000")
                                                BigDecimal increaseAmount);
}

StockApiService.java

This REST interface methods are available at endpoint /app/rest/v2/services/{serviceName}/{methodName} as swagger specification says.

Let's run Postman REST client and check how our validation annotations works in addNewProduct method if inStock parameter is greater than 1000:

Figure 8: Bean validation in REST service

Figure 8: Bean validation in REST service

The server returns 400 Bad Request and an error message like it is specified in the later section Validation errors in REST.

Top

Programmatic Validation

Sometimes, pure validations are not enough for REST services. To validate the entities passed to the method or validate entities have been created at middleware or client tiers, we may need to run bean validations against these entities manually. This is the case when programmatic validation comes to play.

You can perform bean validation using the BeanValidation infrastructure interface, which is available on both middleware and client tiers. It is used to obtain a javax.validation.Validator implementation which runs validation and gives the result as a set of ConstraintViolation objects.

If you perform some custom programmatic validation in a service, use CustomValidationException to inform clients about validation errors in the same format as the standard bean validation does.

The sample application demonstrates this approach in StockApiServiceBean.addNewProduct method to validate parameter product passed into the method and stock object constructed inside it.

@Service(StockApiService.NAME)
public class StockApiServiceBean implements StockApiService {
    ...

    @Override
    public Stock addNewProduct(Product product, BigDecimal inStock, BigDecimal optimalLevel) {
        // validate the product provided
        Validator validator = beanValidation.getValidator();
        Set<ConstraintViolation<Product>> product_violations = validator.validate(product);
        if (product_violations.size() > 0) {
            StringBuilder strBuilder = new StringBuilder();
            product_violations.stream().forEach(violation -> strBuilder.append(violation.getMessage()).append("; "));
            throw new CustomValidationException(strBuilder.toString());
        }

        // check if product already exist in the db
        // we don't check for soft-deleted Product and Stock entities here for simplicity
        // if we'd like to do that might need to load entities and check them with isDeleted() method.
        // see https://doc.cuba-platform.com/manual-6.9/soft_deletion_usage.html for details
        Integer cnt = (Integer) dataManager
                .loadValue("SELECT COUNT(p) FROM orderman$Product p WHERE p.name = :productName", Integer.class)
                .parameter("productName", product.getName())
                .one();
        if (cnt > 0)
            throw new CustomValidationException(messages.formatMainMessage("StockApiService.productExists", product.getName()));

        Product savedProduct = dataManager.commit(product);

        Stock stock = new Stock();
        stock.setInStock(inStock);
        stock.setOptimalStockLevel(optimalLevel);
        stock.setProduct(savedProduct);

        // validate the stock object
        Set<ConstraintViolation<Stock>> stock_violations = validator.validate(stock);
        if (stock_violations.size() > 0) {
            StringBuilder strBuilder = new StringBuilder();
            stock_violations.stream().forEach(violation -> strBuilder.append(violation.getMessage()).append("; "));
            throw new CustomValidationException(strBuilder.toString());
        }

        return dataManager.commit(stock);
    }

    ...
}

StockApiServiceBean.java

Top

Validation errors in REST

Universal REST API automatically performs bean validation for create and update actions. Validation errors are returned to the client in the following way:

  • The MethodParametersValidationException and MethodResultValidationException exceptions are thrown on validation errors.
  • MethodResultValidationException and ValidationException cause 500 Server error HTTP status
  • MethodParametersValidationException, ConstraintViolationException and CustomValidationException cause 400 Bad request HTTP status

Response body with Content-Type: application/json will contain a list of objects with message, messageTemplate, path and invalidValue properties, for example:

[
    {
        "message": "inStock value is limited to 1000",
        "messageTemplate": "{msg://com.haulmont.dyakonoff.orderman.service/StockApiService.inStockLimit}",
        "path": "addNewProduct.arg1",
        "invalidValue": 500000
    }
]
  • path indicates a path to the invalid attribute in the validated object graph.
  • messageTemplate contains a string which is defined in the message annotation attribute.
  • message contains an actual value of the validation message.
  • invalidValue is returned only if its type is one of the followings: String, Date, Number, Enum, UUID.

Top

GUI Validator

CUBA platform offers an UI-level mechanism to verify input data. Let's take a look at gui validator documentation.

Validation and input type checking should be differentiated. If a given component (e.g. TextField) data type is set to anything different than string (this can happen when binding to an entity attribute or setting datatype), then the component will not allow the user to enter a value that does not comply with this data type. When the component loses focus or when the user presses Enter, the component will show the previous correct value.

On the other hand, validation does not act immediately on data entry or focus loss, but rather when the component’s validate() method is invoked. It means that the component (and the entity attribute that it is linked to) may temporarily contain a value, which does not comply with the conditions of validation. It should not be a problem because the validated fields are typically located in edit screens, which automatically invoke validation for all their fields before commit. If the component is located not in an edit screen, its validate() method should be invoked explicitly in the screen controller.

In a screen XML-descriptor, a component validator can be defined in a nested validator elements. The validator element can have the following attributes:

  • class − name of a Java class implementing the Field.Validator interface. You can use one of the classes that come out of the box or implement your own custom implementation of this interface.
  • script − path to a Groovy script performing validation. Script could be embedded into a screen's XML-descriptor or be given as a separate groovy file.

Groovy validator scripts and standard classes of Java validators (that are located in the com.haulmont.cuba.gui.components.validators package) support message attribute − a message displayed to a user when validation fails. The attribute value should contain either a message or a message key from the messages pack of the current screen.

This validation mechanism works only at UI-level (ran on server-side with error messages passed to user's browser) and is called when user submits the form or application calls validation programmatically by calling AbstractWindow.validateAll() method or validate() method of a component.

Let's look at the examples.

Top

Standard validators

There are set of Validator interface implementations that comes out of the box, although many of them repeat annotation-based validators' functionality:

  • DateValidator
  • DoubleValidator
  • EmailValidator
  • IntegerValidator
  • LongValidator
  • PatternValidator
  • ScriptValidator
  • StringValidator

These validators could be added by hands to the screens' XML-descriptors, just like that:

<field property="email">
    <validator class="com.haulmont.cuba.gui.components.validators.EmailValidator"/>
</field>

customer-edit.xml

or the same could be done from CUBA studio UI:

Figure 9: Standard UI validator

Figure 9: Standard UI validator

A validator class can be assigned to a component not only using a screen XML-descriptor, but also programmatically which is discussed in the next section.

Top

Setting validator programmatically

It's possible to set Validator programmatically for a component (in your screen controller, for example), which could be a good option if you need to modify your validation rules on the fly according to some conditions that can be determined only in run-time.

Here is an example of adding EmailValidator programmatically in a screen controller which checks that OrderItem quantity is <= 1000. It reads the error message string from the current screen messages.properties file and throws ValidationException in case of verification fail:

public class OrderItemEdit extends AbstractEditor<OrderItem> {
    ...
    @Named("fieldGroup.quantity")
    private TextField quantityField;

    @Override
    public void init(Map<String, Object> params) {
        quantityField.addValidator(
                new Field.Validator() {
                    @Override
                    public void validate(Object value) throws ValidationException {
                        if (value != null && value instanceof BigDecimal
                                && ((BigDecimal)value).compareTo(new BigDecimal(1000)) > 0) {
                            throw new ValidationException(getMessage("quantityIsTooBig"));
                        }
                    }
                }
        );
        super.init(params);
    }
    ...
}

Alternatively, we can do the same thing using lambda-function syntax:

    @Override
    public void init(Map<String, Object> params) {
        super.postInit();
        quantityField.addValidator(
                (Object value) -> {
                    if (value != null && value instanceof BigDecimal && ((BigDecimal)value).compareTo(new BigDecimal(1000)) > 0)
                        throw new ValidationException(getMessage("quantityIsTooBig"));
                });
    }

OrderItem.java

Another example of adding Field.Validator in runtime can be found here.

Top

Custom Java class validator

Creation of a custom Field.Validator is not rocket science as well. If a Java class is used as a validator, it should have a default constructor without parameters or a constructor with the following set of parameters:

  • org.dom4j.Element, String – this constructor will receive the validator XML-element and the message pack name of the screen.
  • org.dom4j.Element – this constructor will receive the validator XML-element.

If the validator is implemented as an internal class, it should be declared with a static modifier and its name should be separated by "$", for example:

<validator class="com.sample.sales.gui.AddressEdit$ZipValidator"/>

As an exercise let's write a small Java class to validate that product name and description do not contain any swear words. This can be done by creating a class in gui module that implements Field.Validator interface:

public class ProductNameValidator implements Field.Validator {

    private Logger log = LoggerFactory.getLogger(ProductNameValidator.class);

    protected String message;
    protected String messagesPack;
    protected Messages messages = AppBeans.get(Messages.NAME);

    public static String[] swearWords = {
        ...
    };

    public ProductNameValidator(Element element, String messagesPack) {
        message = element.attributeValue("message");
        this.messagesPack = messagesPack;
    }

    @Override
    public void validate(Object value) throws ValidationException {
        String productName = (String)value;

        for (String swearWord : swearWords) {
            Pattern pat = Pattern.compile(swearWord, Pattern.CASE_INSENSITIVE);
            Matcher mat = pat.matcher(productName);
            if (mat.find()) {
                log.warn("Bad word found in a product name: " +  productName);

                String msgFormat = message != null ? messages.getTools().loadString(messagesPack, message) : "Bad word is detected '%s'";
                String wordFound = productName.substring(mat.start(), mat.end());
                String errorMsg = String.format(msgFormat, wordFound);
                throw new ValidationException(errorMsg);
            }
        }
    }
}

ProductNameValidator.java

and then, setting it as a validator component in XML-descriptor of the screen for "name" and "description" fields:

<field property="name">
    <validator class="com.haulmont.dyakonoff.orderman.validation.ProductNameValidator"
                message="msg://badNameInProductName" />
</field>
<field property="description" rows="5">
    <validator class="com.haulmont.dyakonoff.orderman.validation.ProductNameValidator"
                message="msg://badNameInProductDescription" />
</field>

product-edit.xml

optionally, you can provide a message in a screen message pack, as it was done in the example of validating quantityField.

Top

Validating with Groovy scripts

Running groovy dynamically with Scripting interface looks quite appealing from the first glance. What you shall do is to write a small script that has access to a named variable value (which represents the UI component value script needs to check) and return boolean check result back to the application.

This script can be specified either from CUBA studio UI:

Figure 10: Setting up a groovy script for field validation

Figure 10: Setting up a groovy script for field validation.

or by editing XML screen layout directly:

<field property="quantity">
    <validator message="Quantity cant'be equal to 666 or 777">return (value &gt; 0 &amp;&amp; value != 666 &amp;&amp; value != 777)</validator>
</field>

order-item-edit.xml

However, this is not the approach I would recommend for complex cases, mainly because of difficulties with groovy script debugging and lack of support from IDE. If you still chose to try this way I would say that keeping groovy script in a separate file and giving component a reference to it would be better option.

Top

Validation in UI screen controllers

This is a simple and intuitive approach that allows you to perform quite complex checks of a screen data. Here are pros and cons of this way:

Pros:

  • Easy to implement: you just need to override postValidate method in your screen controller.
  • It has access to Entity object, screen UI controls, middleware services etc...
  • It can do checks of arbitrary complexity.
  • It is easy to debug.

Cons:

  • Acts only on one UI layer, so you'd need to repeat yourself if you have two or more UI modules (web and desktop, for example).
  • Can't help with REST calls validation, even with universal REST.
  • Has difficulties with highlighting fields/components that contains incorrect data. (You'd have to do some CSS/JS magic to achieve that result.)

However, combining this approach with statically and dynamically added Field.Validator checks would negate the last flaw.

Let's look at the code:

public class OrderItemEdit extends AbstractEditor<OrderItem> {
    @Inject
    private StockService stockService;

    @Named("fieldGroup.quantity")
    private TextField quantityField;

    ...

    @Override
    protected void postValidate(ValidationErrors errors) {
        super.postValidate(errors);

        OrderItem item = getItem();

        // check that only POUNDs could have a fractional number quantity
        MeasureUnit unit = item.getProduct().getMeasure();
        if (unit != MeasureUnit.POUND &&
                item.getQuantity().remainder( BigDecimal.ONE ).compareTo(BigDecimal.ZERO) != 0) {
            String msg = "You can't get a fractional number of items measured in " + unit.toString();
            errors.add(msg);
        }

        // Check that Stock has enough Product
        // This is a preliminary check that helps User to get the feedback earlier
        // The final check happens in Order's EntityListener, to be 100% safe from run conditions
        BigDecimal countInStock = stockService.getProductAvailability(item.getProduct());

        if (item.getQuantity().compareTo(countInStock) > 0) {
            String msg = String.format("Insufficient product '%s' in stock (%s left)",
                    item.getProduct().getName(), countInStock.toString());

            errors.add(quantityField, msg);
        }
    }
    ...
}

OrderItemEdit.java

Top

Using middleware listeners for data validation

The last two validation methods I'm going to discuss in this article are based on entity listeners and transaction listeners. Which acts on the middle tier.

These listeners are called when DataManager commits the data. However, transaction listener's beforeCommit() method is called before transaction commit after all entity listeners if the transaction is not read-only. The method accepts a current EntityManager and a collection of entities in the current persistence context. So:

  • Entity listener allows you to execute your business logic each time an entity is added, updated or removed from the database. All it's methods get Entity object and EntityManager. Hence, entity listener is a good place to make single-field and cross-field checks of the entity.
  • Transaction listener can be used to enforce complex business rules that involve multiple entities, after all entities in the transaction have been processed by their entity listeners.

Power of this approach is based on the fact that incorrect data would not be able to pass your checks, doesn't matter from where they came.

Performance note: please note that transaction listener is called for EVERY transaction commit (if it's not read-only). Counting the fact that transaction listener might have quite complex business logic and even make some extra-calls to the DB, the transaction listener's approach might be quite expensive in terms of the application performance.

To send error messages to a user you can use standard ValidationException, or if you want to process these error messages with different client-level exception handler (see the next section for details), you can define your custom RuntimeException class in global module and mark it with @SupportedByClient annotation to have your error messages transported from middleware to client tier.

Both entity and transaction listeners could be easily created from the CUBA studio:

Figure 11: Creating listeners in CUBA studio

Figure 11: Creating listeners in CUBA studio

It allows you to create both interfaces and managed beans for listeners with empty methods implementations.

For entity listeners you can specify what kind of eight events you'd like to process. However, for data validation BeforeInsertEntityListener and BeforeUpdateEntityListener are the two most important.

Figure 12: Entity listener designer

Figure 12: Entity listener designer

At this screen the Use for entities list typically contains only one entity. The same entity should be specified in the Entity type field. However, you may want to specify a @MappedSuperclass entity in the Entity type field and add its subclasses to the Use for entities list.

The @Listeners annotation is added to each class specified in the Use for entities list. If the Bean name is defined for the listener, it is used to refer to the listener in the annotation value. Otherwise, the fully qualified listener class name is used.

@Listeners("orderman_OrderEntityListener")
@NamePattern("%s order#: %s|customer,number")
@Table(name = "ORDERMAN_ORDER")
@Entity(name = "orderman$Order")
public class Order extends StandardEntity {
    private static final long serialVersionUID = -5542761764517463640L;
    ...
}

Order.java

Let's look at the examples.

Top

Single Entity Context example

The demo application uses OrderEntityListener to set the unique order serial number and validate that the order price is correct and equals to the sum of it's order items.

/**
 * Sets the serial number for the order and validates that price is correct
 */
@Component("orderman_OrderEntityListener")
public class OrderEntityListener implements BeforeInsertEntityListener<Order>, BeforeUpdateEntityListener<Order> {
    @Inject
    private TimeSource timeSource;

    @Inject
    private UniqueNumbersAPI uniqueNumbersAPI;

    @Override
    public void onBeforeInsert(Order order, EntityManager entityManager) {
        validateOrderPrice(order);

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy_MM_dd");
        String date = sdf.format(timeSource.currentTimestamp());
        long serialNumb = uniqueNumbersAPI.getNextNumber("order_" + date);

        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd");
        String date2 = sdf2.format(timeSource.currentTimestamp());
        order.setNumber(date2 + '-' + Long.toString(serialNumb));
    }

    @Override
    public void onBeforeUpdate(Order order, EntityManager entityManager) {
        validateOrderPrice(order);
    }

    private void validateOrderPrice(Order order) {
        BigDecimal price = order.getPrice();
        for (OrderItem item : order.getItems()) {
            price = price.subtract(item.getSubTotal());
        }
        if (price.compareTo(BigDecimal.ZERO) != 0) {
            throw new ValidationException("Order price does not match to the total cost of Order Items");
        }
    }
}

OrderEntityListener.java

The UniqueNumbersAPI is used to generate sequential integer numbers for every day.

Top

Transactional Context example

This example uses transaction listener's validation. The idea is quite simple: check all changes in Order and OrderItem objects that are going to be committed, calculate the products' quantities difference and compare these values with what database has in the ORDERMAN_STOCK table.

However, this leads to a pretty complex and computationally heavy business logic, which includes a SELECT query to the database and may attach new Stock objects to the transaction.

@Component("orderman_TransactionListener")
public class TransactionListener implements BeforeCommitTransactionListener {

    @Inject
    private Persistence persistence;

    private Logger log = LoggerFactory.getLogger(TransactionListener.class);

    /**
     * Validates that Stock has enough items for all orders to be committed
     * @see https://doc.cuba-platform.com/manual-6.8/transaction_listeners.html for more examples
     * @param entityManager
     * @param managedEntities
     */
    @Override
    public void beforeCommit(EntityManager entityManager, Collection<Entity> managedEntities) {
        // see https://doc.cuba-platform.com/manual-6.9/transaction_listeners.html for more examples
        Set<Order> ordersToCheck = buildListOfOrdersToCheck(entityManager, managedEntities);
        if (ordersToCheck.size() == 0) return;

        HashMap<UUID, BigDecimal> stockChanges = buildStockChangesSet(ordersToCheck);
        if (stockChanges.size() == 0) return;

        validateStockHasEnoughGoods(stockChanges);
    }

    ...
}

TransactionListener.java

We can see from this example that transaction listeners approach is very powerful, but may be quite complex to implement and could be computationally heavy.

Top

Presenting error messages to a user

As we have seen, three of the methods we have discussed above use exception mechanism to notify client that some field has inappropriate value:

  • Field.Validator method
  • Transaction listeners
  • Entity listeners

In most cases `ValidationException' is thrown to send a message to UI layer.

However, by default CUBA platform doesn't handle this exception in a special way and shows a standard 'Unexpected error' dialog as it does for any other exception.

Figure 13: Error message WITHOUT client-level exception handler

Figure 13: Error message WITHOUT client-level exception handler

This dialog is more suited for presenting some kind of system and unexpected errors rather than showing user validation errors that you as a developer kind of expecting to happen from time to time as a part of your application's business logic.

So, it's recommended to implement a client level exception handler to improve the validation error messages look and feel.

Figure 14: Error message WITH client-level exception handler

Figure 14: Error message WITH client-level exception handler

The basic client-level exception handler is quite simple. You just need to write managed bean that extends AbstractGenericExceptionHandler in your web (or desktop) module and implements doHandle method:

@Component("orderman_ValidationExceptionHandler")
public class ValidationExceptionHandler extends AbstractGenericExceptionHandler {

    public ValidationExceptionHandler() {
        super(ValidationException.class.getName());
    }

    @Override
    protected void doHandle(String className, String message, @Nullable Throwable throwable, WindowManager windowManager) {
        windowManager.showNotification(message, Frame.NotificationType.WARNING);
    }
}

ValidationExceptionHandler.java

Additional examples for cases that do not cover common ValidationException handling needs could be found in documentation.

Top

Summary

We have covered most of the mechanisms that CUBA platform offers for input data validation. Let's group them by

Table 1: Validation levels

Generic UI Universal REST Middleware DataStore Transaction DB server
DB level JPA constraints * yes
@NotNull constraint ** yes yes*** yes yes
Bean validation yes yes*** yes
UI validation (Field.Validator) yes
Custom Field.Validator yes
Screen controllers validation yes
Entity listeners yes
Transaction listeners yes

* - @Table and @Column annotations
** - @NotNull that is accompanied with @Column(nullable = false)
*** - only for fields marked with @Validated annotations

You can read more about CUBA application tiers and blocks here.

Table 2: Validation implementation complexity

Elementary Simple Average Complex
DB level JPA constraints yes
Bean validation yes
Bean validation (custom annotations) yes
UI validation (standard Field.Validator) yes
Custom Field.Validator (Java class) yes
Custom Field.Validator (Groovy script) yes
Screen controllers validation yes
Entity listeners yes
Transaction listeners yes

Table 3: Validation scope

Single Field Cross Field DataStore Transaction Context
DB level JPA constraints yes yes*
Bean validation yes
Bean validation (custom annotations) yes yes
UI validation (standard Field.Validator) yes
Custom Field.Validator (Java class) yes
Custom Field.Validator (Groovy script) yes
Screen controllers validation yes yes
Entity listeners yes yes yes
Transaction listeners yes yes yes yes

* - multi-column unique constraints (@UniqueConstraint)

  1. Bean validation could use standard and custom annotations. It works on all tiers and offers the best level of data security. Besides that it is reusable and gives good UI feedback. The limitations of this approach are:
    1. It can't be used for validating the whole data graph when you need to check state of more than one entity.
    2. Business logic at middleware level can change the entities directly and they will not be validated by this mechanism even before saving them to the DB.
  2. Defining custom Validator class and groovy scripts for UI components. Since it works at UI level only, this mechanism offers nice UI integration support (highlighting and pretty error messages formatting) but drawbacks are the same as for annotations, plus:
    1. It won't be able to check Universal REST calls.
    2. Groovy scripts are hard to debug.
  3. Validation in UI screen controllers approach is the simplest way to do validation if there are no standard annotations or Validator classes to do it. However, you get the checks done only at UI level and the code reusability is not at the best level.
  4. Entity and Transaction listeners give the best security level. Transaction listeners are capable to check the whole object graph. But this approach requires more coding, has not that good UI integration and doing data validation right before commit which might lead to inability to fix the data because it could be too late to do something with the error. Also, complex transaction listeners can degrade the system performance as they happen for every data commit.

Top

Appendix A

CUBA Documentation articles related to validation

  1. Bean Validation
  2. List of annotation-based constraints in CUBA applications
  3. "Using entity listeners" recipe

Other reading

  1. A previous version of this article that uses different samples approach: one simple example per validation method. It contains couple extra examples and can be found here.
  2. There is a good article "Improvements in CUBAs REST API v2" by Mario David if you'd like to read more about REST in CUBA.
  3. Mario also wrote a great article "Security constraints in CUBA" that could help you to learn how to use constraints and validations to implement access/security rules in the CUBA app.

Top

Releases

No releases published

Packages

No packages published

Languages