Vlad

Overview

Vlad stands for "validation". This projects indeed aims at offering a simple, high-level, extensible, generic validation framework that can easily be integrated into existing applications.

Features

Vlad sports the following features:

  • Based on the command pattern: a validation rule is a command that performs a specific validation task - given a "validation context"
  • ;
  • validation rules configurable through XML;
  • extensibility through implementation of additional rules - facilitates community efforts by potentially allowing people to use other people's rules;
  • complex validation supported through rulesets and nested rules;
  • whole object graphs can be validated, not only the values corresponding to the getters of a bean...;
  • localization support (error messages in different languages can be provided);
  • etc.

Architecture

Patterns

Vlad's architecture is based on the following main design patterns:

  • Composite: rules and rulesets are instances of the Validatable interface. This allows rules and rulesets to contain other rules and other rulesets, thus implementing validation flows. In addition, the framework supports ruleset and rule "references" - which are also "validatable" instances to the Vlad runtime. Ruleset and rule references, as their name implies, are not rulesets and rules themselves, but rather refer to defined rules and rulesets.
  • Command: The Validatable interface defines a single validate() method, which takes a ValidationContext as an argument. This context is in fact the execution context of a given validation "flow" - a validation flow consists of one to many validation rules that are sequentially applied to an object (or object graph).

Concepts

The framework relies on a few core concepts and entities, which are summarized below:

  • Validation Engine: the validation engine encapsulates the validation rules. The rules are triggered by the client application at runtime.
  • Validation Rules (and Rulesets): a validation rule performs an atomic unit of validation work. Rules can be grouped in "rulesets": a ruleset aggregates one-to-many rules that are sequentially evaluated
  • Validation Context: the validation context holds the object (or object graph) to validate; it is passed to the validation rules, which perform their work on the object/graph contained in the context.
  • Validation Status: the validation status, as its name implies, holds the status of the current validation. Validation rules are responsible for signaling validation errors to the Vlad runtime through the current context's status.

So what does the above correspond to, concretely? Well, the validation engine is represented by the Vlad class. It is instantiated by the client application, and initialized with one or more XML configuration file(s) of a predefined format. The file contains the rule definitions, and the various possible validation flows. A validation flow's entry point is a RuleSet; a validation can contain many rulesets, which are given a unique identifier (if defined at the root level of the configuration). These rulesets can themselves contain other rules and rulesets. The Rule class models a validation rule and represents an atomic unit of work. Yet there is a special subclass of the Rule class: the CompositeRule class. It can contain other rules and rulesets (or, more precisely, other "validatables"...). The CompositeRule can be inherited by rules that "need" to contain other rules/rulesets. The framework has such a class currently implemented: ForEach.

Usage

At a Glance

An image is worth a thousand words, they say. Before digging into the details, going through the major steps of dealing with Vlad is probably the best introduction; these steps are the following:

1. Configuration

This step involves the creation of a configuration file that defines the rules that will be used (of which instances will be created), and the rulesets consisting of the different validation flows. The example below illustrates a typical configuration file:

<vlad xmlns:vlad="http://www.sapia-oss.org/vlad/rules">
  <namespace prefix="vlad">
    <ruleDef name="forEach"   
                class="org.sapia.validator.rules.ForEach"/>
    <ruleDef name="mandatory" 
                class="org.sapia.validator.rules.Mandatory"/>
    <ruleDef name="maxSize"       
                class="org.sapia.validator.rules.MaxSize"/>
    <ruleDef name="minSize"       
                class="org.sapia.validator.rules.MinSize"/>
    <ruleDef name="select"    
                class="org.sapia.validator.rules.Select"/>    
  </namespace>
  
  <ruleSet id="checkCompany">
    <vlad:minSize attribute="employees" 
                 size="1" 
                 id="numberEmployees">
     <message value="Company should have at least 1 employee"/>
    </vlad:minSize>        
    <vlad:forEach attribute="employees" >
      <vlad:mandatory attribute="name" id="employeeName">
        <message value="Employee does not have a name"/>
      </vlad:mandatory>
    </vlad:forEach>
  </ruleSet>
</vlad>

The Root and the XML Namespaces

The configuration starts with the vlad element. An XML namespace is also defined: xmlns:vlad="http://www.sapia-oss.org/vlad/rules". This namespace specifies a prefix that will be used to create instances of validation rules from existing rule definitions.

Define your Rules

The vlad element takes one-to-many namespace element(s). Such elements are used to hold rule definitions (represented by ruleDef elements). Why such a categorization? Why not define the rules straight under the root element of the configuration? Well, imagine that multiple contributors implement rules on their own, and later decide to make these rules available as part of a community effort; without the notion of namespace, "name collisions" could occur: rules implemented by different parties could have the same name. The notion of namespace is analoguous to the one of packages in Java.

As can be seen, the namespace element must specify a prefix attribute; this is necessary to refer to rule definitions later on in the configuration. An instance of a rule is created through the prefix:ruleName combination. For this to be possible (from the underlying XML parser's perspective), the prefix must be defined as part of an XML namespace declaration (hence the "vlad" prefix in the XML namespace that appears at the beginning of the configuration). Rules are defined with ruleDef elements. A ruleDef element must specify the name and class attributes, which respectively specify the name of the defined rule, and its Java class. The name of the rule is later on used in conjonction with its namespace's prefix to create instances of that rule.

Rule definitions need not being declared in the Vlad configuration. When creating a Vlad instance, the latter will look for a file named vlad_rules.xml in its classpath. This file contains global rule definitions that can be used directly in applications (sparing the declaration of repetitive rule definitions every time a configuration is written). As part of a community effort, other rules can be added, categorized by namespace. Thus, the vlad_rules.xml file in fact defines a library of reusable rules. This is exactly what has been done with the rules defined in the example configuration above. The Vlad distribution contains these rules in its global definition file, so applications need not declaring them in their specific Vlad configurations

A rule with a given name, defined in a given namespace, cannot be defined twice in the same namespace, with the same name. This results in an error.

The global definition file, with our rule definitions exported in it, is given below:

<defs>
  <namespace prefix="vlad">
    <ruleDef name="forEach"   
                class="org.sapia.validator.rules.ForEach"/>
    <ruleDef name="mandatory" 
                class="org.sapia.validator.rules.Mandatory"/>
    <ruleDef name="maxSize"       
                class="org.sapia.validator.rules.MaxSize"/>
    <ruleDef name="minSize"       
                class="org.sapia.validator.rules.MinSize"/>
    <ruleDef name="select"    
                 class="org.sapia.validator.rules.Select"/>    
  </namespace>
</defs>

Our new configuration - stripped from the rule definitions that are now part of the global definitions, is the following:

<vlad xmlns:vlad="http://www.sapia-oss.org/vlad/rules">
  
  <ruleSet id="checkCompany">
    <vlad:minSize attribute="employees" 
                 size="1" 
                 id="numberEmployees">
     <message value="Company should have at least 1 employee"/>
    </vlad:minSize>        
    <vlad:forEach attribute="employees" >
      <vlad:mandatory attribute="name" id="employeeName">
        <message value="Employee does not have a name"/>
      </vlad:mandatory>
    </vlad:forEach>
  </ruleSet>
  
</vlad>

2. Programming

Once you have created a Vlad configuration file, you are ready to rock; the following code creates a Vlad instance, loads the rule configuration defined above, and validates Company instances that contains a collection of Employees.

package org.sapia.validator.examples;

import org.sapia.validator.Vlad;
import org.sapia.validator.Status;
import org.sapia.validator.ValidationErr;

import java.util.List;
import java.util.Locale;
import java.io.File;

public class CompanyEg {

  /**
   * Constructor for CompanyEg.
   */
  public CompanyEg() {
    super();
  }

  public static void main(String[] args) {
    try {
      Vlad v = new Vlad();
      v.load(new File("vlad.xml"));
      Status st = v.validate("checkCompany", 
                             new Company(null), 
                             Locale.getDefault());
      List errs = st.getErrors();
      ValidationErr err;
      for(int i = 0; i < errs.size(); i++){
        err = (ValidationErr)errs.get(i);
        if(err.isThrowable()){
          System.out.println("id :" + err.getId());
          err.getThrowable().printStackTrace();
        }
        else{
          System.out.println("id :" + err.getId());
          System.out.println(err.getMsg());
        }
      }
      
     Company comp = new Company(null);
     Employee empl1 = new Employee("empl1");
     Employee empl2 = new Employee(null);
     comp.addEmployee(empl1);
     comp.addEmployee(empl2);
     
     st = v.validate("checkCompany", 
                     comp, 
                     Locale.getDefault());
     errs = st.getErrors();
     for(int i = 0; i < errs.size(); i++){
       err = (ValidationErr)errs.get(i);
       if(err.isThrowable()){
         System.out.println("id :" + err.getId());
         err.getThrowable().printStackTrace();
       }
       else{
         System.out.println("id :" + err.getId());
         System.out.println(err.getMsg());
       }
     }     
      
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Create and Initialize a Vlad instance

The following code creates the Vlad instance (the validation engine) and initializes it:

Vlad v = new Vlad();
v.load(new File("vlad.xml"));

Validate

Once a validation engine has been created, it can be used (by multiple threads) to validate Java objects. The code excerpt below validates a Company:

Status st = v.validate("checkCompany", 
                       new Company(null), 
                       Locale.getDefault());

The first parameter of the validate() method is the name of the ruleset to use for validation - it corresponds to the name of a ruleset in our configuration. In this case, the "checkCompany" ruleset is used.

Handle the Validation Result

The validate() method returns a Status instance. The status holds information about the validation, mainly: validation errors, if any. The class provides the isError() method, which returns true if the validation flow resulted in validation errors being generated. An application can handle validation errors in the following way:

...
if(status.isError()){
   List errs = st.getErrors();
   // do something with errors.
}
...

Validation errors are represented by the ValidationErr class. The class provides a method (getMsg()) to retreive the error message corresponding to the validation rule that generated it. If a validation rule has signaled a "system" error (in the form of a Java exception), the corresponding ValidationErr instance will in that case hold a Throwable instance, rather then an error message in string format. The ValidationErr class provides the isThrowable() method. The latter returns true if the error instance holds a Throwable instance rather then an error message. The following code illustrates how to manipulate a validation error:

...
if(error.isThrowable()){
   error.getThrowable().printStackTrace();
}
else{
   System.out.println(error.getMsg())
}
...

Error messages are added through message elements when configuring rules; each message is associated to a given Locale, which is specified through a locale attribute. The attribute's value is a path of the form language/country/variant. The path can also consist of language/country and language. If no locale is specified, than the message becomes the default one - used for locales that have no matching messages. Messages are resolved according to the algorithm followed when using Java resource bundles: given a locale, they are searched in reverse hierarchichal order until a message is found that most corresponds to the locale. The hierarchy, in our case, is expressed through the path notation.

As was seen above, the validate() method specifies that a Locale be passed to it as a parameter; the instance is internally used to retrieve the proper message when a validation error occurs.

The rule below - taken from our example configuration - has been slightly modified with the addition of an error message that would match all Locale with "fr" (French) as language indicator:

<vlad:minSize attribute="employees" 
             size="1" 
             id="numberEmployees">
 <message value="Company should have at least 1 employee"/>
 <message 
       value="La compagnie devrait avoir au moins un employe" 
       locale="fr"/> 
</vlad:minSize>

In addition, the ValidationErr has a getId() method. The latter returns the identifier of the rule that generated the error. This arbitrary identifier is assigned by the individual who is creating the rule configuration. It can for example be made to correspond to the name of a UI widget from which the validated information comes - you have probably seen forms on web sites that would be popped back to you when some data, in a specific form field, would prove erroneous. In such a case, the web site asks you to fill that specific field again - without having to refill the whole form.

Advanced Issues

Implement your Rules

Implementing your rules is quite straightforward, and is where the real power of Vlad lies. The steps below show how this can be done, through the implementation of a rule that validates if a given number is positive - however useful that may be:

Extend the Rule class

Have your rule class extend the org.sapia.validator.Rule class:

package org.sapia.validator.examples;

import org.sapia.validator.Rule;
import org.sapia.validator.ValidationContext;

public class PositiveRule extends Rule{

  /**
   * Constructor for PositiveRule.
   */
  public PositiveRule() {
    super();
  }
  
  /**
   * @see org.sapia.validator.Rule#validate(ValidationContext)
   */
  public void validate(ValidationContext context) {}
}

Implement the validate() Method

Now, provide and implementation for the validate() method:

public void validate(ValidationContext context) {
  Integer intg = (Integer)context.get(); 
  if(intg.intValue() < 0){
    context.getStatus().error(this);
  }
}

Configure your Rule

Create a configuration that uses your rule:

<vlad xmlns:vlad="http://www.sapia-oss.org/vlad/rules">
  <namespace prefix="vlad">
    <ruleDef name="positive"   
       class="org.sapia.validator.examples.PositiveRule"/>
  </namespace>

  <ruleSet id="checkPositive">
    <vlad:positive>
        <message value="Value is not positive"/>
    </vlad:positive>        
  </ruleSet>

</vlad>

Test your Rule

The following code assumes that the configuration is contained in a file named "positive.xml", placed in "user.dir":

public static void main(String[] args) {
  try {
    Vlad v = new Vlad().load(new File("positive.xml"));
    Status s = v.validate("checkPositive", 
               new Integer(-1),
               java.util.Locale.getDefault());
   
    if(s.isError()){
      List errs = s.getErrors();
      ValidationErr err;
      for(int i = 0; i < errs.size(); i++){
        err = (ValidationErr)errs.get(i);
        if(err.isThrowable()){
          err.getThrowable().printStackTrace();
        }
        else{
          System.out.println(err.getMsg());
        }
      }
    }                 
    else{
      System.out.println("No validation error.");
    }
  } catch (Exception e) {
    e.printStackTrace();
  }
}

When executing the above, our configured error message should appear.

Share your Rule

Once your rule has been designed and tested, you are ready to share it with the rest of the world. Packaging rule implementations involves:

  • Creating a rule definition file;
  • packaging the definition file, along with all the classes necessary to use your rules, in a Java archive (jar file);
  • specifying to the users of your rules how to initialize a Vlad instance with your definition file (this will typically involve calling the loadDefs() method on the Vlad instance passing the path to the definition file).

Given our PositiveRule (see further above), we create the following rule definition file:

<defs>
  <namespace prefix="vlad">
    <ruleDef name="positive"   
      class="org.sapia.validator.examples.PositiveRule"/>
  </namespace>
</defs>

The rule class, together with the definition file, are packaged in a jar file with the following structure:


org/sapia/validator/examples/PositiveRule.class

org/sapia/validator/examples/positivedefs.xml

Packaging the definition in the jar file means that it will be loaded as a resource; do not put it at the root of the jar (since it could be overriden by another resource with the same name in the classpath), but in a path that ensures its unicity (preferrably in the path that corresponds to the package where your rule classes are defined).

Once your rules have been cleanly packaged, users can use them in their applications (making sure they initialize their validation engine properly with your rule definition file):

Vlad v = new Vlad()
 .loadDefs("org/sapia/validator/examples/positivedefs.xml");

All load() methods support chained invocations. Multiple definitions can be loaded conveniently like so - the last load call loads the actual rulesets that the application will use:

Vlad v = new Vlad().loadDefs("someDefs.xml")
 .loadDefs("someOtherDefs.xml").load("someRuleSets.xml");

Rule Instantiation and Initialization

The validation engine uses dynamic class loading and instantiation to create instances of your rule classes - the latter must have a public, no-args constructor. Java's Reflection API is used to assign configuration attributes (and elements) to the rule instances. For example, given the following:

<vlad:minSize attribute="employees" 
              size="1" 
              id="numberEmployees">
    <message value="Company should have at least 1 employee"/>
</vlad:minSize>

The above implies that the rule class corresponding to the "vlad:minSize" rule has setAttribute(), setSize() and setId() methods. For sub-elements (like "message"), the existence of a corresponding createXXXX or addXXXX is assumed, were XXXX corresponds to the name of the sub-element. The creator or adder method must return the created/added instance, which is then initialized in the same manner, and so on, recursively.

For those of you who have implemented Ant tasks, this pattern will ring a bell.

In the case of setters, the attribute values are coerced to the type that is specified by the setters; for booleans, the "true" string will result in a true boolean. Attribute values must correspond to Java's primitive types (including java.lang.String). The Rule class already implements the setId() and createMessage() methods - so inheriting classes all support the "id" and "message" attribute/element.

If a rule must be able to contain other rules, then have its class extend the CompositeRule class. It provides such support.

Rule and Ruleset References

A validation configuration can become quite complex. To strip it from repetitive rules and rulesets, Vlad supports the notion of "reference": a rule or ruleset reference refers to a defined rule or ruleset respectively, yet appears to the validation engine as a Validatable instance.

The following defines a "ruleRef":

<vlad xmlns:vlad="http://www.sapia-oss.org/vlad/rules">

  <vlad:minSize attribute="employees" 
            size="1" 
            id="numberEmployees">
    <message 
      value="Company should have at least 1 employee"/>
  </vlad:minSize>      


  <ruleSet id="checkCompany">
    
    <!-- Refers to the rule defined above --> 
    <ruleRef id="numberEmployees"/>
    
    <vlad:forEach attribute="employees">
      <ruleSet id="checkEmployee">
        <vlad:mandatory attribute="name" id="employeeName">
          <message value="Employee does not have a name"/>
        </vlad:mandatory>
      </ruleSet>
    </vlad:forEach>      
        
  </ruleSet>
</vlad>

The following defines a "ruleSetRef":

<vlad xmlns:vlad="http://www.sapia-oss.org/vlad/rules">

  <ruleSet id="checkEmployee">
    <vlad:mandatory attribute="name" id="employeeName">
      <message value="Employee does not have a name"/>
    </vlad:mandatory>
    
    <!-- Here we could add other 
        "employee validation" rules -->
        
  </ruleSet>


  <ruleSet id="checkCompany">
    <vlad:minSize attribute="employees" 
              size="1" 
              id="numberEmployees">
      <message 
         value="Company should have at least 1 employee"/>
    </vlad:minSize>        
    <vlad:forEach attribute="employees">
    
      <!-- Here we refer to a ruleset that 
           validates employees -->
      <ruleSetRef id="checkEmployee"/>
      
    </vlad:forEach>      
    
  </ruleSet>
</vlad>

When using references, there are some constraints to keep in mind:

  • Only rules and rulesets defined under the vlad element can be referred to;
  • in such a case, they must specify an id attribute, and must be referred to with the value of this attribute. By the same token, ruleRefs and ruleSetRefs must also provide an id attribute, which of course in this case maps to the identifier of defined rule/ruleset.
The order of declaration does not matter; the rules and rulesets that are referred to by references need not be declared before the latter.