This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch JEXL-392 in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
commit cc999410c0b2d62af5628c09eca9b243be2d953e Author: henrib <hen...@apache.org> AuthorDate: Fri Feb 3 15:07:13 2023 +0100 JEXL-392: initial drop; --- pom.xml | 41 +++++++- .../java/org/apache/commons/jexl3/JexlContext.java | 19 ++++ .../java/org/apache/commons/jexl3/JexlOptions.java | 4 +- .../org/apache/commons/jexl3/internal/Engine.java | 115 +++++++++++++++------ .../apache/commons/jexl3/parser/JexlParser.java | 4 + .../java/org/apache/commons/jexl3/PragmaTest.java | 64 ++++++++++++ 6 files changed, 214 insertions(+), 33 deletions(-) diff --git a/pom.xml b/pom.xml index b655a9ee..79f9d919 100644 --- a/pom.xml +++ b/pom.xml @@ -185,7 +185,46 @@ </execution> </executions> </plugin> - +<!-- + <plugin> + <artifactId>maven-antrun-plugin</artifactId> + <version>3.1.0</version> + <executions> + <execution> + <phase>generate-sources</phase> + <configuration> + <target> + <copy todir="/Users/henri.biestro/kShuttle/trunk/trunk/Jexl/src" overwrite='true' verbose='true'> + -overwrite='true' verbose='true'- + <fileset dir="src"> + <include name="**/*.java"/> + </fileset> + <regexpmapper from="(.*)/org/apache/commons/jexl3/(.*)\.java$$" to="\1/com/nellarmonia/jexl/\2.java"/> + <filterchain> + <replacestring from="org.apache.commons.jexl3" to="com.nellarmonia.jexl"/> + <replacestring from="org.apache.whatever" to="com.nellarmonia.whatever"/> + </filterchain> + </copy> + <copy todir="/Users/henri.biestro/kShuttle/trunk/trunk/Jexl/src" overwrite='true' verbose='true'> + -overwrite='true' verbose='true'- + <fileset dir="src"> + <include name="**/*.jjt"/> + </fileset> + <regexpmapper from="(.*)/org/apache/commons/jexl3/(.*)\.jjt$$" to="\1/com/nellarmonia/jexl/\2.jjt"/> + <filterchain> + <replacestring from="org.apache.commons.jexl3" to="com.nellarmonia.jexl"/> + <replacestring from="org.apache.whatever.logging" to="com.nellarmonia.whatever"/> + </filterchain> + </copy> + </target> + </configuration> + <goals> + <goal>run</goal> + </goals> + </execution> + </executions> + </plugin> +--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> diff --git a/src/main/java/org/apache/commons/jexl3/JexlContext.java b/src/main/java/org/apache/commons/jexl3/JexlContext.java index 4eb810cb..397d4e88 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlContext.java +++ b/src/main/java/org/apache/commons/jexl3/JexlContext.java @@ -158,6 +158,25 @@ public interface JexlContext { Object processAnnotation(String name, Object[] args, Callable<Object> statement) throws Exception; } + /** + * A marker interface of the JexlContext that processes module definitions. + * It is used by the interpreter during evaluation of the pragma module definitions. + * @since 3.3 + */ + interface ModuleProcessor { + /** + * Defines a module. + * The module name will be the namespace mapped to the object returned by the evaluation + * of its body. + * @param engine the engine evaluating this module pragma + * @param info the info at the pragma location + * @param name the module name + * @param body the module definition which can be its location or source + * @return the module object + */ + Object processModule(JexlEngine engine, JexlInfo info, String name, String body); + } + /** * A marker interface of the JexlContext that exposes runtime evaluation options. * @since 3.2 diff --git a/src/main/java/org/apache/commons/jexl3/JexlOptions.java b/src/main/java/org/apache/commons/jexl3/JexlOptions.java index c889e468..2f85b5c1 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlOptions.java +++ b/src/main/java/org/apache/commons/jexl3/JexlOptions.java @@ -411,7 +411,7 @@ public final class JexlOptions { * @param ns a namespaces map */ public void setNamespaces(final Map<String, Object> ns) { - this.namespaces = ns == null? Collections.emptyMap() : ns; + this.namespaces = ns == null || ns.isEmpty()? Collections.emptyMap() : ns; } /** @@ -427,7 +427,7 @@ public final class JexlOptions { * @param imports the imported packages */ public void setImports(final Collection<String> imports) { - this.imports = imports == null? Collections.emptySet() : imports; + this.imports = imports == null || imports.isEmpty()? Collections.emptySet() : imports; } /** 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 c27d88a6..bfd2a786 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Engine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Engine.java @@ -48,15 +48,17 @@ import org.apache.commons.logging.LogFactory; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import java.util.function.Predicate; import static org.apache.commons.jexl3.parser.JexlParser.PRAGMA_IMPORT; +import static org.apache.commons.jexl3.parser.JexlParser.PRAGMA_MODULE; import static org.apache.commons.jexl3.parser.JexlParser.PRAGMA_JEXLNS; import static org.apache.commons.jexl3.parser.JexlParser.PRAGMA_OPTIONS; @@ -422,7 +424,6 @@ public class Engine extends JexlEngine { ? (JexlContext.PragmaProcessor) context : null; Map<String, Object> ns = null; - Set<String> is = null; for (final Map.Entry<String, Object> pragma : pragmas.entrySet()) { final String key = pragma.getKey(); final Object value = pragma.getValue(); @@ -434,45 +435,99 @@ public class Engine extends JexlEngine { } } else if (PRAGMA_IMPORT.equals(key)) { // jexl.import, may use a set - final Set<?> values = value instanceof Set<?> - ? (Set<?>) value - : Collections.singleton(value); - for (final Object o : values) { + final Set<String> is = new LinkedHashSet<>(); + withValueSet(value, (o)->{ if (o instanceof String) { - if (is == null) { - is = new LinkedHashSet<>(); - } is.add(o.toString()); } + }); + if (!is.isEmpty()) { + opts.setImports(is); } } else if (key.startsWith(PRAGMA_JEXLNS)) { - if (value instanceof String) { - // jexl.namespace.*** - final String nsname = key.substring(PRAGMA_JEXLNS.length()); - if (!nsname.isEmpty()) { - if (ns == null) { - ns = new HashMap<>(functions); - } - final String nsclass = value.toString(); - final Class<?> clazz = uberspect.getClassByName(nsclass); - if (clazz == null) { - logger.warn(key + ": unable to find class " + nsclass); - } else { - ns.put(nsname, clazz); - } - } - } + if (ns == null) ns = new LinkedHashMap<>(functions); + processPragmaNamespace(ns, key, value); + } else if (key.startsWith(PRAGMA_MODULE)) { + if (ns == null) ns = new LinkedHashMap<>(functions); + processModulePragma(ns, key, value, script.jexlInfo(), context); } if (processor != null) { processor.processPragma(opts, key, value); } } - if (ns != null) { - opts.setNamespaces(ns); - } - if (is != null) { - opts.setImports(is); + opts.setNamespaces(ns); + } + } + + /** + * Utility to deal with single value or set of values. + * @param value the value or the set + * @param vfunc the consumer of values + */ + private void withValueSet(Object value, Consumer<Object> vfunc) { + final Set<?> values = value instanceof Set<?> + ? (Set<?>) value + : Collections.singleton(value); + for (final Object o : values) { + vfunc.accept(o); + } + } + + /** + * Processes jexl.namespace.ns pragma. + * @param ns the namespace map + * @param key the key + * @param value the value, ie the class + */ + private void processPragmaNamespace(Map<String, Object> ns, String key, Object value) { + if (value instanceof String) { + // jexl.namespace.*** + final String nsname = key.substring(PRAGMA_JEXLNS.length()); + if (!nsname.isEmpty()) { + final String nsclass = value.toString(); + final Class<?> clazz = uberspect.getClassByName(nsclass); + if (clazz == null) { + logger.warn(key + ": unable to find class " + nsclass); + } else { + ns.put(nsname, clazz); + } } + } else { + logger.warn(key + ": ambiguous declaration " + value); + } + } + + /** + * Processes jexl.module.ns pragma. + * <p>If the value is empty, the namespace will be cleared which may be useful to debug and force unload + * the object bound to the namespace.</p> + * @param ns the namespace map + * @param key the key the namespace + * @param value the value, ie the expression to evaluate and its result bound to the namespace + * @param info the expression info + * @param context the value-as-expression evaluation context + */ + private void processModulePragma(Map<String, Object> ns, String key, Object value, JexlInfo info, JexlContext context) { + // jexl.module.*** + final String module = key.substring(PRAGMA_MODULE.length()); + if (module.isEmpty()) { + logger.warn(module + ": invalid module declaration"); + } else { + withValueSet(value, (o)->{ + if (!(o instanceof CharSequence)) { + logger.warn(module + ": unable to define module from " + value); + } else { + final String moduleSrc = o.toString(); + final Object functor = context instanceof JexlContext.ModuleProcessor + ? ((JexlContext.ModuleProcessor) context).processModule(this, info, module, moduleSrc) + : createExpression(info, moduleSrc).evaluate(context); + if (functor != null) { + ns.put(module, functor); + } else { + ns.remove(module); + } + } + }); } } diff --git a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java index 00b470a4..e771da58 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java +++ b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java @@ -505,6 +505,10 @@ public abstract class JexlParser extends StringParser { * The prefix of a namespace pragma. */ public static final String PRAGMA_JEXLNS = "jexl.namespace."; + /** + * The prefix of a module pragma. + */ + public static final String PRAGMA_MODULE = "jexl.module."; /** * The import pragma. */ diff --git a/src/test/java/org/apache/commons/jexl3/PragmaTest.java b/src/test/java/org/apache/commons/jexl3/PragmaTest.java index 602465af..40ba6ea8 100644 --- a/src/test/java/org/apache/commons/jexl3/PragmaTest.java +++ b/src/test/java/org/apache/commons/jexl3/PragmaTest.java @@ -22,6 +22,9 @@ import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Assert; import org.junit.Test; @@ -115,6 +118,67 @@ public class PragmaTest extends JexlTestCase { } } + public static class ModuleContext extends MapContext implements JexlContext.ModuleProcessor { + private final ConcurrentMap<String, Object> modules; + private final Map<String, JexlScript> sources; + private final AtomicInteger count = new AtomicInteger(0); + + ModuleContext(Map<String, JexlScript> sources , ConcurrentMap<String, Object> modules) { + this.sources = sources; + this.modules = modules; + } + + public Object script(String name) { + return sources.get(name); + } + + public int getCountCompute() { + return count.get(); + } + @Override + public Object processModule(JexlEngine engine, JexlInfo info, String name, final String body) { + if (body.isEmpty()) { + modules.remove(name); + return null; + } + return modules.computeIfAbsent(name, (n) -> { + Object module = engine.createExpression(info, body).evaluate(this); + if (module instanceof JexlScript) { + module = ((JexlScript) module).execute(this); + } + count.incrementAndGet(); + return module; + }); + } + } + + @Test public void testPragmaModule() { + Map<String, JexlScript> msrcs = new TreeMap<>(); + msrcs.put("module0", JEXL.createScript("function f42(x) { 42 + x; } function f43(x) { 43 + x; }; { 'f42' : f42, 'f43' : f43 }")); + ConcurrentMap<String, Object> modules = new ConcurrentHashMap<>(); + ModuleContext ctxt = new ModuleContext(msrcs, modules); + JexlScript script ; + Object result ; + script = JEXL.createScript("#pragma jexl.module.m0 \"script('module0')\"\n m0:f42(10);"); + result = script.execute(ctxt); + Assert.assertEquals(52, result); + Assert.assertEquals(1, ctxt.getCountCompute()); + result = script.execute(ctxt); + Assert.assertEquals(52, result); + Assert.assertEquals(1, ctxt.getCountCompute()); + script = JEXL.createScript("#pragma jexl.module.m0 \"script('module0')\"\n m0:f43(10);"); + result = script.execute(ctxt); + Assert.assertEquals(53, result); + Assert.assertEquals(1, ctxt.getCountCompute()); + try { + script = JEXL.createScript("#pragma jexl.module.m0 ''\n#pragma jexl.module.m0 \"fubar('module0')\"\n m0:f43(10);"); + result = script.execute(ctxt); + Assert.fail("fubar sshoud fail"); + } catch(JexlException.Method xmethod) { + Assert.assertEquals("fubar", xmethod.getMethod()); + } + } + public static class StaticSleeper { // precludes instantiation