EL 3.0 (JSR-341) not working when argument is a Predicate?

Viewed 420

This test shows my problem:

public static String testMe(Predicate<String> filter) {
    return "yeah!";
}

@Test
public void testPredicateAsArgument() throws ClassNotFoundException, NoSuchMethodException {
    final ELProcessor elp = new ELProcessor();
    elp.defineFunction("", "", "test.EL3Test", "testMe");
    try {
        Object result = elp.eval("testMe(o->true)"); // IllegalArgumentException
        // ...
    } catch (javax.el.ELException ex) {
        fail(Exceptions.getCauseMessage(ex));
    }
}

It throws: IllegalArgumentException: Cannot convert javax.el.LambdaExpression@511baa65 of type class javax.el.LambdaExpression to interface java.util.function.Predicate

Is this a bug or limitation of EL 3 or do i miss something?

Tested versions: org.glassfish:javax.el:jar:3.0.0 and org.glassfish:javax.el:jar:3.0.1-b08

Also posted as github issue

UPDATE as of 2021-09-06

Implemented via https://github.com/eclipse-ee4j/el-ri/commit/bcd0eeb349af607eb15428c04853a0be0948f80c

2 Answers

EL 3.0 was created during Java 1.7 (Java EE 7's target) not Java 1.8. In other words, it predates Java native lambdas and therefore can't possibly know anything about them.

Best what you can do is posting an enhancement issue for next EL version. We're currently already on Java EE 8 (targeted at Java 1.8), but there's no EL 3.1 yet. You'd have to wait for Java EE 9 for any enhancement request to possibly end up in EL.next.

For anyone interested in a solutions:

/**
 * {@code FunctionalInterfaceResolver} intercepts method calls with EL lambda expressions as
 * arguments which need to coerce to Java 8 functional interfaces.
 *
 * A custom `ELResolver` is always consulted before the default resolvers.
 *
 * Example usage:
 * ```` java
 * final ELProcessor elp = new ELProcessor();
 * elp.getELManager().addELResolver(new FunctionalInterfaceResolver());
 * final Object result = elp.eval("p.findResources(o->o.getContentType() eq 'image/png')");
 * ````
 *
 * @author <a href="mailto:rmuller@xiam.nl">Ronald K. Muller</a>
 */
public final class FunctionalInterfaceResolver extends ELResolver {

    @Override
    public Object invoke(final ELContext context, final Object base, final Object method,
        final Class<?>[] paramTypes, final Object[] params) {

        if (context == null || base == null || !(method instanceof String) || params == null) {
            return null;
        }
        // takes about 5ms. Try out caching if it becomes a bottleneck
        for (int i = 0; i < params.length; ++i) {
            if (params[i] instanceof javax.el.LambdaExpression) {
                for (Method m : base.getClass().getMethods()) {
                    if (m.getName().equals(method) && m.getParameterCount() == params.length) {
                        final Class[] types = m.getParameterTypes();
                        if (types[i].isAnnotationPresent(FunctionalInterface.class)) {
                            params[i] = coerceToFunctionalInterface(context,
                                (LambdaExpression)params[i], types[i]);
                        }
                    }
                }
            }
        }
        return null;
    }

    @Override
    public Class<?> getType(ELContext context, Object base, Object property) {
        return null;
    }

    @Override
    public void setValue(ELContext context, Object base, Object property, Object value) {
    }

    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        return false;
    }

    @Override
    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
        return null;
    }

    @Override
    public Class<?> getCommonPropertyType(ELContext context, Object base) {
        return String.class;
    }

    @Override
    public Object convertToType(ELContext context, Object obj, Class<?> targetType) {
        if (obj instanceof LambdaExpression &&
            targetType.isAnnotationPresent(FunctionalInterface.class)) {
            context.setPropertyResolved(obj, targetType);
            return coerceToFunctionalInterface(context, (LambdaExpression)obj, targetType);
       }
       return null;
    }

    private Object coerceToFunctionalInterface(
        final ELContext context, final LambdaExpression elLambda, final Class<?> targetType) {

        assert targetType.isAnnotationPresent(FunctionalInterface.class);
        return Proxy.newProxyInstance(targetType.getClassLoader(),
            new Class[]{targetType}, (Object obj, Method method, Object[] args) -> {

            // a FunctionalInterface has exactly one abstract method
            if (Modifier.isAbstract(method.getModifiers())) {
                // the "functional method"
                return elLambda.invoke(context, args);
            } else if ("toString".equals(method.getName())) {
                return "Proxy[" + targetType.getName() + ", wrapping " +
                    elLambda.getClass().getName() + ']';
            } else {
                throw new AssertionError("Method not expected: " + method.getName());
            }
        });
    }

    @Override
    public Object getValue(ELContext context, Object base, Object property) {
        return null;
    }

}
Related