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

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

commit b9797f63c4e026288a15f62503f246a7fe7d7779
Author: Henrib <[email protected]>
AuthorDate: Sun Nov 9 19:30:15 2025 +0100

    JEXL-448: refined expression cache handling;
    - added test;
---
 .../org/apache/commons/jexl3/internal/Engine.java  |  73 +++++------
 .../apache/commons/jexl3/internal/MetaCache.java   | 104 ++++++++++++++++
 .../org/apache/commons/jexl3/internal/Scope.java   |  12 ++
 .../org/apache/commons/jexl3/internal/Source.java  |  34 +++++-
 .../commons/jexl3/internal/TemplateEngine.java     |  30 ++++-
 .../jexl3/parser/ASTIdentifierAccessJxlt.java      |   2 +-
 .../commons/jexl3/parser/ASTJxltLiteral.java       |   2 +-
 .../org/apache/commons/jexl3/ConcurrentCache.java  |   2 +-
 .../apache/commons/jexl3/internal/RangeTest.java   |  19 +--
 .../commons/jexl3/internal/SourceCacheTest.java    | 136 +++++++++++++++++++++
 .../org/apache/commons/jexl3/internal/Util.java    |   8 +-
 11 files changed, 347 insertions(+), 75 deletions(-)

diff --git a/src/main/java/org/apache/commons/jexl3/internal/Engine.java 
b/src/main/java/org/apache/commons/jexl3/internal/Engine.java
index a275e112..c494aba0 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Engine.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Engine.java
@@ -91,6 +91,7 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
         /** Non-instantiable. */
         private UberspectHolder() {}
     }
+
     /**
      * Utility class to collect variables.
      */
@@ -156,6 +157,7 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
             return root instanceof ASTIdentifier;
         }
     }
+
     /**
      * The features allowed for property set/get methods.
      */
@@ -167,6 +169,7 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
             .arrayReferenceExpr(false)
             .methodCall(false)
             .register(true);
+
     /**
      * Use {@link Engine#getUberspect(Log, JexlUberspect.ResolverStrategy, 
JexlPermissions)}.
      * @deprecated 3.3
@@ -204,6 +207,7 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
         }
         return new Uberspect(logger, strategy, permissions);
     }
+
     /**
      * Solves an optional option.
      * @param conf the option as configured, may be null
@@ -290,32 +294,23 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
      * The expression max length to hit the cache.
      */
     protected final int cacheThreshold;
-
     /**
      * The expression cache.
      */
-    protected final JexlCache<Source, ASTJexlScript> cache;
-
-    /**
-     * The default jxlt engine.
-     */
-    protected volatile TemplateEngine jxlt;
-
+    protected final JexlCache<Source, Object> cache;
     /**
      * Collect all or only dot references.
      */
     protected final int collectMode;
-
     /**
      * A cached version of the options.
      */
     protected final JexlOptions options;
-
     /**
-     * The cache factory method.
+     * The set of caches created by this engine.
+     * <p>Caches are soft-referenced by the engine so they can be cleaned on 
class loader change.</p>
      */
-    protected final IntFunction<JexlCache<?, ?>> cacheFactory;
-
+    protected final MetaCache metaCache;
     /**
      * Creates an engine with default arguments.
      */
@@ -372,8 +367,8 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
         this.charset = conf.charset();
         // caching:
         final IntFunction<JexlCache<?, ?>> factory = conf.cacheFactory();
-        this.cacheFactory = factory == null ? SoftCache::new : factory;
-        this.cache = (JexlCache<Source, ASTJexlScript>) (conf.cache() > 0 ? 
cacheFactory.apply(conf.cache()) : null);
+        this.metaCache = new MetaCache(factory == null ? SoftCache::new : 
factory);
+        this.cache = metaCache.createCache(conf.cache());
         this.cacheThreshold = conf.cacheThreshold();
         if (uberspect == null) {
             throw new IllegalArgumentException("uberspect cannot be null");
@@ -391,6 +386,10 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
         }
     }
 
+    JexlCache<Source, Object> getCache() {
+        return cache;
+    }
+
     @Override
     public Script createExpression(final JexlInfo info, final String 
expression) {
         return createScript(expressionFeatures, info, expression);
@@ -738,24 +737,6 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
         return this.strict;
     }
 
-    /**
-     * Gets and/or creates a default template engine.
-     * @return a template engine
-     */
-    protected TemplateEngine jxlt() {
-        TemplateEngine e = jxlt;
-        if (e == null) {
-            synchronized(this) {
-                e = jxlt;
-                if (e == null) {
-                    e = new TemplateEngine(this, true, 0, '$', '#');
-                    jxlt = e;
-                }
-            }
-        }
-        return e;
-    }
-
     @Override
     public <T> T newInstance(final Class<? extends T> clazz, final Object... 
args) {
         return clazz.cast(doCreateInstance(clazz, args));
@@ -798,16 +779,16 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
     protected ASTJexlScript parse(final JexlInfo info, final JexlFeatures 
parsingf, final String src, final Scope scope) {
         final boolean cached = src.length() < cacheThreshold && cache != null;
         final JexlFeatures features = parsingf != null ? parsingf : 
DEFAULT_FEATURES;
-        final Source source = cached? new Source(features, src) : null;
-        ASTJexlScript script;
+        final Source source = cached ? new Source(features, 
Scope.getSymbolsMap(scope), src) : null;
         if (source != null) {
-            script = cache.get(source);
-            if (script != null && (scope == null || 
scope.equals(script.getScope()))) {
-                return script;
+            final Object c = cache.get(source);
+            if (c instanceof ASTJexlScript) {
+                return (ASTJexlScript) c;
             }
         }
         final JexlInfo ninfo = info == null && debug ? createInfo() : info;
         final JexlEngine se = putThreadEngine(this);
+        ASTJexlScript script;
         try {
             // if parser not in use...
             if (parsing.compareAndSet(false, true)) {
@@ -1003,7 +984,6 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
 
     @Override
     public void setClassLoader(final ClassLoader loader) {
-        jxlt = null;
         uberspect.setClassLoader(loader);
         if (functions != null) {
             final Iterable<String> names = new ArrayList<>(functions.keySet());
@@ -1022,9 +1002,7 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
                 }
             }
         }
-        if (cache != null) {
-            cache.clear();
-        }
+        metaCache.clearCaches();
     }
 
     @Override
@@ -1101,4 +1079,15 @@ public class Engine extends JexlEngine implements 
JexlUberspect.ConstantResolver
             consumer.accept(o);
         }
     }
+
+    /**
+     * Creates a new cache instance.
+     * <p>This uses the metacache instance as factory.</p>
+     * @param capacity the cache capacity
+     * @return a cache instance, null if capacity == 0, the JEXL cache if 
capacity &lt; 0
+     */
+    protected JexlCache<Source, Object> createCache(final int capacity) {
+        return capacity < 0 ? cache : capacity > 0 ? 
metaCache.createCache(capacity) : null;
+    }
+
 }
diff --git a/src/main/java/org/apache/commons/jexl3/internal/MetaCache.java 
b/src/main/java/org/apache/commons/jexl3/internal/MetaCache.java
new file mode 100644
index 00000000..56941d60
--- /dev/null
+++ b/src/main/java/org/apache/commons/jexl3/internal/MetaCache.java
@@ -0,0 +1,104 @@
+/*
+ * 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
+ *
+ *      https://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.internal;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.IntFunction;
+
+import org.apache.commons.jexl3.JexlCache;
+
+/**
+ * A meta-cache that tracks multiple JexlCache instances via weak references.
+ * <p>
+ *  Each JexlCache created by this MetaCache is held via a WeakReference,
+ *  allowing it to be garbage collected as soon as no strong references exist.
+ * </p>
+ * <p>
+ * This allows for collective management of multiple caches, in particular 
clearing all caches at once.
+ * This operation is typically called when the uberspect class loader needs to 
change.
+ * </p>
+ */
+public class MetaCache {
+  // The factory to create new JexlCache instances
+  private final IntFunction<JexlCache<?, ?>> factory;
+  // The set of JexlCache references
+  private final Set<Reference<JexlCache<?,?>>> references;
+  // Queue to receive references whose referent has been garbage collected
+  private final ReferenceQueue<JexlCache<?,?>> queue;
+
+  public MetaCache(final IntFunction<JexlCache<?, ?>> factory) {
+    this.factory = factory;
+    this.references = new HashSet<>();
+    this.queue = new ReferenceQueue<>();
+  }
+
+  @SuppressWarnings("unchecked")
+  public <K,V> JexlCache<K,V> createCache(final int capacity) {
+    if (capacity > 0) {
+      JexlCache<K, V> cache = (JexlCache<K, V>) factory.apply(capacity);
+      if (cache != null) {
+        synchronized(references) {
+          // The reference is created with the queue for automatic cleanup
+          references.add(new WeakReference<>(cache, queue));
+          // Always clean up the queue after modification to keep the set tidy
+          cleanUp();
+        }
+      }
+      return cache;
+    }
+    return null;
+  }
+
+  public void clearCaches() {
+    synchronized (references) {
+      for (Reference<JexlCache<?,?>> ref : references) {
+        JexlCache<?,?> cache = ref.get();
+        if (cache != null) {
+          cache.clear();
+        }
+      }
+      cleanUp();
+    }
+  }
+
+  /**
+   * Cleans up all references whose referent (the cache) has been garbage 
collected.
+   * <p>This method must be invoked while holding the lock on {@code 
references}.</p>
+   *
+   * @return The remaining number of caches.
+   */
+  @SuppressWarnings("unchecked")
+  private int cleanUp() {
+      // The poll() method returns the next reference object in the queue, or 
null if none is available.
+      Reference<JexlCache<?,?>> reference;
+      while ((reference = (Reference<JexlCache<?, ?>>) queue.poll()) != null) {
+        // Remove the reference from the set
+        references.remove(reference);
+      }
+      return references.size();
+  }
+
+  public int size() {
+    synchronized (references) {
+      return cleanUp();
+    }
+  }
+}
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 b90d3840..13f7f563 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Scope.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Scope.java
@@ -18,6 +18,7 @@ package org.apache.commons.jexl3.internal;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -93,6 +94,17 @@ public final class Scope {
         vars = 0;
         parent = scope;
     }
+    /**
+     * Gets an unmodifiable view of a scope&quote;s symbols map.
+     * @param scope the scope
+     * @return the symbols map
+     */
+    public static Map<String, Integer> getSymbolsMap(final Scope scope) {
+        if (scope != null && scope.namedVariables != null) {
+            return Collections.unmodifiableMap(scope.namedVariables);
+        }
+        return Collections.emptyMap();
+    }
 
     /**
      * Marks a symbol as lexical, declared through let or const.
diff --git a/src/main/java/org/apache/commons/jexl3/internal/Source.java 
b/src/main/java/org/apache/commons/jexl3/internal/Source.java
index bbacb5b7..fb1d64f8 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Source.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Source.java
@@ -16,6 +16,8 @@
  */
 package org.apache.commons.jexl3.internal;
 
+import java.util.Collections;
+import java.util.Map;
 import java.util.Objects;
 
 import org.apache.commons.jexl3.JexlFeatures;
@@ -30,26 +32,43 @@ public final class Source implements Comparable<Source> {
     private final int hashCode;
     /** The set of features. */
     private final JexlFeatures features;
+    /** The local symbols, if any. */
+    private final Map<String, Integer> symbols;
     /** The actual source script/expression. */
     private final String str;
 
     /**
      * Default constructor.
      * @param theFeatures the features
+     * @param theSymbols the map of variable name to symbol offset in 
evaluation frame
      * @param theStr the script source
      */
-    Source(final JexlFeatures theFeatures, final String theStr) { // CSOFF: 
MagicNumber
+    Source(final JexlFeatures theFeatures, final Map<String, Integer> 
theSymbols,  final String theStr) {
         this.features = theFeatures;
+        this.symbols = theSymbols == null ? Collections.emptyMap() : 
theSymbols;
         this.str = theStr;
-        int hash = 3;
-        hash = 37 * hash + features.hashCode();
-        hash = 37 * hash + str.hashCode() ;
-        this.hashCode = hash;
+        this.hashCode = Objects.hash(features, symbols, str);
     }
 
     @Override
     public int compareTo(final Source s) {
-        return str.compareTo(s.str);
+        int cmp = str.compareTo(s.str);
+        if (cmp == 0) {
+            cmp = Integer.compare(features.hashCode(), s.features.hashCode());
+            if (cmp == 0) {
+                cmp = Integer.compare(symbols.hashCode(), 
s.symbols.hashCode());
+                if (cmp == 0) {
+                    if (Objects.equals(features, s.features)) {
+                        if (Objects.equals(symbols, s.symbols)) {
+                            return 0;
+                        }
+                       return -1; // Same features, different symbols
+                    }
+                    return +1; // Different features
+                }
+            }
+        }
+        return cmp;
     }
 
     @Override
@@ -67,6 +86,9 @@ public final class Source implements Comparable<Source> {
         if (!Objects.equals(this.features, other.features)) {
             return false;
         }
+        if (!Objects.equals(this.symbols, other.symbols)) {
+            return false;
+        }
         if (!Objects.equals(this.str, other.str)) {
             return false;
         }
diff --git 
a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java 
b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java
index b3995f61..235a3a4e 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java
@@ -30,6 +30,7 @@ import java.util.Set;
 import org.apache.commons.jexl3.JexlCache;
 import org.apache.commons.jexl3.JexlContext;
 import org.apache.commons.jexl3.JexlException;
+import org.apache.commons.jexl3.JexlFeatures;
 import org.apache.commons.jexl3.JexlInfo;
 import org.apache.commons.jexl3.JexlOptions;
 import org.apache.commons.jexl3.JxltEngine;
@@ -461,6 +462,10 @@ public final class TemplateEngine extends JxltEngine {
             return collector.collected();
         }
 
+        public Scope getScope() {
+           return node instanceof ASTJexlScript? ((ASTJexlScript) 
node).getScope() : null;
+        }
+
         @Override
         protected void getVariables(final Engine.VarCollector collector) {
             jexl.getVariables(node instanceof ASTJexlScript? (ASTJexlScript) 
node : null, node, collector);
@@ -558,6 +563,10 @@ public final class TemplateEngine extends JxltEngine {
             return strb.toString();
         }
 
+        public Scope getScope() {
+            return null;
+        }
+
         /**
          * Interprets a sub-expression.
          * @param interpreter a JEXL interpreter
@@ -698,7 +707,6 @@ public final class TemplateEngine extends JxltEngine {
             }
             return strb.toString();
         }
-
     }
 
     /**
@@ -813,7 +821,7 @@ public final class TemplateEngine extends JxltEngine {
     }
 
     /** The TemplateExpression cache. */
-    final JexlCache<String, TemplateExpression> cache;
+    final JexlCache<Source, Object> cache;
 
     /** The JEXL engine instance. */
     final Engine jexl;
@@ -845,7 +853,7 @@ public final class TemplateEngine extends JxltEngine {
                           final char deferred) {
         this.jexl = jexl;
         this.logger = jexl.logger;
-        this.cache = (JexlCache<String, TemplateExpression>) 
jexl.cacheFactory.apply(cacheSize);
+        this.cache = jexl.createCache(cacheSize);
         immediateChar = immediate;
         deferredChar = deferred;
         noscript = noScript;
@@ -870,11 +878,21 @@ public final class TemplateEngine extends JxltEngine {
         final JexlInfo info = jexlInfo == null ?  jexl.createInfo() : jexlInfo;
         Exception xuel = null;
         TemplateExpression stmt = null;
+        final JexlFeatures features = noscript ? jexl.expressionFeatures : 
jexl.scriptFeatures;
+        // do not cache interpolation expression, they are stored in AST node
+        final boolean cached = cache != null && expression.length() < 
jexl.cacheThreshold;
         try {
-            stmt = cache.get(expression);
-            if (stmt == null) {
+            if (!cached) {
+                stmt = parseExpression(info, expression, scope);
+            } else {
+                final Source source = new Source(features, 
Scope.getSymbolsMap(scope), expression);
+                Object c = cache.get(source);
+                stmt = c instanceof TemplateExpression ? (TemplateExpression) 
c : null;
+                if (stmt != null) {
+                    return stmt;
+                }
                 stmt = parseExpression(info, expression, scope);
-                cache.put(expression, stmt);
+                cache.put(source, stmt);
             }
         } catch (final JexlException xjexl) {
             xuel = new Exception(xjexl.getInfo(), "failed to parse '" + 
expression + "'", xjexl);
diff --git 
a/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java 
b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java
index 2fdc54cc..43c77d31 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/ASTIdentifierAccessJxlt.java
@@ -57,7 +57,7 @@ public class ASTIdentifierAccessJxlt extends 
ASTIdentifierAccess implements Jexl
         if (src != null && !src.isEmpty()) {
             final JexlEngine jexl = JexlEngine.getThreadEngine();
             if (jexl != null) {
-                final JxltEngine jxlt = jexl.createJxltEngine();
+                final JxltEngine jxlt = jexl.createJxltEngine(true, -1, 
'$','#');
                 if (jxlt instanceof TemplateEngine) {
                   this.jxltExpression = ((TemplateEngine) 
jxlt).createExpression(jexlInfo(), src, scope);
                 }
diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java 
b/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java
index e39596c5..f0800e59 100644
--- a/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java
+++ b/src/main/java/org/apache/commons/jexl3/parser/ASTJxltLiteral.java
@@ -70,7 +70,7 @@ public final class ASTJxltLiteral extends JexlNode implements 
JexlNode.JxltHandl
         if (src != null && !src.isEmpty()) {
             final JexlEngine jexl = JexlEngine.getThreadEngine();
             if (jexl != null) {
-                final JxltEngine jxlt = jexl.createJxltEngine();
+                final JxltEngine jxlt = jexl.createJxltEngine(true, -1, 
'$','#');
                 if (jxlt instanceof TemplateEngine) {
                   this.jxltExpression = ((TemplateEngine) 
jxlt).createExpression(jexlInfo(), src, scope);
                 }
diff --git a/src/test/java/org/apache/commons/jexl3/ConcurrentCache.java 
b/src/test/java/org/apache/commons/jexl3/ConcurrentCache.java
index 3cdb0395..c6245cdb 100644
--- a/src/test/java/org/apache/commons/jexl3/ConcurrentCache.java
+++ b/src/test/java/org/apache/commons/jexl3/ConcurrentCache.java
@@ -28,7 +28,7 @@ import 
com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
  * @param <K> the cache key entry type
  * @param <V> the cache key value type
  */
-public class ConcurrentCache<K, V>  extends SoftCache<K, V> {
+public class ConcurrentCache<K, V> extends SoftCache<K, V> {
   /**
    * Creates a new instance of a concurrent cache.
    *
diff --git a/src/test/java/org/apache/commons/jexl3/internal/RangeTest.java 
b/src/test/java/org/apache/commons/jexl3/internal/RangeTest.java
index f328eb11..71e1f31e 100644
--- a/src/test/java/org/apache/commons/jexl3/internal/RangeTest.java
+++ b/src/test/java/org/apache/commons/jexl3/internal/RangeTest.java
@@ -19,6 +19,7 @@ package org.apache.commons.jexl3.internal;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
@@ -27,6 +28,8 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 
+import org.apache.commons.jexl3.ConcurrentCache;
+import org.apache.commons.jexl3.JexlCache;
 import org.apache.commons.jexl3.JexlFeatures;
 import org.apache.commons.jexl3.JexlTestCase;
 import org.junit.jupiter.api.AfterEach;
@@ -197,21 +200,5 @@ class RangeTest extends JexlTestCase {
         assertThrows(NoSuchElementException.class, ii0::next);
     }
 
-    @Test
-    void testSource() {
-        final JexlFeatures features = JexlFeatures.createDefault();
-        final Source src0 = new Source(features, "x -> -x");
-        final Source src0b = new Source(features, "x -> -x");
-        final Source src1 = new Source(features, "x -> +x");
-        assertEquals(7, src0.length());
-        assertEquals(src0, src0);
-        assertEquals(src0, src0b);
-        assertNotEquals(src0, src1);
-        assertEquals(src0.hashCode(), src0b.hashCode());
-        assertNotEquals(src0.hashCode(), src1.hashCode());
-        assertTrue(src0.compareTo(src0b) == 0);
-        assertTrue(src0.compareTo(src1) > 0);
-        assertTrue(src1.compareTo(src0) < 0);
-    }
 }
 
diff --git 
a/src/test/java/org/apache/commons/jexl3/internal/SourceCacheTest.java 
b/src/test/java/org/apache/commons/jexl3/internal/SourceCacheTest.java
new file mode 100644
index 00000000..b38b8acb
--- /dev/null
+++ b/src/test/java/org/apache/commons/jexl3/internal/SourceCacheTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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
+ *
+ *      https://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.internal;
+
+import org.apache.commons.jexl3.ConcurrentCache;
+import org.apache.commons.jexl3.JexlBuilder;
+import org.apache.commons.jexl3.JexlCache;
+import org.apache.commons.jexl3.JexlEngine;
+import org.apache.commons.jexl3.JexlFeatures;
+import org.apache.commons.jexl3.JexlScript;
+import org.junit.jupiter.api.Test;
+
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SourceCacheTest {
+
+  @Test
+  void testSource() {
+    final JexlFeatures features = JexlFeatures.createDefault();
+    final Source src0 = new Source(features, null,"x -> -x");
+    final Source src0b = new Source(features, null,"x -> -x");
+    final Source src1 = new Source(features, null,"x -> +x");
+    assertEquals(7, src0.length());
+    assertEquals(src0, src0b);
+    assertNotEquals(src0, src1);
+    assertEquals(src0.hashCode(), src0b.hashCode());
+    assertNotEquals(src0.hashCode(), src1.hashCode());
+    assertEquals(0, src0.compareTo(src0b));
+    assertTrue(src0.compareTo(src1) > 0);
+    assertTrue(src1.compareTo(src0) < 0);
+  }
+
+  @Test
+  public void testSourceCache() {
+    final JexlFeatures features = JexlFeatures.createDefault();
+    // source objects differ when symbol maps differ
+    Map<String, Integer> symbols0 = new HashMap<>();
+    symbols0.put("x", 0);
+    symbols0.put("y", 1);
+    Source src0 = new Source(features, symbols0,"x + y");
+    assertFalse(src0.equals("x + y"));
+    Map<String, Integer> symbols1 = new HashMap<>();
+    symbols0.put("x", 0);
+    symbols0.put("y", 2);
+    Source src1 = new Source(features, symbols1,"x + y");
+    assertNotEquals(src0, src1);
+    assertNotEquals(0, src0.compareTo(src1));
+    Source src2 = new Source(features, null,"x + y");
+    assertNotEquals(src0, src2);
+    assertNotEquals(0, src0.compareTo(src2));
+    Source src3 = new Source(JexlFeatures.createNone(), symbols1,"x + y");
+    assertNotEquals(src0, src3);
+    assertNotEquals(0, src0.compareTo(src3));
+
+    JexlEngine jexl = new JexlBuilder().cache(4).create();
+    JexlCache<Source, Object> cache = ((Engine) jexl).getCache();
+    // order of declaration of variables matters
+    JexlScript script0 = jexl.createScript("x + y", "x", "y");
+    JexlScript script1 = jexl.createScript("x + y", "y", "x");
+    assertEquals(2, cache.size());
+  }
+
+  @Test
+  void testMetaCache() {
+    final MetaCache mc = new MetaCache(ConcurrentCache::new);
+    JexlCache<Integer, String> cache1 = mc.createCache(3);
+    cache1.put(1, "one");
+    cache1.put(2, "two");
+    cache1.put(3, "three");
+    assertEquals(3, cache1.size());
+    assertEquals("one", cache1.get(1));
+    assertEquals("two", cache1.get(2));
+    assertEquals("three", cache1.get(3));
+    cache1.put(4, "four");
+    assertEquals(3, cache1.size());
+    assertNull(cache1.get(1)); // evicted
+    assertEquals("two", cache1.get(2));
+    assertEquals("three", cache1.get(3));
+    assertEquals("four", cache1.get(4));
+
+    JexlCache<String, String> cache2 = mc.createCache(2);
+    cache2.put("a", "A");
+    cache2.put("b", "B");
+    assertEquals(2, cache2.size());
+    assertEquals("A", cache2.get("a"));
+    assertEquals("B", cache2.get("b"));
+    cache2.put("c", "C");
+    assertEquals(2, cache2.size());
+    assertNull(cache2.get("a")); // evicted
+    assertEquals("B", cache2.get("b"));
+    assertEquals("C", cache2.get("c"));
+
+    // metacache weak references test
+    assertEquals(2, mc.size());
+    // drop the strong references to the caches
+    cache1 = null;
+    assertNull(cache1);
+    cache2 = null;
+    assertNull(cache2);
+    // trigger garbage collection
+    System.gc();
+    // wait for the garbage collector to do its work
+    for(int i = 0; i < 5 && mc.size() != 0; ++i) {
+      try {
+        Thread.sleep(100);
+      } catch(final InterruptedException xint) {
+        // ignore
+      }
+    }
+    // the caches should have been removed from the metacache
+    assertEquals(0, mc.size(), "metacache should have no more cache 
references");
+  }
+}
diff --git a/src/test/java/org/apache/commons/jexl3/internal/Util.java 
b/src/test/java/org/apache/commons/jexl3/internal/Util.java
index c8e59b64..26b8601a 100644
--- a/src/test/java/org/apache/commons/jexl3/internal/Util.java
+++ b/src/test/java/org/apache/commons/jexl3/internal/Util.java
@@ -88,8 +88,12 @@ public class Util {
         parser.allowRegisters(true);
         final Debugger dbg = new Debugger();
         // iterate over all expression in
-        for (final Map.Entry<Source, ASTJexlScript> entry : 
jexl.cache.entries()) {
-            final JexlNode node = entry.getValue();
+        for (final Map.Entry<Source, Object> entry : jexl.cache.entries()) {
+            Object c = entry.getValue();
+            if (!(c instanceof JexlNode)) {
+                continue;
+            }
+            final JexlNode node = (ASTJexlScript) c;
             // recreate expr string from AST
             dbg.debug(node);
             final String expressiondbg = dbg.toString();

Reply via email to