Tuesday, 22 April 2008

Java: using EL outside J2EE



   ${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 = (ExpressionFactoryexpressionFactoryClass.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[] argsthrows 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 = (ExpressionFactoryexpressionFactoryClass.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);
  }

}

5 comments:

  1. How do you preserve whitespace in EL? (It is so difficult to find good documentation on EL).

    ReplyDelete
  2. Hi 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.

    Blog 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.

    ReplyDelete
  3. 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.

    Thanks!

    ReplyDelete
  4. Great article! I'd flattr'd this )

    If you interchange these lines, you can use only variableMapper without any custom top-level resolvers:

    compositeELResolver.add(new BeanELResolver());
    compositeELResolver.add(new MapELResolver());

    ReplyDelete
    Replies
    1. Unfortunately, using this approach, if compositeElResolver couldn't find top level variable, it still throws PropertyNotFoundException.

      Delete

All comments are moderated