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 < 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"e;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();
