Advanced Topics

This section covers some advanced JUEL topics.

Expression Trees

An expression tree refers to the parsed representation of an expression string. The basic classes and interfaces related to expression trees are contained in package de.odysseus.el.tree. We won't cover all the tree related classes here. Rather, we focus on the classes that can be used to provide a customized tree cache and builder.

  1. Tree – This class represents a parsed expression string.
  2. TreeBuilder – General interface containing a single build(String) method. A tree builder must be thread safe. The default implementation is de.odysseus.el.tree.impl.Builder.
  3. TreeCache – General interface containing methods get(String) and put(String, Tree). A tree cache must be thread safe, too. The default implementation is de.odysseus.el.tree.impl.Cache.
  4. TreeStore – This class just combines a builder and a cache and contains a single get(String) method.

The expression factory uses its tree store to create tree expressions. The factory class provides a constructor which takes a tree store as an argument.

Using a customized Builder

It should be noted that one could write a builder implementation from scratch. However, we will focus on customizing the default implementation. The default builder currently provides the following methods that can be overridden by subclasses:

  • protected Number parseInteger(String) throws NumberFormatException – The base implementation parses the string into a java.lang.Long. As an example, a subclass could parse the string into a java.math.BigInteger.
  • protected Number parseFloat(String) throws NumberFormatException – The base implementation parses the string into a java.lang.Double. As an example, a subclass could parse the string into a java.math.BigDecimal.

Additionally, JUEL's builder supports method invocations as an extension to the standard. See below on how to enable method invocations.

Having a customized builder implementatation, it can be passed to a factory via

TreeStore store = new TreeStore(new MyBuilder(), new Cache(100));
ExpressionFactory factory = new ExpressionFactoryImpl(store);

Enabling Method Invocations

Many people have noticed the lack of method invocations as a major weakness of the unified expression language. When talking about method invocations, we mean expressions like ${foo.matches('[0-9]+')} that aren't supported by the standard. However, JUEL provides support for method invocations as a proprietary extension. To use them, you will have to do two things:

  1. Customize your builder to accept expressions using method invocations.
  2. Provide an ELResolver to resolve the methods at evaluation time.

The customized builder is created like this:

TreeBuilder builder = new Builder(Builder.Feature.METHOD_INVOCATIONS, ...);

As an alternative, you may set property

javax.el.methodInvocations

to true.

Methods are resolved just like properties. That is, when evaluating an expression with a method invocation, your ELResolver will be asked to resolve the method by calling its getValue(...) method and is expected to return either a java.lang.reflect.Method or a javax.el.MethodInfo object as a result. Having the method, evaluation proceeds as with function invocations.

As an example, we show the getValue(...) method of a resolver that is capable of resolving a single method that has been supplied to it at construction time:

@Override
public Method getValue(ELContext context, Object base, Object prop) {
  if (method.getDeclaringClass().isInstance(base) && method.getName().equals(prop.toString())) {
    context.setPropertyResolved(true);
    return method;
  }
  return null;
}

A full example illustrating method invocations is distributed with JUEL below src/samples in package de.odysseus.el.samples.extensions.

Enabling null Properties

The EL specification describes the evaluation semantics of base[property]. If property is null, the specification states not to resolve null on base. Rather, null should be returned if getValue(...) has been called and a PropertyNotFoundException should be thrown else. As a consequence, it is impossible to resolve null as a key in a map. However, JUEL's builder supports a feature NULL_PROPERTIES to let you resolve null like any other property value.

Assume that identifier map resolves to a java.util.Map.

  • If feature NULL_PROPERTIES has not been enabled, evaluating ${base[null]} as an rvalue (lvalue) will return null (throw an exception).
  • If feature NULL_PROPERTIES has been enabled, evaluating ${base[null]} as an rvalue (lvalue) will get (put) the value for key null in that map.

The customized builder is created like this:

TreeBuilder builder = new Builder(Builder.Feature.NULL_PROPERTIES, ...);

As an alternative, you may set property

javax.el.nullProperties

to true.

Using a customized Cache

The default lru cache implementation can be customized by specifying a maximum cache size. However, it might be desired to use a different caching mechanism. Doing this means to provide a class that implements the TreeCache interface.

Now, having a new cache implementatation, it can be passed to a factoy via

TreeStore store = new TreeStore(new Builder(), new MyCache());
ExpressionFactory factory = new ExpressionFactoryImpl(store);

Tree Expressions

In the basics section, we already presented the TreeValueExpression and TreeMethodExpression classes, which are used to represent parsed expressions.

Equality

As for all objects, the equals(Object) method is used to test for equality. The specification notes that two expressions of the same type are equal if and only if they have an identical parsed representation.

This makes clear, that the expression string cannot serve as a sufficient condition for equality testing. Consider expression string ${foo}. When creating tree expressions from that string using different variable mappings for foo, these expressions must not be considered equal. Similar, an expression string using function invocations may be used to create tree expressions with different function mappings. Even worse, ${foo()} and ${bar()} may be equal if foo and bar refered to the same method at creation time.

To handle these requirements, JUEL separates the variable and function bindings from the pure parse tree. The tree only depends on the expression string and can therefore be reused by all expressions created from a string. The bindings are then created from the tree, variable mapper and function mapper. Together, the tree and bindings form the core of a tree expression.

When comparing tree expressions, the trees are structurally compared, ignoring the names of functions and variables. Instead, the corresponding methods and value expressions bound to them are compared.

Serialization

As required by the specification, all expressions have to be serializable. When serializing a tree expression, the expression string is serialized, not the tree. On deserialization, the tree is rebuilt from the expression string.