Sunday, September 14, 2014

In the Java EE platform programmers have a way to reference values in beans via textual expressions. These textual expressions are then compiled by the implementation (via the Expression Language, AKA EL spec) to instances of ValueExpression.

E.g. the following EL expression can be used to refer to the named bean "foo" and its property "bar":

#{foo.bar}

Expressions can be chains of arbitrary length, and can include method calls as well. E.g.:

#{foo.bar(1).kaz.zak(test)}

An important aspect of these expressions is that they are highly contextual, specifically where it concerns the top level variables. These consists of the object that starts the chain ("foo" here) and any EL variables used as method arguments ("test" here). Because of this, it's not a totally unknown requirement for wanting to resolve the expression when it's still in context in order to obtain the so-called final base and the final property/method, the last one including the resolved and bound parameters.

For any of the five methods, the ELResolver.getValue[...] method is used to resolve all properties up to but excluding the last one. This provides the base object.

There is no mention here that ELResolver.invoke is used as well if any of the intermediate nodes in the chain is a method invocation (like bar(1) in #{foo.bar(1).kaz.zak(test)}).

The fact that there's a ValueReference only supporting properties and no corresponding MethodReference is extra curious, since method invocations in chains and ValueExpressions and the ValueReference type were both introduced in EL 2.2.

So is there any hope of getting the final base and method if a ValueExpression happens to be pointing to a method? There appears to be a way, but it's a little tricky. The trick in question consists of using a special tracing ELResolver and taking advantage of the fact that some methods on ValueExpression are specified to resolve the expression "up to but excluding the last [node]". Using this we can use the following approach:

Instantiate an EL context which contains the special tracing EL resolver

Call a method on the ValueExpression that resolves the chain until the next to last node (e.g. getType()) using the special EL context

In the tracing EL resolver count each intermediate call, so when getType() returns the length of the chain is known

Call a method on the ValueExpression that resolves the entire chain (e.g. getValue()) using the same special EL context instance

When the EL resolver reaches the next to last node (determined by counting intermediate calls again), wrap the return value from ElResolver.getValue or ElResolver.invoke

If either ElResolver.getValue or ElResolver.invoke is called again later with our special wrapped type, we know this is the final node and can collect all details that we need; the base, property or method name and the resolved method parameters (if any). All of these are simply passed to us by the EL implementation

The return value wrapping of the next to last node (at call count N) may need some extra explanation. After all, why not just wait till we're called the Nth + 1 time? The issue is that this Nth + 1 call may be for resolving variables that are passed as parameters into the final node if this final node is a method invocation. The amount of such parameters is unknown and each parameter can consist of a chain of arbitrary length.

E.g. consider the following expression:

#{foo.bar.kaz(test.a.b.c(x.r), bean.x.y.z(o).p)}

In such a case the first pass of the above given approach will count the calls up until the point of resolving "bar", which is thus at call count N. If "kaz" was a simple property, our EL resolver would be asked to resolve [return value of "bar"]."kaz" at call count N + 1. However, since "kaz" is not a simple property but a complex method invocation with EL variables, the next call after N will be for resolving the base of the first EL variable used in the method invocation ("test" here).

One may also wonder why we do not "simply" get the textual EL representation of an EL expression, chop off the last node using simple string manipulation and resolve that. The reason is two fold. First, it may work for very simple expressions (like #{a.b.c}), but doesn't work in general for complex ones (e.g. #{empty foo? a.b.c : x.y.z}). A second issue is that a given ValueExpression instance all too often contains state (like an embedded VariableMapper instance), which is lost when we just get the EL string from a ValueExpression and evaluate that.

class InspectorElResolver extends ELResolverWrapper {
private int passOneCallCount;
private int passTwoCallCount;
private Object lastBase;
private Object lastProperty; // Method name in case VE referenced a method, otherwise property name
private Object[] lastParams; // Actual parameters supplied to a method (if any)
private boolean subchainResolving;
// Marker holder via which we can track our last base. This should become
// the last base in a next iteration. This is needed because if the very last property is a
// method node with a variable, we can't track resolving that variable anymore since it will not have been processed by the
// getType() call of the first pass.
// E.g. a.b.c(var.foo())
private FinalBaseHolder finalBaseHolder;
private InspectorPass pass = InspectorPass.PASS1_FIND_NEXT_TO_LAST_NODE;
public InspectorElResolver(ELResolver elResolver) {
super(elResolver);
}
@Override
public Object getValue(ELContext context, Object base, Object property) {
if (base instanceof FinalBaseHolder) {
// If we get called with a FinalBaseHolder, which was set in the next to last node,
// we know we're done and can set the base and property as the final ones.
lastBase = ((FinalBaseHolder) base).getBase();
lastProperty = property;
context.setPropertyResolved(true);
return ValueExpressionType.PROPERTY;
}
checkSubchainStarted(base);
if (subchainResolving) {
return super.getValue(context, base, property);
}
recordCall(base, property);
return wrapOutcomeIfNeeded(super.getValue(context, base, property));
}
@Override
public Object invoke(ELContext context, Object base, Object method, Class<?>[] paramTypes, Object[] params) {
if (base instanceof FinalBaseHolder) {
// If we get called with a FinalBaseHolder, which was set in the next to last node,
// we know we're done and can set the base, method and params as the final ones.
lastBase = ((FinalBaseHolder) base).getBase();
lastProperty = method;
lastParams = params;
context.setPropertyResolved(true);
return ValueExpressionType.METHOD;
}
checkSubchainStarted(base);
if (subchainResolving) {
return super.invoke(context, base, method, paramTypes, params);
}
recordCall(base, method);
return wrapOutcomeIfNeeded(super.invoke(context, base, method, paramTypes, params));
}
@Override
public Class<?> getType(ELContext context, Object base, Object property) {
// getType is only called on the last element in the chain (if the EL
// implementation actually calls this, which might not be the case if the
// value expression references a method)
//
// We thus do know the size of the chain now, and the "lastBase" and "lastProperty"
// that were set *before* this call are the next to last now.
//
// Alternatively, this method is NOT called by the EL implementation, but then
// "lastBase" and "lastProperty" are still the next to last.
//
// Independent of what the EL implementation does, "passOneCallCount" should thus represent
// the total size of the call chain minus 1. We use this in pass two to capture the
// final base, property/method and optionally parameters.
context.setPropertyResolved(true);
// Special value to signal that getType() has actually been called (this value is
// not used by the algorithm now, but may be useful when debugging)
return InspectorElContext.class;
}
private boolean isAtNextToLastNode() {
return passTwoCallCount == passOneCallCount;
}
private void checkSubchainStarted(Object base) {
if (pass == InspectorPass.PASS2_FIND_FINAL_NODE && base == null && isAtNextToLastNode()) {
// If "base" is null it means a new chain is being resolved.
// The main expression chain likely has ended with a method that has one or more EL variables
// as parameters that now need to be resolved.
// E.g. a.b().c.d(var1)
subchainResolving = true;
}
}
private void recordCall(Object base, Object property) {
switch (pass) {
case PASS1_FIND_NEXT_TO_LAST_NODE:
// In the first "find next to last" pass, we'll be collecting the next to last element
// in an expression.
// E.g. given the expression a.b().c.d, we'll end up with the base returned by b() and "c" as
// the last property.
passOneCallCount++;
lastBase = base;
lastProperty = property;
break;
case PASS2_FIND_FINAL_NODE:
// In the second "find final node" pass, we'll collecting the final node
// in an expression. We need to take care that we're not actually calling / invoking
// that last element as it may have a side-effect that the user doesn't want to happen
// twice (like storing something in a DB etc).
passTwoCallCount++;
if (passTwoCallCount == passOneCallCount) {
// We're at the same call count as the first phase ended with.
// If the chain has resolved the same, we should be dealing with the same base and property now
if (base != lastBase || property != lastProperty) {
throw new IllegalStateException(
"First and second pass of resolver at call #" + passTwoCallCount +
" resolved to different base or property.");
}
}
break;
}
}
private Object wrapOutcomeIfNeeded(Object outcome) {
if (pass == InspectorPass.PASS2_FIND_FINAL_NODE && finalBaseHolder == null && isAtNextToLastNode()) {
// We're at the second pass and at the next to last node in the expression chain.
// "outcome" which we have just resolved should thus represent our final base.
// Wrap our final base in a special class that we can recognize when the EL implementation
// invokes this resolver later again with it.
finalBaseHolder = new FinalBaseHolder(outcome);
return finalBaseHolder;
}
return outcome;
}
public InspectorPass getPass() {
return pass;
}
public void setPass(InspectorPass pass) {
this.pass = pass;
}
public Object getBase() {
return lastBase;
}
public Object getProperty() {
return lastProperty;
}
public Object[] getParams() {
return lastParams;
}
}

As seen, the support for ValueExpressions that point to methods is not optimal in the current EL specification. With some efforts we can workaround this, but arguably such functionality should be present in the specification itself.