Course:
1.
Getting started |
2.
Basic domain model (1) |
3.
Basic domain model (2) |
4.
Refining the user interface |
5.
Agile development |
6.
Mapped superclass inheritance |
7.
Entity inheritance |
8.
View inheritance |
9.
Java properties |
10.
Calculated properties |
11.
@DefaultValueCalculator in collections |
12.
@Calculation and collections totals |
13.
@DefaultValueCalculator from file
|
14. Manual
schema evolution
|
15.
Multi user default value calculation |
16.
Synchronize persistent and computed propierties |
17. Logic from database |
18.
Advanced validation |
19.
Refining the standard behavior |
20.
Behavior & business logic |
21.
References & collections |
A.
Architecture & philosophy |
B.
Java Persistence API |
C.
Annotations |
D.
Automated testing
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:

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:
Then the delivered order is added
and the other rejected, producing the next messages:
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:

It will show a dialog, type ISBN
for the annotation name and click on Finish:
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:
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:
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:
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:
You'll get an empty JSON, like
this:
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.
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 19