From 2915b379fe8603e0576f55ca457cdcbd673b8e74 Mon Sep 17 00:00:00 2001 From: Henrib Date: Fri, 13 Sep 2024 21:10:31 +0200 Subject: [PATCH] JEXL-428: operators improvements; --- .../apache/commons/jexl3/JexlOperator.java | 29 +- .../commons/jexl3/internal/Interpreter.java | 16 +- .../jexl3/internal/InterpreterBase.java | 43 +- .../commons/jexl3/internal/Operators.java | 553 +++++++++++------- .../internal/introspection/Uberspect.java | 27 +- .../jexl3/introspection/JexlSandbox.java | 71 ++- .../apache/commons/jexl3/Issues400Test.java | 38 ++ 7 files changed, 493 insertions(+), 284 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JexlOperator.java b/src/main/java/org/apache/commons/jexl3/JexlOperator.java index 01290c89f..ca3eec090 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlOperator.java +++ b/src/main/java/org/apache/commons/jexl3/JexlOperator.java @@ -47,7 +47,6 @@ * @since 3.0 */ public enum JexlOperator { - /** * Add operator. *
Syntax: {@code x + y} @@ -379,6 +378,7 @@ public enum JexlOperator { * Marker for side effect. *
Returns this from 'self*' overload method to let the engine know the side effect has been performed and * there is no need to assign the result. + * @deprecated 3.4.1 */ ASSIGN("=", null, null), @@ -424,7 +424,32 @@ public enum JexlOperator { *
Method: {@code boolean testCondition(R y);}. * @since 3.3 */ - CONDITION("?", "testCondition", 1); + CONDITION("?", "testCondition", 1), + + /** + * Compare overload as in compare(x, y). + *
Method: {@code boolean compare(L x, R y);}. + * @since 3.4.1 + */ + COMPARE("<>", "compare", 2), + + /** + * Not-Contains operator. + *

Not overridable, calls !(contain(...))

+ */ + NOT_CONTAINS("!~", null, CONTAINS), + + /** + * Not-Starts-With operator. + *

Not overridable, calls !(startsWith(...))

+ */ + NOT_STARTSWITH("!^", null, STARTSWITH), + + /** + * Not-Ends-With operator. + *

Not overridable, calls !(endsWith(...))

+ */ + NOT_ENDSWITH("!$", null, ENDSWITH),; /** * The operator symbol. diff --git a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java index e97aa2f3a..7b9178416 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -1421,14 +1421,14 @@ protected Object visit(final ASTERNode node, final Object data) { final Object right = node.jjtGetChild(1).jjtAccept(this, data); // note the arguments inversion between 'in'/'matches' and 'contains' // if x in y then y contains x - return operators.contains(node, "=~", right, left); + return operators.contains(node, JexlOperator.CONTAINS, right, left); } @Override protected Object visit(final ASTEWNode node, final Object data) { final Object left = node.jjtGetChild(0).jjtAccept(this, data); final Object right = node.jjtGetChild(1).jjtAccept(this, data); - return operators.endsWith(node, "$=", left, right); + return operators.endsWith(node, JexlOperator.ENDSWITH, left, right); } @Override @@ -1740,7 +1740,7 @@ protected Object visit(final ASTNESNode node, final Object data) { protected Object visit(final ASTNEWNode node, final Object data) { final Object left = node.jjtGetChild(0).jjtAccept(this, data); final Object right = node.jjtGetChild(1).jjtAccept(this, data); - return !operators.endsWith(node, "$!", left, right); + return operators.endsWith(node, JexlOperator.NOT_ENDSWITH, left, right); } @Override @@ -1768,14 +1768,14 @@ protected Object visit(final ASTNRNode node, final Object data) { final Object right = node.jjtGetChild(1).jjtAccept(this, data); // note the arguments inversion between (not) 'in'/'matches' and (not) 'contains' // if x not-in y then y not-contains x - return !operators.contains(node, "!~", right, left); + return operators.contains(node, JexlOperator.NOT_CONTAINS, right, left); } @Override protected Object visit(final ASTNSWNode node, final Object data) { final Object left = node.jjtGetChild(0).jjtAccept(this, data); final Object right = node.jjtGetChild(1).jjtAccept(this, data); - return !operators.startsWith(node, "^!", left, right); + return operators.startsWith(node, JexlOperator.NOT_STARTSWITH, left, right); } @Override @@ -2099,7 +2099,7 @@ protected Object visit(final ASTSubNode node, final Object data) { protected Object visit(final ASTSWNode node, final Object data) { final Object left = node.jjtGetChild(0).jjtAccept(this, data); final Object right = node.jjtGetChild(1).jjtAccept(this, data); - return operators.startsWith(node, "^=", left, right); + return operators.startsWith(node, JexlOperator.STARTSWITH, left, right); } @Override @@ -2244,7 +2244,7 @@ protected Object visit(final ASTTryStatement node, final Object data) { protected Object visit(final ASTUnaryMinusNode node, final Object data) { // use cached value if literal final Object value = node.jjtGetValue(); - if (value != null && !(value instanceof JexlMethod)) { + if (value instanceof Number) { return value; } final JexlNode valNode = node.jjtGetChild(0); @@ -2273,7 +2273,7 @@ protected Object visit(final ASTUnaryMinusNode node, final Object data) { protected Object visit(final ASTUnaryPlusNode node, final Object data) { // use cached value if literal final Object value = node.jjtGetValue(); - if (value != null && !(value instanceof JexlMethod)) { + if (value instanceof Number) { return value; } final JexlNode valNode = node.jjtGetChild(0); 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 412e42a7e..22e1e8727 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java +++ b/src/main/java/org/apache/commons/jexl3/internal/InterpreterBase.java @@ -55,24 +55,6 @@ * @since 3.0 */ public abstract class InterpreterBase extends ParserVisitor { - /** - * Cached arithmetic function call. - */ - protected static class ArithmeticFuncall extends Funcall { - /** - * Constructs a new instance. - * @param jme the method - * @param flag the narrow flag - */ - protected ArithmeticFuncall(final JexlMethod jme, final boolean flag) { - super(jme, flag); - } - - @Override - protected Object tryInvoke(final InterpreterBase ii, final String name, final Object target, final Object[] args) { - return me.tryInvoke(name, ii.arithmetic, ii.functionArguments(target, narrow, args)); - } - } /** * Helping dispatch function calls. */ @@ -208,6 +190,26 @@ protected Object tryEval(final Object ntarget, final String methodName, final Ob return JexlEngine.TRY_FAILED; } } + + /** + * Cached arithmetic function call. + */ + protected static class ArithmeticFuncall extends Funcall { + /** + * Constructs a new instance. + * @param jme the method + * @param flag the narrow flag + */ + protected ArithmeticFuncall(final JexlMethod jme, final boolean flag) { + super(jme, flag); + } + + @Override + protected Object tryInvoke(final InterpreterBase ii, final String name, final Object target, final Object[] args) { + return me.tryInvoke(name, ii.arithmetic, ii.functionArguments(target, narrow, args)); + } + } + /** * Cached context function call. */ @@ -274,8 +276,10 @@ protected Object tryInvoke(final InterpreterBase ii, final String name, final Ob return me.tryInvoke(name, target, ii.functionArguments(null, narrow, args)); } } + /** Empty parameters for method matching. */ protected static final Object[] EMPTY_PARAMS = {}; + /** * Pretty-prints a failing property value (de)reference. *

Used by calls to unsolvableProperty(...).

@@ -285,6 +289,7 @@ protected Object tryInvoke(final InterpreterBase ii, final String name, final Ob protected static String stringifyPropertyValue(final JexlNode node) { return node != null ? new Debugger().depth(1).data(node) : "???"; } + /** The JEXL engine. */ protected final Engine jexl; /** The logger. */ @@ -774,7 +779,7 @@ protected boolean isVariableDefined(final Frame frame, final LexicalScope block, /** * Triggered when an operator fails. * @param node the node where the error originated from - * @param operator the method name + * @param operator the operator symbol * @param cause the cause of error (if any) * @return throws JexlException if strict and not silent, null otherwise */ 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 2c951ca75..93e391ffa 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Operators.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Operators.java @@ -16,14 +16,15 @@ */ package org.apache.commons.jexl3.internal; -import java.lang.reflect.Method; +import java.util.EnumSet; +import java.util.Set; import java.util.function.Consumer; import org.apache.commons.jexl3.JexlArithmetic; 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.parser.JexlNode; @@ -32,7 +33,7 @@ * Helper class to deal with operator overloading and specifics. * @since 3.0 */ -public class Operators { +public final class Operators { /** * Helper for postfix assignment operators. * @param operator the operator @@ -41,17 +42,25 @@ public class Operators { private static boolean isPostfix(final JexlOperator operator) { return operator == JexlOperator.GET_AND_INCREMENT || operator == JexlOperator.GET_AND_DECREMENT; } + + /** + * The comparison operators. + *

Used to determine if a compare method overload might be used.

+ */ + private static final Set CMP_OPS = + EnumSet.of(JexlOperator.GT, JexlOperator.LT, JexlOperator.EQ, JexlOperator.GTE, JexlOperator.LTE); + /** The owner. */ - protected final InterpreterBase interpreter; + private final InterpreterBase interpreter; /** The overloaded arithmetic operators. */ - protected final JexlArithmetic.Uberspect operators; + private final JexlArithmetic.Uberspect operators; /** * Constructs a new instance. * @param owner the owning interpreter */ - protected Operators(final InterpreterBase owner) { + Operators(final InterpreterBase owner) { final JexlArithmetic arithmetic = owner.arithmetic; final JexlUberspect uberspect = owner.uberspect; this.interpreter = owner; @@ -62,62 +71,13 @@ protected Operators(final InterpreterBase owner) { * Tidy arguments based on operator arity. *

The interpreter may add a null to the arguments of operator expecting only one parameter.

* @param operator the operator - * @param args the arguements (as seen by the interpreter) + * @param args the arguments (as seen by the interpreter) * @return the tidied arguments */ private Object[] arguments(final JexlOperator operator, final Object...args) { return operator.getArity() == 1 && args.length > 1 ? new Object[]{args[0]} : args; } - /** - * The 'match'/'in' operator implementation. - *

- * Note that 'x in y' or 'x matches y' means 'y contains x' ; - * the JEXL operator arguments order syntax is the reverse of this method call. - *

- * @param node the node - * @param op the calling operator, =~ or !~ - * @param right the left operand - * @param left the right operand - * @return true if left matches right, false otherwise - */ - protected boolean contains(final JexlNode node, final String op, final Object left, final Object right) { - final JexlArithmetic arithmetic = interpreter.arithmetic; - final JexlUberspect uberspect = interpreter.uberspect; - try { - // try operator overload - final Object result = tryOverload(node, JexlOperator.CONTAINS, left, right); - if (result instanceof Boolean) { - return (Boolean) result; - } - // use arithmetic / pattern matching ? - final Boolean matched = arithmetic.contains(left, right); - if (matched != null) { - return matched; - } - // try a contains method (duck type set) - try { - final Object[] argv = {right}; - JexlMethod vm = uberspect.getMethod(left, "contains", argv); - if (returnsBoolean(vm)) { - return (Boolean) vm.invoke(left, argv); - } - if (arithmetic.narrowArguments(argv)) { - vm = uberspect.getMethod(left, "contains", argv); - if (returnsBoolean(vm)) { - return (Boolean) vm.invoke(left, argv); - } - } - } catch (final Exception e) { - throw new JexlException(node, op + " error", e); - } - // defaults to equal - return arithmetic.equals(left, right); - } catch (final ArithmeticException xrt) { - throw new JexlException(node, op + " error", xrt); - } - } - /** * Throw a NPE if operator is strict and one of the arguments is null. * @param arithmetic the JEXL arithmetic instance @@ -125,7 +85,7 @@ protected boolean contains(final JexlNode node, final String op, final Object le * @param args the operands * @throws JexlArithmetic.NullOperand if operator is strict and an operand is null */ - protected void controlNullOperands(final JexlArithmetic arithmetic, final JexlOperator operator, final Object...args) { + private void controlNullOperands(final JexlArithmetic arithmetic, final JexlOperator operator, final Object...args) { for (final Object arg : args) { // only check operator if necessary if (arg == null) { @@ -139,97 +99,28 @@ protected void controlNullOperands(final JexlArithmetic arithmetic, final JexlOp } /** - * Check for emptyness of various types: Collection, Array, Map, String, and anything that has a boolean isEmpty() - * method. - *

Note that the result may not be a boolean. - * - * @param node the node holding the object - * @param object the object to check the emptyness of - * @return the evaluation result + * 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 */ - protected Object empty(final JexlNode node, final Object object) { - if (object == null) { - return true; - } - Object result = tryOverload(node, JexlOperator.EMPTY, object); - if (result != JexlEngine.TRY_FAILED) { - return result; + 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); } 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 - final JexlMethod vm = uberspect.getMethod(object, "isEmpty", InterpreterBase.EMPTY_PARAMS); + final Object[] argv = { right }; + if (arithmetic.narrowArguments(argv)) { + vm = uberspect.getMethod(left, methodName, argv); if (returnsBoolean(vm)) { - try { - result = vm.invoke(object, InterpreterBase.EMPTY_PARAMS); - } catch (final Exception xany) { - interpreter.operatorError(node, JexlOperator.EMPTY, xany); - } + return (Boolean) vm.invoke(left, argv); } } - return !(result instanceof Boolean) || (Boolean) result; - } - - /** - * The 'endsWith' operator implementation. - * @param node the node - * @param operator the calling operator, ^= or ^! - * @param left the left operand - * @param right the right operand - * @return true if left ends with right, false otherwise - */ - protected boolean endsWith(final JexlNode node, final String operator, final Object left, final Object right) { - final JexlArithmetic arithmetic = interpreter.arithmetic; - final JexlUberspect uberspect = interpreter.uberspect; - try { - // try operator overload - final Object result = tryOverload(node, JexlOperator.ENDSWITH, left, right); - if (result instanceof Boolean) { - return (Boolean) result; - } - // use arithmetic / pattern matching ? - final Boolean matched = arithmetic.endsWith(left, right); - if (matched != null) { - return matched; - } - // try a endsWith method (duck type) - try { - final Object[] argv = {right}; - JexlMethod vm = uberspect.getMethod(left, "endsWith", argv); - if (returnsBoolean(vm)) { - return (Boolean) vm.invoke(left, argv); - } - if (arithmetic.narrowArguments(argv)) { - vm = uberspect.getMethod(left, "endsWith", argv); - if (returnsBoolean(vm)) { - return (Boolean) vm.invoke(left, argv); - } - } - } catch (final Exception e) { - throw new JexlException(node, operator + " error", e); - } - // defaults to equal - return arithmetic.equals(left, right); - } catch (final ArithmeticException xrt) { - throw new JexlException(node, operator + " error", xrt); - } - } - - /** - * Checks whether a method is a JexlArithmetic method. - * @param vm the JexlMethod (may be null) - * @return true of false - */ - private boolean isArithmetic(final JexlMethod vm) { - if (vm instanceof MethodExecutor) { - final Method method = ((MethodExecutor) vm).getMethod(); - return JexlArithmetic.class.equals(method.getDeclaringClass()); - } - return false; + return null; } /** @@ -238,7 +129,7 @@ private boolean isArithmetic(final JexlMethod vm) { * @return true of false */ private boolean returnsBoolean(final JexlMethod vm) { - if (vm !=null) { + if (vm != null) { final Class rc = vm.getReturnType(); return Boolean.TYPE.equals(rc) || Boolean.class.equals(rc); } @@ -251,46 +142,129 @@ private boolean returnsBoolean(final JexlMethod vm) { * @return true of false */ private boolean returnsInteger(final JexlMethod vm) { - if (vm !=null) { + if (vm != null) { final Class rc = vm.getReturnType(); return Integer.TYPE.equals(rc) || Integer.class.equals(rc); } return false; } + /** + * Check for emptiness of various types: Collection, Array, Map, String, and anything that has a boolean isEmpty() + * method. + *

Note that the result may not be a boolean. + * + * @param node the node holding the object + * @param object the object to check the emptiness of + * @return the evaluation result + */ + Object empty(final JexlNode node, final Object object) { + if (object == null) { + return true; + } + Object result = operators.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 + final JexlMethod vm = uberspect.getMethod(object, "isEmpty", InterpreterBase.EMPTY_PARAMS); + if (returnsBoolean(vm)) { + try { + result = vm.invoke(object, InterpreterBase.EMPTY_PARAMS); + } catch (final Exception xany) { + return interpreter.operatorError(node, JexlOperator.EMPTY, xany); + } + } + } + } + return !(result instanceof Boolean) || (Boolean) result; + } + /** * Calculate the {@code size} of various types: - * Collection, Array, Map, String, and anything that has a int size() method. + * Collection, Array, Map, String, and anything that has an int size() method. *

Note that the result may not be an integer. * * @param node the node that gave the value to size * @param object the object to get the size of * @return the evaluation result */ - protected Object size(final JexlNode node, final Object object) { + Object size(final JexlNode node, final Object object) { if (object == null) { return 0; } - Object result = tryOverload(node, JexlOperator.SIZE, object); - if (result != JexlEngine.TRY_FAILED) { - return result; + Object result = operators.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); + if (returnsInteger(vm)) { + try { + result = vm.invoke(object, InterpreterBase.EMPTY_PARAMS); + } catch (final Exception xany) { + interpreter.operatorError(node, JexlOperator.SIZE, xany); + } + } + } } + return result instanceof Number ? ((Number) result).intValue() : 0; + } + + /** + * The 'match'/'in' operator implementation. + *

+ * Note that 'x in y' or 'x matches y' means 'y contains x' ; + * the JEXL operator arguments order syntax is the reverse of this method call. + *

+ * @param node the node + * @param operator the calling operator, =~ or !~ + * @param right the left operand + * @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; - 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); - if (returnsInteger(vm)) { - try { - result = vm.invoke(object, InterpreterBase.EMPTY_PARAMS); - } catch (final Exception xany) { - interpreter.operatorError(node, JexlOperator.SIZE, xany); + final boolean contained; + try { + // try operator overload + final Object result = operators.overloads(JexlOperator.CONTAINS) + ? tryOverload(node, JexlOperator.CONTAINS, left, right) + : null; + if (result instanceof Boolean) { + contained = (Boolean) result; + } else { + // use arithmetic / pattern matching ? + final Boolean matched = arithmetic.contains(left, right); + if (matched != null) { + contained = matched; + } else { + // try a left.contains(right) method + final Boolean duck = booleanDuckCall("contains", left, right); + if (duck != null) { + contained = duck; + } else { + // defaults to equal + contained = arithmetic.equals(left, right); + } } } + return (JexlOperator.CONTAINS == operator) == contained; + } catch (final Exception xrt) { + interpreter.operatorError(node, operator, xrt); + return false; } - return result instanceof Number ? ((Number) result).intValue() : 0; } /** @@ -301,40 +275,77 @@ protected Object size(final JexlNode node, final Object object) { * @param right the right operand * @return true if left starts with right, false otherwise */ - protected boolean startsWith(final JexlNode node, final String operator, final Object left, final Object right) { + boolean startsWith(final JexlNode node, final JexlOperator operator, final Object left, final Object right) { final JexlArithmetic arithmetic = interpreter.arithmetic; - final JexlUberspect uberspect = interpreter.uberspect; + final boolean starts; try { // try operator overload - final Object result = tryOverload(node, JexlOperator.STARTSWITH, left, right); + final Object result = operators.overloads(JexlOperator.STARTSWITH) + ? tryOverload(node, JexlOperator.STARTSWITH, left, right) + : null; if (result instanceof Boolean) { - return (Boolean) result; - } - // use arithmetic / pattern matching ? - final Boolean matched = arithmetic.startsWith(left, right); - if (matched != null) { - return matched; - } - // try a startsWith method (duck type) - try { - final Object[] argv = {right}; - JexlMethod vm = uberspect.getMethod(left, "startsWith", argv); - if (returnsBoolean(vm)) { - return (Boolean) vm.invoke(left, argv); + starts = (Boolean) result; + } else { + // use arithmetic / pattern matching ? + final Boolean matched = arithmetic.startsWith(left, right); + if (matched != null) { + starts = matched; + } else { + // try a left.startsWith(right) method + final Boolean duck = booleanDuckCall("startsWith", left, right); + if (duck != null) { + starts = duck; + } else { + // defaults to equal + starts = arithmetic.equals(left, right); + } } - if (arithmetic.narrowArguments(argv)) { - vm = uberspect.getMethod(left, "startsWith", argv); - if (returnsBoolean(vm)) { - return (Boolean) vm.invoke(left, argv); + } + return (JexlOperator.STARTSWITH == operator) == starts; + } catch (final Exception xrt) { + interpreter.operatorError(node, operator, xrt); + return false; + } + } + + /** + * The 'endsWith' operator implementation. + * @param node the node + * @param operator the calling operator, ^= or ^! + * @param left the left operand + * @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; + try { + final boolean ends; + // try operator overload + final Object result = operators.overloads(JexlOperator.ENDSWITH) + ? tryOverload(node, JexlOperator.ENDSWITH, left, right) + : null; + if (result instanceof Boolean) { + ends = (Boolean) result; + } else { + // use arithmetic / pattern matching ? + final Boolean matched = arithmetic.endsWith(left, right); + if (matched != null) { + ends = matched; + } else { + // try a left.endsWith(right) method + final Boolean duck = booleanDuckCall("endsWith", left, right); + if (duck != null) { + ends = duck; + } else { + // defaults to equal + ends = arithmetic.equals(left, right); } } - } catch (final Exception e) { - throw new JexlException(node, operator + " error", e); } - // defaults to equal - return arithmetic.equals(left, right); - } catch (final ArithmeticException xrt) { - throw new JexlException(node, operator + " error", xrt); + return (JexlOperator.ENDSWITH == operator) == ends; + } catch (final Exception xrt) { + interpreter.operatorError(node, operator, xrt); + return false; } } @@ -342,44 +353,42 @@ protected boolean startsWith(final JexlNode node, final String operator, final O * Evaluates an assign operator. *

* This takes care of finding and caching the operator method when appropriate. - * If an overloads returns Operator.ASSIGN, it means the side-effect is complete. + * If an overloads returns a value not-equal to TRY_FAILED, it means the side-effect is complete. * Otherwise, {@code a += b <=> a = a + b} *

* @param node the syntactic node * @param operator the operator * @param args the arguments, the first one being the target of assignment - * @return JexlOperator.ASSIGN if operation assignment has been performed, - * JexlEngine.TRY_FAILED if no operation was performed, + * @return JexlEngine.TRY_FAILED if no operation was performed, * the value to use as the side effect argument otherwise */ - protected Object tryAssignOverload(final JexlNode node, - final JexlOperator operator, - final Consumer assignFun, - final Object...args) { + Object tryAssignOverload(final JexlNode node, + final JexlOperator operator, + final Consumer assignFun, + final Object...args) { final JexlArithmetic arithmetic = interpreter.arithmetic; if (args.length < operator.getArity()) { return JexlEngine.TRY_FAILED; } Object result; try { - // if some overloads exist... - if (operators != null) { - // try to call overload with side effect; the object is modified + // try to call overload with side effect; the object is modified + if (operators.overloads(operator)) { result = tryOverload(node, operator, arguments(operator, args)); if (result != JexlEngine.TRY_FAILED) { return result; // 1 } - // try to call base overload (ie + for +=) - final JexlOperator base = operator.getBaseOperator(); - if (base != null && operators.overloads(base)) { - result = tryOverload(node, base, arguments(base, args)); - if (result != JexlEngine.TRY_FAILED) { - assignFun.accept(result); - return isPostfix(operator) ? args[0] : result; // 2 - } + } + // try to call base overload (ie + for +=) + final JexlOperator base = operator.getBaseOperator(); + if (base != null && operators.overloads(base)) { + result = tryOverload(node, base, arguments(base, args)); + if (result != JexlEngine.TRY_FAILED) { + assignFun.accept(result); + return isPostfix(operator) ? args[0] : result; // 2 } } - // base eval + // default implementation for self-* operators switch (operator) { case SELF_ADD: result = arithmetic.add(args[0], args[1]); @@ -454,35 +463,125 @@ protected Object tryAssignOverload(final JexlNode node, * @param args the arguments * @return the result of the operator evaluation or TRY_FAILED */ - protected Object tryOverload(final JexlNode node, final JexlOperator operator, final Object... args) { + Object tryOverload(final JexlNode node, final JexlOperator operator, final Object... args) { final JexlArithmetic arithmetic = interpreter.arithmetic; controlNullOperands(arithmetic, operator, args); - if (operators != null && operators.overloads(operator)) { + try { final boolean cache = interpreter.cache; - try { - if (cache) { - final Object cached = node.jjtGetValue(); - if (cached instanceof JexlMethod) { - final JexlMethod me = (JexlMethod) cached; - final Object eval = me.tryInvoke(operator.getMethodName(), arithmetic, args); - if (!me.tryFailed(eval)) { - return eval; - } + 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; } - } - final JexlMethod vm = operators.getOperator(operator, args); - if (vm != null && !isArithmetic(vm)) { - final Object result = vm.invoke(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; } - } catch (final Exception xany) { - // ignore return if lenient, will return try_failed - interpreter.operatorError(node, operator, xany); } + // 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); + } + 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); + } + } catch (final Exception xany) { + // ignore return if lenient, will return try_failed + interpreter.operatorError(node, operator, xany); } return JexlEngine.TRY_FAILED; } + + /** + * Special handling of overloads where another attempt at finding a method may be attempted. + *

As of 3.4.1, only the comparison operators attempting to use compare() are handled.

+ * @param operator the operator + * @param args the arguments + * @return an instance or null + */ + 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 != null) { + return new CompareMethod(operator, cmp); + } + } + return null; + } + + /** + * Delegates a comparison operator to a compare method. + * The expected signature of the derived JexlArithmetic method is: + * int compare(L left, R right); + */ + private static class CompareMethod implements JexlMethod { + protected final JexlOperator operator; + protected final JexlMethod compare; + + CompareMethod(JexlOperator op, JexlMethod m) { + operator = op; + compare = m; + } + + @Override + public Class getReturnType() { + return Boolean.TYPE; + } + + @Override + public Object invoke(Object arithmetic, Object... params) throws Exception { + return operate((int) compare.invoke(arithmetic, params)); + } + + @Override + public boolean isCacheable() { + return true; + } + + @Override + public boolean tryFailed(Object rval) { + return rval == JexlEngine.TRY_FAILED; + } + + @Override + public Object tryInvoke(String name, Object arithmetic, Object... params) throws JexlException.TryFailed { + Object cmp = compare.tryInvoke(JexlOperator.COMPARE.getMethodName(), arithmetic, params); + if (cmp instanceof Integer) { + return operate((int) cmp); + } + return JexlEngine.TRY_FAILED; + } + + private boolean operate(final int cmp) { + switch(operator) { + case EQ: return cmp == 0; + case LT: return cmp < 0; + case LTE: return cmp <= 0; + case GT: return cmp > 0; + case GTE: return cmp >= 0; + } + throw new ArithmeticException("unexpected operator " + operator); + } + } } 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 7608e7384..3454d3591 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 @@ -50,7 +50,7 @@ */ public class Uberspect implements JexlUberspect { /** - * The concrete uberspect Arithmetic class. + * The concrete Uberspect Arithmetic class. */ protected class ArithmeticUberspect implements JexlArithmetic.Uberspect { /** The arithmetic instance being analyzed. */ @@ -71,8 +71,8 @@ protected class ArithmeticUberspect implements JexlArithmetic.Uberspect { @Override public JexlMethod getOperator(final JexlOperator operator, final Object... args) { return overloads.contains(operator) && args != null - ? getMethod(arithmetic, operator.getMethodName(), args) - : null; + ? uberspectOperator(arithmetic, operator, args) + : null; } @Override @@ -80,6 +80,7 @@ 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. */ @@ -94,7 +95,6 @@ public boolean overloads(final JexlOperator operator) { private volatile Reference ref; /** The class loader reference; used to recreate the introspector when necessary. */ private volatile Reference loader; - /** * The map from arithmetic classes to overloaded operator sets. *

@@ -192,6 +192,25 @@ public JexlArithmetic.Uberspect getArithmetic(final JexlArithmetic arithmetic) { return jau; } + /** + * Seeks an implementation of an operator method in an arithmetic instance. + *

Method must <>not/em belong to JexlArithmetic

+ * @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/introspection/JexlSandbox.java b/src/main/java/org/apache/commons/jexl3/introspection/JexlSandbox.java index c3b06e1f0..3015f2917 100644 --- a/src/main/java/org/apache/commons/jexl3/introspection/JexlSandbox.java +++ b/src/main/java/org/apache/commons/jexl3/introspection/JexlSandbox.java @@ -367,30 +367,6 @@ public String read(final Class clazz, final String name) { return get(clazz).read().get(name); } - /** - * Gets the read permission value for a given property of a class. - * - * @param clazz the class name - * @param name the property name - * @return null if not allowed, the name of the property to use otherwise - * @deprecated 3.3 - */ - @Deprecated - public String read(final String clazz, final String name) { - return get(clazz).read().get(name); - } - - /** - * Use allow() instead. - * - * @param clazz the allowed class name - * @return the permissions instance - * @deprecated 3.3 - */ - @Deprecated - public Permissions white(final String clazz) { - return allow(clazz); - } /** * Gets the write permission value for a given property of a class. @@ -684,4 +660,51 @@ public Permissions write(final String... propertyNames) { } } + /** + * @deprecated since 3.2, use {@link BlockSet} + */ + @Deprecated + public static final class BlackSet extends BlockSet {} + + /** + * @deprecated since 3.2, use {@link AllowSet} + */ + @Deprecated + public static final class WhiteSet extends AllowSet {} + + /** + * Use block() instead. + * + * @param clazz the blocked class name + * @return the permissions instance + * @deprecated 3.3 + */ + @Deprecated + public Permissions black(final String clazz) { + return block(clazz); + } + /** + * Gets the read permission value for a given property of a class. + * + * @param clazz the class name + * @param name the property name + * @return null if not allowed, the name of the property to use otherwise + * @deprecated 3.3 + */ + @Deprecated + public String read(final String clazz, final String name) { + return get(clazz).read().get(name); + } + + /** + * Use allow() instead. + * + * @param clazz the allowed class name + * @return the permissions instance + * @deprecated 3.3 + */ + @Deprecated + public Permissions white(final String clazz) { + return allow(clazz); + } } diff --git a/src/test/java/org/apache/commons/jexl3/Issues400Test.java b/src/test/java/org/apache/commons/jexl3/Issues400Test.java index 34c63ee7a..1a2550988 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues400Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues400Test.java @@ -28,6 +28,8 @@ 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; @@ -505,4 +507,40 @@ void testPatterns() { 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); + } + } + + @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)); + } + }