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 2d2300e0 JEXL-428: operators improvements; - use compare method as base for operators when possible; - cache resolution using method key; JEXL-429: namespace syntax disambiguation; - added feature flag (namespaceIdentifier); - variable used as function name added hint of non-ns call; 2d2300e0 is described below commit 2d2300e0fea55e1da432d3ef1b04ba49afc5b025 Author: Henrib <hbies...@gmail.com> AuthorDate: Wed Sep 25 07:44:56 2024 +0200 JEXL-428: operators improvements; - use compare method as base for operators when possible; - cache resolution using method key; JEXL-429: namespace syntax disambiguation; - added feature flag (namespaceIdentifier); - variable used as function name added hint of non-ns call; --- .../org/apache/commons/jexl3/JexlArithmetic.java | 37 ++- .../java/org/apache/commons/jexl3/JexlCache.java | 17 ++ .../org/apache/commons/jexl3/JexlFeatures.java | 37 ++- .../commons/jexl3/internal/InterpreterBase.java | 8 +- .../apache/commons/jexl3/internal/Operators.java | 299 +++++++++++++-------- .../jexl3/internal/introspection/MethodKey.java | 2 +- .../jexl3/internal/introspection/Uberspect.java | 58 +--- .../org/apache/commons/jexl3/parser/JexlNode.java | 13 +- .../apache/commons/jexl3/parser/JexlParser.java | 15 +- .../commons/jexl3/ArithmeticOperatorTest.java | 222 +++++++++++++++ .../org/apache/commons/jexl3/Issues400Test.java | 130 +++------ 11 files changed, 558 insertions(+), 280 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java index a9c3f137..8c8e894c 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java +++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java @@ -157,10 +157,22 @@ public class JexlArithmetic { * Gets the most specific method for an operator. * * @param operator the operator - * @param arg the arguments + * @param args the arguments * @return the most specific method or null if no specific override could be found */ - JexlMethod getOperator(JexlOperator operator, Object... arg); + JexlMethod getOperator(JexlOperator operator, Object... args); + + /** + * Try to find the most specific method and evaluate an operator. + * <p>This method does not call {@link #overloads(JexlOperator)} and shall not be called with an + * assignment operator.</p> + * + * @param reference an optional cache reference storing actual method or failing signature + * @param operator the operator + * @param args the arguments + * @return TRY_FAILED if no specific method could be found, the evaluation result otherwise + */ + Object tryEval(JexlCache.Reference reference, JexlOperator operator, Object...args); /** * Checks whether this uberspect has overloads for a given operator. @@ -476,9 +488,9 @@ public class JexlArithmetic { protected Boolean collectionContains(final Object collection, final Object value) { // convert arrays if needed final Object left = arrayWrap(collection); - if (left instanceof Collection<?>) { + if (left instanceof Collection) { final Object right = arrayWrap(value); - if (right instanceof Collection<?>) { + if (right instanceof Collection) { return ((Collection<?>) left).containsAll((Collection<?>) right); } return ((Collection<?>) left).contains(value); @@ -798,12 +810,23 @@ public class JexlArithmetic { final Comparable<Object> comparable = (Comparable<Object>) left; try { return comparable.compareTo(right); - } catch(final ClassCastException castException) { + } catch (final ClassCastException castException) { + // ignore it, continue in sequence + } + } + if (right instanceof Comparable<?>) { + @SuppressWarnings("unchecked") // OK because of instanceof check above + final Comparable<Object> comparable = (Comparable<Object>) right; + try { + return -comparable.compareTo(left); + } catch (final ClassCastException castException) { // ignore it, continue in sequence } } } - throw new ArithmeticException("Object comparison:(" + left + " " + operator + " " + right + ")"); + throw new ArithmeticException("Object comparison:(" + left + + " " + operator.getOperatorSymbol() + + " " + right + ")"); } /** @@ -971,7 +994,7 @@ public class JexlArithmetic { * Check for emptiness of various types: Number, Collection, Array, Map, String. * * @param object the object to check the emptiness of - * @param def the default value if object emptyness can not be determined + * @param def the default value if object emptiness can not be determined * @return the boolean or null if there is no arithmetic solution */ public Boolean isEmpty(final Object object, final Boolean def) { diff --git a/src/main/java/org/apache/commons/jexl3/JexlCache.java b/src/main/java/org/apache/commons/jexl3/JexlCache.java index 6e783480..f5475d9a 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlCache.java +++ b/src/main/java/org/apache/commons/jexl3/JexlCache.java @@ -76,4 +76,21 @@ public interface JexlCache<K, V> { * @return the cache size */ int size(); + + /** + * A cached reference. + */ + interface Reference { + /** + * Gets the referenced object. + * @return the referenced object + */ + Object getCache(); + + /** + * Sets the referenced object. + * @param cache the referenced object + */ + void setCache(Object cache); + } } diff --git a/src/main/java/org/apache/commons/jexl3/JexlFeatures.java b/src/main/java/org/apache/commons/jexl3/JexlFeatures.java index 7ec7f1a2..c05b776c 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlFeatures.java +++ b/src/main/java/org/apache/commons/jexl3/JexlFeatures.java @@ -53,6 +53,7 @@ import java.util.function.Predicate; * <li>Thin-arrow: use the thin-arrow, ie {@code ->} for lambdas as in {@code x -> x + x} * <li>Fat-arrow: use the fat-arrow, ie {@code =>} for lambdas as in {@code x => x + x} * <li>Namespace pragma: whether the {@code #pragma jexl.namespace.ns namespace} syntax is allowed</li> + * <li>Namespace identifier: whether the {@code ns:fun(...)} parser treats the ns:fun as one identifier, no spaces allowed</li> * <li>Import pragma: whether the {@code #pragma jexl.import fully.qualified.class.name} syntax is allowed</li> * <li>Comparator names: whether the comparator operator names can be used (as in {@code gt} for >, * {@code lt} for <, ...)</li> @@ -71,7 +72,7 @@ public final class JexlFeatures { "register", "reserved variable", "local variable", "assign/modify", "global assign/modify", "array reference", "create instance", "loop", "function", "method call", "set/map/array literal", "pragma", "annotation", "script", "lexical", "lexicalShade", - "thin-arrow", "fat-arrow", "namespace pragma", "import pragma", "comparator names", "pragma anywhere", + "thin-arrow", "fat-arrow", "namespace pragma", "namespace identifier", "import pragma", "comparator names", "pragma anywhere", "const capture", "ref capture" }; /** Registers feature ordinal. */ @@ -112,16 +113,18 @@ public final class JexlFeatures { public static final int FAT_ARROW = 17; /** Namespace pragma feature ordinal. */ public static final int NS_PRAGMA = 18; + /** Namespace syntax as an identifier (no space). */ + public static final int NS_IDENTIFIER = 19; /** Import pragma feature ordinal. */ - public static final int IMPORT_PRAGMA = 19; + public static final int IMPORT_PRAGMA = 20; /** Comparator names (legacy) syntax. */ - public static final int COMPARATOR_NAMES = 20; + public static final int COMPARATOR_NAMES = 21; /** The pragma anywhere feature ordinal. */ - public static final int PRAGMA_ANYWHERE = 21; + public static final int PRAGMA_ANYWHERE = 22; /** Captured variables are const. */ - public static final int CONST_CAPTURE = 22; + public static final int CONST_CAPTURE = 23; /** Captured variables are reference. */ - public static final int REF_CAPTURE = 23; + public static final int REF_CAPTURE = 24; /** * All features. * N.B. ensure this is updated if additional features are added. @@ -574,6 +577,21 @@ public final class JexlFeatures { return this; } + /** + * Sets whether namespace as identifier syntax is enabled. + * <p> + * When enabled, a namespace call must be of the form <code>ns:fun(...)</code> with no + * spaces between the namespace name and the function. + * </p> + * @param flag true to enable, false to disable + * @return this features instance + * @since 3.4.1 + */ + public JexlFeatures namespaceIdentifier(final boolean flag) { + setFeature(NS_IDENTIFIER, flag); + return this; + } + /** * @return the declared namespaces test. */ @@ -828,6 +846,13 @@ public final class JexlFeatures { public boolean supportsNamespacePragma() { return getFeature(NS_PRAGMA); } + /** + * @return true if namespace identifier syntax is enabled, false otherwise + * @since 3.4.1 + */ + public boolean supportsNamespaceIdentifier() { + return getFeature(NS_IDENTIFIER); + } /** * @return true if creating new instances is enabled, false otherwise 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 22e1e872..bd40e865 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java +++ b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java @@ -304,22 +304,16 @@ public abstract class InterpreterBase extends ParserVisitor { protected final JexlOptions options; /** Cache executors. */ protected final boolean cache; - /** Cancellation support. */ protected final AtomicBoolean cancelled; - /** The namespace resolver. */ protected final JexlContext.NamespaceResolver ns; - /** The class name resolver. */ protected final JexlContext.ClassNameResolver fqcnSolver; - /** The operators evaluation delegate. */ protected final Operators operators; - /** The map of 'prefix:function' to object resolving as namespaces. */ protected final Map<String, Object> functions; - /** The map of dynamically created namespaces, NamespaceFunctor or duck-types of those. */ protected Map<String, Object> functors; @@ -355,7 +349,7 @@ public abstract class InterpreterBase extends ParserVisitor { this.cancelled = acancel != null ? acancel : new AtomicBoolean(); this.functions = options.getNamespaces(); this.functors = null; - this.operators = new Operators(this); + this.operators = (Operators) uberspect.getArithmetic(arithmetic); // the import package facility final Collection<String> imports = options.getImports(); this.fqcnSolver = imports.isEmpty() diff --git a/src/main/java/org/apache/commons/jexl3/internal/Operators.java b/src/main/java/org/apache/commons/jexl3/internal/Operators.java index 93e391ff..6417d27a 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Operators.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Operators.java @@ -21,27 +21,27 @@ import java.util.Set; import java.util.function.Consumer; import org.apache.commons.jexl3.JexlArithmetic; +import org.apache.commons.jexl3.JexlCache; import org.apache.commons.jexl3.JexlEngine; import org.apache.commons.jexl3.JexlException; import org.apache.commons.jexl3.JexlOperator; +import org.apache.commons.jexl3.internal.introspection.MethodExecutor; import org.apache.commons.jexl3.internal.introspection.MethodKey; import org.apache.commons.jexl3.introspection.JexlMethod; -import org.apache.commons.jexl3.introspection.JexlUberspect; +import org.apache.commons.jexl3.internal.introspection.Uberspect; import org.apache.commons.jexl3.parser.JexlNode; /** * Helper class to deal with operator overloading and specifics. * @since 3.0 */ -public final class Operators { - /** - * Helper for postfix assignment operators. - * @param operator the operator - * @return true if operator is a postfix operator (x++, y--) - */ - private static boolean isPostfix(final JexlOperator operator) { - return operator == JexlOperator.GET_AND_INCREMENT || operator == JexlOperator.GET_AND_DECREMENT; - } +public final class Operators implements JexlArithmetic.Uberspect { + /** The uberspect. */ + protected final Uberspect uberspect; + /** The arithmetic instance being analyzed. */ + protected final JexlArithmetic arithmetic; + /** The set of overloaded operators. */ + protected final Set<JexlOperator> overloads; /** * The comparison operators. @@ -50,21 +50,35 @@ public final class Operators { private static final Set<JexlOperator> CMP_OPS = EnumSet.of(JexlOperator.GT, JexlOperator.LT, JexlOperator.EQ, JexlOperator.GTE, JexlOperator.LTE); - /** The owner. */ - private final InterpreterBase interpreter; - - /** The overloaded arithmetic operators. */ - private final JexlArithmetic.Uberspect operators; + /** + * The postfix operators. + * <p>Used to determine the returned value in assignment.</p> + */ + private static final Set<JexlOperator> POSTFIX_OPS = + EnumSet.of(JexlOperator.GET_AND_INCREMENT, JexlOperator.GET_AND_DECREMENT); /** - * Constructs a new instance. - * @param owner the owning interpreter + * Creates an instance. + * @param theUberspect the uberspect instance + * @param theArithmetic the arithmetic instance + * @param theOverloads the overloaded operators */ - Operators(final InterpreterBase owner) { - final JexlArithmetic arithmetic = owner.arithmetic; - final JexlUberspect uberspect = owner.uberspect; - this.interpreter = owner; - this.operators = uberspect.getArithmetic(arithmetic); + public Operators(final Uberspect theUberspect, final JexlArithmetic theArithmetic, final Set<JexlOperator> theOverloads) { + this.uberspect = theUberspect; + this.arithmetic = theArithmetic; + this.overloads = theOverloads; + } + + @Override + public JexlMethod getOperator(final JexlOperator operator, final Object... args) { + return overloads.contains(operator) && args != null + ? uberspectOperator(arithmetic, operator, args) + : null; + } + + @Override + public boolean overloads(final JexlOperator operator) { + return overloads.contains(operator); } /** @@ -78,6 +92,29 @@ public final class Operators { return operator.getArity() == 1 && args.length > 1 ? new Object[]{args[0]} : args; } + /** + * Attempts finding a method in left and eventually narrowing right. + * @param methodName the method name + * @param right the left argument in the operator + * @param left the right argument in the operator + * @return a boolean is call was possible, null otherwise + * @throws Exception if invocation fails + */ + private Boolean booleanDuckCall(final String methodName, final Object left, final Object right) throws Exception { + JexlMethod vm = uberspect.getMethod(left, methodName, right); + if (returnsBoolean(vm)) { + return (Boolean) vm.invoke(left, right); + } + final Object[] argv = { right }; + if (arithmetic.narrowArguments(argv)) { + vm = uberspect.getMethod(left, methodName, argv); + if (returnsBoolean(vm)) { + return (Boolean) vm.invoke(left, argv); + } + } + return null; + } + /** * Throw a NPE if operator is strict and one of the arguments is null. * @param arithmetic the JEXL arithmetic instance @@ -99,26 +136,39 @@ public final class Operators { } /** - * Attempts finding a method in left and eventually narrowing right. - * @param methodName the method name - * @param right the left argument in the operator - * @param left the right argument in the operator - * @return a boolean is call was possible, null otherwise - * @throws Exception if invocation fails + * Triggered when an operator fails. + * @param ref the node where the error originated from + * @param operator the operator symbol + * @param cause the cause of error (if any) + * @return throws JexlException if strict and not silent, null otherwise */ - private Boolean booleanDuckCall(final String methodName, final Object left, final Object right) throws Exception { - final JexlUberspect uberspect = interpreter.uberspect; - JexlMethod vm = uberspect.getMethod(left, methodName, right); - if (returnsBoolean(vm)) { - return (Boolean) vm.invoke(left, right); + private Object operatorError(final JexlCache.Reference ref, final JexlOperator operator, final Throwable cause) { + JexlNode node = (ref instanceof JexlNode) ? (JexlNode) ref : null; + Engine engine = (Engine) JexlEngine.getThreadEngine(); + if (engine == null || engine.isStrict()) { + throw new JexlException.Operator(node, operator.getOperatorSymbol(), cause); } - final JexlArithmetic arithmetic = interpreter.arithmetic; - final Object[] argv = { right }; - if (arithmetic.narrowArguments(argv)) { - vm = uberspect.getMethod(left, methodName, argv); - if (returnsBoolean(vm)) { - return (Boolean) vm.invoke(left, argv); - } + if (engine.logger.isDebugEnabled()) { + engine.logger.debug(JexlException.operatorError(node, operator.getOperatorSymbol()), cause); + } + return null; + } + + /** + * Seeks an implementation of an operator method in an arithmetic instance. + * <p>Method must <em><>not/em belong to JexlArithmetic</p> + * @param arithmetic the arithmetic instance + * @param operator the operator + * @param args the arguments + * @return a JexlMethod instance or null + */ + private JexlMethod uberspectOperator(final JexlArithmetic arithmetic, + final JexlOperator operator, + final Object... args) { + final JexlMethod me = uberspect.getMethod(arithmetic, operator.getMethodName(), args); + if (!(me instanceof MethodExecutor) || + !JexlArithmetic.class.equals(((MethodExecutor) me).getMethod().getDeclaringClass())) { + return me; } return null; } @@ -158,18 +208,16 @@ public final class Operators { * @param object the object to check the emptiness of * @return the evaluation result */ - Object empty(final JexlNode node, final Object object) { + Object empty(final JexlCache.Reference node, final Object object) { if (object == null) { return true; } - Object result = operators.overloads(JexlOperator.EMPTY) + Object result = overloads(JexlOperator.EMPTY) ? tryOverload(node, JexlOperator.EMPTY, object) : JexlEngine.TRY_FAILED; if (result == JexlEngine.TRY_FAILED) { - final JexlArithmetic arithmetic = interpreter.arithmetic; result = arithmetic.isEmpty(object, null); if (result == null) { - final JexlUberspect uberspect = interpreter.uberspect; result = false; // check if there is an isEmpty method on the object that returns a // boolean and if so, just use it @@ -178,7 +226,7 @@ public final class Operators { try { result = vm.invoke(object, InterpreterBase.EMPTY_PARAMS); } catch (final Exception xany) { - return interpreter.operatorError(node, JexlOperator.EMPTY, xany); + return operatorError(node, JexlOperator.EMPTY, xany); } } } @@ -195,18 +243,16 @@ public final class Operators { * @param object the object to get the size of * @return the evaluation result */ - Object size(final JexlNode node, final Object object) { + Object size(final JexlCache.Reference node, final Object object) { if (object == null) { return 0; } - Object result = operators.overloads(JexlOperator.SIZE) + Object result = overloads(JexlOperator.SIZE) ? tryOverload(node, JexlOperator.SIZE, object) : JexlEngine.TRY_FAILED; if (result == JexlEngine.TRY_FAILED) { - final JexlArithmetic arithmetic = interpreter.arithmetic; result = arithmetic.size(object, null); if (result == null) { - final JexlUberspect uberspect = interpreter.uberspect; // check if there is a size method on the object that returns an // integer and if so, just use it final JexlMethod vm = uberspect.getMethod(object, "size", InterpreterBase.EMPTY_PARAMS); @@ -214,7 +260,7 @@ public final class Operators { try { result = vm.invoke(object, InterpreterBase.EMPTY_PARAMS); } catch (final Exception xany) { - interpreter.operatorError(node, JexlOperator.SIZE, xany); + operatorError(node, JexlOperator.SIZE, xany); } } } @@ -234,12 +280,11 @@ public final class Operators { * @param left the right operand * @return true if left matches right, false otherwise */ - boolean contains(final JexlNode node, final JexlOperator operator, final Object left, final Object right) { - final JexlArithmetic arithmetic = interpreter.arithmetic; + boolean contains(final JexlCache.Reference node, final JexlOperator operator, final Object left, final Object right) { final boolean contained; try { // try operator overload - final Object result = operators.overloads(JexlOperator.CONTAINS) + final Object result = overloads(JexlOperator.CONTAINS) ? tryOverload(node, JexlOperator.CONTAINS, left, right) : null; if (result instanceof Boolean) { @@ -260,9 +305,10 @@ public final class Operators { } } } + // not-contains is !contains return (JexlOperator.CONTAINS == operator) == contained; } catch (final Exception xrt) { - interpreter.operatorError(node, operator, xrt); + operatorError(node, operator, xrt); return false; } } @@ -275,12 +321,11 @@ public final class Operators { * @param right the right operand * @return true if left starts with right, false otherwise */ - boolean startsWith(final JexlNode node, final JexlOperator operator, final Object left, final Object right) { - final JexlArithmetic arithmetic = interpreter.arithmetic; + boolean startsWith(final JexlCache.Reference node, final JexlOperator operator, final Object left, final Object right) { final boolean starts; try { // try operator overload - final Object result = operators.overloads(JexlOperator.STARTSWITH) + final Object result = overloads(JexlOperator.STARTSWITH) ? tryOverload(node, JexlOperator.STARTSWITH, left, right) : null; if (result instanceof Boolean) { @@ -301,9 +346,10 @@ public final class Operators { } } } + // not-startswith is !starts-with return (JexlOperator.STARTSWITH == operator) == starts; } catch (final Exception xrt) { - interpreter.operatorError(node, operator, xrt); + operatorError(node, operator, xrt); return false; } } @@ -316,12 +362,11 @@ public final class Operators { * @param right the right operand * @return true if left ends with right, false otherwise */ - boolean endsWith(final JexlNode node, final JexlOperator operator, final Object left, final Object right) { - final JexlArithmetic arithmetic = interpreter.arithmetic; + boolean endsWith(final JexlCache.Reference node, final JexlOperator operator, final Object left, final Object right) { try { final boolean ends; // try operator overload - final Object result = operators.overloads(JexlOperator.ENDSWITH) + final Object result = overloads(JexlOperator.ENDSWITH) ? tryOverload(node, JexlOperator.ENDSWITH, left, right) : null; if (result instanceof Boolean) { @@ -342,9 +387,10 @@ public final class Operators { } } } + // not-endswith is !ends-with return (JexlOperator.ENDSWITH == operator) == ends; } catch (final Exception xrt) { - interpreter.operatorError(node, operator, xrt); + operatorError(node, operator, xrt); return false; } } @@ -366,14 +412,13 @@ public final class Operators { final JexlOperator operator, final Consumer<Object> assignFun, final Object...args) { - final JexlArithmetic arithmetic = interpreter.arithmetic; if (args.length < operator.getArity()) { return JexlEngine.TRY_FAILED; } Object result; try { // try to call overload with side effect; the object is modified - if (operators.overloads(operator)) { + if (overloads(operator)) { result = tryOverload(node, operator, arguments(operator, args)); if (result != JexlEngine.TRY_FAILED) { return result; // 1 @@ -381,11 +426,11 @@ public final class Operators { } // try to call base overload (ie + for +=) final JexlOperator base = operator.getBaseOperator(); - if (base != null && operators.overloads(base)) { + if (base != null && overloads(base)) { result = tryOverload(node, base, arguments(base, args)); if (result != JexlEngine.TRY_FAILED) { assignFun.accept(result); - return isPostfix(operator) ? args[0] : result; // 2 + return POSTFIX_OPS.contains(operator) ? args[0] : result; // 2 } } // default implementation for self-* operators @@ -445,7 +490,7 @@ public final class Operators { assignFun.accept(result); return result; // 5 } catch (final Exception xany) { - interpreter.operatorError(node, operator, xany); + operatorError(node, operator, xany); } return JexlEngine.TRY_FAILED; } @@ -463,51 +508,56 @@ public final class Operators { * @param args the arguments * @return the result of the operator evaluation or TRY_FAILED */ - Object tryOverload(final JexlNode node, final JexlOperator operator, final Object... args) { - final JexlArithmetic arithmetic = interpreter.arithmetic; + Object tryOverload(final JexlCache.Reference node, final JexlOperator operator, final Object... args) { controlNullOperands(arithmetic, operator, args); + Engine engine = (Engine) JexlEngine.getThreadEngine(); try { - final boolean cache = interpreter.cache; - if (cache) { - final Object cached = node.jjtGetValue(); - if (cached instanceof JexlMethod) { - // we found a method on previous call; try and reuse it (*1) - final JexlMethod me = (JexlMethod) cached; - final Object eval = me.tryInvoke(operator.getMethodName(), arithmetic, args); - if (!me.tryFailed(eval)) { - return eval; - } - } else if (cached instanceof MethodKey) { - // check for a fail-fast, we tried to find an overload before but could not (*2) - final MethodKey cachedKey = (MethodKey) cached; - final MethodKey key = new MethodKey(operator.getMethodName(), args); - if (key.equals(cachedKey)) { - return JexlEngine.TRY_FAILED; - } + return tryEval(engine == null || engine.cache != null ? node : null, operator, args); + } catch (final Exception xany) { + // ignore return if lenient, will return try_failed + operatorError(node, operator, xany); + } + return JexlEngine.TRY_FAILED; + } + + @Override + public Object tryEval(JexlCache.Reference node, JexlOperator operator, Object...args) { + if (node != null) { + final Object cached = node.getCache(); + if (cached instanceof JexlMethod) { + // we found a method on previous call; try and reuse it (*1) + final JexlMethod me = (JexlMethod) cached; + final Object eval = me.tryInvoke(operator.getMethodName(), arithmetic, args); + if (!me.tryFailed(eval)) { + return eval; } - } - // trying to find an operator overload - JexlMethod vm = operators.overloads(operator) ? operators.getOperator(operator, args) : null; - // no direct overload, any special case ? - if (vm == null) { - vm = getAlternateOverload(operator, args); - } - // *1: found a method, try it and cache it if successful - if (vm != null) { - final Object result = vm.tryInvoke(operator.getMethodName(), arithmetic, args); - if (cache && !vm.tryFailed(result)) { - node.jjtSetValue(vm); + } else if (cached instanceof MethodKey) { + // check for a fail-fast, we tried to find an overload before but could not (*2) + final MethodKey cachedKey = (MethodKey) cached; + final MethodKey key = new MethodKey(operator.getMethodName(), args); + if (key.equals(cachedKey)) { + return JexlEngine.TRY_FAILED; } - return result; } - // *2: could not find an overload for this operator and arguments, keep track of the fail - if (cache) { - MethodKey key = new MethodKey(operator.getMethodName(), args); - node.jjtSetValue(key); + } + // trying to find an operator overload + JexlMethod vm = getOperator(operator, args); + // no direct overload, any special case ? + if (vm == null) { + vm = getAlternateOverload(operator, args); + } + // *1: found a method, try it and cache it if successful + if (vm != null) { + final Object result = vm.tryInvoke(operator.getMethodName(), arithmetic, args); + if (node != null && !vm.tryFailed(result)) { + node.setCache(vm); } - } catch (final Exception xany) { - // ignore return if lenient, will return try_failed - interpreter.operatorError(node, operator, xany); + return result; + } + if (node != null) { + // *2: could not find an overload for this operator and arguments, keep track of the fail + MethodKey key = new MethodKey(operator.getMethodName(), args); + node.setCache(key); } return JexlEngine.TRY_FAILED; } @@ -521,10 +571,14 @@ public final class Operators { */ private JexlMethod getAlternateOverload(final JexlOperator operator, final Object... args) { // comparison operators may use the compare overload in derived arithmetic - if (CMP_OPS.contains(operator)) { - JexlMethod cmp = operators.getOperator(JexlOperator.COMPARE, args); + if (CMP_OPS.contains(operator) && args.length == 2) { + JexlMethod cmp = getOperator(JexlOperator.COMPARE, args); + if (cmp != null) { + return new Operators.CompareMethod(operator, cmp); + } + cmp = getOperator(JexlOperator.COMPARE, args[1], args[0]); if (cmp != null) { - return new CompareMethod(operator, cmp); + return new Operators.AntiCompareMethod(operator, cmp); } } return null; @@ -573,7 +627,7 @@ public final class Operators { return JexlEngine.TRY_FAILED; } - private boolean operate(final int cmp) { + protected boolean operate(final int cmp) { switch(operator) { case EQ: return cmp == 0; case LT: return cmp < 0; @@ -584,4 +638,29 @@ public final class Operators { throw new ArithmeticException("unexpected operator " + operator); } } + + /** + * The reverse compare swaps left and right arguments and changes the sign of the + * comparison result. + */ + private class AntiCompareMethod extends CompareMethod { + + AntiCompareMethod(JexlOperator op, JexlMethod m) { + super(op, m); + } + + @Override + public Object tryInvoke(String name, Object arithmetic, Object... params) throws JexlException.TryFailed { + Object cmp = compare.tryInvoke(JexlOperator.COMPARE.getMethodName(), arithmetic, params[1], params[0]); + if (cmp instanceof Integer) { + return operate(-(int) cmp); + } + return JexlEngine.TRY_FAILED; + } + + @Override + public Object invoke(Object arithmetic, Object... params) throws Exception { + return operate(-(int) compare.invoke(arithmetic, params[1], params[0])); + } + } } diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/MethodKey.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/MethodKey.java index 360878f1..36bcca68 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/MethodKey.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/MethodKey.java @@ -589,7 +589,7 @@ public final class MethodKey { * @param aMethod the method to generate the key from * @param args the intended method arguments */ - public MethodKey(final String aMethod, final Object[] args) { + public MethodKey(final String aMethod, final Object... args) { // !! keep this in sync with the other ctor (hash code) !! this.method = aMethod; int hash = this.method.hashCode(); diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java index 3454d359..332800e6 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java @@ -32,6 +32,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.jexl3.JexlArithmetic; import org.apache.commons.jexl3.JexlEngine; import org.apache.commons.jexl3.JexlOperator; +import org.apache.commons.jexl3.internal.Operators; import org.apache.commons.jexl3.introspection.JexlMethod; import org.apache.commons.jexl3.introspection.JexlPermissions; import org.apache.commons.jexl3.introspection.JexlPropertyGet; @@ -49,38 +50,6 @@ import org.apache.commons.logging.LogFactory; * @since 1.0 */ public class Uberspect implements JexlUberspect { - /** - * The concrete Uberspect Arithmetic class. - */ - protected class ArithmeticUberspect implements JexlArithmetic.Uberspect { - /** The arithmetic instance being analyzed. */ - private final JexlArithmetic arithmetic; - /** The set of overloaded operators. */ - private final Set<JexlOperator> overloads; - - /** - * Creates an instance. - * @param theArithmetic the arithmetic instance - * @param theOverloads the overloaded operators - */ - ArithmeticUberspect(final JexlArithmetic theArithmetic, final Set<JexlOperator> theOverloads) { - this.arithmetic = theArithmetic; - this.overloads = theOverloads; - } - - @Override - public JexlMethod getOperator(final JexlOperator operator, final Object... args) { - return overloads.contains(operator) && args != null - ? uberspectOperator(arithmetic, operator, args) - : null; - } - - @Override - public boolean overloads(final JexlOperator operator) { - return overloads.contains(operator); - } - } - /** Publicly exposed special failure object returned by tryInvoke. */ public static final Object TRY_FAILED = JexlEngine.TRY_FAILED; /** The logger to use for all warnings and errors. */ @@ -154,8 +123,8 @@ public class Uberspect implements JexlUberspect { // CSON: DoubleCheckedLocking @Override - public JexlArithmetic.Uberspect getArithmetic(final JexlArithmetic arithmetic) { - JexlArithmetic.Uberspect jau = null; + public Operators getArithmetic(final JexlArithmetic arithmetic) { + Operators jau = null; if (arithmetic != null) { final Class<? extends JexlArithmetic> aclass = arithmetic.getClass(); final Set<JexlOperator> ops = operatorMap.computeIfAbsent(aclass, k -> { @@ -187,30 +156,11 @@ public class Uberspect implements JexlUberspect { } return newOps; }); - jau = new ArithmeticUberspect(arithmetic, ops); + jau = new Operators(this, arithmetic, ops); } return jau; } - /** - * Seeks an implementation of an operator method in an arithmetic instance. - * <p>Method must <em><>not/em belong to JexlArithmetic</p> - * @param arithmetic the arithmetic instance - * @param operator the operator - * @param args the arguments - * @return a JexlMethod instance or null - */ - final JexlMethod uberspectOperator(final JexlArithmetic arithmetic, - final JexlOperator operator, - final Object... args) { - final JexlMethod me = getMethod(arithmetic, operator.getMethodName(), args); - if (!(me instanceof MethodExecutor) || - !JexlArithmetic.class.equals(((MethodExecutor) me).getMethod().getDeclaringClass())) { - return me; - } - return null; - } - /** * Gets a class by name through this introspector class loader. * @param className the class name 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 2b505913..b44e70de 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java +++ b/src/main/java/org/apache/commons/jexl3/parser/JexlNode.java @@ -17,6 +17,7 @@ package org.apache.commons.jexl3.parser; import org.apache.commons.jexl3.JexlArithmetic; +import org.apache.commons.jexl3.JexlCache; import org.apache.commons.jexl3.JexlInfo; import org.apache.commons.jexl3.JxltEngine; import org.apache.commons.jexl3.introspection.JexlMethod; @@ -28,7 +29,7 @@ import org.apache.commons.jexl3.introspection.JexlPropertySet; * * @since 2.0 */ -public abstract class JexlNode extends SimpleNode { +public abstract class JexlNode extends SimpleNode implements JexlCache.Reference { /** * A marker interface for constants. * @param <T> the literal type @@ -58,6 +59,16 @@ public abstract class JexlNode extends SimpleNode { void setExpression(JxltEngine.Expression expr); } + @Override + public Object getCache() { + return jjtGetValue(); + } + + @Override + public void setCache(Object cache) { + jjtSetValue(cache); + } + /** * An info bound to its node. * <p>Used to parse expressions for templates. 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 11018604..d0e5df66 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java +++ b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java @@ -628,11 +628,18 @@ public abstract class JexlParser extends StringParser { if (!"(".equals(paren.image)) { return false; } - // if namespace name is shared with a variable name, use syntactic hint - final String name = ns.image; - if (isVariable(name)) { + // namespace as identifier means no spaces in between ns, colon and fun, no matter what + if (featureController.getFeatures().supportsNamespaceIdentifier()) { + return colon.beginColumn - 1 == ns.endColumn + && colon.endColumn == fun.beginColumn - 1; + } + // if namespace name is shared with a variable name + // or if fun is a variable name (likely a function call), + // use syntactic hint + if (isVariable(ns.image) || isVariable(fun.image)) { // the namespace sticks to the colon as in 'ns:fun()' (vs 'ns : fun()') - return colon.beginColumn - 1 == ns.endColumn && (colon.endColumn == fun.beginColumn - 1 || isNamespace(name)); + return colon.beginColumn - 1 == ns.endColumn + && (colon.endColumn == fun.beginColumn - 1 || isNamespace(ns.image)); } return true; } diff --git a/src/test/java/org/apache/commons/jexl3/ArithmeticOperatorTest.java b/src/test/java/org/apache/commons/jexl3/ArithmeticOperatorTest.java index b023d2b2..c42c591b 100644 --- a/src/test/java/org/apache/commons/jexl3/ArithmeticOperatorTest.java +++ b/src/test/java/org/apache/commons/jexl3/ArithmeticOperatorTest.java @@ -18,16 +18,22 @@ package org.apache.commons.jexl3; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.io.StringWriter; +import java.math.MathContext; import java.text.DecimalFormat; import java.text.SimpleDateFormat; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -39,6 +45,7 @@ import java.util.Set; import java.util.SortedSet; import java.util.TimeZone; import java.util.TreeSet; +import java.util.regex.Pattern; import org.apache.commons.jexl3.junit.Asserter; import org.junit.jupiter.api.BeforeEach; @@ -641,4 +648,219 @@ public class ArithmeticOperatorTest extends JexlTestCase { asserter.assertExpression("x.y =$ 'foo'", Boolean.TRUE); } + + /** + * A comparator using an evaluated expression on objects as comparison arguments. + */ + public static class PropertyComparator implements JexlCache.Reference, Comparator<Object> { + private final JexlContext context = JexlEngine.getThreadContext(); + private final JexlArithmetic arithmetic; + private final JexlArithmetic.Uberspect uber; + private final JexlScript expr; + private Object cache; + + PropertyComparator(JexlArithmetic jexla, JexlScript expr) { + this.arithmetic = jexla; + this.uber = JexlEngine.getThreadEngine().getUberspect().getArithmetic(arithmetic); + this.expr = expr; + } + @Override + public int compare(Object o1, Object o2) { + final Object left = expr.execute(context, o1); + final Object right = expr.execute(context, o2); + Object result = uber.tryEval(this, JexlOperator.COMPARE, left, right); + if (result instanceof Integer) { + return (int) result; + } + return arithmetic.compare(left, right, JexlOperator.COMPARE); + } + + @Override + public Object getCache() { + return cache; + } + + @Override + public void setCache(Object cache) { + this.cache = cache; + } + } + + public static class SortingArithmetic extends JexlArithmetic { + public SortingArithmetic(boolean strict) { + this( strict, null, Integer.MIN_VALUE); + } + + private SortingArithmetic(boolean strict, MathContext context, int scale) { + super(strict, context, scale); + } + + public int compare(Integer left, Integer right) { + return left.compareTo(right); + } + + public int compare(String left, String right) { + return left.compareTo(right); + } + + /** + * Sorts an array using a script to evaluate the property used to compare elements. + * @param array the elements array + * @param expr the property evaluation lambda + */ + public void sort(final Object[] array, final JexlScript expr) { + Arrays.sort(array, new PropertyComparator(this, expr)); + } + } + + @Test + void testSortArray() { + final JexlEngine jexl = new JexlBuilder().arithmetic(new SortingArithmetic(true)).safe(false).strict(true).silent(false).create(); + // test data, json like + final String src = "[{'id':1,'name':'John','type':9},{'id':2,'name':'Doe','type':7},{'id':3,'name':'Doe','type':10}]"; + final Object a = jexl.createExpression(src).evaluate(null); + assertNotNull(a); + // row 0 and 1 are not ordered + final Map[] m = (Map[]) a; + assertEquals(9, m[0].get("type")); + assertEquals(7, m[1].get("type")); + // sort the elements on the type + jexl.createScript("array.sort( e -> e.type )", "array").execute(null, a); + // row 0 and 1 are now ordered + assertEquals(7, m[0].get("type")); + assertEquals(9, m[1].get("type")); + } + + + public static class MatchingArithmetic extends JexlArithmetic { + public MatchingArithmetic(final boolean astrict) { + super(astrict); + } + + public boolean contains(final Pattern[] container, final String str) { + for(final Pattern pattern : container) { + if (pattern.matcher(str).matches()) { + return true; + } + } + return false; + } + } + + @Test + void testPatterns() { + final JexlEngine jexl = new JexlBuilder().arithmetic(new MatchingArithmetic(true)).create(); + final JexlScript script = jexl.createScript("str =~ [~/abc.*/, ~/def.*/]", "str"); + assertTrue((boolean) script.execute(null, "abcdef")); + assertTrue((boolean) script.execute(null, "defghi")); + assertFalse((boolean) script.execute(null, "ghijkl")); + } + + + public static class Arithmetic428 extends JexlArithmetic { + public Arithmetic428(boolean strict) { + this( strict, null, Integer.MIN_VALUE); + } + + private Arithmetic428(boolean strict, MathContext context, int scale) { + super(strict, context, scale); + } + + public int compare(Instant lhs, String str) { + Instant rhs = Instant.parse(str); + return lhs.compareTo(rhs); + } + } + + @Test + void test428() { + // see JEXL-428 + final JexlEngine jexl = new JexlBuilder().cache(32).arithmetic(new Arithmetic428(true)).create(); + final String rhsstr ="2024-09-09T10:42:42.00Z"; + final Instant rhs = Instant.parse(rhsstr); + final String lhs = "2020-09-09T01:24:24.00Z"; + JexlScript script; + script = jexl.createScript("x < y", "x", "y"); + final JexlScript s0 = script; + assertThrows(JexlException.class, () -> s0.execute(null, 42, rhs)); + assertTrue((boolean) script.execute(null, lhs, rhs)); + assertTrue((boolean) script.execute(null, lhs, rhs)); + assertFalse((boolean) script.execute(null, rhs, lhs)); + assertFalse((boolean) script.execute(null, rhs, lhs)); + assertTrue((boolean) script.execute(null, lhs, rhs)); + assertFalse((boolean) script.execute(null, rhs, lhs)); + + script = jexl.createScript("x <= y", "x", "y"); + final JexlScript s1 = script; + assertThrows(JexlException.class, () -> s1.execute(null, 42, rhs)); + assertTrue((boolean) script.execute(null, lhs, rhs)); + assertFalse((boolean) script.execute(null, rhs, lhs)); + + script = jexl.createScript("x >= y", "x", "y"); + final JexlScript s2 = script; + assertThrows(JexlException.class, () -> s2.execute(null, 42, rhs)); + assertFalse((boolean) script.execute(null, lhs, rhs)); + assertFalse((boolean) script.execute(null, lhs, rhs)); + assertTrue((boolean) script.execute(null, rhs, lhs)); + assertTrue((boolean) script.execute(null, rhs, lhs)); + assertFalse((boolean) script.execute(null, lhs, rhs)); + assertTrue((boolean) script.execute(null, rhs, lhs)); + + script = jexl.createScript("x > y", "x", "y"); + final JexlScript s3 = script; + assertThrows(JexlException.class, () -> s3.execute(null, 42, rhs)); + assertFalse((boolean) script.execute(null, lhs, rhs)); + assertTrue((boolean) script.execute(null, rhs, lhs)); + + script = jexl.createScript("x == y", "x", "y"); + assertFalse((boolean) script.execute(null, 42, rhs)); + assertFalse((boolean) script.execute(null, lhs, rhs)); + assertFalse((boolean) script.execute(null, lhs, rhs)); + assertTrue((boolean) script.execute(null, rhs, rhsstr)); + assertTrue((boolean) script.execute(null, rhsstr, rhs)); + assertFalse((boolean) script.execute(null, lhs, rhs)); + + script = jexl.createScript("x != y", "x", "y"); + assertTrue((boolean) script.execute(null, 42, rhs)); + assertTrue((boolean) script.execute(null, lhs, rhs)); + assertFalse((boolean) script.execute(null, rhs, rhsstr)); + } + + public static class Arithmetic429 extends JexlArithmetic { + public Arithmetic429(boolean astrict) { + super(astrict); + } + + public int compare(String lhs, Number rhs) { + return lhs.compareTo(rhs.toString()); + } + } + + @Test + void test429a() { + final JexlEngine jexl = new JexlBuilder() + .arithmetic(new Arithmetic429(true)) + .cache(32) + .create(); + String src; + JexlScript script; + src = "'1.1' > 0"; + script = jexl.createScript(src); + assertTrue((boolean) script.execute(null)); + src = "1.2 <= '1.20'"; + script = jexl.createScript(src); + assertTrue((boolean) script.execute(null)); + src = "1.2 >= '1.2'"; + script = jexl.createScript(src); + assertTrue((boolean) script.execute(null)); + src = "1.2 < '1.2'"; + script = jexl.createScript(src); + assertFalse((boolean) script.execute(null)); + src = "1.2 > '1.2'"; + script = jexl.createScript(src); + assertFalse((boolean) script.execute(null)); + src = "1.20 == 'a'"; + script = jexl.createScript(src); + assertFalse((boolean) script.execute(null)); + } } diff --git a/src/test/java/org/apache/commons/jexl3/Issues400Test.java b/src/test/java/org/apache/commons/jexl3/Issues400Test.java index 1a255098..81e7cd55 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues400Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues400Test.java @@ -28,18 +28,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.lang.reflect.Method; import java.math.BigDecimal; -import java.math.MathContext; -import java.time.Instant; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; -import java.util.regex.Pattern; import org.apache.commons.jexl3.introspection.JexlPermissions; import org.junit.jupiter.api.Test; @@ -449,98 +445,52 @@ public class Issues400Test { assertEquals(42, r); } - public static class SortingContext extends MapContext { - /** - * Sorts an array using a script to evaluate the property used to compare elements. - * @param array the elements array - * @param expr the property evaluation lambda - */ - public void sort(final Object[] array, final JexlScript expr) { - final Comparator<Object> cmp = (o1, o2) -> { - final Comparable left = (Comparable<?>) expr.execute(SortingContext.this, o1); - final Comparable right = (Comparable<?>) expr.execute(SortingContext.this, o2); - return left.compareTo(right); - }; - Arrays.sort(array, cmp); + public static class Ns429 { + public int f(final int x) { + return x * 10000 + 42; } } @Test - void testSortArray() { - final JexlEngine jexl = new JexlBuilder().safe(false).strict(true).silent(false).create(); - // test data, json like - final String src = "[{'id':1,'name':'John','type':9},{'id':2,'name':'Doe','type':7},{'id':3,'name':'Doe','type':10}]"; - final Object a = jexl.createExpression(src).evaluate(null); - assertNotNull(a); - // row 0 and 1 are not ordered - final Map[] m = (Map[]) a; - assertEquals(9, m[0].get("type")); - assertEquals(7, m[1].get("type")); - // sort the elements on the type - jexl.createScript("array.sort( e -> e.type )", "array").execute(new SortingContext(), a); - // row 0 and 1 are now ordered - assertEquals(7, m[0].get("type")); - assertEquals(9, m[1].get("type")); - } - - - public static class MatchingArithmetic extends JexlArithmetic { - public MatchingArithmetic(final boolean astrict) { - super(astrict); - } - - public boolean contains(final Pattern[] container, final String str) { - for(final Pattern pattern : container) { - if (pattern.matcher(str).matches()) { - return true; - } - } - return false; - } - } - - @Test - void testPatterns() { - final JexlEngine jexl = new JexlBuilder().arithmetic(new MatchingArithmetic(true)).create(); - final JexlScript script = jexl.createScript("str =~ [~/abc.*/, ~/def.*/]", "str"); - assertTrue((boolean) script.execute(null, "abcdef")); - assertTrue((boolean) script.execute(null, "defghi")); - assertFalse((boolean) script.execute(null, "ghijkl")); - } - - - public static class Arithmetic428 extends JexlArithmetic { - public Arithmetic428(boolean strict) { - this( strict, null, Integer.MIN_VALUE); - } - - private Arithmetic428(boolean strict, MathContext context, int scale) { - super(strict, context, scale); - } - - public int compare(Instant lhs, String str) { - Instant rhs = Instant.parse(str); - return lhs.compareTo(rhs); - } - - public int compare(String str, Instant date) { - return -compare(date, str); - } + void test429a() { + MapContext ctxt = new MapContext(); + //ctxt.set("b", 1); + JexlFeatures features = JexlFeatures.createDefault(); + final JexlEngine jexl = new JexlBuilder() + .features(features) + .safe(false).strict(true).silent(false).create(); + JexlScript f = jexl.createScript("x -> x"); + ctxt.set("f", f); + String src = "#pragma jexl.namespace.b "+Ns429.class.getName() +"\n" + +"b ? b : f(2);"; + JexlScript script = jexl.createScript(src, "b"); + assertEquals(1, (int) script.execute(ctxt, 1)); + + src = "#pragma jexl.namespace.b "+Ns429.class.getName() +"\n" + +"b ? b:f(2) : 1;"; + script = jexl.createScript(src, "b"); + assertEquals(20042, (int) script.execute(ctxt, 1)); } @Test - void testIssue428() { - final JexlEngine jexl = new JexlBuilder().cache(32).arithmetic(new Arithmetic428(true)).create(); - Instant rhs = Instant.parse("2024-09-09T10:42:42.00Z"); - String lhs = "2020-09-09T01:24:24.00Z"; - JexlScript script; - script = jexl.createScript("x < y", "x", "y"); - assertTrue((boolean) script.execute(null, lhs, rhs)); - assertTrue((boolean) script.execute(null, lhs, rhs)); - assertFalse((boolean) script.execute(null, rhs, lhs)); - assertFalse((boolean) script.execute(null, rhs, lhs)); - assertTrue((boolean) script.execute(null, lhs, rhs)); - assertFalse((boolean) script.execute(null, rhs, lhs)); + void test429b() { + MapContext ctxt = new MapContext(); + ctxt.set("b", 1); + JexlFeatures features = JexlFeatures.createDefault(); + features.namespaceIdentifier(true); + final JexlEngine jexl = new JexlBuilder() + .features(features) + .safe(false).strict(true).silent(false).create(); + JexlScript f = jexl.createScript("x -> x"); + ctxt.set("f", f); + String src = "#pragma jexl.namespace.b "+Ns429.class.getName() +"\n" + +"b ? b : f(2);"; + JexlScript script = jexl.createScript(src); + assertEquals(1, (int) script.execute(ctxt)); + + src = "#pragma jexl.namespace.b "+Ns429.class.getName() +"\n" + +"b ? b:f(2) : 1;"; + script = jexl.createScript(src); + assertEquals(20042, (int) script.execute(ctxt)); } - }