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 by implementing the de.odysseus.el.tree.TreeBuilder interface from scratch. However, you may also want to subclass the Builder class and override the createParser() to work with a modified parser implementation.

Either way, the new tree builder can be passed to a factory via

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

As an alternative, you may set property

de.odysseus.el.tree.TreeBuilder

to the fully qualified class name of your builder implementation.

Enabling/Disabling 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 2.1 standard. However, method invocations have been added in maintenance release 2 of JSR 245, which is supported by JUEL.

Warning
JUEL's proprietary API for method invocations has been removed in version 2.2.

To enable (disable) expressions using method invocations, you may set property

javax.el.methodInvocations

to true (false).

Method invocations are enabled in profile JEE6 (default) and disabled in JEE5.

Enabling/Disabling VarArgs

The EL specification does not support function calls with variable argument lists. That is, if we bind String.format(String, Object...) to function str:format, the expression ${str:format('Hey %s','Joe')} will not work.

To enable (disable) VarArgs in function and method invocations, you may set property

javax.el.varArgs

to true (false).

VarArg invocations are enabled in profile JEE6 (default) and disabled in JEE5.

Enabling/Disabling 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 expression factory may be configured to resolve null like any other property value.

To enable (disable) null as an EL property value, you may set property

javax.el.nullProperties

to true (false).

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

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

The default is not to allow null as an EL property value.

Enabling/Disabling ignoring of expected return type

The EL specification allows to determine an expected return type for method expressions. The return type should then be checked to match the actual return type of the method to invoke. Unfortunately, the EL reference implementation ignores this parameter completely. This caused some "legacy" code to not recognize that they're passing wrong types. When switching to JUEL as their EL implementation, this causes an exception to be thrown.

For compatibility, JUEL lets you choose to ignore the expected return type passed to EpressionFactory.createMethodExpression() when looking up a method to invoke. (This option has no effect when evaluating literal method expressions, where the expected return type acts as coercion target type.)

To enable (disable) ignoring of the expected return type parameter, you may set property

javax.el.ignoreReturnType

to true (false).

The default is respect (i.e. not to ignore) the expected return type parameter.

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

Customizing Type Conversions

The rules to apply when coercing objects is described in the specification. However, in a non-JEE environment, it might be desired to apply application-specific or additional type conversions. To do this, you must provide JUEL's expression factory with an implementation of

de.odysseus.el.misc.TypeConverter

which defines a single method:

public <T> T convert(Object value, Class<T> type) throws ELException

The default converter is implemented in de.odysseus.el.misc.TypeConverterImpl. To use your new type converter, pass an instance of it to the factory constructor

ExpressionFactoryImpl(TreeStore store, TypeConverter converter)

As an alternative, you may set property

de.odysseus.el.misc.TypeConverter

to the fully qualified name of your converter class (requires your class to provide a default constructor).

The BeanELResolver.invoke(...) method needs type conversions to convert actual method parameters to the method's formal parameter types. Unfortunately, the resolver API doesn't provide access the an ExpressionFactory to use our customized conversions via ExpressionFactory.coerceToType(...). JUEL's bean resolver implementation consults the javax.el.ExpressionFactory context property to get a factory before creating a default using ExpressionFactory.getInstance(). That is, if you're using JUEL's EL API, you may do

elContext.putContext(javax.el.ExpressionFactory.class, factory)

to make your customized type conversions available to the resolver.