{#========================================== Docs : "Validation" ==========================================#}

Validation

Spincast provides a good set of utilities to validate objects. We decided to base those utilities on plain Java code and not on annotations. Validation annotations may seem convenient at first, but, in a real project, you quickly realize that you can't express all the details you need : you can't use complex logic, you can't validate an element based on the result of the validation of another element, etc. In general, you end up writing plain Java code anyway in addition to those annotations... But then your validation logic is scattered in many places and this is not the ideal.

In Spincast, you validate an object using three main steps :

  1. You create a Validation Set instance. This is a container where the results of the validation will be saved.
  2. You use Predefined Validations or custom validation code to actually validate the object. Those validations may result in Validation Messages. Most of the time those are Error Validation Messages resulting from failed validations ("This email is invalid", for example).

  3. When the validation is all done, you use the resulting Validation Messages by retrieving them from the Validation Set. In general, you want to display them to a user, or you generate some kind of report.

Here's a quick example :

// Creation of a Validation Set
ValidationSet myValidationSet = getValidationFactory().createValidationSet();

// The object we're going to validate
User user = getUser();

// Validates the email of the user using the 
// "validationEmail()" predefined validation
myValidationSet.validationEmail()
               .key("email")
               .element(user.getEmail())
               .validate();
                                           
// Validates the username of the user, using
// custom validation code
String username = user.getUsername();
if(username == null || username.length() < 3 || username.length() > 42) {
    myValidationSet.addError("username",
                             "USERNAME_INVALID",
                             "Must contain between 3 and 42 characters.");  
}

// Uses/Displays the resulting Validation Messages
if(!myValidationSet.isValid()) {
    
    Map<String, List<ValidationMessage>> validationMessages = myValidationSet.getMessages();
    
    // ... do something with the messages
}

In the JsonObject validation section, we'll see that validation is even easier when done using JsonObjects...

The Validation Set

At the core of a validation process, is a ValidationSet. A Validation Set is a set of Validation Messages and of utility methods. It acts as a container to save the results of the various validations run on an object.

For example, let's say we validate a User object. At the end of this validation, the resulting Validation Set may contain two Validation Messages :

There are two ways of creating a Validation Set instance :

A Validation Set is more than a simple container to store the validation results. It also provides Predefined Validations and a bunch of other utility methods. Those utility methods are :

Validation Messages

A ValidationMessage is composed of three things :

When you add a Validation Message to a Validation Set, you do so using a validation key. The validation key allows you to retrieve the Validation Messages associated with a specific element from the Validation Set.

Here's a quick example where we perform a validation and create a Validation Message :


ValidationSet validatorSet = validationFactory.createValidationSet();
JsonObject user = getUser();
        
String thirdBookTitle = user.getString("user.books[2].title");
if(containsNsfwWords(thirdBookTitle)) {
    
    ValidationMessage message = 
            getValidationFactory().createMessage(ValidationLevel.WARNING, 
                                                 "NSFW_MATERIAL", 
                                                 "This book contains NSFW material.");
                                                 
    validatorSet.addMessage("user.books[2].title", message);
}

Explanation :

Validation Sets also provides some methods to quickly add Validation Messages, without having to create the messages manually. For example :

// Creates and adds an Error Validation Message
validatorSet.addError("user.books[2].title", "LENGTH", "The title is too long");

// Creates and adds a Warning Validation Message
validatorSet.addWarning("user.books[2].title", "LENGTH", "The title is rather long");

// Creates and adds a Success Validation Message
validatorSet.addSuccess("user.books[2].title", "LENGTH", "The title length is perfect");

Validation Keys

A validation key is used to associate Validation Messages to the validated element they have been created for. You can think of a Validation Set as a big Map<String, List<ValidationMessage>> where the keys are validation keys and where the values are the Validation Messages.

If you validate a simple email element for example, you could use "email" or "email-validation" as the validation key. It's up to you to choose a meaningful name for the key : you will later use it to retrieve all the Validation Messages associated with this element so you can display them, create a report from them, etc.

Even if you can use any string as a validation key, we suggest that you stick to some conventions since a validation key must be unique and should well represent the element it is associated with.

The convention we suggest is that, when possible, you use the JsonPath of the validated element as the validation key.

Let's say we validate this user object :

{
    "name": "Stromgol",
    "email": "test@example.com"
    "books": [
        {
            "title" : "Dune",
            "author": "Frank Herbert"
        },
        {
            "title" : "The Hitchhiker's Guide to the Galaxy",
            "author" : "Douglas Adams"
        }
    ]
}

First, we will validate the email :

JsonObject user = getUser();

// We create a Validation Set
ValidationSet validationSet = getValidationFactory().createValidationSet();

if(!isEmailValid(user.getString("email"))) {

    // We add a Validation Message using "email" as the validation key
    validationSet.addError("email", "EMAIL_VALIDATION", "Invalid email");  
}

We can later retrieve the Validation Messages associated with the email element using that "email" validation key :


List<ValidationMessage> validationMessages = validationSet.getMessages("email");

Very straightforward! But now let's try to do the thing with the title of the first book :

// Gets the title of the first book, using its JsonPath
String title = user.getString("books[0].title");

if(!isTitleValid(title) {

    // We add a Validation Message using "title" as the validation key...
    // This may not be a good idea!
    validationSet.addError("title", "TITLE_VALIDATION", "Invalid title");  
}
 
List<ValidationMessage> validationMessages = validationSet.getMessages("title");

We may be tempted to use a very short name as the validation key, "title" for example. It would work, but it's not a very good idea... The problem is that now the validation key doesn't well represent the validated element it is associated with. Remember that each validation key must be unique. What happens if we also want to validate the title of the second book? Or if there are more than one user with books to validate? What validation keys will we use then? We can't use the same "title" validation key for more than one element!

Since each element of a JsonObject already has a unique identifier, which is its JsonPath, why not use this identifier as its validation key?

// Gets the title of the first book, using its JsonPath
String title = user.getString("books[0].title");

if(!isTitleValid(title) {

    // We add a Validation Message using the JsonPath
    // of the validated element as the validation key : 
    // much better!
    validationSet.addError("books[0].title", "TITLE_VALIDATION", "Invalid title");  
}
 
List<ValidationMessage> validationMessages = validationSet.getMessages("books[0].title");

Now there won't be any conflict, even if there are a lot of validation keys in our Validation Set, and every key will be clearly indicate which validated element it is associated with!

Note that there are situations where you can't use a JsonPath for a validation key. You don't always validate elements individually, for instance. Sometimes, a combination of elements is invalid, and therefore no JsonPath is available.

In those situations, we suggest that you scope the validation key as much as possible, and that you use creativity in order to make the key unique and meaningful!

For example, let's say that an HTML form has been submitted and you need to validate that two passwords from it match. If they don't match, you could had an Error Validation Message for both of those elements, or only for the second one (you could say that only the second one is invalid since it doesn't match the first one). But you could also want to add a Validation Message to represent that invalid combination, not the fields taken individually. You may want to display a "The passwords don't match" error in the section where both fields are, for example.

In such situations, you can't use a JsonPath as the validation key, since you are not referring to a single element. But we suggest that you still try to scope the validation key, so it is as unique and meaningful as possible. For example, that key could be "myForm.user.passwordsMatch" :

ValidationSet validationSet = getValidationFactory().createValidationSet();

JsonObject formData = context.request().getFormData();

// Gets the passwords using their JsonPath
String password1 = formData.getString("myForm.user.password1", "");
String password2 = formData.getString("myForm.user.password2", "");
if(!password1.equals(password2)) {

    // Use a scoped and meaningful
    // validation key, even if it's not 
    // a true JsonPath!
    validationSet.addError("myForm.user.passwordsMatch", 
                           "PASSWORDS_MATCH", 
                           "The password don't match!");  

A question remains : what happens to this "Use the JsonPath of a validated element as its validation key" convention when we validate an element using an external Validator? For example, we may have a TitleValidator that can be reused in multiple places in our application. For example :

ValidationSet validationSet = getValidationFactory().createValidationSet();

String title = getUser().getString("books[0].title");

TitleValidator titleValidator = getTitleValidator();

ValidationSet titleValidationSet = titleValidator.validate(title);

validationSet.mergeValidationSet(titleValidationSet);

// Oups! 
// The validation key is "title" here, not "books[0].title" as we
// would like...
List<ValidationMessage> messages = validationSet.getMessages("title");

External validators return a Validation Set and you merge this set to your own local set [9] (you can learn more about merging Validation Sets in the Sharing / Merging Validation Sets section).

The problem here is that TitleValidator has no idea of the position of the "title" element inside our local user object! It will see the "title" element as being a root element and therefore it won't generate a validation key respecting our local JsonPath... It will probably simply use "title" as the validation key.

For this reason, the mergeValidationSet(...) method accepts a "validationKeyPrefix" parameter. If this parameter is specified, all the validation keys will be prefixed with it when merged :

ValidationSet validationSet = getValidationFactory().createValidationSet();

String title = getUser().getString("books[0].title");

TitleValidator titleValidator = getTitleValidator();

// We use the external validator
ValidationSet titleValidationSet = titleValidator.validate(title);

// We prefixe the merged validation keys
validationSet.mergeValidationSet("books[0].", titleValidationSet);

// The Validation Messages can now be retrieved using
// JsonPaths thast are valid on our validated root object!
List<ValidationMessage> messages = validationSet.getMessages("books[0].title");

There is also a prefixValidationKeys(...) method which can be used to prefix the validation keys of a Validation Set directly, without having to merge this set into another one. This can be useful, for example if you want to prefix the validation keys before adding a Validation Set to a JsonObject :

// Uses an external validator
ValidationSet validationSet = getExternalValidator().validate(someObject);
        
// Prefixes the resulting validation keys
// *directly on the Validation Set*
validationSet.prefixValidationKeys("someObject.");

// Adds the Validation Set (with the validation keys
// now modified) to the response model
context.response().getModel().set("validation", validationSet);

Predefined Validations

Spincast provides some predefined validations to help validate an object. They are defined on the ValidationSet interface so there are easily accessible during a validation process. Let's have a look at an example :

// We create an inital Validation Set
ValidationSet myValidatorSet = getValidationFactory().createValidationSet();

// This is a username we're going to validate!
String username = getSomeUsername();

// Validates that the username is not blank and, only if 
// it's not, validates that it contains at least 3 characters.
ValidationSet lastResult = myValidatorSet.validationNotBlank()
                                         .key("username")
                                         .element(username)
                                         .validate();
if(lastResult.isValid()) {
    lastResult = myValidatorSet.validationMinLength(3)
                               .key("username")
                               .element(username)
                               .validate();
}

Notice that a predefined validation returns it's own Validation Set [9]! This allows you to conditionally apply a validation depending on the result of a previous one [13].

To use a predefined validation :

When the validate() method is called, the validation is perform and :

Here's the list the predefined validations available on a Validation Set :

Predefine Validation options

Some options are available during the process of using a predefine validation :

If you are validating a JsonArray, by using the "all(...)" method, then a couple of extra methods are also available :

JsonObject validation

Validating a JsonObject is very easy : you simply use its "validationSet()" method to get a Validation Set specifically made to validate it. For example :

JsonObject user = getUser();
JsonObjectValidationSet userValidationSet = user.validationSet();
ValidationSet lastResult = userValidationSet.validationEmail()
                                            .jsonPath("email")
                                            .validate();

Explanation :

By calling the "validationSet()" method on a JsonObject, you get a JsonObjectValidationSet instance. This type extends the ValidationSet base type and adds some extra functionalities.

With a JsonObjectValidationSet, you don't need to specify a "key(...)" to use or an "element(...)" to validate! You simply specify the jsonPath(...) to the element to validate. The resulting Validation Messages, if any, will have that JsonPath as their validation keys. For example :

JsonObject user = getJsonManager().create();
user.set("email", "nope");

JsonObjectValidationSet userValidationSet = user.validationSet();

// The Validation Set is bound to the "user" object here,
// so we use a JsonPath starting from this object as the root
// to target the "email" element to validate 
ValidationSet lastResult = userValidationSet.validationEmail()
                                            .jsonPath("email")
                                            .validate();

// The generated validation key is the JsonPath of
// the validated element!
List<:ValidationMessage> messages = userValidationSet.getMessages("email");
System.out.println(messages.size()); // prints "1"

ValidationMessage message = messages.get(0);
System.out.println(message.getValidationLevel()); // prints "ERROR"
System.out.println(message.getCode()); // prints "VALIDATION_TYPE_EMAIL"
System.out.println(message.getText()); // prints "Invalid email address"

Finally, note that there is also a "jsonPathAll(...)" method to validate all the elements of a JsonArray.

Sharing / Merging Validation Sets

You can merge a Validation Set into another one using the mergeValidationSet() method. This allows you to make some elements being validated by an external validator and then merge the resulting Validation Messages in your local Validation Set.

Let's say you have a CompanyValidator object that provides a validateCompany(...) method able to validate a "company" JsonObject :

// The user object we're going to validate
JsonObject user = getUser();

// The local Validation Set bound to our "user" object
JsonObjectValidationSet userValidationSet = user.validationSet();

// We validate the email of the user directly
ValidationSet lastResult = userValidationSet.validationEmail()
                                            .jsonPath("email")
                                            .validate();
                                            
// We use an *external validator* to validate the company
// of the user
ValidationSet companyValidationSet = 
        getCompanyValidator().validateCompany(user.getJsonObject("company"));

// We merge the Validation Set of the company validation
// to our local set!
userValidationSet.mergeValidationSet("company.", companyValidationSet);

Explanation :

When you merge two Validation Sets, don't forget to scope the merged validation keys (if required)... Learn more about this in the Validation Keys section!

Being able to share and merge Validation Sets allows you to be creative and structure your application as you wish. You can have standalone validators that are used in multiple places and situations.

Converting to a JsonObject

When your validation is done, you will want to use the Validation Messages saved in the Validation Set. In some cases, you need to serialize them to Json, for example to send them as a response to an Ajax call.

To do that, you simply call the convertToJsonObject() method on the Validation Set. This will convert the set to a JsonObject which, in turn, can easily be serialized to a plain Json string.

When you convert a Validation Set to a JsonObject, an extra root element is added (in addition to the Validation Messages) : "_". This special "_" element summarizes the validation performed using the Validation Set. It contains those fields :

Have a look at the Displaying validation messages section to see the use of this "_" element in action!