View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3.internal;
18  
19  import java.lang.reflect.Method;
20  import java.util.function.Consumer;
21  
22  import org.apache.commons.jexl3.JexlArithmetic;
23  import org.apache.commons.jexl3.JexlEngine;
24  import org.apache.commons.jexl3.JexlException;
25  import org.apache.commons.jexl3.JexlOperator;
26  import org.apache.commons.jexl3.internal.introspection.MethodExecutor;
27  import org.apache.commons.jexl3.introspection.JexlMethod;
28  import org.apache.commons.jexl3.introspection.JexlUberspect;
29  import org.apache.commons.jexl3.parser.JexlNode;
30  
31  /**
32   * Helper class to deal with operator overloading and specifics.
33   * @since 3.0
34   */
35  public class Operators {
36      /**
37       * Helper for postfix assignment operators.
38       * @param operator the operator
39       * @return true if operator is a postfix operator (x++, y--)
40       */
41      private static boolean isPostfix(final JexlOperator operator) {
42          return operator == JexlOperator.GET_AND_INCREMENT || operator == JexlOperator.GET_AND_DECREMENT;
43      }
44      /** The owner. */
45      protected final InterpreterBase interpreter;
46  
47      /** The overloaded arithmetic operators. */
48      protected final JexlArithmetic.Uberspect operators;
49  
50      /**
51       * Constructs a new instance.
52       * @param owner the owning interpreter
53       */
54      protected Operators(final InterpreterBase owner) {
55          final JexlArithmetic arithmetic = owner.arithmetic;
56          final JexlUberspect uberspect = owner.uberspect;
57          this.interpreter = owner;
58          this.operators = uberspect.getArithmetic(arithmetic);
59      }
60  
61      /**
62       * Tidy arguments based on operator arity.
63       * <p>The interpreter may add a null to the arguments of operator expecting only one parameter.</p>
64       * @param operator the operator
65       * @param args the arguements (as seen by the interpreter)
66       * @return the tidied arguments
67       */
68      private Object[] arguments(final JexlOperator operator, final Object...args) {
69          return operator.getArity() == 1 && args.length > 1 ? new Object[]{args[0]} : args;
70      }
71  
72      /**
73       * The 'match'/'in' operator implementation.
74       * <p>
75       * Note that 'x in y' or 'x matches y' means 'y contains x' ;
76       * the JEXL operator arguments order syntax is the reverse of this method call.
77       * </p>
78       * @param node  the node
79       * @param op    the calling operator, =~ or !~
80       * @param right the left operand
81       * @param left  the right operand
82       * @return true if left matches right, false otherwise
83       */
84      protected boolean contains(final JexlNode node, final String op, final Object left, final Object right) {
85          final JexlArithmetic arithmetic = interpreter.arithmetic;
86          final JexlUberspect uberspect = interpreter.uberspect;
87          try {
88              // try operator overload
89              final Object result = tryOverload(node, JexlOperator.CONTAINS, left, right);
90              if (result instanceof Boolean) {
91                  return (Boolean) result;
92              }
93              // use arithmetic / pattern matching ?
94              final Boolean matched = arithmetic.contains(left, right);
95              if (matched != null) {
96                  return matched;
97              }
98              // try a contains method (duck type set)
99              try {
100                 final Object[] argv = {right};
101                 JexlMethod vm = uberspect.getMethod(left, "contains", argv);
102                 if (returnsBoolean(vm)) {
103                     return (Boolean) vm.invoke(left, argv);
104                 }
105                 if (arithmetic.narrowArguments(argv)) {
106                     vm = uberspect.getMethod(left, "contains", argv);
107                     if (returnsBoolean(vm)) {
108                         return (Boolean) vm.invoke(left, argv);
109                     }
110                 }
111             } catch (final Exception e) {
112                 throw new JexlException(node, op + " error", e);
113             }
114             // defaults to equal
115             return arithmetic.equals(left, right);
116         } catch (final ArithmeticException xrt) {
117             throw new JexlException(node, op + " error", xrt);
118         }
119     }
120 
121     /**
122      * Throw a NPE if operator is strict and one of the arguments is null.
123      * @param arithmetic the JEXL arithmetic instance
124      * @param operator the operator to check
125      * @param args the operands
126      * @throws JexlArithmetic.NullOperand if operator is strict and an operand is null
127      */
128     protected void controlNullOperands(final JexlArithmetic arithmetic, final JexlOperator operator, final Object...args) {
129         for (final Object arg : args) {
130             // only check operator if necessary
131             if (arg == null) {
132                 // check operator only once if it is not strict
133                 if (arithmetic.isStrict(operator)) {
134                     throw new JexlArithmetic.NullOperand();
135                 }
136                 break;
137             }
138         }
139     }
140 
141     /**
142      * Check for emptyness of various types: Collection, Array, Map, String, and anything that has a boolean isEmpty()
143      * method.
144      * <p>Note that the result may not be a boolean.
145      *
146      * @param node   the node holding the object
147      * @param object the object to check the emptyness of
148      * @return the evaluation result
149      */
150     protected Object empty(final JexlNode node, final Object object) {
151         if (object == null) {
152             return true;
153         }
154         Object result = tryOverload(node, JexlOperator.EMPTY, object);
155         if (result != JexlEngine.TRY_FAILED) {
156             return result;
157         }
158         final JexlArithmetic arithmetic = interpreter.arithmetic;
159         result = arithmetic.isEmpty(object, null);
160         if (result == null) {
161             final JexlUberspect uberspect = interpreter.uberspect;
162             result = false;
163             // check if there is an isEmpty method on the object that returns a
164             // boolean and if so, just use it
165             final JexlMethod vm = uberspect.getMethod(object, "isEmpty", InterpreterBase.EMPTY_PARAMS);
166             if (returnsBoolean(vm)) {
167                 try {
168                     result = vm.invoke(object, InterpreterBase.EMPTY_PARAMS);
169                 } catch (final Exception xany) {
170                     interpreter.operatorError(node, JexlOperator.EMPTY, xany);
171                 }
172             }
173         }
174         return !(result instanceof Boolean) || (Boolean) result;
175     }
176 
177     /**
178      * The 'endsWith' operator implementation.
179      * @param node     the node
180      * @param operator the calling operator, ^= or ^!
181      * @param left     the left operand
182      * @param right    the right operand
183      * @return true if left ends with right, false otherwise
184      */
185     protected boolean endsWith(final JexlNode node, final String operator, final Object left, final Object right) {
186         final JexlArithmetic arithmetic = interpreter.arithmetic;
187         final JexlUberspect uberspect = interpreter.uberspect;
188         try {
189             // try operator overload
190             final Object result = tryOverload(node, JexlOperator.ENDSWITH, left, right);
191             if (result instanceof Boolean) {
192                 return (Boolean) result;
193             }
194             // use arithmetic / pattern matching ?
195             final Boolean matched = arithmetic.endsWith(left, right);
196             if (matched != null) {
197                 return matched;
198             }
199             // try a endsWith method (duck type)
200             try {
201                 final Object[] argv = {right};
202                 JexlMethod vm = uberspect.getMethod(left, "endsWith", argv);
203                 if (returnsBoolean(vm)) {
204                     return (Boolean) vm.invoke(left, argv);
205                 }
206                 if (arithmetic.narrowArguments(argv)) {
207                     vm = uberspect.getMethod(left, "endsWith", argv);
208                     if (returnsBoolean(vm)) {
209                         return (Boolean) vm.invoke(left, argv);
210                     }
211                 }
212             } catch (final Exception e) {
213                 throw new JexlException(node, operator + " error", e);
214             }
215             // defaults to equal
216             return arithmetic.equals(left, right);
217         } catch (final ArithmeticException xrt) {
218             throw new JexlException(node, operator + " error", xrt);
219         }
220     }
221 
222     /**
223      * Checks whether a method is a JexlArithmetic method.
224      * @param vm the JexlMethod (may be null)
225      * @return true of false
226      */
227     private boolean isArithmetic(final JexlMethod vm) {
228         if (vm instanceof MethodExecutor) {
229             final Method method = ((MethodExecutor) vm).getMethod();
230             return JexlArithmetic.class.equals(method.getDeclaringClass());
231         }
232         return false;
233     }
234 
235     /**
236      * Checks whether a method returns a boolean or a Boolean.
237      * @param vm the JexlMethod (may be null)
238      * @return true of false
239      */
240     private boolean returnsBoolean(final JexlMethod vm) {
241         if (vm !=null) {
242             final Class<?> rc = vm.getReturnType();
243             return Boolean.TYPE.equals(rc) || Boolean.class.equals(rc);
244         }
245         return false;
246     }
247 
248     /**
249      * Checks whether a method returns an int or an Integer.
250      * @param vm the JexlMethod (may be null)
251      * @return true of false
252      */
253     private boolean returnsInteger(final JexlMethod vm) {
254         if (vm !=null) {
255             final Class<?> rc = vm.getReturnType();
256             return Integer.TYPE.equals(rc) || Integer.class.equals(rc);
257         }
258         return false;
259     }
260 
261     /**
262      * Calculate the <code>size</code> of various types:
263      * Collection, Array, Map, String, and anything that has a int size() method.
264      * <p>Note that the result may not be an integer.
265      *
266      * @param node   the node that gave the value to size
267      * @param object the object to get the size of
268      * @return the evaluation result
269      */
270     protected Object size(final JexlNode node, final Object object) {
271         if (object == null) {
272             return 0;
273         }
274         Object result = tryOverload(node, JexlOperator.SIZE, object);
275         if (result != JexlEngine.TRY_FAILED) {
276             return result;
277         }
278         final JexlArithmetic arithmetic = interpreter.arithmetic;
279         result = arithmetic.size(object, null);
280         if (result == null) {
281             final JexlUberspect uberspect = interpreter.uberspect;
282             // check if there is a size method on the object that returns an
283             // integer and if so, just use it
284             final JexlMethod vm = uberspect.getMethod(object, "size", InterpreterBase.EMPTY_PARAMS);
285             if (returnsInteger(vm)) {
286                 try {
287                     result = vm.invoke(object, InterpreterBase.EMPTY_PARAMS);
288                 } catch (final Exception xany) {
289                     interpreter.operatorError(node, JexlOperator.SIZE, xany);
290                 }
291             }
292         }
293         return result instanceof Number ? ((Number) result).intValue() : 0;
294     }
295 
296     /**
297      * The 'startsWith' operator implementation.
298      * @param node     the node
299      * @param operator the calling operator, $= or $!
300      * @param left     the left operand
301      * @param right    the right operand
302      * @return true if left starts with right, false otherwise
303      */
304     protected boolean startsWith(final JexlNode node, final String operator, final Object left, final Object right) {
305         final JexlArithmetic arithmetic = interpreter.arithmetic;
306         final JexlUberspect uberspect = interpreter.uberspect;
307         try {
308             // try operator overload
309             final Object result = tryOverload(node, JexlOperator.STARTSWITH, left, right);
310             if (result instanceof Boolean) {
311                 return (Boolean) result;
312             }
313             // use arithmetic / pattern matching ?
314             final Boolean matched = arithmetic.startsWith(left, right);
315             if (matched != null) {
316                 return matched;
317             }
318             // try a startsWith method (duck type)
319             try {
320                 final Object[] argv = {right};
321                 JexlMethod vm = uberspect.getMethod(left, "startsWith", argv);
322                 if (returnsBoolean(vm)) {
323                     return (Boolean) vm.invoke(left, argv);
324                 }
325                 if (arithmetic.narrowArguments(argv)) {
326                     vm = uberspect.getMethod(left, "startsWith", argv);
327                     if (returnsBoolean(vm)) {
328                         return (Boolean) vm.invoke(left, argv);
329                     }
330                 }
331             } catch (final Exception e) {
332                 throw new JexlException(node, operator + " error", e);
333             }
334             // defaults to equal
335             return arithmetic.equals(left, right);
336         } catch (final ArithmeticException xrt) {
337             throw new JexlException(node, operator + " error", xrt);
338         }
339     }
340 
341     /**
342      * Evaluates an assign operator.
343      * <p>
344      * This takes care of finding and caching the operator method when appropriate.
345      * If an overloads returns Operator.ASSIGN, it means the side-effect is complete.
346      * Otherwise, a += b &lt;=&gt; a = a + b
347      * </p>
348      * @param node     the syntactic node
349      * @param operator the operator
350      * @param args     the arguments, the first one being the target of assignment
351      * @return JexlOperator.ASSIGN if operation assignment has been performed,
352      *         JexlEngine.TRY_FAILED if no operation was performed,
353      *         the value to use as the side effect argument otherwise
354      */
355     protected Object tryAssignOverload(final JexlNode node,
356                                        final JexlOperator operator,
357                                        final Consumer<Object> assignFun,
358                                        final Object...args) {
359         final JexlArithmetic arithmetic = interpreter.arithmetic;
360         if (args.length < operator.getArity()) {
361             return JexlEngine.TRY_FAILED;
362         }
363         Object result;
364         try {
365         // if some overloads exist...
366         if (operators != null) {
367             // try to call overload with side effect; the object is modified
368             result = tryOverload(node, operator, arguments(operator, args));
369             if (result != JexlEngine.TRY_FAILED) {
370                 return result; // 1
371             }
372             // try to call base overload (ie + for +=)
373             final JexlOperator base = operator.getBaseOperator();
374             if (base != null && operators.overloads(base)) {
375                 result = tryOverload(node, base, arguments(base, args));
376                 if (result != JexlEngine.TRY_FAILED) {
377                     assignFun.accept(result);
378                     return isPostfix(operator) ? args[0] : result; // 2
379                 }
380             }
381         }
382         // base eval
383         switch (operator) {
384             case SELF_ADD:
385                 result = arithmetic.add(args[0], args[1]);
386                 break;
387             case SELF_SUBTRACT:
388                 result = arithmetic.subtract(args[0], args[1]);
389                 break;
390             case SELF_MULTIPLY:
391                 result = arithmetic.multiply(args[0], args[1]);
392                 break;
393             case SELF_DIVIDE:
394                 result = arithmetic.divide(args[0], args[1]);
395                 break;
396             case SELF_MOD:
397                 result = arithmetic.mod(args[0], args[1]);
398                 break;
399             case SELF_AND:
400                 result = arithmetic.and(args[0], args[1]);
401                 break;
402             case SELF_OR:
403                 result = arithmetic.or(args[0], args[1]);
404                 break;
405             case SELF_XOR:
406                 result = arithmetic.xor(args[0], args[1]);
407                 break;
408             case SELF_SHIFTLEFT:
409                 result = arithmetic.shiftLeft(args[0], args[1]);
410                 break;
411             case SELF_SHIFTRIGHT:
412                 result = arithmetic.shiftRight(args[0], args[1]);
413                 break;
414             case SELF_SHIFTRIGHTU:
415                 result = arithmetic.shiftRightUnsigned(args[0], args[1]);
416                 break;
417             case INCREMENT_AND_GET:
418                 result = arithmetic.increment(args[0]);
419                 break;
420             case DECREMENT_AND_GET:
421                 result = arithmetic.decrement(args[0]);
422                 break;
423             case GET_AND_INCREMENT:
424                 result = args[0];
425                 assignFun.accept(arithmetic.increment(result));
426                 return result; // 3
427             case GET_AND_DECREMENT: {
428                 result = args[0];
429                 assignFun.accept(arithmetic.decrement(result));
430                 return result; // 4
431             }
432             default:
433                 // unexpected, new operator added?
434                 throw new UnsupportedOperationException(operator.getOperatorSymbol());
435             }
436             assignFun.accept(result);
437             return result; // 5
438         } catch (final Exception xany) {
439             interpreter.operatorError(node, operator, xany);
440         }
441         return JexlEngine.TRY_FAILED;
442     }
443 
444     /**
445      * Attempts to call an operator.
446      * <p>
447      *     This performs the null argument control against the strictness of the operator.
448      * </p>
449      * <p>
450      * This takes care of finding and caching the operator method when appropriate.
451      * </p>
452      * @param node     the syntactic node
453      * @param operator the operator
454      * @param args     the arguments
455      * @return the result of the operator evaluation or TRY_FAILED
456      */
457     protected Object tryOverload(final JexlNode node, final JexlOperator operator, final Object... args) {
458         final JexlArithmetic arithmetic = interpreter.arithmetic;
459         controlNullOperands(arithmetic, operator, args);
460         if (operators != null && operators.overloads(operator)) {
461             final boolean cache = interpreter.cache;
462             try {
463                 if (cache) {
464                     final Object cached = node.jjtGetValue();
465                     if (cached instanceof JexlMethod) {
466                         final JexlMethod me = (JexlMethod) cached;
467                         final Object eval = me.tryInvoke(operator.getMethodName(), arithmetic, args);
468                         if (!me.tryFailed(eval)) {
469                             return eval;
470                         }
471                     }
472                 }
473                 final JexlMethod vm = operators.getOperator(operator, args);
474                 if (vm != null && !isArithmetic(vm)) {
475                     final Object result = vm.invoke(arithmetic, args);
476                     if (cache && !vm.tryFailed(result)) {
477                         node.jjtSetValue(vm);
478                     }
479                     return result;
480                 }
481             } catch (final Exception xany) {
482                 // ignore return if lenient, will return try_failed
483                 interpreter.operatorError(node, operator, xany);
484             }
485         }
486         return JexlEngine.TRY_FAILED;
487     }
488 }