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

henrib pushed a commit to branch JEXL-368
in repository https://gitbox.apache.org/repos/asf/commons-jexl.git

commit 0a5417c68a9722b40b36002f227ab799f7d4f94f
Author: henrib <hen...@apache.org>
AuthorDate: Tue May 3 18:35:03 2022 +0200

    JEXL-368: refactored namespace resolution and cache functor properly
---
 .../commons/jexl3/internal/InterpreterBase.java    | 126 ++++++++++++---------
 .../internal/introspection/ConstructorMethod.java  |  37 +++---
 .../java/org/apache/commons/jexl3/AssignTest.java  |  72 ++++++++++--
 .../apache/commons/jexl3/ContextNamespaceTest.java |  74 ++++++++++++
 4 files changed, 227 insertions(+), 82 deletions(-)

diff --git 
a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java 
b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
index ac90cc37..f09ca21f 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java
@@ -189,74 +189,92 @@ public abstract class InterpreterBase extends 
ParserVisitor {
                 throw new JexlException(node, "no such function namespace " + 
prefix, null);
             }
         }
-        // shortcut if ns is known to be not-a-functor
-        final boolean cacheable = cache;
-        final Object cached = cacheable ? node.jjtGetValue() : null;
-        if (cached != JexlContext.NamespaceFunctor.class) {
-            // allow namespace to instantiate a functor with context if 
possible, not an error otherwise
-            Object functor = null;
-            if (namespace instanceof JexlContext.NamespaceFunctor) {
-                functor = ((JexlContext.NamespaceFunctor) 
namespace).createFunctor(context);
-            } else if (namespace instanceof Class<?> || namespace instanceof 
String) {
-                // attempt to reuse last ctor cached in volatile JexlNode.value
-                if (cached instanceof JexlMethod) {
-                    try {
-                        final Object eval = ((JexlMethod) 
cached).tryInvoke(null, context);
-                        if (JexlEngine.TRY_FAILED != eval) {
-                            functor = eval;
-                        }
-                    } catch (final JexlException.TryFailed xtry) {
-                        throw new JexlException(node, "unable to instantiate 
namespace " + prefix, xtry.getCause());
-                    }
+        Object functor = null;
+        // class or string (*1)
+        if (namespace instanceof Class<?> || namespace instanceof String) {
+            // the namespace(d) identifier
+            final ASTIdentifier nsNode = (ASTIdentifier) node.jjtGetChild(0);
+            final boolean cacheable = cache && prefix != null;
+            final Object cached = cacheable ? nsNode.jjtGetValue() : null;
+            // we know the class is used as namespace of static methods, no 
functor
+            if (cached instanceof Class<?>) {
+                return (Class<?>) cached;
+            }
+            // attempt to reuse last cached constructor
+            if (cached instanceof JexlContext.NamespaceFunctor) {
+                Object eval = ((JexlContext.NamespaceFunctor) 
cached).createFunctor(context);
+                if (JexlEngine.TRY_FAILED != eval) {
+                    functor = eval;
+                    namespace = cached;
                 }
-                // find a ctor with that context class
-                if (functor == null) {
-                    JexlMethod ctor = uberspect.getConstructor(namespace, 
context);
+            }
+            if (functor == null) {
+                // find a constructor with that context as argument or without
+                for (int tried = 0; tried < 2; ++tried) {
+                    final boolean withContext = tried == 0;
+                    final JexlMethod ctor = withContext
+                            ? uberspect.getConstructor(namespace, context)
+                            : uberspect.getConstructor(namespace);
                     if (ctor != null) {
                         try {
-                            functor = ctor.invoke(namespace, context);
-                            if (cacheable && ctor.isCacheable()) {
-                                node.jjtSetValue(ctor);
+                            functor = withContext
+                                    ? ctor.invoke(namespace, context)
+                                    : ctor.invoke(namespace);
+                            // defensive
+                            if (functor != null) {
+                                // wrap the namespace in a NamespaceFunctor to 
shield us from the actual
+                                // number of arguments to call it with.
+                                final Object ns = namespace;
+                                // make it a class (not a lambda!) so 
instanceof (see *2) will catch it
+                                namespace = new JexlContext.NamespaceFunctor() 
{
+                                    @Override
+                                    public Object createFunctor(JexlContext 
context) {
+                                        return withContext
+                                                ? ctor.tryInvoke(null, ns, 
context)
+                                                : ctor.tryInvoke(null, ns);
+                                    }
+                                };
+                                if (cacheable && ctor.isCacheable()) {
+                                    nsNode.jjtSetValue(namespace);
+                                }
+                                break; // we found a constructor that did 
create a functor
                             }
                         } catch (final Exception xinst) {
                             throw new JexlException(node, "unable to 
instantiate namespace " + prefix, xinst);
                         }
                     }
-                    // try again; find a ctor with no arg
-                    if (functor == null) {
-                        ctor = uberspect.getConstructor(namespace);
-                        if (ctor != null) {
-                            try {
-                                functor = ctor.invoke(namespace);
-                            } catch (final Exception xinst) {
-                                throw new JexlException(node, "unable to 
instantiate namespace " + prefix, xinst);
-                            }
-                        }
-                        // try again; use a class, namespace of static methods
+                }
+                // did not, will not create a functor instance; use a class, 
namespace of static methods
+                if (functor == null) {
+                    try {
                         // try to find a class with that name
-                        if (functor == null && namespace instanceof String) {
-                            try {
-                                namespace = 
uberspect.getClassLoader().loadClass((String) namespace);
-                            } catch (final ClassNotFoundException xignore) {
-                                // not a class
-                                namespace = null;
-                            }
-                        } // we know it's a class
+                        if (namespace instanceof String) {
+                            namespace = 
uberspect.getClassLoader().loadClass((String) namespace);
+                        }
+                        // we know it's a class in all cases (see *1)
+                        if (cacheable) {
+                            nsNode.jjtSetValue(namespace);
+                        }
+                    } catch (final ClassNotFoundException xignore) {
+                        // not a class
+                        throw new JexlException(node, "no such class namespace 
" + prefix, null);
                     }
                 }
             }
-            // got a functor, store it and return it
-            if (functor != null) {
-                synchronized (this) {
-                    if (functors == null) {
-                        functors = new HashMap<>();
-                    }
-                    functors.put(prefix, functor);
+        }
+        // if a namespace functor, instantiate the functor (if not done 
already) and store it (*2)
+        if (functor == null && namespace instanceof 
JexlContext.NamespaceFunctor) {
+            functor = ((JexlContext.NamespaceFunctor) 
namespace).createFunctor(context);
+        }
+        // got a functor, store it and return it
+        if (functor != null) {
+            synchronized (this) {
+                if (functors == null) {
+                    functors = new HashMap<>();
                 }
-                return functor;
+                functors.put(prefix, functor);
             }
-            // use the NamespaceFunctor class to tag this node as not-a-functor
-            node.jjtSetValue(JexlContext.NamespaceFunctor.class);
+            return functor;
         }
         return namespace;
     }
diff --git 
a/src/main/java/org/apache/commons/jexl3/internal/introspection/ConstructorMethod.java
 
b/src/main/java/org/apache/commons/jexl3/internal/introspection/ConstructorMethod.java
index bb578730..e38dddab 100644
--- 
a/src/main/java/org/apache/commons/jexl3/internal/introspection/ConstructorMethod.java
+++ 
b/src/main/java/org/apache/commons/jexl3/internal/introspection/ConstructorMethod.java
@@ -79,25 +79,28 @@ public final class ConstructorMethod implements JexlMethod {
     }
 
     @Override
-    public Object tryInvoke(final String name, final Object obj, final 
Object... params) {
-        try {
-            final Class<?> ctorClass = ctor.getDeclaringClass();
-            boolean invoke = true;
-            if (obj != null) {
-                if (obj instanceof Class<?>) {
-                    invoke = ctorClass.equals(obj);
-                } else {
-                    invoke = ctorClass.getName().equals(obj.toString());
+    public Object tryInvoke(final String name, final Object obj, final 
Object... args) {
+        // dont try to invoke if no parameter but call has arguments
+        if (ctor.getParameterCount() > 0 || args.length == 0) {
+            try {
+                final Class<?> ctorClass = ctor.getDeclaringClass();
+                boolean invoke = true;
+                if (obj != null) {
+                    if (obj instanceof Class<?>) {
+                        invoke = ctorClass.equals(obj);
+                    } else {
+                        invoke = ctorClass.getName().equals(obj.toString());
+                    }
                 }
+                invoke &= name == null || ctorClass.getName().equals(name);
+                if (invoke) {
+                    return ctor.newInstance(args);
+                }
+            } catch (InstantiationException | IllegalArgumentException | 
IllegalAccessException xinstance) {
+                return Uberspect.TRY_FAILED;
+            } catch (final InvocationTargetException xinvoke) {
+                throw JexlException.tryFailed(xinvoke); // throw
             }
-            invoke &= name == null || ctorClass.getName().equals(name);
-            if (invoke) {
-                return ctor.newInstance(params);
-            }
-        } catch (InstantiationException | IllegalArgumentException | 
IllegalAccessException xinstance) {
-            return Uberspect.TRY_FAILED;
-        } catch (final InvocationTargetException xinvoke) {
-            throw JexlException.tryFailed(xinvoke); // throw
         }
         return Uberspect.TRY_FAILED;
     }
diff --git a/src/test/java/org/apache/commons/jexl3/AssignTest.java 
b/src/test/java/org/apache/commons/jexl3/AssignTest.java
index f4d3fcdb..82c40ba1 100644
--- a/src/test/java/org/apache/commons/jexl3/AssignTest.java
+++ b/src/test/java/org/apache/commons/jexl3/AssignTest.java
@@ -19,6 +19,8 @@ package org.apache.commons.jexl3;
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.util.Arrays;
+
 /**
  * Test cases for assignment.
  *
@@ -72,10 +74,9 @@ public class AssignTest extends JexlTestCase {
     /**
      * Make sure bean assignment works
      *
-     * @throws Exception on any error
      */
     @Test
-    public void testAntish() throws Exception {
+    public void testAntish() {
         final JexlExpression assign = JEXL.createExpression("froboz.value = 
10");
         final JexlExpression check = JEXL.createExpression("froboz.value");
         final JexlContext jc = new MapContext();
@@ -86,7 +87,7 @@ public class AssignTest extends JexlTestCase {
     }
 
     @Test
-    public void testAntishInteger() throws Exception {
+    public void testAntishInteger() {
         final JexlExpression assign = JEXL.createExpression("froboz.0 = 10");
         final JexlExpression check = JEXL.createExpression("froboz.0");
         final JexlContext jc = new MapContext();
@@ -97,7 +98,7 @@ public class AssignTest extends JexlTestCase {
     }
 
     @Test
-    public void testBeanish() throws Exception {
+    public void testBeanish() {
         final JexlExpression assign = JEXL.createExpression("froboz.value = 
10");
         final JexlExpression check = JEXL.createExpression("froboz.value");
         final JexlContext jc = new MapContext();
@@ -110,7 +111,7 @@ public class AssignTest extends JexlTestCase {
     }
 
     @Test
-    public void testAmbiguous() throws Exception {
+    public void testAmbiguous() {
         final JexlExpression assign = JEXL.createExpression("froboz.nosuchbean 
= 10");
         final JexlContext jc = new MapContext();
         final Froboz froboz = new Froboz(-169);
@@ -129,7 +130,7 @@ public class AssignTest extends JexlTestCase {
     }
 
     @Test
-    public void testArray() throws Exception {
+    public void testArray() {
         final JexlExpression assign = JEXL.createExpression("froboz[\"value\"] 
= 10");
         final JexlExpression check = 
JEXL.createExpression("froboz[\"value\"]");
         final JexlContext jc = new MapContext();
@@ -142,7 +143,7 @@ public class AssignTest extends JexlTestCase {
     }
 
     @Test
-    public void testMini() throws Exception {
+    public void testMini() {
         final JexlContext jc = new MapContext();
         final JexlExpression assign = JEXL.createExpression("quux = 10");
         final Object o = assign.evaluate(jc);
@@ -151,7 +152,7 @@ public class AssignTest extends JexlTestCase {
     }
 
     @Test
-    public void testMore() throws Exception {
+    public void testMore() {
         final JexlContext jc = new MapContext();
         jc.set("quuxClass", Quux.class);
         final JexlExpression create = JEXL.createExpression("quux = 
new(quuxClass, 'xuuq', 100)");
@@ -167,7 +168,7 @@ public class AssignTest extends JexlTestCase {
     }
 
     @Test
-    public void testUtil() throws Exception {
+    public void testUtil() {
         final Quux quux = JEXL.newInstance(Quux.class, "xuuq", 
Integer.valueOf(100));
         Assert.assertNotNull(quux);
         JEXL.setProperty(quux, "froboz.value", Integer.valueOf(100));
@@ -179,7 +180,7 @@ public class AssignTest extends JexlTestCase {
     }
 
     @Test
-    public void testRejectLocal() throws Exception {
+    public void testRejectLocal() {
         final JexlContext jc = new MapContext();
         JexlScript assign = JEXL.createScript("var quux = null; 
quux.froboz.value = 10");
         try {
@@ -194,4 +195,53 @@ public class AssignTest extends JexlTestCase {
         final Object o = assign.execute(jc);
         Assert.assertEquals(10, o);
     }
-}
\ No newline at end of file
+
+    @Test
+    public void testPropertyInError0() {
+        JexlScript script;
+        for(String op : Arrays.asList(
+                " = ", "+= ", " -= "," *= "," /= "," %= ",
+                " &= ", " |= ", " ^= ",
+                " <<= ", " >>= ", " >>>= ")) {
+            script = JEXL.createScript("x -> x.y " +op+ "42");
+            try {
+                script.execute(null, "the_x_value");
+            } catch (final JexlException.Property xprop) {
+                Assert.assertEquals("y", xprop.getProperty());
+            }
+        }
+        script = JEXL.createScript("x -> x.y ");
+        try {
+            script.execute(null, "the_x_value");
+        } catch (final JexlException.Property xprop) {
+            Assert.assertEquals("y", xprop.getProperty());
+        }
+    }
+
+    @Test
+    public void testSetInError1() {
+        try {
+            JEXL.setProperty("the_x_value", "y", 42);
+        } catch (final JexlException.Property xprop) {
+            Assert.assertEquals("y", xprop.getProperty());
+        }
+        try {
+            JEXL.setProperty(null, "y", 42);
+        } catch (final JexlException xprop) {
+            //
+        }
+    }
+    @Test
+    public void testGetInError1() {
+        try {
+            JEXL.getProperty("the_x_value", "y");
+        } catch (final JexlException.Property xprop) {
+            Assert.assertEquals("y", xprop.getProperty());
+        }
+        try {
+            JEXL.getProperty(null, "y");
+        } catch (final JexlException xprop) {
+            //
+        }
+    }
+}
diff --git a/src/test/java/org/apache/commons/jexl3/ContextNamespaceTest.java 
b/src/test/java/org/apache/commons/jexl3/ContextNamespaceTest.java
index 942fa95c..ad8d9c1d 100644
--- a/src/test/java/org/apache/commons/jexl3/ContextNamespaceTest.java
+++ b/src/test/java/org/apache/commons/jexl3/ContextNamespaceTest.java
@@ -21,9 +21,11 @@ import org.junit.Ignore;
 import org.junit.Test;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Tests JexlContext (advanced) features.
@@ -225,6 +227,9 @@ public class ContextNamespaceTest extends JexlTestCase {
         ctxt.set("x", "42");
         result = script.execute(ctxt);
         Assert.assertEquals(169, result);
+        //ctxt.set("x", "42");
+        result = script.execute(ctxt);
+        Assert.assertEquals(169, result);
     }
 
     private void run348c(JexlEngine jexl, JexlContext ctxt) {
@@ -318,4 +323,73 @@ public class ContextNamespaceTest extends JexlTestCase {
         }
     }
 
+    static AtomicInteger nsnsCtor = new AtomicInteger(0);
+
+    public static class NsNs {
+        private final int constVar;
+        public NsNs(JexlContext ctxt) {
+            nsnsCtor.incrementAndGet();
+            Object n = ctxt.get("NUMBER");
+            constVar = (n instanceof Number) ? ((Number) n).intValue() : -1;
+        }
+
+        public int callIt(int n) {
+            return n + constVar;
+        }
+    }
+
+    @Test
+    public void testNsNsContext0() throws Exception {
+        nsnsCtor.set(0);
+        String clsName = NsNs.class.getName();
+        runNsNsContext(Collections.singletonMap("nsns", clsName));
+    }
+
+    @Test
+    public void testNsNsContext1() throws Exception {
+        nsnsCtor.set(0);
+        runNsNsContext(Collections.singletonMap("nsns", NsNs.class));
+    }
+
+    private void runNsNsContext(Map<String,Object> nsMap) {
+        JexlContext ctxt = new MapContext();
+        ctxt.set("NUMBER", 19);
+        final JexlEngine jexl = new 
JexlBuilder().strict(true).silent(false).cache(32)
+                .namespaces(nsMap).create();
+        final JexlScript script = jexl.createScript("x ->{ nsns:callIt(x); 
nsns:callIt(x); }");
+        Number result = (Number) script.execute(ctxt, 23);
+        Assert.assertEquals(42, result);
+        Assert.assertEquals(1, nsnsCtor.get());
+        result = (Number) script.execute(ctxt, 623);
+        Assert.assertEquals(642, result);
+        Assert.assertEquals(2, nsnsCtor.get());
+    }
+
+    public static class StaticNs {
+        private StaticNs() { }
+        public static int callIt(int n) {
+            return n + 19;
+        }
+    }
+
+    @Test
+    public void testStaticNs0() {
+        runStaticNsContext(Collections.singletonMap("sns", StaticNs.class));
+    }
+
+    @Test
+    public void testStaticNs1() {
+        runStaticNsContext(Collections.singletonMap("sns", 
StaticNs.class.getName()));
+    }
+
+    private void runStaticNsContext(Map<String,Object> nsMap) {
+        JexlContext ctxt = new MapContext();
+        final JexlEngine jexl = new 
JexlBuilder().strict(true).silent(false).cache(32)
+                .namespaces(nsMap).create();
+        final JexlScript script = jexl.createScript("x ->{ sns:callIt(x); 
sns:callIt(x); }");
+        Number result = (Number) script.execute(ctxt, 23);
+        Assert.assertEquals(42, result);
+        result = (Number) script.execute(ctxt, 623);
+        Assert.assertEquals(642, result);
+    }
 }

Reply via email to