Spring Modules Validation

FRAMEWORK/SPRING 2013. 7. 31. 15:09

Spring Modules Validation

David Winterfeldt

2009


The Spring Modules project has a number of subprojects, including validation. This module is based on Spring ModulesValidation Version 0.9 and has a number of enhancements to the Valang part of the project.

Thanks to everyone that has worked on this project previously and currently.

1. Valang

Valang is short for Va-lidation Lang-uage. It provides a very readable language for expressing validation rules, as well as providing the ability to add custom functions to the language. Once the ValangValidator is configured, using it isn't different than any other Spring Validator since it implements org.springframework.validation.Validator.

Below is a list of current enhancements.

Version 0.91

  • Bytecode generation added to DefaultVisitor as a replacement for reflection accessing simple properties (BeanPropertyFunction) for a significant performance improvement.
  • Basic enum comparison support. In the expression below the personType is an enum and the value STUDENT will be convereted to an enum for comparison. The value must match an enum value on the type being compared or an exception will be thrown.
    personType EQUALS ['STUDENT']
    For better performance the full class name can be specified so the enum can be retrieved during parsing. The first example is for standard enum and the second one is for an inner enum class .
    personType EQUALS ['org.springmodules.validation.example.PersonType.STUDENT']
    personType EQUALS ['org.springmodules.validation.example.Person$PersonType.STUDENT']
  • Where clause support. In the expression below, the part of the expression price < 100 will only be evaluated if thepersonType is 'STUDENT'. Otherwise the validation will be skipped.
    price < 100 WHERE personType EQUALS ['STUDENT']
    [Note]Note

    Support for the where clause has not been added to the JavaScript custom tag currently.

  • Improved performance of 'IN'/'NOT IN' if comparing a value to a java.util.Set it will use Set.contains(value). Static lists of Strings (ex: 'A', 'B', 'C') are now stored in a Set instead of an ArrayList.
  • Functions can be configured in Spring, but need to have their scope set as prototype and use a FunctionWrapper that is also a prototype bean with <aop:scoped-proxy> set on it.
  • Removed servlet dependency from Valang project except for the custom JSP tag ValangValidateTag needing it, but running Valang no longer requires it. This involved removing ServletContextAware from it's custom dependency injection. If someone was using this in a custom function, the function can now be configured directly in Spring and Spring can inject any "aware" values.
  • Changed logging to use SLF4J api.

Version 0.92

  • Removed custom dependency injection since functions can be configured in Spring.
  • Added auto-discovery of FunctionWrapper beans from the Spring context to go with existing auto-discovery of FunctionDefinition beans.

Version 0.93

  • Made specific comparison classes for each operator for a performance improvement.
  • Changed IS WORD and IS BLANK to use Commons Lang StringUtils, which will change the behavior slightly but should be more accurate to the description of the validation.
  • Change Operator from interfaces to an enum and removed OperatorConstants.
  • Fixed bytecode generation to handle a Map, a List, and an Array.

Version 0.94

  • Upgraded to Spring 3.0 and changed group & artifact IDs to match standard Spring naming conventions.

Version 0.95

  • Upgraded to Spring 3.1 and minor improvements to bytecode generation.

Rule Syntax

The basic construction of a Valang rule is to have it begin and end with a brace. Within the braces, the default property name for the rule is specified first. Then the Valang expression, followed by the default error message. These are all the required values for a Valang rule. The other optional values for a rule are the error message key and arguments for it. Each of the values of the rule are delimitted by a colon.

{ <property-name> : <expression> : <default-error-message> : <error-message-key> : <error-message-args> }

Table 1. Rule Syntax

Rule ValueDescriptionRequired
property-nameThis is the default property of the bean being targeted for validation, and can be referred to with the shortcut ? in an expression.true
expressionThe Valang expression.true
default-error-messageThe default error message. If this isn't needed, it can be left blank even though it's required.true
error-message-keyThe message resource key for the i18n error message.false
error-message-argIf the error-message-key is specified, arguments for the error message can also be set as the final value of the rule. This accepts a comma delimited list of values.false

Expression Syntax

The expression language provides an English like syntax for expressing validation rules. There are a number of operators for comparing a value to another. Logical expressions, resulting in true or false, can be grouped together with parentheses to form more complex expressions.

Just to give some context to the explanation of all the rules, below is a simple example. The bean being validated has the properties getFirstName()getLastName(), and getAge(). The first two return a String and the last returns an int. The default property is 'firstName', which is referred to by the question mark. The first part of the rule enclosed in parentheses checks if the first name is either 'Joe' or it's length is greater than 5. The next part checks if the last name is one of the values in the list, and the final part checks if the age is over 18.

(? EQUALS 'Joe' OR length(?) > 5) AND lastName IN 'Johnson', 'Jones', 'Smith' AND age > 18

Operator Syntax

The parser is not case sensitive when processing the operators.

Table 2. Expression Operators

Comparison OperatorDescriptionSupportsExample
= | == | IS | EQUALSChecks for equality.Strings, booleans, numbers, dates, and enums.firstName EQUALS 'Joe'
!= | <> | >< | IS NOT | NOT EQUALSChecks for inequality.Strings, booleans, numbers, dates, and enums.firstName NOT EQUALS 'Joe'
> | GREATER THAN | IS GREATER THANChecks if a value is greater than another.Numbers and dates.age > 18
< | LESS THAN | IS LESS THANChecks if a value is less than another.Numbers and dates.age > 18
>= | => | GREATER THAN OR EQUALS | IS GREATER THAN OR EQUALSChecks if a value is greater than or equal to another.Numbers and dates.age >= 18
<= | =< | LESS THAN OR EQUALS | IS LESS THAN OR EQUALSChecks if a value is less than or equal to another.Numbers and dates.age <= 18
NULL | IS NULLChecks if a value is null.Objects.firstName IS NULL
NOT NULL | IS NOT NULLChecks if a value is not null.Objects.firstName IS NOT NULL
HAS TEXTChecks if the value has at least one non-whitespace character.Strings.firstName HAS TEXT
HAS NO TEXTChecks if the value doesn't have a non-whitespace character.Strings.firstName HAS NO TEXT
HAS LENGTHChecks if the value's length is greater than zero.Strings.firstName HAS LENGTH
HAS NO LENGTHChecks if the value's length is zero.Strings.firstName HAS NO LENGTH
IS BLANKChecks if the value is blank (null or zero length).Strings.firstName IS BLANK
IS NOT BLANKChecks if the value isn't blank (not null, length greater than zero).Strings.firstName IS NOT BLANK
IS UPPERCASE | IS UPPER CASE | IS UPPERChecks if the value is uppercase.Strings.firstName IS UPPERCASE
IS NOT UPPERCASE | IS NOT UPPER CASE | IS NOT UPPERChecks if the value isn't uppercase.Strings.firstName IS NOT UPPERCASE
IS LOWERCASE | IS LOWER CASE | IS LOWERChecks if the value is lowercase.Strings.firstName IS LOWERCASE
IS NOT LOWERCASE | IS NOT LOWER CASE | IS NOT LOWERChecks if the value isn't lowercase.Strings.firstName IS NOT LOWERCASE
IS WORDChecks if the value has one or more letters or numbers (no spaces or special characters).Strings.firstName IS WORD
IS NOT WORDChecks if the value doesn't have one or more letters or numbers (no spaces or special characters).Strings.firstName IS NOT WORD
BETWEENChecks if a value is between two other values.Numbers and dates.age BETWEEN 18 AND 65
NOT BETWEENChecks if a value isn't between two other values.Numbers and dates.age NOT BETWEEN 18 AND 65
INChecks if a value is in a list.Strings, booleans, numbers, dates, and enums.firstName IN 'Joe', 'Jack', 'Jane', 'Jill'
NOT INChecks if a value isn't in a list.Strings, booleans, numbers, dates, and enums.firstName NOT IN 'Joe', 'Jack', 'Jane', 'Jill'
NOTChecks for the opposite of the following expression.Any expression.NOT firstName EQUALS 'Joe'
!Changes a boolean expression to it's opposite.Booleansmatches('\\s+', firstName) IS !(TRUE)
ANDUsed to join together the logical comparisons on either side of the operator. Both must evaluate totrue.Any expression.firstName EQUALS 'Joe' AND age > 21
ORUsed to join together the logical comparisons on either side of the operator. Only one must evaluate totrue.Any expression.firstName EQUALS 'Joe' OR age > 21
WHEREIf the where expression is true, then the main expression for validation is performed. Otherwise it isn't evaluated and no errors are generated.Any expression.firstName EQUALS 'Joe' WHERE age > 21
thisA reference to the bean passed in for validation, which could be passed into a custom function for example.Any expression.isValid(this) IS TRUE

Literal Syntax

Table 3. Literals

Literal TypeDescriptionExample
StringString literals are surrounded by single quotes.'Joe'
NumbersNumbers can be expressed without any special syntax. Numbers are all parsed usingBigDecimal.1, 100, 0.73, -2.48
Dates

Date literals are surrounded by brackets.

These are the supported formats supported by the DefaultDateParser.

yyyyMMddyyyy-MM-ddyyyy-MM-dd HH:mm:ssyyyyMMdd HHmmssyyyyMMdd HH:mm:ss,yyyy-MM-dd HHmmss

[20081230], [2008-12-30], [2008-12-30 12:20:31]
BooleansThere are four different constants for boolean values. The values 'TRUE' and 'YES' represent true, and the values 'FALSE' and 'NO' represent falseTRUE, YES, FALSE, NO
EnumsEnums are surrounded by bracket and single quotes. If the full path to the enum isn't specified, it will be resolved when the expression is evaluated by looking up the enum value from enum on the opposite side of the expression.['FAIL'], ['org.springmodules.validation.valang.CreditStatus.FAIL'], ['org.springmodules.validation.valang.Person$CreditRating.EXCELLENT']

Mathematical Operator Syntax

Valang supports basic mathematical formulas based on numeric literals and property values.

Table 4. Mathematical Expression Operators

Mathematical OperatorDescriptionExample
+Addition operator.price + 12
-Subtraction operator.price - 12
*Multiplication operator.price * 1.2
/ | DIVDivision operator.price / 2
% | MODModulo operator.age % 10

Property Syntax

Valang supports standard property and nested property access to the bean passed in for validation.

Table 5. Property Syntax

Property TypeDescriptionExample
StandardUsing standard JavaBean property notation, a value from the bean being validated may be retrieved. The address represents getAddress() on the bean.address IS NOT NULL
NestedUsing standard JavaBean property notation, a nested value from the bean being validated may be retrieved. The address.city represents getAddress().getCity() on the bean.address.city IS NOT BLANK
ListFrom an array, List, or Set, a value from it can be returned by specifying it's index. Only arrays and lists are supported by bytecode generation.addresses[1] IS NOT NULL
MapFrom a Map, the value based on the key specified is retrieved.addresses[home] IS NOT NULL

Functions

These are built in functions that come with Valang. The function framework is pluggable, so it's easy to add custom functions. Adding custom functions will be covered in the next section.

Table 6. Functions

FunctionDescriptionExample
length | len | size | countReturns the size of a collection or an array, and otherwise returns the length of string by called toString() on the object.length(firstName) < 20
match | matchesPerforms a match on a regular expression. The first argument is the regular expression and the second is the value match on.matches('\\w+', firstName) IS TRUE
emailChecks if the value is a valid e-mail address.email(email) IS TRUE
upperConverts the value to uppercase.upper(firstName) EQUALS 'JOE'
lowerConverts the value to lowercase.lower(firstName) EQUALS 'joe'
resolveWraps a string in DefaultMessageSourceResolvable.resolve('personForm.firstName') EQUALS 'First Name'
inRoleChecks if the user authenticated by Spring Security is in a role.inRole('ADMIN') IS TRUE

Custom Functions

Custom functions can either be explicitly registered or instances of FunctionDefinition and FunctionWrapper are automatically registered with a ValangValidator. If just specifying a class name, it must have a constructor with the signature Function[] arguments, int line, int column. The FunctionWrapper is specifically for Spring configured functions. If the Function in a FunctionWrappertakes any arguments, it must implement ConfigurableFunction which allows the parser to configure the arguments, line number, and column number. Otherwise the line & column number will not be set on a Spring configured function.

[Note]Note

It's important for a FunctionWrapper around a custom Function to be of the scope prototype as well as theFunctionWrapper. Also the FunctionWrapper must have <aop:scoped-proxy/> defined so each call to it will get a new instance of the function. This is because as the validation language is parsed a new instance of a function is made each time and has the arguments specific to that function set on it.

Spring Configuration

The example below shows how to explicitly register a custom function directly with a validator. The custom functions 'validLastName' and 'creditApproval' are registered on the customFunctions property as a Map. The key is the name of the function to be used in the validation language and the value if the function being registered, which can either be the fully qualified name of the class or an instance of FunctionWrapper.

ValangValidatorCustomFunctionTest-context.xml
                        
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/aop 
                        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="creditApprovalFunction" 
          class="org.springmodules.validation.valang.CreditApprovalFunction"
          scope="prototype">
        <property name="creditRatingList">
            <list>
                <value>GOOD</value>
                <value>EXCELLENT</value>
            </list>
        </property>
    </bean>

    <bean id="personValidator" class="org.springmodules.validation.valang.ValangValidator">
        <property name="className" value="org.springmodules.validation.valang.Person"/>
        <property name="customFunctions">
            <map>
                <entry key="validLastName">
                    <value>org.springmodules.validation.valang.ValidLastNameFunction</value>
                </entry>
                <entry key="creditApproval">
                    <bean class="org.springmodules.validation.valang.functions.FunctionWrapper"
                          scope="prototype">
                        <aop:scoped-proxy/>
                        
                        <property name="function" ref="creditApprovalFunction" />
                    </bean>
                </entry>
            </map>
        </property>
        <!--
            Final validation tests that the aop:scoped-proxy is working since if the same instance 
            of CreditApprovalFunction is used it will be set to a failing value for both sides of the or.
            While if two instances are made the first condition should pass while the second will fail.
        -->
        <property name="valang">
            <value><![CDATA[
              { lastName : validLastName(?) is true : '' }
              { lastName : creditApproval(age, creditRating) is true : '' }
              { lastName : validLastName(?) is true AND creditApproval(age, creditRating) is true : '' }
              { lastName : validLastName(?) is true AND 
                           (creditApproval(age, creditRating) is true OR 
                            creditApproval(age, ['org.springmodules.validation.valang.Person$CreditRating.FAIR']) is true) : '' }
            ]]<</value>
        </property>
    </bean>
   
</beans>
                        
                    

Instances of FunctionDefinition and FunctionWrapper are automatically registered with a ValangValidator The custom functions 'validLastName' and 'creditApproval' are registered. If a FunctionWrapper doesn't have a function name specified, the name of the bean will be used for the function name.

ValangValidatorCustomFunctionDiscoveryTest-context.xml
                        
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/aop 
                        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean class="org.springmodules.validation.valang.functions.FunctionDefinition"
          p:name="validLastName"
          p:className="org.springmodules.validation.valang.ValidLastNameFunction"/>

    <!-- Uses bean name for function name if not explicitly set on the wrapper -->
    <bean id="creditApproval" 
          class="org.springmodules.validation.valang.functions.FunctionWrapper"
          scope="prototype">
        <aop:scoped-proxy/>
        
        <property name="function">
            <bean id="creditApprovalFunction" 
                  class="org.springmodules.validation.valang.CreditApprovalFunction"
                  scope="prototype">
                <property name="creditRatingList">
                    <list>
                        <value>GOOD</value>
                        <value>EXCELLENT</value>
                    </list>
                </property>
            </bean>
        </property>
    </bean>
    
    <bean id="personValidator" class="org.springmodules.validation.valang.ValangValidator">
        <property name="className" value="org.springmodules.validation.valang.Person"/>
        <!--
            Final validation tests that the aop:scoped-proxy is working since if the same instance 
            of CreditApprovalFunction is used it will be set to a failing value for both sides of the or.
            While if two instances are made the first condition should pass while the second will fail.
        -->
        <property name="valang">
            <value><![CDATA[
              { lastName : validLastName(?) is true : '' }
              { lastName : creditApproval(age, creditRating) is true : '' }
              { lastName : validLastName(?) is true AND creditApproval(age, creditRating) is true : '' }
              { lastName : validLastName(?) is true AND 
                           (creditApproval(age, creditRating) is true OR 
                            creditApproval(age, ['org.springmodules.validation.valang.Person$CreditRating.FAIR']) is true) : '' }
            ]]<</value>
        </property>
    </bean>
   
</beans>
                        
                    

Code Example

Checks if the last name is in a list, and if it isn't false is returned.

Example 1. ValidLastNameFunction

                        
public class ValidLastNameFunction extends AbstractFunction {

    final Logger logger = LoggerFactory.getLogger(ValidLastNameFunction.class);
    
    final Set<String> lValidLastNames = new HashSet<String>();
    
    /**
     * Constructor
     */
    public ValidLastNameFunction(Function[] arguments, int line, int column) {
        super(arguments, line, column);
        definedExactNumberOfArguments(1);

        lValidLastNames.add("Anderson");
        lValidLastNames.add("Jackson");
        lValidLastNames.add("Johnson");
        lValidLastNames.add("Jones");
        lValidLastNames.add("Smith");
    }

    /**
     * Checks if the last name is blocked.
     * 
     * @return      Object      Returns a <code>boolean</code> for 
     *                          whether or not the last name is blocked.
     */
    @Override
    protected Object doGetResult(Object target) {
        boolean result = true;
        
        String symbol = getArguments()[0].getResult(target).toString();
        
        if (!lValidLastNames.contains(symbol)) {
            result = false;
        }
        
        return result;
    }
    
}
                        
                    

The function checks if a person can get credit approval. Their credit rating is checked against a list only if they are over 18 years old.

Example 2. ConfigurableFunction

                        
public class CreditApprovalFunction extends AbstractFunction 
        implements ConfigurableFunction {

    final Logger logger = LoggerFactory.getLogger(CreditApprovalFunction.class);
    
    Set<Person.CreditRating> lCreditRatings = new HashSet<Person.CreditRating>();

    /**
     * Constructor
     */
    public CreditApprovalFunction() {}

    /**
     * Constructor
     */
    public CreditApprovalFunction(Function[] arguments, int line, int column) {
        super(arguments, line, column);
        definedExactNumberOfArguments(2);
        
        lCreditRatings.add(Person.CreditRating.FAIR);
        lCreditRatings.add(Person.CreditRating.GOOD);
        lCreditRatings.add(Person.CreditRating.EXCELLENT);
    }

    /**
     * Gets number of expected arguments.
     * Implementation of <code>ConfigurableFunction</code>.
     */
    public int getExpectedNumberOfArguments() {
        return 2;
    }

    /**
     * Sets arguments, line number, and column number.
     * Implementation of <code>ConfigurableFunction</code>.
     */
    public void setArguments(int expectedNumberOfArguments, Function[] arguments,
                             int line, int column) {
        // important to set template first or can cause a NullPointerException 
        // if number of arguments don't match the expected number since 
        // the template is used to create the exception
        super.setTemplate(line, column);
        super.setArguments(arguments);
        super.definedExactNumberOfArguments(expectedNumberOfArguments);
    }
    
    /**
     * Sets valid credit rating approval list.
     */
    public void setCreditRatingList(Set<Person.CreditRating> lCreditRatings) {
        this.lCreditRatings = lCreditRatings;
    }

    /**
     * If age is over 18, check if the person has good credit, 
     * and otherwise reject.
     * 
     * @return      Object      Returns a <code>boolean</code> for 
     *                          whether or not the person has good enough 
     *                          credit to get approval.
     */
    @Override
    protected Object doGetResult(Object target) {
        boolean result = true;
        
        int age = (Integer) getArguments()[0].getResult(target);
        Person.CreditRating creditRating = (Person.CreditRating)getArguments()[1].getResult(target);

        // must be over 18 to get credit approval
        if (age > 18) {
            if (!lCreditRatings.contains(creditRating)) {
                result = false;
            }
        }
        
        return result;
    }
    
}
                        
                    

Bytecode Generation

If the validator will only be used to validate a specific class, the property 'className' can be specified to avoid reflection. If it's set, a custom Function will be generated that directly retrieves a property to avoid reflection. This provides a significant performance improvement if that is a concern, which typically isn't if the validation is being used to validate a web page since the delay is so small either way.

[Note]Note

Only a Map, a List, or an Array is supported by bytecode generation, not a Set. Primitive arrays currently aren't supported, but any object one is. Also, nested properties are currently not supported.

This is a small excerpt from the logging of the performance unit test. As you can see from the logging, as the validator is initialized it generates bytecode and shows for which class and method, as well as what the generated class name is. The package and name of the original class is used and then has what property is being retrieved appended along with 'BeanPropertyFunction$$Valang' to make a unique class name to try to avoid any collisions.

DefaultVisitor - Generated bytecode for org.springmodules.validation.valang.Person.getLastName() 
    as 'org.springmodules.validation.valang.PersonLastNameBeanPropertyFunction$$Valang'.
DefaultVisitor - Generated bytecode for org.springmodules.validation.valang.Person.getAge() 
    as 'org.springmodules.validation.valang.PersonAgeBeanPropertyFunction$$Valang'.
DefaultVisitor - Generated bytecode for org.springmodules.validation.valang.Person.getCreditRating() 
    as 'org.springmodules.validation.valang.PersonCreditRatingBeanPropertyFunction$$Valang'.
DefaultVisitor - Generated bytecode for org.springmodules.validation.valang.Person.getFirstName() 
    as 'org.springmodules.validation.valang.PersonFirstNameBeanPropertyFunction$$Valang'.
DefaultVisitor - Generated bytecode for org.springmodules.validation.valang.Person.getCreditStatus() 
    as 'org.springmodules.validation.valang.PersonCreditStatusBeanPropertyFunction$$Valang'.
ValangValidatorPerformanceTest - Took 7098.0ns.
ValangValidatorPerformanceTest - Took 2124.0ns.
ValangValidatorPerformanceTest - Message validator took 7098.0ns, and bytecode message valdiator took 2124.0ns.
                

Results from ValangValidatorPerformanceTest which was run on a Macbook Pro (2.3GHz Intel Core i7 with 8 GB RAM with OS X 10.6.8) with Java 6. All the expressions are identical, but adjusted to either retrieve the values being compared from a JavaBean, MapList, or an array.

Table 7. Bytecode Generation Performance Comparison

ExpressionReflectionBytcode Generation
{ lastName : validLastName(?) is true AND creditApproval(age, creditRating) is true WHERE firstName IN 'Joe', 'Jack', 'Jill', 'Jane' AND creditStatus IN ['org.springmodules.validation.valang.CreditStatus.PENDING'], ['org.springmodules.validation.valang.CreditStatus.FAIL'] AND creditRating EQUALS ['org.springmodules.validation.valang.Person$CreditRating.EXCELLENT'] AND age > 18 : '' }1176ns327ns
{ mapVars[lastName] : validLastName(?) is true AND creditApproval(mapVars[age], mapVars[creditRating]) is true WHERE mapVars[firstName] IN 'Joe', 'Jack', 'Jill', 'Jane' AND mapVars[creditStatus] IN ['org.springmodules.validation.valang.CreditStatus.PENDING'], ['org.springmodules.validation.valang.CreditStatus.FAIL'] AND mapVars[creditRating] EQUALS ['org.springmodules.validation.valang.Person$CreditRating.EXCELLENT'] AND mapVars[age] > 18 : '' }905ns48ns
{ listVars[1] : validLastName(?) is true AND creditApproval(listVars[2], listVars[4]) is true WHERE listVars[0] IN 'Joe', 'Jack', 'Jill', 'Jane' AND listVars[3] IN ['org.springmodules.validation.valang.CreditStatus.PENDING'], ['org.springmodules.validation.valang.CreditStatus.FAIL'] AND listVars[4] EQUALS ['org.springmodules.validation.valang.Person$CreditRating.EXCELLENT'] AND listVars[2] > 18 : '' }575ns43ns
{ vars[1] : validLastName(?) is true AND creditApproval(vars[2], vars[4]) is true WHERE vars[0] IN 'Joe', 'Jack', 'Jill', 'Jane' AND vars[3] IN ['org.springmodules.validation.valang.CreditStatus.PENDING'], ['org.springmodules.validation.valang.CreditStatus.FAIL'] AND vars[4] EQUALS ['org.springmodules.validation.valang.Person$CreditRating.EXCELLENT'] AND vars[2] > 18 : '' }563ns40ns

Spring Configuration

By specifying the 'className' property, bytecode will be generated for each method being called to avoid reflection. This gives a significant performance improvement.

Excerpt from ValangValidatorCustomFunctionTest-context.xml

                        
<!-- 
    Only perform validation if valid first name, credit status is failed or pending, 
    and the credit rating is excellent where the person's age is over 18.
-->
<bean id="expression" class="java.lang.String">
    <constructor-arg>
        <value><![CDATA[
          { lastName : validLastName(?) is true AND creditApproval(age, creditRating) is true
                       WHERE firstName IN 'Joe', 'Jack', 'Jill', 'Jane' AND
                             creditStatus IN ['org.springmodules.validation.valang.CreditStatus.PENDING'], 
                                             ['org.springmodules.validation.valang.CreditStatus.FAIL'] AND
                             creditRating EQUALS ['org.springmodules.validation.valang.Person$CreditRating.EXCELLENT'] AND
                             age > 18 : '' } 
        ]]<</value>
    </constructor-arg>
</bean>

...

<bean id="bytecodePersonValidator" class="org.springmodules.validation.valang.ValangValidator">
    <property name="className" value="org.springmodules.validation.valang.Person"/>
    <property name="valang" ref="expression" />
</bean>
                        
                    

Date Examples

The default date parser provides support for a number of different date literals, and also has support for shifting and manipulating dates. Below are a few examples, but see the DefaultDateParser for more detailed information.

Spring Configuration

ValangValidatorDateTest-context.xml
                        
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/aop 
                        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="personValidator" class="org.springmodules.validation.valang.ValangValidator">
        <property name="className" value="org.springmodules.validation.valang.Person"/>
        <property name="valang">
            <!-- 
                Third to last validation shifts '2008-12-30<y' to '2008-01-01 00:00:00'
                Second to last validation shifts '2005-04-09 23:30:00<M+10d+8H' to '2005-04-11 08:00:00'.
                Last shifts '2009-02-06 00:00:00<M+20y' to '2029-02-01 00:00:00'.
            -->
            <value><![CDATA[            
              { lastUpdated : ? > [20081230] : '' }
              { lastUpdated : ? > [2008-12-30] : '' }
              { lastUpdated : ? > [2008-12-30 12:20:31] : '' }
              { lastUpdated : ? > [20081230 122031] : '' }
              { lastUpdated : ? > [20081230 12:20:31] : '' }
              { lastUpdated : ? > [2008-12-30 122031] : '' }
              
              { lastUpdated : ? BETWEEN [20081230] AND [2009-02-06 00:00:00<M+20y] : '' }
              
              { lastUpdated : ? > [2008-12-30<y] : '' }
              { lastUpdated : ? > [2005-04-09 23:30:00<M+10d+8H] : '' }
              { lastUpdated : ? < [2009-02-06 00:00:00<M+20y] : '' }
            ]]<</value>
        </property>
    </bean>
   
</beans>
                        
                    


: