Intake Service

Intake uses an xml specification to perform web form input validation and mapping input data to a bean's properties. In other words, Intake will allow a web application to take web form input, validate it and then map the data to an object. Tools like the Torque help provide mapping of Objects to the database and Intake helps map web form data to Objects. Intake can be used standalone by any application that supports Avalon components. However it has extensive integration with the Turbine application framework. This HowTo focuses on using it with Turbine.

The visual picture of where Intake fits into a Turbine web application looks something like this:

------------------
    HTML Form
------------------
     Intake
------------------
 Business Objects  <- e.g. Torque Generated
------------------
      Peers        <- e.g. Torque Generated
------------------
      RDBMS
------------------

There are several advantages to using Intake. First off, it provides for a centralized management system for dealing with form data. All of the configuration of Intake is done through a single XML file. Next, Intake is good for validating the form data. Intake provides the ability to do regular expression matching in order to make sure that form fields contain the data that they should contain. For example, if someone should only enter a number into a form field, it can be validated with a regular expression. Intake also supports the input of localized values and does the right thing based on the Locale the user requested. Lastly, Intake can provide a centralized source for error messages. If the validation fails, the error message defined in the XML file can be shown to the user.

Usage

Intake is implemented as a Avalon component. Access to it is provided by a Turbine Pull Tool. One or more XML specifications are parsed during the service initialization and used by the pull tool while processing request data and generating the response.

Intake is made available in the Velocity context with the default value of $intake. The name of the variable that is used is what is configured for the tool. For example, the current configuration is "tool.request.intake". To change the name of the variable to "foo", it would be "tool.request.foo".

Intake is made available in Java code by adding the following imports to the top of a .java file:

import org.apache.fulcrum.intake.model.*;
import org.apache.turbine.services.intake.IntakeTool;

Xml File

The following example comes from Scarab. These are a couple of groups from Scarab's intake.xml:

<input-data xmlns="http://turbine.apache.org/schema/intake/4.0" basePackage="org.tigris.scarab.">
  <group name="AttributeValue" key="attv"
            mapToObject="om.AttributeValue">
    <field name="Value" key="val" type="String">
        <rule name="maxLength" value="255">Value length cannot be &gt; 255</rule>
        <rule name="required" value="true">This module requires data for 
                this attribute.
        </rule>
    </field>
    <field name="Url" key="url" type="String" mapToProperty="Value">
        <rule name="maxLength" value="255">Url length cannot be &gt; 255</rule>
        <rule name="mask" value="^$|http.+">Please enter an url starting with "http"</rule>
        <rule name="required" value="true">This module requires a valid url.</rule>
    </field>
    <field name="OptionId" key="optionid" type="NumberKey">
        <rule name="mask" value="^$|[0-9]+">Please select a valid choice</rule>
        <rule name="required" value="true">This module requires that you select 
                an option for this attribute.
        </rule>
    </field>
  </group>

  <group name="Login" key="login">
    <field name="Username" key="u" type="String">
        <rule name="minLength" value="1">Please enter an email address</rule>
        <rule name="mask" value=".+@.+\..+">Please enter a valid email address</rule>
    </field>
    <field name="Password" key="p" type="String">
        <rule name="minLength" value="1">Please enter a password</rule>
    </field>
  </group>
</input-data>

A group is a set of fields that have been aligned so that they form a logical unit. The first group includes some of the properties from a om.AttributeValue business object (BO). This object is a Java Bean object with get/set methods for each of the properties in the object. In this case, the object has been auto-generated by Torque from Scarab's SQL schema.

The group tag has a name attribute which is the name that will be used within templates and java code to refer to the group. It also contains a key attribute which will be used in the query parameters. The key is not referenced in any code, so it can be a short name (even 1 character) as long as it is uniquely identifies the group from the rest.

An object that the group's fields will map can also be specified. This will be a default; the individual fields within a group can map to different objects.

Fields have attributes: name and key which serve similar function to the group's name and key attributes. It has mapToObject and mapToProperty fields that can be used to associate a business object to the field for prepopulating the field as well as assigning the field data to the bean after successful validation. The field must have a type out of the following supported types:

  • boolean
  • BigDecimal This field type supports localization.
  • int
  • float This field type supports localization.
  • FileItem
  • String (default)
  • DateString
  • double This field type supports localization.
  • short
  • long
  • custom Custom field types can be defined by using this type. The class name of the customized field must be given in the fieldClass attribute then. The custom class must extend the class org.apache.fulcrum.intake.model.Field. This way, more complex types, such as Email, Url, or Date, can be added that add functionality that is difficult or not possible with a simple regex mask.
The field types supporting localization will parse the given strings from the request using the locale from the parser to allow the entry of floating point numbers with localized decimal separators.

The field can define rule elements. The basic fields include rules for minimum and maximum, lengths and values, as well as a regex mask.

Login Example

<group name="Login" key="login">

The name="Login" is a descriptive name for the group. The key="login" is the value that is used in the web forms to identify the group. The key= value is not directly referenced. In other words, you do not need to know it exists unless you are debugging your application. Both of these attribute values must be unique across all groups in the XML file. Now, lets look at the fields in the group.

<field name="Username" key="u" type="String">
    <rule name="minLength" value="1">Please enter an email address</rule>
    <rule name="mask" value=".+@.+\..+">Please enter a valid email address</rule>
</field>
<field name="Password" key="p" type="String">
    <rule name="minLength" value="1">Please enter a password</rule>
</field>

The name="Username" is the descriptive name for the field. The key="u" is the value that is used in the web forms to identify the field. Both of these attributes must be unique across the fields within the group. The type="String" specifies what the system expects the input for that field to be (please see the intake.dtd for the allowed values). Within the field, it is possible to specify one or more rules. These rules define how Intake should validate web form data. There are minLength, maxLength and mask attributes to the rule tag. The message inside the rule tag is a text message which can be used to display an error within the template.

At this point, it is best to show an example form of how to use Intake within a Velocity template:

(1) <form action="$link.setPage("Login.vm")" method="POST" name="login" >
(2)    <input type="hidden" name="action" value="Login">
(3)    #if ($data.Parameters.nextTemplate)
(4)    <input type="hidden" name="nextTemplate"
              value="$data.Parameters.nextTemplate">
       #else
(5)    <input type="hidden" name="nextTemplate" value="Start.vm">
       #end

<p>
Email Address:

(6)    #set ( $loginGroup = $intake.Login.Default )

(7)    #if ( !$loginGroup.Username.isValid() )
(8)        $loginGroup.Username.Message<br>
       #end
(9)    <input name= "$loginGroup.Username.Key"
              value="$!loginGroup.Username" size="25" type="text">
</p>

<p>
Password:

(10)    #if ( !$loginGroup.Password.isValid() )
(11)        $loginGroup.Password.Message<br>
        #end
(12)   <input name= "$loginGroup.Password.Key"
              value="" size="25" type="text"
              onChange="document.login.submit();">
</p>

(13) <input type="submit" name="eventSubmit_doLogin" value="Login">
(14) $intake.declareGroups()
</form>

The example above shows quite a few different concepts with regards to web application design, so lets break them down a bit, starting from the top. Each of the important lines have been numbered for easy reference.

  1. Create the <form> tag. Within it, we use the $link object to create a URI for the template "Login.vm". In other words, when the button is clicked, the page will submit upon itself.
  2. Set the Action to execute to be "Login". This can either be a hidden input field or be defined with the $link.setPage().setAction("Login") method
  3. Check to see if there is a "nextTemplate" defined in the GET/POST/PATH_INFO information. On success, the Action can use the nextTemplate field to decide what page to show next.
  4. If (3), then create a hidden input tag that holds the value for nextTemplate.
  5. If not (3), then set the nextTemplate to be the "Start.vm" page.
  6. This retrieves the default Login Group object from Intake. What this means is that the group "Login" as defined in Scarab's intake.xml is represented as an object.
  7. It is then possible to query the object to confirm if the information within it is valid.
  8. This will display the invalid error message as defined in the intake.xml <rule> definitions.
  9. Here we define a form input text field. The $loginGroup.Username.Key specifies an Intake system generated key. The value attribute $!loginGroup.Username will tell Intake to either display an empty String or display the previous form submission.
  10. Repeat the same procedure as for the Username field.
  11. Repeat the same procedure as for the Username field.
  12. A bit of JavaScript will cause the form to submit itself if one hits tab after entering the password.
  13. eventSubmit_doLogin is special. It tells Turbine to execute the doLogin method in the Action. This is based on the Action Events system.
  14. $intake.declareGroups() tells Intake to add a couple hidden input fields to the page output. These fields represent the Groups that were used in the template.

Below is an example of the HTML that is sent to the browser after the page has been requested:

<form action="http://foo:8080/scarab/servlet/scarab/template/Login.vm"
      method="POST" name="login" >
    <input type="hidden" name="action" value="Login">
        <input type="hidden" name="nextTemplate" value="Start.vm">

<p>
Email Address:

       <input name= "login_0u"
        value="" size="25" type="text">
</p>

<p>
Password:

       <input name= "login_0p"
        value="" size="25" type="text" onchange="document.login.submit();">
</p>

<input type="submit" name="eventSubmit_doLogin" value="Login">

<input type="hidden" name="intake-grp" value="login"></input>
<input type="hidden" name="login" value="_0"></input>

</form>

Some notes to consider:

  1. The _0 signifies the "default" group.
  2. The login_0u signifies the login group combined with the _0 and the "u" is from the intake.xml file for the field "Username".
  3. The two hidden input fields are what is generated from the $intake.declareGroups()

The Java Action code which handles the submission of the form looks like this:

public void doLogin( PipelineData pipelineData, Context context ) throws Exception
{
	// get a reference to the run data object
	RunData data = (RunData) pipelineData;
	
    IntakeTool intake = (IntakeTool) context.get("intake");

    if ( intake.isAllValid() &amp;&amp; checkUser(data, context) )
    {
        String template = data.getParameters()
            .getString(ScarabConstants.NEXT_TEMPLATE,
            TurbineResources.getString("template.homepage", "Start.vm") );
        setTemplate(data, template);
    }
    else
    {
        // Retrieve an anonymous user
        data.setUser (TurbineSecurity.getAnonymousUser());
        setTemplate(data,
            data.getParameters()
                .getString(ScarabConstants.TEMPLATE, "Login.vm"));
    }
}

/**
    Checks to make sure that the user exists, has been confirmed.
*/
public boolean checkUser(RunData data, Context context)
    throws Exception
{
    User user = null;
    IntakeTool intake = (IntakeTool)context
        .get(ScarabConstants.INTAKE_TOOL);

    try
    {
        String username = null;
        String password = null;
        try
        {
            Group login = intake.get("Login", IntakeTool.DEFAULT_KEY);
            username = login.get("Username").toString();
            password = login.get("Password").toString();
        }
        catch ( Exception e )
        {
            throw new TurbineSecurityException(
                "Login information was not supplied.");
        }

        // Authenticate the user and get the object.
        user = TurbineSecurity.getAuthenticatedUser( username, password );

        ...
    }
}

Intake is retrieved from the context and asked whether all the inputs that it knows about were valid. If not the login form will be quickly reshown and error messages will be given. If the data is valid, the field data is extracted manually in this case, as the Intake fields do not map directly to a bean object. The next example will use the group.setProperties() method to directly assign Intake's field data to the matching beans.

Attribute Value example

<group name="AttributeValue" key="attv"
            mapToObject="om.AttributeValue">

The name="AttributeValue" is simply a descriptive name for the group. The key="attv" is the value that is used in the web forms to identify the group. Both of these attributes must be unique across all groups in the XML file. The mapToObject="om.AttributeValue" is an optional attribute. This specifies what Java Bean object that this group maps to. If a mapToObject is not specified, then it is possible to use Intake to retrieve the values of the data directly instead of getting it from a populated object. This will be covered in detail further on.

<field name="Value" key="val" type="String">
    <rule name="maxLength" value="255">Value length cannot be &gt; 255</rule>
    <rule name="required" value="true">This module requires data for 
        this attribute.
    </rule>
</field>
<field name="Url" key="url" type="String" mapToProperty="Value">
    <rule name="maxLength" value="255">Url length cannot be &gt; 255</rule>
    <rule name="mask" value="^$|http.+">Please enter an url starting with "http"</rule>
    <rule name="required" value="true">This module requires a valid url.</rule>
</field>

The fields within a group relate to the form fields on a web page. At this point, it is probably best to show an example rather than explaining in detail what each part of the field tag is. Therefore, using the fields above in a simple example, one might have a form with a text entry box that allows you to edit a Url. The filename is: "EditUrl.vm".

#set ( $action = $link.setPage("EditUrl.vm").setAction("SaveUrl") )
<form action="$action"
      method="post">

#set ( $attributeValue = $issue.AttributeValue("URL") )
#set ( $group = $intake.AttributeValue.mapTo($attributeValue) )

Enter Url:
<input type="text" name="$group.Url.Key" value="$!group.Url.Value">

<input type="submit" name="eventSubmit_doSave" value="Submit>

$intake.declareGroups()
</form>

To explain the template above, the first #set is done simply for convenience. The second #set is part of Scarab. It uses the $issue object to retrieve a "URL" AttributeValue object for a particular issue.

The next #set tells Intake to map that object to the AttributeValue group. What it does is tell Intake to create an AttributeValue Group object which has been mapped to the AttributeValue retrieved from the $issue object. This Group object represents the XML file <group> as a Java object.

Moving down further into the example, there is the <input> field which has a name and value attributes. The $group.Url.Key tells Intake to retrieve the key information for the field. This would evaluate to "attv_0url". What this is a combination of the group key (attv), a "_0" is the result of retrieving the "$intake.AttributeValue.Default" and the "url" is the field key. The value attribute would evaluate to just an empty String the first time around. The $intake.declareGroups() is a special method that will create a hidden input field that declares which groups have been added to the page. We will discuss that in more detail further down.

View source on the HTML page after executing the template and this is what the form above would look like:

<form action="http://server/s/servlet/s/template/EnterUrl.vm/action/EnterUrlAction"
      method="post">

Enter Url:
<input type="text" name="attv_0url" value="">

<input type="submit" name="eventSubmit_doEnter" value="Submit>

<input type="hidden" name="attv" value="_0">
<input type="hidden" name="intake-grp" value="attv">
</form>

When the form is submitted to the server (the user clicks the submit button), the following code in the EnterUrlAction.java class is executed.

public void doEnter( PipelineData pipelineData, Context context ) throws Exception
{
    IntakeTool intake = (IntakeTool)context
        .get(ScarabConstants.INTAKE_TOOL);

    // check to see if the fields are valid
    if ( intake.isAllValid() )
    {
        // get the "AttributeValue" Group
        AttributeValue av = new AttributeValue();
        Group group = intake.get("AttributeValue", IntakeTool.DEFAULT_KEY);
        group.setProperties (av);
        // now av is properly populated with the form data
    }
}

If the form fields are invalid as a result of not matching one of the rules that are defined in the fields in the XML file, then the action does nothing and the page is displayed again.

Back to explaining the fields, lets look at the example again:

<field name="Value" key="val" type="String">
    <rule name="maxLength" value="255">Value length cannot be &gt; 255</rule>
    <rule name="required" value="true">This module requires data for 
        this attribute.
    </rule>
</field>
<field name="Url" key="url" type="String" mapToProperty="Value">
    <rule name="maxLength" value="255">Url length cannot be &gt; 255</rule>
    <rule name="mask" value="^$|http.+">Please enter an url starting with "http"</rule>
    <rule name="required" value="true">This module requires a valid url.</rule>
</field>

Multiple groups of the same class in one form

This example uses a form from Scarab that assigns values to various attribute's that are relevant for an issue (bug). Attributes include summary, operating system, platform, assigned to, etc. Some of the attributes are required, but not all.

The template:

#set ( $action = $link.setPage("entry,Wizard3.vm").setAction("ReportIssue")
.addPathInfo("nextTemplate", "entry,Wizard4.vm") )
#set ($user = $scarabR.User)
#set ($module = $user.CurrentModule)
#set ($issue = $user.ReportingIssue)

<form method="get" action="$action">

<hr><br>Please fill in the following:<br><br>

#foreach ( $attVal in $issue.OrderedModuleAttributeValues )
  #set ( $attrInput = $intake.AttributeValue.mapTo($attVal) )

  #if ( $attVal.Attribute.AttributeType.ValidationKey )
        #set ( $field = $attVal.Attribute.AttributeType.ValidationKey )
  #elseif ($attVal.Attribute.AttributeType.Name == "combo-box" )
        #set ( $field = "OptionId" )
  #else
        #set ( $field = "Value" )
  #end

    #if ( $attVal.isRequired() )
        $attrInput.get($field).setRequired(true)
        <b>*</b>
    #end

    $attVal.Attribute.Name:

    #if ($attVal.Attribute.AttributeType.Name == "combo-box" )
          <font color="red">
            #attrValueErrorMsg ( $attVal $field )
          </font>
          <br>
          #attrValueSelect ($attVal $field "")
    #else
          <font color="red">
            #attrValueErrorMsg ( $attVal $field )
          </font>
          <br>
        #if ($attVal.Attribute.AttributeType.Name == "long-string" )
           <textarea name= "$attrInput.Value.Key" cols="40"
               rows="5">$!attrInput.Value</textarea>
        #else
           <input name= "$attrInput.Value.Key"
                value="$!attrInput.Value" size="20" type="text">
        #end
<br><br>
  #end
#end

<p>
    <input type="submit"
        name="eventSubmit_doEnterissue" value="Submit Issue">

$intake.declareGroups()
</form>

The main new thing added here is that the $intake group is mapped to a business object. A business object that is to be used in this way is expected to implement the Retrievable interface, which provides a method to get a String key which uniquely identifies the object.

The action:

public void doEnterissue( PipelineData pipelineData, Context context ) 
	throws Exception
{
	// get a reference to the run data object
	RunData data = (RunData) pipelineData;

    IntakeTool intake = (IntakeTool)context
        .get(ScarabConstants.INTAKE_TOOL);

    // Summary is always required.
    ScarabUser user = (ScarabUser)data.getUser();
    Issue issue = user.getReportingIssue();
    AttributeValue aval = (AttributeValue)issue
        .getModuleAttributeValuesMap().get("SUMMARY");
    Group group = intake.get("AttributeValue", aval.getQueryKey());
    Field summary = group.get("Value");
    summary.setRequired(true);
    issue.setVocabulary(new Vocabulary(summary.toString()));

    if ( intake.isAllValid() )
    {
        Iterator i = issue.getModuleAttributeValuesMap()
            .values().iterator();
        while (i.hasNext())
        {
            aval = (AttributeValue)i.next();
            group = intake.get("AttributeValue", aval.getQueryKey());
            if ( group != null )
            {
                group.setProperties(aval);
            }
        }

        if ( issue.containsMinimumAttributeValues() )
        {
            issue.save();

            String template = data.getParameters()
                .getString(ScarabConstants.NEXT_TEMPLATE,
                           "entry,Wizard3.vm");
            setTemplate(data, template);
        }
    }
}

The action shows how the business object or action can let intake know if a field is required. It also shows how multiple groups of the same class can be added to a template and then the information is easily passed on to an associated bean.

Dan Diephouse wrote:
>
> I want to validate a form that updates my business object properties
> with intake.  I got the latest cvs of turbine-2 repository and built a
> new distribution of the turbine jar.  I can easily use intake to add
> validate items I add to my databse, but I'm running in to a snag when
> trying to modify them.  I've defined by business object in the intake
> validation file.  I use the following code to update my item:
>
>          IntakeTool intake = (IntakeTool) context.get("intake");
>
>          ParameterParser pp = data.getParameters();
>
>          if ( intake.isAllValid() ) {
>              Job j = new Job();
>              j.setNew(false);
>              group.setProperties(j);
>
>              JobPeer.doUpdate(j);
>
> Error-->    data.getParameters().add("jobid", j.getJobId().toString());
>              data.setMessage("Job updated.");
>          } else {
>              data.setMessage("There was an error updating the job.
> Check below for further information.");
>          }
>
> It runs OK, until I try and retrieve the JobId.  I get a Null Pointer
> exception.  Is there something I'm doing wrong here?  Or is this a bug?
>   Do I need to specify that this is not a new object to intake?  I would
> include my template, but there are lots of fields so its really long.
> But here's a little summary with most fields gone:
>
> #set ( $job = $basecamp.getJob() )   - This gets a job from a pull tool
> #set ( $jobGroup = $intake.Job.mapTo($job) )
> <input type=hidden name="$jobGroup.JobId.Key"
> value="$jobGroup.JobId.Value">
>
> #if ( !$jobGroup.Title.isValid() )
> $jobGroup.Title.Message<br>
> #end
> <input type=text name="$jobGroup.Title.Key"
> value="$!jobGroup.Title.Value" size="50">
> .
> .
> .
> .
> $intake.declareGroups()
>
> Thanks,
>
> Dan Diephouse

This is not how i usually do things, but I guess it could work.  One
thing that is wrong is that you are using mapTo(job) in the template and
then in the action you are using IntakeTool.DEFAULT_KEY.  Unless
job.getQueryKey() returns "_0", this combination is not going to work.
Print out data.getParameters().toString() in the action to see what the
parameters look like.

Here is what I consider a normal course of events:

1.  In the template:

$job = $foo.Job
#set ( $jobGroup = $intake.Job.mapTo($job) )

(This job is can be a new Job or one that is already saved.)

2. In the action:

// get the same (not necessarilary the same java Object, but has the
exact same attribute values) job
Job job = foo.getJob()
Group group = intake.get("Job", job.getQueryKey());
group.setProperties(job);

It appears as though you are trying to map the parameters given for one
job to another job.

john mcnally

Default values for fields

If you want to use input fields which should default to non-empty values for, new objects, you can use the defaultValue field:

<group name="test" key="test">
    <field name="Value" key="val" type="String" defaultValue="preset">
    </field>
</group>

If you set up a form like this:

#if($!dz)
#set ($frm = $intake.test.mapTo($t))
$frm.Mode.
#else
#set ($frm = $intake.test.default )
#end

<FORM NAME="entryForm">
<INPUT type="text" name="$frm.get("val").Key" value="$!frm.get("val")">
</FORM>

then you can either map your form to the fields of the $t object or to the default values, which, for the val Field is the string 'preset'.

Empty fields

Sometimes you have form fields which are not required (can be left empty) but should be mapped onto a non null value in your bean. This is especially true if you use string fields in conjunction with "required" columns in a database. For this case, you can preset an "empty" value for each field which is used if a field is missing or empty in the form parameters returned by the browser. If you don't set this, the default is "null".

<group name="test" key="test">
    <field name="Value" key="val" type="String" emptyValue=""/>
</group>

This would return the empty String (not a null value) for the "val" field if the user does not enter anything. Without the emptyValue parameter, intake would assign the null value to the bean field.

Display attributes for fields

The Intake DTD allows additional attributes that can help you in your template. The displayName can be used to provide a label for the given field. The displaySize can be used to define the displayed size of the field (either numerical or as a CSS-style). The Intake tool $intake provides these values as $field.DisplayName and $field.DisplaySize, respectively.

<group name="test" key="test">
    <field name="Value" key="val" type="String" 
           displayName="Test Value"
           displaySize="150px"
    />
</group>

This could be used in the template like this:

#set ($tg = $intake.test.Default)

$tg.Value.DisplayName:
<input type="text" name="$tg.Value.Key" value="$!tg.Value.HTMLString"
    style="width:${tg.Value.DisplaySize};" />

Custom validators

Fields can have custom validators through the help of the validator attribute. If the validator class is not qualified (i.e. has no package) the package org.apache.turbine.services.intake.validator is assumed. Validator classes must implement the org.apache.turbine.services.intake.validator.Validator interface. If the validator needs initialization from the list of constraints (based on the rule elements in the definition file), it also must implement org.apache.turbine.services.intake.validator.InitableByConstraintMap. It is recommended to extend one of the existing validator classes which live in the org.apache.turbine.services.intake.validator package. In this validator class, override assertValidity(String), don't forget to call super.assertValidity(testValue) and add your own checks to the validator.

For validator classes extending DefaultValidator the most important method to implement is assertValidity(). There are two signatures of this method. If you just need to compare the value entered to a list of database entries, for example, it is sufficient to implement assertValidity(String). The default code handles multi-valued fields and other stuff for you. If you want to do advanced checks and comparisons, you need to implement assertValidity(Field). Here you can check for locale, associated group and values of other fields in the same group. See the following section for examples.

Validating dependent fields

Sometimes it is necessary to validate fields not only against a set of constant rules but also against the values of other fields in the same form or group. For these cases, Intake provides the helper class FieldReference. It provides additional rules to compare fields to other fields:

Rule Name Comparison Rule Value
less-than < <name of other field>
greater-than > <name of other field>
less-than-or-equal <= <name of other field>
greater-than-or-equal >= <name of other field>

Two examples of implementations of these comparison rules are available as DateRangeValidator and IntegerRangeValidator. An example on how to use these is shown below.

<group name="DateRange" key="dr">
    <field name="DateFrom" key="dmin" type="DateString">
        <rule value="MM/dd/yyyy" name="format">Invalid format</rule>
    </field>
    <field name="DateTo" key="dmax" type="DateString"
        validator="DateRangeValidator">
        <rule value="MM/dd/yyyy" name="format">Invalid format</rule>
        <rule value="DateFrom" name="greater-than">
            To-Date must be greater than From-Date</rule>
    </field>
</group>