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

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


The following commit(s) were added to refs/heads/master by this push:
     new 45ba717  JEXL-363: getting captured variables and avoiding caching 
mutable literals
45ba717 is described below

commit 45ba71775d7df9762101a36f6894ff4a14425b77
Author: henrib <[email protected]>
AuthorDate: Mon Mar 14 16:32:32 2022 +0100

    JEXL-363: getting captured variables and avoiding caching mutable literals
---
 .../org/apache/commons/jexl3/internal/Scope.java   |  21 +++
 .../apache/commons/jexl3/internal/SetBuilder.java  |   2 +-
 .../apache/commons/jexl3/parser/ASTJexlLambda.java |   6 +
 .../apache/commons/jexl3/parser/ASTJexlScript.java |   8 ++
 .../org/apache/commons/jexl3/parser/JexlNode.java  |   7 +-
 .../commons/jexl3/CollectionLiteralTest.java       | 159 +++++++++++++++++++++
 6 files changed, 200 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/apache/commons/jexl3/internal/Scope.java 
b/src/main/java/org/apache/commons/jexl3/internal/Scope.java
index 1bd40a9..733c251 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Scope.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Scope.java
@@ -253,6 +253,27 @@ public final class Scope {
     }
 
     /**
+     * Gets this script captured symbols names, i.e. local variables defined 
in outer scopes and used
+     * by this scope.
+     * @return the captured names
+     */
+    public String[] getCapturedVariables() {
+        if (capturedVariables != null) {
+            final List<String> captured = new ArrayList<String>(vars);
+            for (final Map.Entry<String, Integer> entry : 
namedVariables.entrySet()) {
+                final int symnum = entry.getValue();
+                if (symnum >= parms && capturedVariables.containsKey(symnum)) {
+                    captured.add(entry.getKey());
+                }
+            }
+            if (!captured.isEmpty()) {
+                return captured.toArray(new String[captured.size()]);
+            }
+        }
+        return EMPTY_STRS;
+    }
+
+    /**
      * Gets the (maximum) number of arguments this script expects.
      * @return the number of parameters
      */
diff --git a/src/main/java/org/apache/commons/jexl3/internal/SetBuilder.java 
b/src/main/java/org/apache/commons/jexl3/internal/SetBuilder.java
index bb33ff2..65430ff 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/SetBuilder.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/SetBuilder.java
@@ -41,7 +41,7 @@ public class SetBuilder implements JexlArithmetic.SetBuilder {
     }
 
     @Override
-    public Object create() {
+    public Set<?> create() {
         return set;
     }
 }
diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTJexlLambda.java 
b/src/main/java/org/apache/commons/jexl3/parser/ASTJexlLambda.java
index 95d8cb9..3d883c4 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/ASTJexlLambda.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/ASTJexlLambda.java
@@ -34,4 +34,10 @@ public final class ASTJexlLambda extends ASTJexlScript {
     public boolean isTopLevel() {
         return jjtGetParent() == null;
     }
+
+    @Override
+    protected boolean isConstant(final boolean literal) {
+        // a closure is constant if it does not capture any variables
+        return getCapturedVariables().length == 0;
+    }
 }
diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java 
b/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java
index 25eb84e..d7d3046 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java
@@ -169,4 +169,12 @@ public class ASTJexlScript extends JexlLexicalNode  {
     public boolean isCapturedSymbol(final int symbol) {
         return scope != null && scope.isCapturedSymbol(symbol);
     }
+
+    /**
+     * Gets this script captured variable, i.e. symbols captured from outer 
scopes.
+     * @return the captured variable names
+     */
+    public String[] getCapturedVariables() {
+        return scope != null? scope.getCapturedVariables() : null;
+    }
 }
diff --git a/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java 
b/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java
index 70ad3c9..6b2bde5 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java
@@ -19,6 +19,8 @@ package org.apache.commons.jexl3.parser;
 import org.apache.commons.jexl3.JexlArithmetic;
 import org.apache.commons.jexl3.JexlException;
 import org.apache.commons.jexl3.JexlInfo;
+import org.apache.commons.jexl3.internal.LexicalScope;
+import org.apache.commons.jexl3.internal.Scope;
 import org.apache.commons.jexl3.internal.ScriptVisitor;
 import org.apache.commons.jexl3.introspection.JexlMethod;
 import org.apache.commons.jexl3.introspection.JexlPropertyGet;
@@ -128,8 +130,9 @@ public abstract class JexlNode extends SimpleNode {
     }
 
     /**
-     * Whether this node is a constant node Its value can not change after the 
first evaluation and can be cached
-     * indefinitely.
+     * Whether this node is a constant node.
+     * <p>Its value can not change after the first evaluation and can be cached
+     * indefinitely.</p>
      *
      * @return true if constant, false otherwise
      */
diff --git a/src/test/java/org/apache/commons/jexl3/CollectionLiteralTest.java 
b/src/test/java/org/apache/commons/jexl3/CollectionLiteralTest.java
new file mode 100644
index 0000000..4a242ec
--- /dev/null
+++ b/src/test/java/org/apache/commons/jexl3/CollectionLiteralTest.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.jexl3;
+
+import org.apache.commons.jexl3.internal.ArrayBuilder;
+import org.apache.commons.jexl3.internal.MapBuilder;
+import org.apache.commons.jexl3.internal.SetBuilder;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+/**
+ * Counting the number of times map,sets,array literals are created.
+ * <p>
+ * Originally intended to prove one could cache results of constant map 
literals when used to create
+ * libraries (a map of lamba).
+ * However, those literals must be newly instantiated since their result may 
be modified; there is
+ * thus no point in trying to cache them. (Think harder and nod, dont try 
again.)
+ * These pointless tests as a reminder of 'why' those behave the way they do.
+ * </p>
+ */
+public class CollectionLiteralTest extends JexlTestCase {
+    public CollectionLiteralTest() {
+        super("CollectionLiteralTest");
+    }
+
+
+    public static class Arithmetic363 extends JexlArithmetic {
+        final AtomicInteger maps = new AtomicInteger(0);
+        final AtomicInteger sets = new AtomicInteger(0);
+        final AtomicInteger arrays = new AtomicInteger(0);
+
+        public Arithmetic363(boolean strict) {
+            super(strict);
+        }
+
+        @Override public MapBuilder mapBuilder(int size) {
+            return new CountingMapBuilder(maps, size);
+        }
+        @Override public SetBuilder setBuilder(int size) {
+            return new CountingSetBuilder(sets, size);
+        }
+        @Override public ArrayBuilder arrayBuilder(int size) {
+            return new CountingArrayBuilder(arrays, size);
+        }
+    }
+
+    static class CountingSetBuilder extends SetBuilder {
+        final AtomicInteger count;
+        public CountingSetBuilder(AtomicInteger ai, int size) {
+            super(size);
+            count = ai;
+        }
+        @Override public Set<?> create() {
+            Set<?> set = super.create();
+            count.incrementAndGet();
+            return set;
+        }
+    }
+
+    static class CountingMapBuilder extends MapBuilder {
+        final AtomicInteger count;
+        public CountingMapBuilder(AtomicInteger ai, int size) {
+            super(size);
+            count = ai;
+        }
+        @Override public Map<Object, Object> create() {
+            Map<Object, Object> map = super.create();
+            count.incrementAndGet();
+            return map;
+        }
+    }
+
+    static class CountingArrayBuilder extends ArrayBuilder {
+        final AtomicInteger count;
+
+        public CountingArrayBuilder(AtomicInteger ai, int size) {
+            super(size);
+            count = ai;
+        }
+
+        @Override public Object create(boolean extended) {
+            Object array = super.create(extended);
+            count.incrementAndGet();
+            return array;
+        }
+    }
+
+    @Test
+    public void testMapLBuilder() {
+        Arithmetic363 jc = new Arithmetic363(true);
+        final JexlEngine jexl = new 
JexlBuilder().cache(4).arithmetic(jc).create();
+        JexlScript script;
+        Object result;
+
+        script = jexl.createScript("{ 'x':(x)->{ 1 + x; }, 'y' : (y)->{ y - 1; 
} }");
+        Object previous = null;
+        for(int i = 0; i < 4; ++i) {
+            result = script.execute(null);
+            Assert.assertNotNull(result);
+            Assert.assertNotSame(previous, result);
+            previous = result;
+            Assert.assertEquals(1 + i, jc.maps.get());
+        }
+    }
+
+    @Test
+    public void testSetBuilder() {
+        Arithmetic363 jc = new Arithmetic363(true);
+        final JexlEngine jexl = new 
JexlBuilder().cache(4).arithmetic(jc).create();
+        JexlScript script;
+        Object result;
+
+        script = jexl.createScript("{ (x)->{ 1 + x; }, (y)->{ y - 1; } }");
+        Object previous = null;
+        for(int i = 0; i < 4; ++i) {
+            result = script.execute(null);
+            Assert.assertNotNull(result);
+            Assert.assertNotSame(previous, result);
+            previous = result;
+            Assert.assertEquals(1 + i, jc.sets.get());
+        }
+    }
+
+    @Test
+    public void testArrayBuilder() {
+        Arithmetic363 jc = new Arithmetic363(true);
+        final JexlEngine jexl = new 
JexlBuilder().cache(4).arithmetic(jc).create();
+        JexlScript script;
+        Object result;
+
+        script = jexl.createScript("[ (x)->{ 1 + x; }, (y)->{ y - 1; } ]");
+        Object previous = null;
+        for(int i = 0; i < 4; ++i) {
+            result = script.execute(null);
+            Assert.assertNotNull(result);
+            Assert.assertNotSame(previous, result);
+            previous = result;
+            Assert.assertEquals( 1 + i, jc.arrays.get());
+        }
+    }
+
+}

Reply via email to