openxava / documentation / Lesson 6: Advanced validation

Course: 1. Getting started | 2. Modeling with Java | 3. Automated testing | 4. Inheritance | 5. Basic business logic | 6. Advanced validation | 7. Refining the standard behavior | 8. Behavior & business logic | 9. References & collections | A. Architecture & philosophy | B. Java Persistence API | C. Annotations

Table of contents

Lesson 6: Advanced validation
Validation alternatives
Adding delivered property to Order
Validating with @EntityValidator
Validating with a JPA callback method
Validating in the setter
Validating with Bean Validation
Validating on removal with @RemoveValidator
Validating on removal with a JPA callback method
What's the best way of validating?
Creating your own Bean Validation annotation
Using a Bean Validation from your entity
Defining your own ISBN annotation
Using Apache Commons Validator to implement the validation logic
Call to a REST web service to validate the ISBN
Adding attributes to your annotation
JUnit tests
Testing validation for adding to a collection
Testing validation assigning a reference and validation on removal
Testing the custom Bean Validation
Summary
So far we have only done some basic validations using the @Required annotation. However, sometimes it's convenient to write our own logic for the validation. In this lesson we are going to describe custom validation methods which allow us to add specific business logic to your application.

Validation alternatives

We are going to enhance your code with this logic: if the orders are not delivered yet, then the user cannot assign them to an invoice. That is, only delivered orders can be associated with an invoice.

Adding delivered property to Order

First you have to add a new property to the Order entity. The delivered property:
@Column(columnDefinition="BOOLEAN DEFAULT FALSE")
boolean delivered;
Moreover it's necessary to add the delivered property to the view. Modify the Order view as shown in the following code:
@View(extendsView="super.DEFAULT", 
    members=
        "estimatedDeliveryDays, delivered," + // Add delivered
        "invoice { invoice }" 
)
...
public class Order extends CommercialDocument {
There is a new delivered property now which indicates the delivery state of an order. Try the new code and mark some of the existing orders as delivered.

Validating with @EntityValidator

Up to now the user can add any order to any invoice from the Invoice module, and he can associate a particular invoice with any order from the Order module. We are going to restrict this: only delivered orders are allowed to be added to an invoice.
The first alternative to implement this validation is by using an @EntityValidator. This annotation allows you to assign the desired validation logic to your entity. Let's annotate the Order entity as show in following code:
@EntityValidator(
    value=com.yourcompany.invoicing.validators.DeliveredToBeInInvoiceValidator.class, // The class with the validation logic
    properties= {
        @PropertyValue(name="year"), // The content of these properties
        @PropertyValue(name="number"), // is moved from the 'Order' entity
        @PropertyValue(name="invoice"), // to the validator before
        @PropertyValue(name="delivered") // executing the validation
    }
)
public class Order extends CommercialDocument {
Every time an Order object is created or modified an object of type DeliveredToBeInInvoiceValidator is created. Then its properties year, number, invoice and delivered are initialized with the properties of the same name from the Order object. After that, the validate() method of the validator is executed. Let's write the validator code, first create the com.yourcompany.invoicing.validators package, then put the next class inside:
package com.yourcompany.invoicing.validators; // In 'validators' package

import com.yourcompany.invoicing.model.*;
import org.openxava.util.*;
import org.openxava.validators.*;
import lombok.*;
 
@Getter @Setter 
public class DeliveredToBeInInvoiceValidator
    implements IValidator { // Must implement IValidator
 
    int year; // Properties to be injected from Order
    int number;
    boolean delivered;
    Invoice invoice;
 
    public void validate(Messages errors) // The validation logic
        throws Exception
    {
        if (invoice == null) return;
        if (!delivered) {
            errors.add( // By adding messages to errors the validation will fail
                "order_must_be_delivered", // An id from i18n file
                year, number); // Arguments for the message
         }
    }

}
The validation logic is absolutely straightforward: if an invoice is present and this order is not marked as delivered we add an error message, so the validation will fail. You should add the error message in the Invoicing/i18n/Invoicing-messages_en.properties file. As shown below:
# Messages for the Invoicing application
order_must_be_delivered=Order {0}/{1} must be delivered in order to be added to an Invoice
Now you can try to add orders to an invoice with the application, you will see how the undelivered orders are rejected. Go to Invoices module, select the ORDERS tab of an invoice, and from there click on Add button:
validation_en010.png
It will show a dialog with a list of orders to choose from. Choose two, one of them not delivered yet, then click on ADD:
validation_en013.png
Then the delivered order is added and the other rejected, producing the next messages:
validation_en017.png
Your validation is implemented correctly with @EntityValidator. It's not difficult, but a little “verbose”, because you need to write a “fully featured” new class merely to add 2 lines of code logic. Let's learn other ways to do the same validation.

Validating with a JPA callback method

We're going to try another, maybe even simpler, way to do this validation, we'll transfer the validation logic from the validator class into the Order entity itself, in this case in a @PrePersist and @PreUpdate method.
First, remove the DeliveredToBeInInvoiceValidator class from your project. Then remove the @EntityValidator annotation from your Order entity:
//@EntityValidator( // Remove the '@EntityValidator' annotation
//    value=com.yourcompany.invoicing.validators.DeliveredToBeInInvoiceValidator.class,
//    properties= {
//        @PropertyValue(name="year"),
//        @PropertyValue(name="number"),
//        @PropertyValue(name="invoice"),
//        @PropertyValue(name="delivered")
//    }
//)
public class Order extends CommercialDocument {
After that we're going to add the validation again, but now inside the Order class itself. Add the validate() method to your Order class:
@PrePersist @PreUpdate // Just before creating or updating
private void validate()  throws Exception {
    if (invoice != null && !isDelivered()) { // The validation logic
        // The validation exception from Bean Validation
        throw new javax.validation.ValidationException(
            XavaResources.getString(
                "order_must_be_delivered",
                getYear(),
                getNumber()
            )
        );
    }
}
Before saving an order this validation will be executed, if it fails a ValidationException is thrown. This exception is from the Bean Validation framework, so OpenXava knows that it is a validation exception. This way with only one method within your entity you have the validation done.
Only one @PrePersist method and one @PreUpdate method by entity are allowed, so before executing the above code you have to comment the @PrePersiste and @PreUpdate annotations you have in recalculateDeliveryDays(), in this way:
// @PrePersist @PreUpdate // Comment these annotations
private void recalculateDeliveryDays() {
    setDeliveryDays(getEstimatedDeliveryDays());
}
Don't worry, we'll uncomment these annotations later. Although JPA only allows one @PrePersist/@PreUpdate method you always can create a unique callback method and call all other methods from it, but not needed for our case, because we're not going to keep this validation style as definitive.
Now, try to add orders not delivered to an invoice and see the validation errors, like in our first example.

Validating in the setter

Another alternative to do your validation is to put the validation logic inside the setter method. That's a simple approach.
First, put back again the @PrePersist and @PreUpdate annotations in recalculateDeliveryDays(), also remove the validate() method from the Order entity:
@PrePersist @PreUpdate // Add back the callback annotations
private void recalculateDeliveryDays() {
    setDeliveryDays(getEstimatedDeliveryDays());
}
    
// Remove the validate() method

// @PrePersist @PreUpdate // Just before creating or updating
// private void validate()  throws Exception {
//     if (invoice != null && !isDelivered()) { // The validation logic
//         // The validation exception from Bean Validation
//         throw new javax.validation.ValidationException(
//             XavaResources.getString( 
//                 "order_must_be_delivered",
//                 getYear(),
//                 getNumber()
//             )
//         );
//     }
// }    
Then add the next setInvoice() setter method to Order:
public void setInvoice(Invoice invoice) {
    if (invoice != null && !isDelivered()) { // The validation logic
        // The validation exception from Bean Validation
        throw new javax.validation.ValidationException(
            XavaResources.getString(
                "order_must_be_delivered",
                getYear(),
                getNumber()
            )
        );
    }
    this.invoice = invoice; // The regular setter assignment
}
This works exactly the same way as the two other options. This is like the @PrePersist/@PreUpdate alternative, only that it does not depend on JPA, it's a basic Java implementation.

Validating with Bean Validation

As a last option we are going to do the shortest one: The validation logic is put into a boolean method annotated with the @AssertTrue Bean Validation annotation.
To implement this alternative first remove the setInvoice() method:
// Remove the setter method

// public void setInvoice(Invoice invoice) {
//    if (invoice != null && !isDelivered()) { // The validation logic
//        // The validation exception from Bean Validation
//        throw new javax.validation.ValidationException(
//            XavaResources.getString(
//                "order_must_be_delivered",
//                getYear(),
//                getNumber()
//            )
//        );
//    }
//    this.invoice = invoice; // The regular setter assignment
// }
Then add the isDeliveredToBeInInvoice() method to your Order entity:
@AssertTrue( // Before saving it asserts if this method returns true, if not it throws an exception
    message="order_must_be_delivered" // Error message in case false
)
private boolean isDeliveredToBeInInvoice() {
    return invoice == null || isDelivered(); // The validation logic
}

In previous forms of validation our error message was constructed using two arguments, year and number, which in our i18n file are represented by {0}/{1} respectively. For the validation case with @AssertTrue we can not pass these two arguments to construct our error message, but we can declare properties and qualified properties of the validated bean in the definition of the message, for that change in Invoicing-messages_en.properties the entry:
order_must_be_delivered=Order {0}/{1} must be delivered in order to be added to an Invoice
By:
order_must_be_delivered=Order {year}/{number} must be delivered in order to be added to an Invoice
Note as we change {0}/{1} by {year}/{number}. OpenXava will fill {year}/{number} with values of year and number that has the Order being updated and does not fulfill the condition of validation.
This is the simplest way to validate, because the method with the validation only has to be annotated. The Bean Validation is responsible for calling this method when saving takes place, and throws the corresponding ValidationException if the validation does not succeed.

Validating on removal with @RemoveValidator

The validations we have seen until now are processed when the entity is modified, but sometimes it's useful or it's required to process the validation before the removal of the entity, and to use the validation to cancel the entity removal.
We are going to modify the application to reject the removal of an order if it has an invoice associated. To achieve this annotate your Order entity with @RemoveValidator, as show in following code:
@RemoveValidator(com.yourcompany.invoicing.validators.OrderRemoveValidator.class) // The class with the validation
public class Order extends CommercialDocument {
Now, before removing an order the logic in OrderRemoveValidator is executed, and if validation fails the order is not removed. Let's look at the code for the validator:
package com.yourcompany.invoicing.validators; // In 'validators' package

import com.yourcompany.invoicing.model.*;
import org.openxava.util.*;
import org.openxava.validators.*;
 
public class OrderRemoveValidator
    implements IRemoveValidator { // Must implement IRemoveValidator
 
    private Order order;
 
    public void setEntity(Object entity) // The entity to remove will be injected
        throws Exception // with this method before validating
    {
        this.order = (Order) entity;
    }
 
    public void validate(Messages errors) // The validation logic
        throws Exception
    {
        if (order.getInvoice() != null) {
            // By adding messages to errors the validation
            // will fail and the removal will be aborted
            errors.add("cannot_delete_order_with_invoice");
        }
    }
}
The validation logic is in the validate() method. Before calling the entity to be validated, it is injected using setEntity(). If messages are added to the errors object the validation will fail and the entity will not be removed. You have to add the error message in the Invoicing/i18n/Invoicing-messages_en.properties file:
cannot_delete_order_with_invoice=An order with an invoice cannot be deleted
If you try to remove an order with an associated invoice now, you will get an error message and the removal will be rejected.
You can see that using a @RemoveValidator is not difficult but verbose. You have to write a full new class to add a simple if. Let's examine a briefer alternative.

Validating on removal with a JPA callback method

We're going to try another, maybe simpler, way to do this removal validation just by moving the validation logic from the validator class to the Order entity itself, in this case in a @PreRemove method.
First, remove the OrderRemoveValidator class from your project. Also remove the @RemoveValidator annotation from your Order entity:
//@RemoveValidator(com.yourcompany.invoicing.validators.OrderRemoveValidator.class) // Remove the @RemoveValidator
public class Order extends CommercialDocument {
We have just removed the validation. Let's add the functionality again, but now inside the Order class itself. Add the validateOnRemove() method in your Order class:
@PreRemove // Just before removing the entity
private void validateOnRemove() {
    if (invoice != null) { // The validation logic
        throw new javax.validation.ValidationException( // Throws a runtime exception
            XavaResources.getString( // To get the text message
                "cannot_delete_order_with_invoice"));
    }
}
This validation will be processed before the removal of an order. If it fails a ValidationException is thrown. You can throw any runtime exception in order to abort the removal. You have done the validation with a single method inside the entity.

What's the best way of validating?

You have learned several ways to do validations in your model classes. Which of them is the best one? All of them are valid options. It depends on your circumstances and personal preferences. If you have a validation that is non-trivial and reusable across your application, then to use @EntityValidator and @RemoveValidator is a good option. On the other hand, if you want to use your model classes from outside OpenXava and without JPA, then the use of validation in setters is better.
In our example we'll use the @AssertTrue for the “delivered to be in invoice” validation and @PreRemove for the removal validation, because this is the simplest procedure.

Creating your own Bean Validation annotation

The techniques in the previous section are very useful for many validations. Nevertheless, sometimes you will face some validations that are very generic and you will want to reuse them over and over again. In this case to define your own Bean Validation annotation can be a good option. Defining a Bean Validation is more verbose but usage and reuse is simple; just adding an annotation to your property or class.
We are going to learn how to create a validator from Bean Validation.

Using a Bean Validation from your entity

It is very easy. Just annotate your property, as you see in the next code:
@ISBN // This annotation indicates this property must be validated as an ISBN
String isbn;
By merely adding @ISBN to your property, it will be validated before the entity is saved into the database. Great! The problem is that @ISBN is not included as a built-in constraint in the Bean Validation framework. This is not a big deal. If you want an @ISBN annotation, just create it. Indeed, we are going to create the @ISBN validation annotation in this section.
First of all, let's add a new isbn property to Product. Edit your Product class and add to it the code bellow:
@Column(length=13)
String isbn;
Try out your Product module with the browser. Yes, the isbn property is already there. Now, you can add the validation.

Defining your own ISBN annotation

Let's create the @ISBN annotation. First, create a package in your project called com.yourcompany.invoicing.annotations. Then click with the right mouse button on it and choose New > Annotation, as following:
validation_en020.png
It will show a dialog, type ISBN for the annotation name and click on Finish:
validation_en030.png
Edit the code of your recently created ISBN annotation and leave it as in the next code:
package com.yourcompany.invoicing.annotations; // In 'annotations' package

import java.lang.annotation.*;
import javax.validation.*;
 
@Constraint(validatedBy = com.yourcompany.invoicing.validators.ISBNValidator.class)
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ISBN { // A regular Java annotation definition
 
    Class<?>[] groups() default{};
    Class<? extends Payload>[] payload() default{};
    String message() default "isbn_invalid"; // Message id from i18n file
}
As you can see, this is a regular annotation definition. The message attribute is the message to show to the user if the validation fails, you can write the message "as is" or put an i18n id. The developer can specify its own message when he uses the annotation, although we provide one by default, "isbn_invalid", so we have to add the next entry into Invoicing-messages_en.properties:
isbn_invalid=ISBN invalid or nonexistent
The @Constraint indicates the class with the validation logic. Let's write the ISBNValidator class.

Using Apache Commons Validator to implement the validation logic

We are going to write the ISBNValidator class with the validation logic for an ISBN. Instead of writing the ISBN validation logic by ourselves we'll use the Commons Validator project from Apache. Commons Validator contains validation algorithms for email addresses, dates, URLs and so on. The commons-validator.jar is included by default in OpenXava projects, so you can use it without further configuration. The code for ISBNValidator:
package com.yourcompany.invoicing.validators; // In 'validators' package

import javax.validation.*;
 
import com.yourcompany.invoicing.annotations.*;
import org.openxava.util.*;
 
public class ISBNValidator
    implements ConstraintValidator<ISBN, Object> { // Must implement ConstraintValidator
 
    private static org.apache.commons.validator.routines.ISBNValidator
        validator = // From 'Commons Validator' framework
            new org.apache.commons.validator.routines.ISBNValidator();
 
    public void initialize(ISBN isbn) {
 
    }
 
    // Contains the validation logic
    public boolean isValid(Object value, ConstraintValidatorContext context) { 
        if (Is.empty(value)) return true;
        return validator.isValid(value.toString()); // Relies on 'Commons Validator'
    }
}
As you see, the validator class must implement ConstraintValidator from the javax.validation package. This forces your validator to implement initialize() and isValid(). The isValid() method contains the validation logic. Note that if the value to validate is empty we assume that it is valid. Validating when the value is present is the responsibility of other annotations like @Required.
In this case the validation logic is plain vanilla, because we only call the ISBN validator from the Apache Commons Validator project.
@ISBN is ready to be used. Just annotate your isbn property with it. You can see how:
@Column(length=13) @ISBN
String isbn;
In this case when you save the class the import for @ISBN is not added automatically. This is because there is another @ISBN available (from Hibernate Validator library included in OpenXava), so OpenXava Studio does not know which one to choose. Don't worry, just put your mouse over the @ISBN annotation and a popup will be shown with several possible solutions, choose Import 'ISBN' (com.yourcompany.invoicing.annotations) so the correct import will be added to Product class:
validation_en035.png
Now, you can test your module, and verify that the ISBN values you enter are validated correctly. Congratulations, you have written your first Bean Validation. It's not so difficult. One annotation, one class.
This @ISBN is good enough for use in real life. Nevertheless, we'll try to improve it, simply to have the chance to experiment with a few interesting possibilities.

Call to a REST web service to validate the ISBN

Though most validators have simple logic, you can create validator with complex logic if necessary. For example, in the case of our ISBN, we want, not only to verify the correct format, but also to check that a book with that ISBN actually exists. A way to do this is by using web services.
As you already know, a web service is a functionality hosted in web servers and can be called by a program. The traditional way to develop and use web services is by means of WS-* standards, like SOAP, UDDI, etc., although, the simplest way to develop services today is REST. The basic idea of REST is to use the already existing “way to work” of the internet for inter-program communication. Calling a REST service consists of using a regular web URL to get a resource from a web server; this resource is usually data in XML, HTML, JSON or any other format. In other words, the programs use the internet just as regular users with their browsers.
There are a lot of sites with SOAP and REST web services that enable us to consult a book ISBN, we're going to use openlibrary.org that provides a free REST API to consult its book catalog. To try the Open Library API open a browser and go to the next URL:
https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:9780932633439
Where the last parameter is the ISBN of the book, from it you will get a JSON with the data of the book, something like this:
validation040.png
A JSON is just data with key/value using {} and [] for nesting and repeating. If you try to get the data of a non-existen book, like in this URL:
https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:9791034369997
You'll get an empty JSON, like this:
validation050.png
That is, an empty JSON, just {}.
To call this web service we'll use JAX-RS. JAX-RS is the Java standard to call REST web services. OpenXava includes support to call web services using JAX-RS, so you don't need to add any additional library.
Let's modify ISBNValidator to use this REST service. See the result:
package com.yourcompany.invoicing.validators; 

import javax.validation.*;
import javax.ws.rs.client.*; // To use JAX-RS
import com.yourcompany.invoicing.annotations.*;
import org.apache.commons.logging.*; // To use Log
import org.openxava.util.*;
 
public class ISBNValidator
    implements ConstraintValidator<ISBN, Object> {
	
    private static Log log = LogFactory.getLog(ISBNValidator.class); // Instantiate 'log'
 
    private static org.apache.commons.validator.routines.ISBNValidator
        validator = 
            new org.apache.commons.validator.routines.ISBNValidator();
 
    public void initialize(ISBN isbn) {
 
    }
 
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (Is.empty(value)) return true;
        if (!validator.isValid(value.toString())) return false;
        return isbnExists(value); // Here we do the REST call
    }
    
    private boolean isbnExists(Object isbn) {
        try {
            // Here we use JAX-RS to call a REST service
            String response = ClientBuilder.newClient()
                .target("http://openlibrary.org/") // The site
                .path("/api/books") // The path of the service
                .queryParam("jscmd", "data") // Parameters
                .queryParam("format", "json")
                .queryParam("bibkeys", "ISBN:" + isbn) // The ISBN is a parameter
                .request()
                .get(String.class); // A String with the JSON
            return !response.equals("{}"); // Is the JSON empty? Enough for our case.
        }
        catch (Exception ex) {
            log.warn("Impossible to connect to openlibrary.org " +
                "to validate the ISBN. Validation fails", ex);
            return false; // If there are errors we assume that validation fails
        }
    }
    
}
We simply open the URL with the ISBN as the request parameter. If the resulting JSON is an empty JSON, that is {}, the search has failed, otherwise we have found the book. For this case, getting the JSON as a String to do a simple comparison is the simplest approach, however JAX-RS could parse the JSON as a Java object of your own class (Book for example) filling the corresponding properties, just use .get(Book.class) instead of .get(String.class) as last line of the call.
Try out your application now and you'll see that the validation will fail if you enter a non-existent ISBN.

Adding attributes to your annotation

It's a good idea to create a new Bean Validation annotation if you reuse the validation several times, usually across several projects. To improve the reusability you may want to parametrize the validation code. For example, for your current project to do the search in openlibrary.org for ISBN is OK, but in another project, or even in another entity of your current project, you do not want to call this particular URL. The code of the annotation has to be more flexible.
This flexibility can be achieved by attributes. For example, we can add a boolean search attribute to our ISBN annotation in order to switch on or off the internet search for validation. To implement this functionality, just add the search attribute to the ISBN annotation code:
public @interface ISBN {
    boolean search() default true; // To (de)activate web search on validate
 
    // ...
}
This new search attribute can be read from the validator class:
public class ISBNValidator implements ConstraintValidator<ISBN, Object> {
    // ...
    private boolean search; // Stores the search option
 
    public void initialize(ISBN isbn) { // Read the annotation attributes values
        this.search = isbn.search();
    }
 
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (Is.empty(value)) return true;
        if (!validator.isValid(value.toString())) return false;
        return search ? isbnExists(value) : true; // Using 'search'
    }
    // ...
}
Here you see the use of the initialize() method: the source annotation can be used to initialize the validator, in this case simply by storing the isbn.search() value to evaluate it in isValid().
Now you can choose whether you want to call our REST service or skip the ISBN validation:
@ISBN(search=false) // In this case no internet search is done to validate the ISBN
private String isbn;
Using this simple method you can add any attribute you need to add more flexibility to your ISBN annotation.

Congratulations! You have learned how to create your own Bean Validation annotation, and by the way, to use JAX-RS for calling REST services.

JUnit tests

Our goal is not to develop a huge quantity of software, but to create quality software. At the end of the day, if you create quality software you will deliver more functionality, because you will spend more time on new and exciting things and less time on debugging legions of bugs. And you know that the only way to quality is automated testing, so lets update our test code.

Testing validation for adding to a collection

Recall that we have refined the code in a way that the user cannot assign orders to an invoice if the orders are not marked as delivered yet. After that, your current testAddOrders() of InvoiceTest can fail, because it tries to add the first order, and this first order might not be marked as delivered yet.
Let's modify the test method to run correctly and also to test your new validation functionality:
public void testAddOrders() throws Exception {
    login("admin", "admin");
    assertListNotEmpty();
    execute("List.orderBy", "property=number");
    execute("List.viewDetail", "row=0");
    execute("Sections.change", "activeSection=1");
    assertCollectionRowCount("orders", 0);
    execute("Collection.add",
        "viewObject=xava_view_section1_orders");
    // execute("AddToCollection.add", "row=0"); // Now we don't select randomly
 
    checkFirstOrderWithDeliveredEquals("Delivered"); // Selects one delivered order
    checkFirstOrderWithDeliveredEquals(""); // Selects one not delivered order
    execute("AddToCollection.add"); // We try to add both
    assertError( // An error, because the not delivered order cannot be added
        "ERROR! 1 element(s) NOT added to Orders of Invoice");
    assertMessage( // A confirm message, because the delivered order has been added
        "1 element(s) added to Orders of Invoice");
 
    assertCollectionRowCount("orders", 1);
    checkRowCollection("orders", 0);
    execute("Collection.removeSelected",
        "viewObject=xava_view_section1_orders");
    assertCollectionRowCount("orders", 0);
}
We have modified the part for selecting orders to add. Before we selected the first order, no matter if it's delivered or not. Now we select one order delivered and one order not delivered. In this way we test if the delivered one is added and the not delivered one is rejected.
The missing piece here is the way to check the orders. This is the task of the checkFirstOrderWithDeliveredEquals() method:
private void checkFirstOrderWithDeliveredEquals(String value) throws Exception {
    int c = getListRowCount(); // The total displayed rows in list
    for (int i=0; i<c; i++) {
        if (value.equals(
            getValueInList(i, 12))) // 12 is the 'delivered' column
        {
            checkRow(i);
            return;
        }
    }
    fail("There must be at least one row with delivered=" + value);
}
Here you see a good technique to do a loop over the displayed list elements in order to check them, get data or do whatever you want with the list data. In order this test works the first invoice has to have no deliveries and moreover it must be at least one order delivered, but not the first one.

Testing validation assigning a reference and validation on removal

From the Invoice module the user cannot add orders to an invoice if they are not delivered yet, therefore, from the Order module the user cannot assign an invoice to an order if the order is not delivered. That is, we have to test the other side of the association too. We'll do it by modifying the existing testSetInvoice() of OrderTest.
Moreover, we'll use this case to test the remove validation we introduced in sections Validating on removal with @RemoveValidator and Validating on removal with a JPA callback method. There we modified the application to prevent the user from removing an order which has an invoice associated with it. Now we will test this restriction.
The revision of testSetInvoice() of OrderTest with all these enhancements is below:
public void testSetInvoice() throws Exception {
    login("admin", "admin");
    assertListNotEmpty();
    execute("List.orderBy", "property=number"); // To set the list order
    execute("List.viewDetail", "row=0");
    assertValue("delivered", "false"); // The order must be not delivered
    execute("Sections.change", "activeSection=1");
    assertValue("invoice.number", "");
    assertValue("invoice.year", "");
    execute("Reference.search",
        "keyProperty=invoice.year");
    execute("List.orderBy", "property=number");
    String year = getValueInList(0, "year");
    String number = getValueInList(0, "number");
    execute("ReferenceSearch.choose", "row=0");
    assertValue("invoice.year", year);
    assertValue("invoice.number", number);
 
    // Not delivered order cannot have invoice
    execute("CRUD.save");
    assertErrorsCount(1); // We cannot save because it is not delivered
    setValue("delivered", "true");
    execute("CRUD.save"); // With delivered=true we can save the order
    assertNoErrors();
 
    // Order with invoice cannot be deleted
    execute("Mode.list"); // We go to list and
    execute("CRUD.deleteRow", "row=0"); // we delete the saved order
    assertError("Impossible to remove Order because: " + // We cannot delete because
        "An order with an invoice cannot be deleted"); // it has an invoice associated
 
    // Restoring original values
    execute("List.viewDetail", "row=0");
    setValue("invoice.year", "");
    setValue("delivered", "false");
    execute("CRUD.save");
    assertNoErrors();
}
The original test only searched for an invoice, but did not even save. Now, we added test code at the end which tries to save the order with delivered=false and with delivered=true, in this way we test the validation. After that, we try to delete the order, that has an invoice. Thus we test the validation on removal too. Before running this test make sure that first order is not delivered and has not invoice.

Testing the custom Bean Validation

The last step is to test the ISBN Bean Validation, which uses a REST service to do validation. We simply have to write a test case that tries to assign an incorrect, a nonexistent and a correct ISBN to a product and checks the results for these cases. To do so let's add a testISBNValidator() method to ProductTest.
public void testISBNValidator() throws Exception {
    login("admin", "admin");
 
    // Searching the product1
    execute("CRUD.new");
    setValue("number", Integer.toString(product1.getNumber()));
    execute("CRUD.refresh");
    assertValue("description", "JUNIT Product 1");
    assertValue("isbn", "");
 
    // With incorrect ISBN format
    setValue("isbn", "1111");
    execute("CRUD.save"); // Fails because of format (apache commons validator)
    assertError("1111 is not a valid value for ISBN of " +
        "Product: ISBN invalid or nonexistent");
 
    // ISBN does not exist though it has correct format
    setValue("isbn", "9791034369997");
    execute("CRUD.save"); // Fails because it does not exist (REST service)
    assertError("9791034369997 is not a valid value for ISBN of " +
        "Product: ISBN invalid or nonexistent");
 
    // ISBN exists
    setValue("isbn", "9780932633439");
    execute("CRUD.save"); // It does not fail
    assertNoErrors();
}
Surely the manual testing you were doing during the development of the @ISBN validator was like the one above. Therefore, if you write your JUnit test before the application code, you can use it as you proceed. This is more efficient than repeating the test procedures by hand using the browser over and over again.
Note that if you use @ISBN(search=false) this test will not work because it checks the result of the REST service. So, you have to use the @ISBN annotation of the isbn property without the search attribute in order to run this test successfully.
Now execute all the tests for your Invoicing application to verify that everything works as expected.

Summary

In this lesson you have learned several ways to do validation in an OpenXava application. Also, you know how to encapsulate the reusable validation logic in annotations with custom Bean Validation.
Validation is an important part of the logic of your application, and we encourage you to put it into the model, i. e. into your entities. We demonstrated several examples for this technique in the lesson. Sometimes it is more convenient to put logic outside your model classes. You will learn that in the next lessons.

Download source code of this lesson

Any problem with this lesson? Ask in the forum Everything fine? Go to Lesson 7