${example.expression.language.expression}
If you have done much JSP programming, you will be familiar with the
Expression Language (EL), also now known as the Unified Expression Language. EL is used in JSPs to help remove business logic from the view while keeping the data content dynamic. Uses for EL can go beyond J2EE platforms and it is relatively easy to incorporate it into your own applications.
javadoc:
EL API
EL Implementations
Fortunately, it isn't necessary to write an EL parser. The table below details two open source implementations available under the relatively permissive Apache License.
Implementation |
Tomcat 6 |
JUEL 2.1 |
Source |
http://tomcat.apache.org/ |
http://juel.sourceforge.net/ |
Libraries |
el-api.jar jasper-el.jar |
juel-2.1.0.jar |
License |
Apache License Version 2.0 |
Apache License Version 2.0 |
ExpressionFactory Class |
org.apache.el.ExpressionFactoryImpl |
de.odysseus.el.ExpressionFactoryImpl |
The sample code below should work with either implementation.
ELContext
In order to utilise the library in a new context, we need to provide a
javax.el.ELContext
. The ELContext needs to provide three objects:
javax.el.ELResolver |
This resolves the components to of an expression. For an
expression ${foo.bar} the ELResolver would need to resolve
"foo" to an object and then decide what "bar"
represented on that object. The ELResolver is used to interact with
the model. |
javax.el.FunctionMapper |
The function mapper is used to resolve functions of
the form ${myprefix:myfunction()} to a public, static method encapsulated
by the type java.lang.reflect.Method .
|
javax.el.VariableMapper |
The variable mapper provides a way for elements
operating in different contexts (but using the same ELContext) to share value expressions.
It is by this mechanism that a standard tag library JSP tag could set an expression
used by a JavaServer Faces child component, for example. |
An ELContext is stateful, so it cannot be used in more than one thread at a time.
Once an ELContext has been created, all that is required is a
javax.el.ExpressionFactory
instance; the first table above lists the names of two types that implement it.
//load the expression factory
System.out.println("javax.el.ExpressionFactory="+args[0]);
ClassLoader cl = DemoEL.class.getClassLoader();
Class<?> expressionFactoryClass = cl.loadClass(args[0]);
ExpressionFactory expressionFactory = (ExpressionFactory) expressionFactoryClass.newInstance();
//create a map with some variables in it
Map<Object, Object> userMap = new HashMap<Object, Object>();
userMap.put("x", new Integer(123));
userMap.put("y", new Integer(456));
//get the method for ${myprefix:hello(string)}
Method sayHello = DemoEL.class.getMethod("sayHello", new Class[]{String.class});
//create the context
ELResolver demoELResolver = new DemoELResolver(userMap);
final VariableMapper variableMapper = new DemoVariableMapper();
final DemoFunctionMapper functionMapper = new DemoFunctionMapper();
functionMapper.addFunction("myprefix", "hello", sayHello);
final CompositeELResolver compositeELResolver = new CompositeELResolver();
compositeELResolver.add(demoELResolver);
compositeELResolver.add(new ArrayELResolver());
compositeELResolver.add(new ListELResolver());
compositeELResolver.add(new BeanELResolver());
compositeELResolver.add(new MapELResolver());
ELContext context = new ELContext() {
@Override
public ELResolver getELResolver() {
return compositeELResolver;
}
@Override
public FunctionMapper getFunctionMapper() {
return functionMapper;
}
@Override
public VariableMapper getVariableMapper() {
return variableMapper;
}
};
|
Implementing ELResolver
An ELResolver is required to turn expression elements into Object references. An expression
${x.y.z}
describes a
base object
x
with a
property y
that has a property
z
. Calls into an ELResolver provide a base and a property for the resolver to return as an instance, if it supports the
base and if it can locate the given
property on that base. The
javax.el
API provides classes for resolving several common types (arrays, beans, maps and lists) and provides the
javax.el.CompositeELResolver
to aggregate them. When evaluating the expression
${x.y.z}
, the API will make the ask the resolver to resolve the following in sequence:
base: null
property: x
base: x (as resolved above)
property: y
base: y (as resolved above)
property: z
Although the resolvers provided in
javax.el
can be used with their corresponding data structures, none of them can automatically resolve
x
on the first call from a
null
base.
This example ELResolver uses a
java.util.Map
as an implicit object on which to resolve properties with a null base. It delegates to a
javax.el.MapELResolver
to introspect its contents.
public class DemoELResolver extends ELResolver {
private ELResolver delegate = new MapELResolver();
private Map<Object, Object> userMap;
public DemoELResolver(Map<Object, Object> userMap) {
this.userMap = userMap;
}
@Override
public Object getValue(ELContext context, Object base, Object property) {
if(base==null) {
base = userMap;
}
return delegate.getValue(context, base, property);
}
|
If the base is not
null
, it behaves just like a regular map resolver.
Implementing FunctionMapper
The function mapper is required to resolve an expression
${myprefix:myfunction()}
to a method. As in TLD functions, the expression does not have to have any kind of string match with the method. The following code simply resolves a map of functions.
public class DemoFunctionMapper extends FunctionMapper {
private Map<String, Method> functionMap = new HashMap<String, Method>();
@Override
public Method resolveFunction(String prefix, String localName) {
String key = prefix + ":" + localName;
return functionMap.get(key);
}
|
Both the Tomcat and JUEL implementations provide concrete FunctionMapper implementations similar to the above. However, using these ties the calling application to the implementation; the EL API does not define how methods can be added to the FunctionMapper.
Things to note:
- Returned methods must be static. EL has no way of associating a function with an object.
- Returned methods should return a value. JUEL in particular reacts badly to a
void
return type.
- No argument information is passed to the function mapper. Functions are expected to take a fixed number of arguments.
- Functions do not necessarily need a prefix (but namespacing in general is a good thing).
- The limitations of the EL language mean that dynamically mapping functions to arbitrary (say, non-static) Java methods requires some level of indirection.
Implementing VariableMapper
The variable mapper is a simple class required only to store ValueExpression instances in a map.
Evaluating Expressions
Both value expressions and function calls can be evaluated by using the
javax.el.ExpressionFactory
to create a
ValueExpression
.
//create and resolve a value expression
String sumExpr = "${x+y}";
ValueExpression ve = expressionFactory.createValueExpression(context, sumExpr, Object.class);
Object result = ve.getValue(context);
System.out.println("Result="+result);
//call a function
String fnExpr = "#{myprefix:hello('Dave')}";
ValueExpression fn = expressionFactory.createValueExpression(context, fnExpr, Object.class);
fn.getValue(context);
}
public static String sayHello(String argument) {
System.out.println("Hello, "+argument);
return "OK";
}
|
The finished code emits the following output:
javax.el.ExpressionFactory=de.odysseus.el.ExpressionFactoryImpl
Result=579
Hello, Dave
A ValueExpression can also be used to set the value of the property it resolves to. The ExpressionFactory can also create a
javax.el.MethodExpression
, which is fine if you know that the expression resolves to a method and know the expected parameter types.
Available from Subversion
Repository: http://illegalargumentexception.googlecode.com/svn/trunk/code/java/
License: MIT
Project:
ExpressionLanguageDemo
Listing: eldemo.DemoEL
package eldemo;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import javax.el.ArrayELResolver;
import javax.el.BeanELResolver;
import javax.el.CompositeELResolver;
import javax.el.ELContext;
import javax.el.ELResolver;
import javax.el.ExpressionFactory;
import javax.el.FunctionMapper;
import javax.el.ListELResolver;
import javax.el.MapELResolver;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
/**
* A simple application that demonstrates the use of the
* Unified Expression Language.
* @author McDowell
*/
public class DemoEL {
/**
* takes the javax.el.ExpressionFactory implementation class as an argument
* @param args
*/
public static void main(String[] args) throws Exception {
if(args.length<1) {
System.out.println("Provide javax.el.ExpressionFactory implementation class as argument");
System.exit(1);
}
//load the expression factory
System.out.println("javax.el.ExpressionFactory="+args[0]);
ClassLoader cl = DemoEL.class.getClassLoader();
Class<?> expressionFactoryClass = cl.loadClass(args[0]);
ExpressionFactory expressionFactory = (ExpressionFactory) expressionFactoryClass.newInstance();
//create a map with some variables in it
Map<Object, Object> userMap = new HashMap<Object, Object>();
userMap.put("x", new Integer(123));
userMap.put("y", new Integer(456));
//get the method for ${myprefix:hello(string)}
Method sayHello = DemoEL.class.getMethod("sayHello", new Class[]{String.class});
//create the context
ELResolver demoELResolver = new DemoELResolver(userMap);
final VariableMapper variableMapper = new DemoVariableMapper();
final DemoFunctionMapper functionMapper = new DemoFunctionMapper();
functionMapper.addFunction("myprefix", "hello", sayHello);
final CompositeELResolver compositeELResolver = new CompositeELResolver();
compositeELResolver.add(demoELResolver);
compositeELResolver.add(new ArrayELResolver());
compositeELResolver.add(new ListELResolver());
compositeELResolver.add(new BeanELResolver());
compositeELResolver.add(new MapELResolver());
ELContext context = new ELContext() {
@Override
public ELResolver getELResolver() {
return compositeELResolver;
}
@Override
public FunctionMapper getFunctionMapper() {
return functionMapper;
}
@Override
public VariableMapper getVariableMapper() {
return variableMapper;
}
};
//create and resolve a value expression
String sumExpr = "${x+y}";
ValueExpression ve = expressionFactory.createValueExpression(context, sumExpr, Object.class);
Object result = ve.getValue(context);
System.out.println("Result="+result);
//call a function
String fnExpr = "#{myprefix:hello('Dave')}";
ValueExpression fn = expressionFactory.createValueExpression(context, fnExpr, Object.class);
fn.getValue(context);
}
public static String sayHello(String argument) {
System.out.println("Hello, "+argument);
return "OK";
}
}
|
Listing: eldemo.DemoELResolver
package eldemo;
import java.beans.FeatureDescriptor;
import java.util.Iterator;
import java.util.Map;
import javax.el.ELContext;
import javax.el.ELResolver;
import javax.el.MapELResolver;
/**
* @author McDowell
*/
public class DemoELResolver extends ELResolver {
private ELResolver delegate = new MapELResolver();
private Map<Object, Object> userMap;
public DemoELResolver(Map<Object, Object> userMap) {
this.userMap = userMap;
}
@Override
public Object getValue(ELContext context, Object base, Object property) {
if(base==null) {
base = userMap;
}
return delegate.getValue(context, base, property);
}
@Override
public Class<?> getCommonPropertyType(ELContext context, Object base) {
if(base==null) {
base = userMap;
}
return delegate.getCommonPropertyType(context, base);
}
@Override
public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context,
Object base) {
if(base==null) {
base = userMap;
}
return delegate.getFeatureDescriptors(context, base);
}
@Override
public Class<?> getType(ELContext context, Object base, Object property) {
if(base==null) {
base = userMap;
}
return delegate.getType(context, base, property);
}
@Override
public boolean isReadOnly(ELContext context, Object base, Object property) {
if(base==null) {
base = userMap;
}
return delegate.isReadOnly(context, base, property);
}
@Override
public void setValue(ELContext context, Object base, Object property, Object value) {
if(base==null) {
base = userMap;
}
delegate.setValue(context, base, property, value);
}
}
|
Listing: eldemo.FunctionMapper
package eldemo;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import javax.el.FunctionMapper;
/**
* @author McDowell
*/
public class DemoFunctionMapper extends FunctionMapper {
private Map<String, Method> functionMap = new HashMap<String, Method>();
@Override
public Method resolveFunction(String prefix, String localName) {
String key = prefix + ":" + localName;
return functionMap.get(key);
}
public void addFunction(String prefix, String localName, Method method) {
if(prefix==null || localName==null || method==null) {
throw new NullPointerException();
}
int modifiers = method.getModifiers();
if(!Modifier.isPublic(modifiers)) {
throw new IllegalArgumentException("method not public");
}
if(!Modifier.isStatic(modifiers)) {
throw new IllegalArgumentException("method not static");
}
Class<?> retType = method.getReturnType();
if(retType == Void.TYPE) {
throw new IllegalArgumentException("method returns void");
}
String key = prefix + ":" + localName;
functionMap.put(key, method);
}
}
|
Listing: eldemo.DemoVariableMapper
package eldemo;
import java.util.HashMap;
import java.util.Map;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
/**
* @author McDowell
*/
public class DemoVariableMapper extends VariableMapper {
private Map<String, ValueExpression> expressions = new HashMap<String, ValueExpression>();
@Override
public ValueExpression resolveVariable(String variable) {
return expressions.get(variable);
}
@Override
public ValueExpression setVariable(String variable, ValueExpression expression) {
return expressions.put(variable, expression);
}
}
|
How do you preserve whitespace in EL? (It is so difficult to find good documentation on EL).
ReplyDeleteHi Pete - I'm not sure exactly what you mean. An EL string literal such as ' Foo Bar ' will preserve spaces during evaluation. I suspect you are referring to the behaviour of a view technology that is leveraging EL for data binding and the resulting rendering behaviour. For example, HTML rendering in a browser generally collapses whitespace.
ReplyDeleteBlog comments aren't great places for working out these sorts of problems and it isn't related to the post. I suggest you post a question on stackoverflow.com detailing the technologies you are using, some sample code and a description of the output you're trying to achieve.
Implemented the same stuff a long time ago before the Unified Expression Language. So your blog entry was a short cut to getting it updated.
ReplyDeleteThanks!
Great article! I'd flattr'd this )
ReplyDeleteIf you interchange these lines, you can use only variableMapper without any custom top-level resolvers:
compositeELResolver.add(new BeanELResolver());
compositeELResolver.add(new MapELResolver());
Unfortunately, using this approach, if compositeElResolver couldn't find top level variable, it still throws PropertyNotFoundException.
Delete