You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2021/07/08 21:05:06 UTC

[tomcat] 03/03: Add support for coercing LambdaExpression to any functional interface

This is an automated email from the ASF dual-hosted git repository.

markt pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 2fcb104294b2676154cb08f00d5665d668792280
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Thu Jul 8 22:02:26 2021 +0100

    Add support for coercing LambdaExpression to any functional interface
    
    This addresses this currently open issue against the EL spec
    https://github.com/eclipse-ee4j/el-ri/issues/45
    This is an initial implementation so users can provide feedback
    The implementation was inspired by rmuller's suggestion for an
    ELResolver that performs a similar function:
    https://stackoverflow.com/questions/46573761
---
 java/org/apache/el/LocalStrings.properties |  2 +
 java/org/apache/el/lang/ELSupport.java     | 27 +++++++++++
 test/org/apache/el/lang/TestELSupport.java | 76 ++++++++++++++++++++++++++++++
 webapps/docs/changelog.xml                 |  5 ++
 4 files changed, 110 insertions(+)

diff --git a/java/org/apache/el/LocalStrings.properties b/java/org/apache/el/LocalStrings.properties
index 1bf600d..d9f5503 100644
--- a/java/org/apache/el/LocalStrings.properties
+++ b/java/org/apache/el/LocalStrings.properties
@@ -13,6 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+elSupport.coerce.nonAbstract=Unable to coerce a LambdaExpression to the functional interface [{0}] because the method [{1}] is not abstract
+
 # General Errors
 error.cannotSetVariables=Cannot set variables on factory
 error.convert=Cannot convert [{0}] of type [{1}] to [{2}]
diff --git a/java/org/apache/el/lang/ELSupport.java b/java/org/apache/el/lang/ELSupport.java
index 808ad79..c37fbac 100644
--- a/java/org/apache/el/lang/ELSupport.java
+++ b/java/org/apache/el/lang/ELSupport.java
@@ -19,6 +19,9 @@ package org.apache.el.lang;
 import java.beans.PropertyEditor;
 import java.beans.PropertyEditorManager;
 import java.lang.reflect.Array;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Proxy;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.security.AccessController;
@@ -29,6 +32,7 @@ import java.util.Set;
 
 import jakarta.el.ELContext;
 import jakarta.el.ELException;
+import jakarta.el.LambdaExpression;
 
 import org.apache.el.util.MessageFactory;
 
@@ -588,6 +592,11 @@ public class ELSupport {
             return result;
         }
 
+        if (obj instanceof LambdaExpression && type.getAnnotation(FunctionalInterface.class) != null) {
+            T result = coerceToFunctionalInterface(ctx, (LambdaExpression) obj, type);
+            return result;
+        }
+
         throw new ELException(MessageFactory.get("error.convert",
                 obj, obj.getClass(), type));
     }
@@ -613,6 +622,24 @@ public class ELSupport {
         return result;
     }
 
+
+    private static <T> T coerceToFunctionalInterface(final ELContext ctx, final LambdaExpression lambdaExpression,
+            final Class<T> type) {
+        // Create a dynamic proxy for the functional interface
+        @SuppressWarnings("unchecked")
+        T result = (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type },
+                (Object obj, Method method, Object[] args) -> {
+            // Functional interfaces have a single, abstract method
+            if (!Modifier.isAbstract(method.getModifiers())) {
+                // TODO
+                throw new ELException(MessageFactory.get("elSupport.coerce.nonAbstract", type, method));
+            }
+            return lambdaExpression.invoke(ctx, args);
+        });
+        return result;
+    }
+
+
     public static final boolean isBigDecimalOp(final Object obj0,
             final Object obj1) {
         return (obj0 instanceof BigDecimal || obj1 instanceof BigDecimal);
diff --git a/test/org/apache/el/lang/TestELSupport.java b/test/org/apache/el/lang/TestELSupport.java
index 84d3ed4..672afd5 100644
--- a/test/org/apache/el/lang/TestELSupport.java
+++ b/test/org/apache/el/lang/TestELSupport.java
@@ -19,9 +19,11 @@ package org.apache.el.lang;
 import java.beans.PropertyEditorManager;
 import java.math.BigDecimal;
 import java.math.BigInteger;
+import java.util.function.Predicate;
 
 import jakarta.el.ELException;
 import jakarta.el.ELManager;
+import jakarta.el.ELProcessor;
 
 import org.junit.Assert;
 import org.junit.Test;
@@ -276,4 +278,78 @@ public class TestELSupport {
         VALB1,
         VALB2
     }
+
+
+    @Test
+    public void testCoercetoFunctionalInterface01() throws Exception {
+        final ELProcessor elp = new ELProcessor();
+        elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateA");
+        Object result = elp.eval("testPredicateA(x -> x.equals('data'))");
+        Assert.assertEquals("PASS", result);
+    }
+
+
+    @Test
+    public void testCoercetoFunctionalInterface02() throws Exception {
+        final ELProcessor elp = new ELProcessor();
+        elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateA");
+        Object result = elp.eval("testPredicateA(x -> !x.equals('data'))");
+        Assert.assertEquals("BLOCK", result);
+    }
+
+
+    @Test
+    public void testCoercetoFunctionalInterface03() throws Exception {
+        final ELProcessor elp = new ELProcessor();
+        elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateB");
+        Object result = elp.eval("testPredicateB(x -> x > 50)");
+        Assert.assertEquals("PASS", result);
+    }
+
+
+    @Test
+    public void testCoercetoFunctionalInterface04() throws Exception {
+        final ELProcessor elp = new ELProcessor();
+        elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateB");
+        Object result = elp.eval("testPredicateB(x -> x < 50)");
+        Assert.assertEquals("BLOCK", result);
+    }
+
+
+    @Test(expected = ELException.class)
+    public void testCoercetoFunctionalInterface05() throws Exception {
+        final ELProcessor elp = new ELProcessor();
+        elp.defineFunction("", "", "org.apache.el.lang.TestELSupport", "testPredicateC");
+        elp.eval("testPredicateC(x -> x > 50)");
+    }
+
+
+    public static String testPredicateA(Predicate<String> filter) {
+        String s = "data";
+        if (filter.test(s)) {
+            return "PASS";
+        } else {
+            return "BLOCK";
+        }
+    }
+
+
+    public static String testPredicateB(Predicate<Long> filter) {
+        Long l = Long.valueOf(100);
+        if (filter.test(l)) {
+            return "PASS";
+        } else {
+            return "BLOCK";
+        }
+    }
+
+
+    public static String testPredicateC(Predicate<String> filter) {
+        String s = "text";
+        if (filter.test(s)) {
+            return "PASS";
+        } else {
+            return "BLOCK";
+        }
+    }
 }
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 2e78aa9..4cec1f7 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -148,6 +148,11 @@
         Add additional generics to the EL API to align with the latest changes
         in the EL specification project. (markt)
       </scode>
+      <add>
+        Enable EL lambda expressions to be coerced to functional interfaces.
+        This is an implementation of a proposed extension to the Jakarta
+        Expression Language specification. (markt)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Web applications">

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


Re: [tomcat] 03/03: Add support for coercing LambdaExpression to any functional interface

Posted by Mark Thomas <ma...@apache.org>.
On 09/07/2021 11:40, Konstantin Kolinko wrote:
> пт, 9 июл. 2021 г. в 12:19, Mark Thomas <ma...@apache.org>:
>>
>> On 09/07/2021 09:58, Konstantin Kolinko wrote:
>>
>> Thanks Konstantin. This is good feedback.
>>
>> [...]
>>
>>> I wonder how Java itself (a java compiler) deals with coercion of
>>> lambdas to interfaces. Either it generates calls to some helper API,
>>> or it just repeats the same boilerplate code over and over.
>>
>> I'm not sure. I did look to see if there was anything in the public API
>> around this I could use to help but didn't find anything.
> 
> I found some info:
> 
> https://blogs.oracle.com/javamagazine/behind-the-scenes-how-do-lambda-expressions-really-work-in-java
> "Behind the scenes: How do lambda expressions really work in Java?"
> 
> https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html
> 
>  From a quick read, it looks like it does not use reflection API, but a
> newer invocation API.
> I am not sure whether it is useful at this point. Just sharing.

I looked at that but the parsed lambda expression from the EL isn't in 
the same form as a lambda expression in Java code would be in. You'd 
need to convert it. You might be able to use something like 
https://github.com/greenjoe/lambdaFromString to do this conversion.

After looking at things like this for a while, I couldn't see a way to 
use them to create a solution that was an improvement on the Proxy approach.

Mark

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


Re: [tomcat] 03/03: Add support for coercing LambdaExpression to any functional interface

Posted by Konstantin Kolinko <kn...@gmail.com>.
пт, 9 июл. 2021 г. в 12:19, Mark Thomas <ma...@apache.org>:
>
> On 09/07/2021 09:58, Konstantin Kolinko wrote:
>
> Thanks Konstantin. This is good feedback.
>
> [...]
>
> > I wonder how Java itself (a java compiler) deals with coercion of
> > lambdas to interfaces. Either it generates calls to some helper API,
> > or it just repeats the same boilerplate code over and over.
>
> I'm not sure. I did look to see if there was anything in the public API
> around this I could use to help but didn't find anything.

I found some info:

https://blogs.oracle.com/javamagazine/behind-the-scenes-how-do-lambda-expressions-really-work-in-java
"Behind the scenes: How do lambda expressions really work in Java?"

https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html

From a quick read, it looks like it does not use reflection API, but a
newer invocation API.
I am not sure whether it is useful at this point. Just sharing.

Best regards,
Konstantin Kolinko

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


Re: [tomcat] 03/03: Add support for coercing LambdaExpression to any functional interface

Posted by Mark Thomas <ma...@apache.org>.
On 09/07/2021 09:58, Konstantin Kolinko wrote:

Thanks Konstantin. This is good feedback.

> пт, 9 июл. 2021 г. в 00:07, Mark Thomas <ma...@apache.org>:

<snip/>

>>>       Add support for coercing LambdaExpression to any functional interface
>>
>> The implementation for this turned out to be a lot simpler than I
>> initially thought. I can;t help be think I've missed something. Please
>> try and find ways to break this. If you do, feel free to add a disabled
>> out test case and I'll take a look.
> 
> Re: " if (obj instanceof LambdaExpression &&
> type.getAnnotation(FunctionalInterface.class) != null) "
> 
> 1. IIRC, using @FunctionalInterface annotation is a hint, not a requirement.
> 
> E.g. at https://docs.oracle.com/javase/8/docs/api/java/lang/FunctionalInterface.html
> "the compiler will treat any interface meeting the definition"...
> 
> E.g. lambdas can be used with legacy code that does not use that annotation.

I'll look into how easy it would be to handle the case where the 
annotation isn't present. I'm a little concerned about the overhead of 
determining if the functional interface definition is met but if that 
turns out to be an issue then some form of caching is likely to help.

> 2. Inheritance? If the annotation is applied not on this class or
> interface, but on some of its parents.
> 
> Docs for "AnnotatedElement.getAnnotation(...)" - reading their
> definition of "present", I think that that method goes up a hierarchy
> of classes, but it does not go up a hierarchy of interfaces.
> https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/AnnotatedElement.html
> 
> Example: a "SerializablePredicate" interface,
> https://vaadin.com/api/framework/8.13.2/com/vaadin/server/SerializablePredicate.html
> https://github.com/vaadin/framework/blob/master/server/src/main/java/com/vaadin/server/SerializablePredicate.java
> 
> The SerializablePredicate interface does not redeclare
> @FunctionalInterface, but it is a functional interface.

I'll look into that too.

>> Re: "if (!Modifier.isAbstract(method.getModifiers())) { throw...."
> 
> 3. While only one method is abstract, other methods may be called:
> (a) methods defined by Object -  equals(), hashCode() and toString(),
> (b) default methods defined in an interface.
> 
> The current code throws an ELException.
> 
> There is some sample code (though maybe not best) to deal with (a) at
> https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html
> 
> I do not know how people deal with (b). It may be that
> InvocationHandler won't see those calls to default methods. (When I
> studied java proxies, that feature did not exist yet.)

My thinking was that the proxy is only ever going to be used to map the 
LambdaExpression to the functional interface call. I couldn't see any 
way for any of those other methods to be called. Am I missing something?

> I wonder how Java itself (a java compiler) deals with coercion of
> lambdas to interfaces. Either it generates calls to some helper API,
> or it just repeats the same boilerplate code over and over.

I'm not sure. I did look to see if there was anything in the public API 
around this I could use to help but didn't find anything.

Thanks,

Mark

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


Re: [tomcat] 03/03: Add support for coercing LambdaExpression to any functional interface

Posted by Konstantin Kolinko <kn...@gmail.com>.
пт, 9 июл. 2021 г. в 00:07, Mark Thomas <ma...@apache.org>:
>
> On 08/07/2021 22:05, markt@apache.org wrote:
> > This is an automated email from the ASF dual-hosted git repository.
> >
> > markt pushed a commit to branch main
> > in repository https://gitbox.apache.org/repos/asf/tomcat.git
> >
> > commit 2fcb104294b2676154cb08f00d5665d668792280
> > Author: Mark Thomas <ma...@apache.org>
> > AuthorDate: Thu Jul 8 22:02:26 2021 +0100
> >
> >      Add support for coercing LambdaExpression to any functional interface
>
> The implementation for this turned out to be a lot simpler than I
> initially thought. I can;t help be think I've missed something. Please
> try and find ways to break this. If you do, feel free to add a disabled
> out test case and I'll take a look.

Re: " if (obj instanceof LambdaExpression &&
type.getAnnotation(FunctionalInterface.class) != null) "

1. IIRC, using @FunctionalInterface annotation is a hint, not a requirement.

E.g. at https://docs.oracle.com/javase/8/docs/api/java/lang/FunctionalInterface.html
"the compiler will treat any interface meeting the definition"...

E.g. lambdas can be used with legacy code that does not use that annotation.


2. Inheritance? If the annotation is applied not on this class or
interface, but on some of its parents.

Docs for "AnnotatedElement.getAnnotation(...)" - reading their
definition of "present", I think that that method goes up a hierarchy
of classes, but it does not go up a hierarchy of interfaces.
https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/AnnotatedElement.html

Example: a "SerializablePredicate" interface,
https://vaadin.com/api/framework/8.13.2/com/vaadin/server/SerializablePredicate.html
https://github.com/vaadin/framework/blob/master/server/src/main/java/com/vaadin/server/SerializablePredicate.java

The SerializablePredicate interface does not redeclare
@FunctionalInterface, but it is a functional interface.

> Re: "if (!Modifier.isAbstract(method.getModifiers())) { throw...."

3. While only one method is abstract, other methods may be called:
(a) methods defined by Object -  equals(), hashCode() and toString(),
(b) default methods defined in an interface.

The current code throws an ELException.

There is some sample code (though maybe not best) to deal with (a) at
https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html

I do not know how people deal with (b). It may be that
InvocationHandler won't see those calls to default methods. (When I
studied java proxies, that feature did not exist yet.)

I wonder how Java itself (a java compiler) deals with coercion of
lambdas to interfaces. Either it generates calls to some helper API,
or it just repeats the same boilerplate code over and over.

Best regards,
Konstantin Kolinko

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


Re: [tomcat] 03/03: Add support for coercing LambdaExpression to any functional interface

Posted by Mark Thomas <ma...@apache.org>.
On 08/07/2021 22:05, markt@apache.org wrote:
> This is an automated email from the ASF dual-hosted git repository.
> 
> markt pushed a commit to branch main
> in repository https://gitbox.apache.org/repos/asf/tomcat.git
> 
> commit 2fcb104294b2676154cb08f00d5665d668792280
> Author: Mark Thomas <ma...@apache.org>
> AuthorDate: Thu Jul 8 22:02:26 2021 +0100
> 
>      Add support for coercing LambdaExpression to any functional interface

The implementation for this turned out to be a lot simpler than I 
initially thought. I can;t help be think I've missed something. Please 
try and find ways to break this. If you do, feel free to add a disabled 
out test case and I'll take a look.

Thanks,

Mark

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org