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 2915b379 JEXL-428: operators improvements;
2915b379 is described below

commit 2915b379fe8603e0576f55ca457cdcbd673b8e74
Author: Henrib <hbies...@gmail.com>
AuthorDate: Fri Sep 13 21:10:31 2024 +0200

    JEXL-428: operators improvements;
---
 .../org/apache/commons/jexl3/JexlOperator.java     |  29 +-
 .../apache/commons/jexl3/internal/Interpreter.java |  16 +-
 .../commons/jexl3/internal/InterpreterBase.java    |  43 +-
 .../apache/commons/jexl3/internal/Operators.java   | 553 ++++++++++++---------
 .../jexl3/internal/introspection/Uberspect.java    |  27 +-
 .../commons/jexl3/introspection/JexlSandbox.java   |  71 ++-
 .../org/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 01290c89..ca3eec09 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 @@ package org.apache.commons.jexl3;
  * @since 3.0
  */
 public enum JexlOperator {
-
     /**
      * Add operator.
      * <br><strong>Syntax:</strong> {@code x + y}
@@ -379,6 +378,7 @@ public enum JexlOperator {
      * Marker for side effect.
      * <br>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 {
      * <br><strong>Method:</strong> {@code boolean testCondition(R y);}.
      * @since 3.3
      */
-    CONDITION("?", "testCondition", 1);
+    CONDITION("?", "testCondition", 1),
+
+    /**
+     * Compare overload as in compare(x, y).
+     * <br><strong>Method:</strong> {@code boolean compare(L x, R y);}.
+     * @since 3.4.1
+     */
+    COMPARE("<>", "compare", 2),
+
+    /**
+     * Not-Contains operator.
+     * <p>Not overridable, calls !(contain(...))</p>
+     */
+    NOT_CONTAINS("!~", null, CONTAINS),
+
+    /**
+     * Not-Starts-With operator.
+     * <p>Not overridable, calls !(startsWith(...))</p>
+     */
+    NOT_STARTSWITH("!^", null, STARTSWITH),
+
+    /**
+     * Not-Ends-With operator.
+     * <p>Not overridable, calls !(endsWith(...))</p>
+     */
+    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 e97aa2f3..7b917841 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 @@ public class Interpreter extends InterpreterBase {
         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 @@ public class Interpreter extends InterpreterBase {
     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 @@ public class Interpreter extends InterpreterBase {
         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 @@ public class Interpreter extends InterpreterBase {
     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 @@ public class Interpreter extends InterpreterBase {
     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 @@ public class Interpreter extends InterpreterBase {
     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 412e42a7..22e1e872 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 @@ import org.apache.commons.logging.Log;
  * @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 @@ public abstract class InterpreterBase extends 
ParserVisitor {
             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 @@ public abstract class InterpreterBase extends 
ParserVisitor {
             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.
      * <p>Used by calls to unsolvableProperty(...).</p>
@@ -285,6 +289,7 @@ public abstract class InterpreterBase extends ParserVisitor 
{
     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 @@ public abstract class InterpreterBase extends ParserVisitor 
{
     /**
      * 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 2c951ca7..93e391ff 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 @@ import org.apache.commons.jexl3.parser.JexlNode;
  * 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.
+     * <p>Used to determine if a compare method overload might be used.</p>
+     */
+    private static final Set<JexlOperator> 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 @@ public class Operators {
      * Tidy arguments based on operator arity.
      * <p>The interpreter may add a null to the arguments of operator 
expecting only one parameter.</p>
      * @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.
-     * <p>
-     * 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.
-     * </p>
-     * @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 @@ public class Operators {
      * @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 @@ public class Operators {
     }
 
     /**
-     * Check for emptyness of various types: Collection, Array, Map, String, 
and anything that has a boolean isEmpty()
-     * method.
-     * <p>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 @@ public class Operators {
      * @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 @@ public class Operators {
      * @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.
+     * <p>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.
      * <p>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.
+     * <p>
+     * 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.
+     * </p>
+     * @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 @@ public class Operators {
      * @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 @@ public class Operators {
      * Evaluates an assign operator.
      * <p>
      * 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}
      * </p>
      * @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<Object> assignFun,
-                                       final Object...args) {
+    Object tryAssignOverload(final JexlNode node,
+                               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 {
-        // 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 @@ public class Operators {
      * @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.
+     * <p>As of 3.4.1, only the comparison operators attempting to use 
compare() are handled.</p>
+     * @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 7608e738..3454d359 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 @@ import org.apache.commons.logging.LogFactory;
  */
 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 @@ public class Uberspect implements JexlUberspect {
         @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 class Uberspect implements JexlUberspect {
             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 class Uberspect implements JexlUberspect {
     private volatile Reference<Introspector> ref;
     /** The class loader reference; used to recreate the introspector when 
necessary. */
     private volatile Reference<ClassLoader> loader;
-
     /**
      * The map from arithmetic classes to overloaded operator sets.
      * <p>
@@ -192,6 +192,25 @@ public class Uberspect implements JexlUberspect {
         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/introspection/JexlSandbox.java 
b/src/main/java/org/apache/commons/jexl3/introspection/JexlSandbox.java
index c3b06e1f..3015f291 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 final class JexlSandbox {
         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 final class JexlSandbox {
         }
     }
 
+    /**
+     * @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 34c63ee7..1a255098 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 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;
@@ -505,4 +507,40 @@ public class Issues400Test {
         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));
+    }
+
 }

Reply via email to