The Spring Modules project has a number of subprojects, including validation. This module is based on Spring Modules Validation Version 0.9 and has a number of enhancements to the Valang part of the project. Please refer to the original documentation for additional information since this section will primarily focus on explaining Valang and the enhancements
Thanks to everyone that has worked on this project previously and currently.
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
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']
personType EQUALS ['org.springmodules.validation.example.PersonType.STUDENT']
personType EQUALS ['org.springmodules.validation.example.Person$PersonType.STUDENT']
price < 100
will only be evaluated if the personType
is 'STUDENT'.
Otherwise the validation will be skipped.
price < 100 WHERE personType EQUALS ['STUDENT']
Version 0.92
Version 0.93
Map
, a List
, and an Array.
Version 0.94
Note | |
---|---|
Support for the where clause has not been added to the JavaScript custom tag currently. |
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.
Table 1. Rule Syntax
Rule Value | Description | Required |
---|---|---|
property-name |
This is the default property of the bean being targeted for validation,
and can be referred to with the shortcut ? in an expression.
| true |
expression | The Valang expression. | true |
default-error-message | The default error message. If this isn't needed, it can be left blank even though it's required. | true |
error-message-key | The message resource key for the i18n error message. | false |
error-message-arg | If 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 |
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
The parser is not case sensitive when processing the operators.
Table 2. Expression Operators
Comparison Operator | Description | Supports | Example |
---|---|---|---|
= | == | IS | EQUALS | Checks for equality. | Strings, booleans, numbers, dates, and enums. | firstName EQUALS 'Joe' |
!= | <> | >< | IS NOT | NOT EQUALS | Checks for inequality. | Strings, booleans, numbers, dates, and enums. | firstName NOT EQUALS 'Joe' |
> | GREATER THAN | IS GREATER THAN | Checks if a value is greater than another. | Numbers and dates. | age > 18 |
< | LESS THAN | IS LESS THAN | Checks if a value is less than another. | Numbers and dates. | age > 18 |
>= | => | GREATER THAN OR EQUALS | IS GREATER THAN OR EQUALS | Checks if a value is greater than or equal to another. | Numbers and dates. | age >= 18 |
<= | =< | LESS THAN OR EQUALS | IS LESS THAN OR EQUALS | Checks if a value is less than or equal to another. | Numbers and dates. | age <= 18 |
NULL | IS NULL |
Checks if a value is null .
| Objects. | firstName IS NULL |
NOT NULL | IS NOT NULL |
Checks if a value is not null .
| Objects. | firstName IS NOT NULL |
HAS TEXT | Checks if the value has at least one non-whitespace character. | Strings. | firstName HAS TEXT |
HAS NO TEXT | Checks if the value doesn't have a non-whitespace character. | Strings. | firstName HAS NO TEXT |
HAS LENGTH | Checks if the value's length is greater than zero. | Strings. | firstName HAS LENGTH |
HAS NO LENGTH | Checks if the value's length is zero. | Strings. | firstName HAS NO LENGTH |
IS BLANK |
Checks if the value is blank (null or zero length).
| Strings. | firstName IS BLANK |
IS NOT BLANK |
Checks if the value isn't blank (not null , length greater than zero).
| Strings. | firstName IS NOT BLANK |
IS UPPERCASE | IS UPPER CASE | IS UPPER | Checks if the value is uppercase. | Strings. | firstName IS UPPERCASE |
IS NOT UPPERCASE | IS NOT UPPER CASE | IS NOT UPPER | Checks if the value isn't uppercase. | Strings. | firstName IS NOT UPPERCASE |
IS LOWERCASE | IS LOWER CASE | IS LOWER | Checks if the value is lowercase. | Strings. | firstName IS LOWERCASE |
IS NOT LOWERCASE | IS NOT LOWER CASE | IS NOT LOWER | Checks if the value isn't lowercase. | Strings. | firstName IS NOT LOWERCASE |
IS WORD | Checks if the value has one or more letters or numbers (no spaces or special characters). | Strings. | firstName IS WORD |
IS NOT WORD | Checks if the value doesn't have one or more letters or numbers (no spaces or special characters). | Strings. | firstName IS NOT WORD |
BETWEEN | Checks if a value is between two other values. | Numbers and dates. | age BETWEEN 18 AND 65 |
NOT BETWEEN | Checks if a value isn't between two other values. | Numbers and dates. | age NOT BETWEEN 18 AND 65 |
IN | Checks if a value is in a list. | Strings, booleans, numbers, dates, and enums. | firstName IN 'Joe', 'Jack', 'Jane', 'Jill' |
NOT IN | Checks if a value isn't in a list. | Strings, booleans, numbers, dates, and enums. | firstName NOT IN 'Joe', 'Jack', 'Jane', 'Jill' |
NOT | Checks for the opposite of the following expression. | Any expression. | NOT firstName EQUALS 'Joe' |
! | Changes a boolean expression to it's opposite. | Booleans | matches('\\s+', firstName) IS !(TRUE) |
AND |
Used to join together the logical comparisons on
either side of the operator. Both must evaluate to true .
| Any expression. | firstName EQUALS 'Joe' AND age > 21 |
OR |
Used to join together the logical comparisons on
either side of the operator. Only one must evaluate to true .
| Any expression. | firstName EQUALS 'Joe' OR age > 21 |
WHERE | If 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 |
this | A reference to the bean passed in for validation, which could be passed into a custom function for example. | Any expression. | isValid(this) IS TRUE |
Table 3. Literals
Literal Type | Description | Example |
---|---|---|
String |
String literals are surrounded by single quotes.
| 'Joe' |
Numbers |
Numbers can be expressed without any special syntax.
Numbers are all parsed using BigDecimal .
| 1, 100, 0.73, -2.48 |
Dates |
Date literals are surrounded by brackets.
These are the supported formats supported by the
yyyyMMdd, yyyy-MM-dd, yyyy-MM-dd HH:mm:ss, yyyyMMdd HHmmss, yyyyMMdd HH:mm:ss, yyyy-MM-dd HHmmss | [20081230], [2008-12-30], [2008-12-30 12:20:31] |
Booleans |
There are four different constants for boolean values.
The values 'TRUE' and 'YES' represent true ,
and the values 'FALSE' and 'NO' represent false
| TRUE, YES, FALSE, NO |
Enums | Enums 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'] |
Valang supports basic mathematical formulas based on numeric literals and property values.
Table 4. Mathematical Expression Operators
Mathematical Operator | Description | Example |
---|---|---|
+ | Addition operator. | price + 12 |
- | Subtraction operator. | price - 12 |
* | Multiplication operator. | price * 1.2 |
/ | DIV | Division operator. | price / 2 |
% | MOD | Modulo operator. | age % 10 |
Valang supports standard property and nested property access to the bean passed in for validation.
Table 5. Property Syntax
Property Type | Description | Example |
---|---|---|
Standard |
Using 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 |
Nested |
Using 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 |
List |
From 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 |
Map |
From a Map , the value based on the key specified is retrieved.
| addresses[home] IS NOT NULL |
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
Function | Description | Example |
---|---|---|
length | len | size | count |
Returns 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 | matches | Performs 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 |
Checks if the value is a valid e-mail address. | email(email) IS TRUE | |
upper | Converts the value to uppercase. | upper(firstName) EQUALS 'JOE' |
lower | Converts the value to lowercase. | lower(firstName) EQUALS 'joe' |
resolve |
Wraps a string in DefaultMessageSourceResolvable .
| resolve('personForm.firstName') EQUALS 'First Name' |
inRole | Checks if the user authenticated by Spring Security is in a role. | inRole('ADMIN') IS TRUE |
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 FunctionWrapper
takes 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 | |
---|---|
It's important for a |
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
.
<?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.
<?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>
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; } }
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 | |
---|---|
Only a |
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.5GHz Intel Dual Core 2 Duo with 4 GB RAM with OS X 10.5.6)
with Java 5. All the expressions are identical, but adjusted to either retrieve the values being compared from a JavaBean, Map
,
List
, or an array.
Table 7. Bytecode Generation Performance Comparison
Expression | Reflection | Bytcode 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 : '' } | 7098ns | 2124ns |
{ 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 : '' } | 4902ns | 237ns |
{ 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 : '' } | 2704ns | 226ns |
{ 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 : '' } | 2918ns | 212ns |
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>
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.
<?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>