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    *      https://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.util.Collections;
20  import java.util.EnumSet;
21  import java.util.Set;
22  import java.util.function.Consumer;
23  
24  import org.apache.commons.jexl3.JexlArithmetic;
25  import org.apache.commons.jexl3.JexlCache;
26  import org.apache.commons.jexl3.JexlEngine;
27  import org.apache.commons.jexl3.JexlException;
28  import org.apache.commons.jexl3.JexlOperator;
29  import org.apache.commons.jexl3.internal.introspection.MethodExecutor;
30  import org.apache.commons.jexl3.internal.introspection.MethodKey;
31  import org.apache.commons.jexl3.introspection.JexlMethod;
32  import org.apache.commons.jexl3.introspection.JexlUberspect;
33  import org.apache.commons.jexl3.parser.JexlNode;
34  
35  /**
36   * Helper class to deal with operator overloading and specifics.
37   *
38   * @since 3.0
39   */
40  public final class Operator implements JexlOperator.Uberspect {
41      private static final String METHOD_IS_EMPTY = "isEmpty";
42      private static final String METHOD_SIZE = "size";
43      private static final String METHOD_CONTAINS = "contains";
44      private static final String METHOD_STARTS_WITH = "startsWith";
45      private static final String METHOD_ENDS_WITH = "endsWith";
46  
47      /**
48       * The comparison operators.
49       * <p>Used to determine if a compare method overload might be used.</p>
50       */
51      private static final Set<JexlOperator> CMP_OPS =
52              EnumSet.of(JexlOperator.GT, JexlOperator.LT, JexlOperator.EQ, JexlOperator.GTE, JexlOperator.LTE);
53  
54      /**
55       * The postfix operators.
56       * <p>Used to determine the returned value in assignment.</p>
57       */
58      private static final Set<JexlOperator> POSTFIX_OPS =
59              EnumSet.of(JexlOperator.GET_AND_INCREMENT, JexlOperator.GET_AND_DECREMENT);
60  
61      /** The uberspect. */
62      private final JexlUberspect uberspect;
63  
64      /** The arithmetic instance being analyzed. */
65      private final JexlArithmetic arithmetic;
66  
67      /** The set of overloaded operators. */
68      private final Set<JexlOperator> overloads;
69  
70      /** The delegate if built as a 3.4 legacy. */
71      private final JexlArithmetic.Uberspect delegate;
72  
73      /** Caching state: -1 unknown, 0 false, 1 true. */
74      private volatile int caching = -1;
75  
76      /**
77       * Creates an instance.
78       * <p>Mostly used as a compatibility measure by delegating instead of extending.</p>
79       *
80       * @param theUberspect  the uberspect instance
81       * @param theArithmetic the arithmetic instance used to delegate operator overloads
82       */
83      public Operator(final JexlUberspect theUberspect, final JexlArithmetic theArithmetic) {
84          this.uberspect = theUberspect;
85          this.arithmetic = theArithmetic;
86          this.overloads = Collections.emptySet();
87          this.delegate = theUberspect.getArithmetic(theArithmetic);
88      }
89  
90      /**
91       * Creates an instance.
92       *
93       * @param theUberspect the uberspect instance
94       * @param theArithmetic the arithmetic instance
95       * @param theOverloads  the overloaded operators
96       */
97      public Operator(final JexlUberspect theUberspect,
98                      final JexlArithmetic theArithmetic,
99                      final Set<JexlOperator> theOverloads) {
100         this(theUberspect, theArithmetic, theOverloads, -1);
101     }
102 
103     /**
104      * Creates an instance.
105      *
106      * @param theUberspect the uberspect instance
107      * @param theArithmetic the arithmetic instance
108      * @param theOverloads  the overloaded operators
109      * @param theCache the caching state
110      */
111     public Operator(final JexlUberspect theUberspect,
112                     final JexlArithmetic theArithmetic,
113                     final Set<JexlOperator> theOverloads,
114                     final int theCache) {
115         this.uberspect = theUberspect;
116         this.arithmetic = theArithmetic;
117         this.overloads = theOverloads;
118         this.delegate = null;
119         this.caching = theCache;
120     }
121 
122     @Override
123     public JexlMethod getOperator(final JexlOperator operator, final Object... args) {
124         if (delegate != null) {
125             return delegate.getOperator(operator, args);
126         }
127         if (overloads.contains(operator) && args != null && args.length == operator.getArity()) {
128             return uberspectOperator(arithmetic, operator, args);
129         }
130         return null;
131     }
132 
133     @Override
134     public boolean overloads(final JexlOperator operator) {
135         return delegate != null
136             ? delegate.overloads(operator)
137             : overloads.contains(operator);
138     }
139 
140     /**
141      * @return whether caching is enabled in the engine
142      */
143     private boolean isCaching() {
144         int c = caching;
145         if (c < 0) {
146             synchronized(this) {
147                 c = caching;
148                 if (c < 0) {
149                     final JexlEngine jexl = JexlEngine.getThreadEngine();
150                     caching = c = jexl instanceof Engine && ((Engine) jexl).cache != null ? 1 : 0;
151                 }
152             }
153         }
154         return c > 0;
155     }
156 
157     /**
158      * Tidy arguments based on operator arity.
159      * <p>The interpreter may add a null to the arguments of operator expecting only one parameter.</p>
160      *
161      * @param operator the operator
162      * @param args the arguments (as seen by the interpreter)
163      * @return the tidied arguments
164      */
165     private Object[] arguments(final JexlOperator operator, final Object...args) {
166         return operator.getArity() == 1 && args.length > 1 ? new Object[]{args[0]} : args;
167     }
168 
169     /**
170      * Attempts finding a method in left and eventually narrowing right.
171      *
172      * @param methodName the method name
173      * @param right the left argument in the operator
174      * @param left the right argument in the operator
175      * @return a boolean is call was possible, null otherwise
176      * @throws Exception if invocation fails
177      */
178     private Boolean booleanDuckCall(final String methodName, final Object left, final Object right) throws Exception {
179         JexlMethod vm = uberspect.getMethod(left, methodName, right);
180         if (returnsBoolean(vm)) {
181             return (Boolean) vm.invoke(left, right);
182         }
183         final Object[] argv = { right };
184         if (arithmetic.narrowArguments(argv)) {
185             vm = uberspect.getMethod(left, methodName, argv);
186             if (returnsBoolean(vm)) {
187                 return (Boolean) vm.invoke(left, argv);
188             }
189         }
190         return null;
191     }
192 
193     /**
194      * Throw a NPE if operator is strict and one of the arguments is null.
195      *
196      * @param arithmetic the JEXL arithmetic instance
197      * @param operator the operator to check
198      * @param args the operands
199      * @throws JexlArithmetic.NullOperand if operator is strict and an operand is null
200      */
201      private void controlNullOperands(final JexlArithmetic arithmetic, final JexlOperator operator, final Object...args) {
202         for (final Object arg : args) {
203             // only check operator if necessary
204             if (arg == null) {
205                 // check operator only once if it is not strict
206                 if (arithmetic.isStrict(operator)) {
207                     throw new JexlArithmetic.NullOperand();
208                 }
209                 break;
210             }
211         }
212     }
213 
214     /**
215      * Triggered when an operator fails.
216      *
217      * @param ref     the node where the error originated from
218      * @param operator the operator symbol
219      * @param cause    the cause of error (if any)
220      * @param alt      what to return if not strict
221      * @param <T>      the return type
222      * @return throws JexlException if strict and not silent, null otherwise
223      */
224     private <T> T operatorError(final JexlCache.Reference ref, final JexlOperator operator, final Throwable cause, final T alt) {
225         final JexlNode node = ref instanceof JexlNode ? (JexlNode) ref : null;
226         final Engine engine = (Engine) JexlEngine.getThreadEngine();
227         if (engine == null || engine.isStrict()) {
228             throw new JexlException.Operator(node, operator.getOperatorSymbol(), cause);
229         }
230         if (engine.logger.isDebugEnabled()) {
231             engine.logger.debug(JexlException.operatorError(node, operator.getOperatorSymbol()), cause);
232         }
233         return alt;
234     }
235 
236     /**
237      * Seeks an implementation of an operator method in an arithmetic instance.
238      * <p>Method must <em><>not/em belong to JexlArithmetic</p>
239      *
240      * @param arithmetic the arithmetic instance
241      * @param operator the operator
242      * @param args the arguments
243      * @return a JexlMethod instance or null
244      */
245     private JexlMethod uberspectOperator(final JexlArithmetic arithmetic,
246                                        final JexlOperator operator,
247                                        final Object... args) {
248         final JexlMethod me = uberspect.getMethod(arithmetic, operator.getMethodName(), args);
249         if (!(me instanceof MethodExecutor) ||
250             !JexlArithmetic.class.equals(((MethodExecutor) me).getMethod().getDeclaringClass())) {
251             return me;
252         }
253         return null;
254     }
255 
256     /**
257      * Checks whether a method returns a boolean or a Boolean.
258      *
259      * @param vm the JexlMethod (can be null)
260      * @return true of false
261      */
262     private boolean returnsBoolean(final JexlMethod vm) {
263         if (vm != null) {
264             final Class<?> rc = vm.getReturnType();
265             return Boolean.TYPE.equals(rc) || Boolean.class.equals(rc);
266         }
267         return false;
268     }
269 
270     /**
271      * Checks whether a method returns an int or an Integer.
272      *
273      * @param vm the JexlMethod (can be null)
274      * @return true of false
275      */
276     private boolean returnsInteger(final JexlMethod vm) {
277         if (vm != null) {
278             final Class<?> rc = vm.getReturnType();
279             return Integer.TYPE.equals(rc) || Integer.class.equals(rc);
280         }
281         return false;
282     }
283 
284     @Override
285     public Object empty(final JexlCache.Reference node, final Object object) {
286         if (object == null) {
287             return true;
288         }
289         Object result = overloads(JexlOperator.EMPTY)
290                 ? tryOverload(node, JexlOperator.EMPTY, object)
291                 : JexlEngine.TRY_FAILED;
292         if (result == JexlEngine.TRY_FAILED) {
293             result = arithmetic.isEmpty(object, null);
294             if (result == null) {
295                 result = false;
296                 // check if there is an isEmpty method on the object that returns a
297                 // boolean and if so, just use it
298                 final JexlMethod vm = uberspect.getMethod(object, METHOD_IS_EMPTY, InterpreterBase.EMPTY_PARAMS);
299                 if (returnsBoolean(vm)) {
300                     try {
301                         result = vm.invoke(object, InterpreterBase.EMPTY_PARAMS);
302                     } catch (final Exception any) {
303                         return operatorError(node, JexlOperator.EMPTY, any, false);
304                     }
305                 }
306             }
307         }
308         return result;
309     }
310 
311     @Override
312     public Object size(final JexlCache.Reference node, final Object object) {
313         if (object == null) {
314             return 0;
315         }
316         Object result = overloads(JexlOperator.SIZE)
317                 ? tryOverload(node, JexlOperator.SIZE, object)
318                 : JexlEngine.TRY_FAILED;
319         if (result == JexlEngine.TRY_FAILED) {
320             result = arithmetic.size(object, null);
321             if (result == null) {
322                 // check if there is a size method on the object that returns an
323                 // integer and if so, just use it
324                 final JexlMethod vm = uberspect.getMethod(object, METHOD_SIZE, InterpreterBase.EMPTY_PARAMS);
325                 if (returnsInteger(vm)) {
326                     try {
327                         result = vm.invoke(object, InterpreterBase.EMPTY_PARAMS);
328                     } catch (final Exception any) {
329                         return operatorError(node, JexlOperator.SIZE, any, 0);
330                     }
331                 }
332             }
333         }
334         return result instanceof Number ? ((Number) result).intValue() : 0;
335     }
336 
337     @Override
338     public boolean contains(final JexlCache.Reference node, final JexlOperator operator, final Object left, final Object right) {
339         final boolean contained;
340         try {
341             // try operator overload
342             final Object result = overloads(JexlOperator.CONTAINS)
343                     ? tryOverload(node, JexlOperator.CONTAINS, left, right)
344                     : null;
345             if (result instanceof Boolean) {
346                 contained = (Boolean) result;
347             } else {
348                 // use arithmetic / pattern matching ?
349                 final Boolean matched = arithmetic.contains(left, right);
350                 if (matched != null) {
351                     contained = matched;
352                 } else {
353                     // try a left.contains(right) method
354                     final Boolean duck = booleanDuckCall(METHOD_CONTAINS, left, right);
355                     if (duck != null) {
356                         contained = duck;
357                     } else {
358                         // defaults to equal
359                         contained = arithmetic.equals(left, right);
360                     }
361                 }
362             }
363             // not-contains is !contains
364             return JexlOperator.CONTAINS == operator == contained;
365         } catch (final Exception any) {
366             return operatorError(node, operator, any, false);
367         }
368     }
369 
370     @Override
371     public boolean startsWith(final JexlCache.Reference node, final JexlOperator operator, final Object left, final Object right) {
372         final boolean starts;
373         try {
374             // try operator overload
375             final Object result = overloads(JexlOperator.STARTSWITH)
376                     ? tryOverload(node, JexlOperator.STARTSWITH, left, right)
377                     : null;
378             if (result instanceof Boolean) {
379                 starts = (Boolean) result;
380             } else {
381                 // use arithmetic / pattern matching ?
382                 final Boolean matched = arithmetic.startsWith(left, right);
383                 if (matched != null) {
384                     starts = matched;
385                 } else {
386                     // try a left.startsWith(right) method
387                     final Boolean duck = booleanDuckCall(METHOD_STARTS_WITH, left, right);
388                     if (duck != null) {
389                         starts = duck;
390                     } else {
391                         // defaults to equal
392                         starts = arithmetic.equals(left, right);
393                     }
394                 }
395             }
396             // not-startswith is !starts-with
397             return JexlOperator.STARTSWITH == operator == starts;
398         } catch (final Exception any) {
399             return operatorError(node, operator, any, false);
400         }
401     }
402 
403     @Override
404     public boolean endsWith(final JexlCache.Reference node, final JexlOperator operator, final Object left, final Object right) {
405         try {
406             final boolean ends;
407             // try operator overload
408             final Object result = overloads(JexlOperator.ENDSWITH)
409                 ? tryOverload(node, JexlOperator.ENDSWITH, left, right)
410                 : null;
411             if (result instanceof Boolean) {
412                 ends = (Boolean) result;
413             } else {
414                 // use arithmetic / pattern matching ?
415                 final Boolean matched = arithmetic.endsWith(left, right);
416                 if (matched != null) {
417                     ends = matched;
418                 } else {
419                     // try a left.endsWith(right) method
420                     final Boolean duck = booleanDuckCall(METHOD_ENDS_WITH, left, right);
421                     if (duck != null) {
422                         ends = duck;
423                     } else {
424                         // defaults to equal
425                         ends = arithmetic.equals(left, right);
426                     }
427                 }
428             }
429             // not-endswith is !ends-with
430             return JexlOperator.ENDSWITH == operator == ends;
431         } catch (final Exception any) {
432             return operatorError(node, operator, any, false);
433         }
434     }
435 
436     @Override
437     public Object tryAssignOverload(final JexlCache.Reference node,
438                              final JexlOperator operator,
439                              final Consumer<Object> assignFun,
440                              final Object... args) {
441         if (args.length < operator.getArity()) {
442             return JexlEngine.TRY_FAILED;
443         }
444         Object result = JexlEngine.TRY_FAILED;
445         try {
446             // if the operator is strict, the left-hand side can not be null
447             controlNullOperands(arithmetic, operator, args[0]);
448             // attempt assignment operator overload
449             if (overloads(operator)) {
450                 result = tryOverload(node, operator, arguments(operator, args));
451                 if (result != JexlEngine.TRY_FAILED) {
452                     return result;
453                 }
454             }
455             // let's attempt base operator overload
456             final JexlOperator base = operator.getBaseOperator();
457             if (base != null && overloads(base)) {
458                 result = tryOverload(node, base, arguments(base, args));
459             }
460             // no overload or overload failed, use base operator
461             if (result == JexlEngine.TRY_FAILED) {
462                 result = performBaseOperation(operator, args);
463             }
464             // on success, assign value
465             if (result != JexlEngine.TRY_FAILED) {
466                 assignFun.accept(result);
467                 // postfix implies return initial argument value
468                 if (POSTFIX_OPS.contains(operator)) {
469                     result = args[0];
470                 }
471             }
472             return result;
473         } catch (final Exception any) {
474             return operatorError(node, operator, any, JexlEngine.TRY_FAILED);
475         }
476     }
477 
478     /**
479      * Performs the base operation of an assignment.
480      *
481      * @param operator the operator
482      * @param args the arguments
483      * @return the result
484      */
485     private Object performBaseOperation(final JexlOperator operator, final Object... args) {
486         switch (operator) {
487             case SELF_ADD: return arithmetic.add(args[0], args[1]);
488             case SELF_SUBTRACT: return arithmetic.subtract(args[0], args[1]);
489             case SELF_MULTIPLY: return arithmetic.multiply(args[0], args[1]);
490             case SELF_DIVIDE: return arithmetic.divide(args[0], args[1]);
491             case SELF_MOD: return arithmetic.mod(args[0], args[1]);
492             case SELF_AND: return arithmetic.and(args[0], args[1]);
493             case SELF_OR: return arithmetic.or(args[0], args[1]);
494             case SELF_XOR: return arithmetic.xor(args[0], args[1]);
495             case SELF_SHIFTLEFT: return arithmetic.shiftLeft(args[0], args[1]);
496             case SELF_SHIFTRIGHT: return arithmetic.shiftRight(args[0], args[1]);
497             case SELF_SHIFTRIGHTU: return arithmetic.shiftRightUnsigned(args[0], args[1]);
498             case INCREMENT_AND_GET:
499             case GET_AND_INCREMENT:
500                 return arithmetic.increment(args[0]);
501             case DECREMENT_AND_GET:
502             case GET_AND_DECREMENT:
503                 return arithmetic.decrement(args[0]);
504             default:
505                 throw new UnsupportedOperationException(operator.getOperatorSymbol());
506         }
507     }
508 
509     @Override
510     public Object tryOverload(final JexlCache.Reference node, final JexlOperator operator, final Object... args) {
511         controlNullOperands(arithmetic, operator, args);
512         try {
513             return tryEval(isCaching() ? node : null, operator, args);
514         } catch (final Exception any) {
515             // ignore return if lenient, will return try_failed
516             return operatorError(node, operator, any, JexlEngine.TRY_FAILED);
517         }
518     }
519 
520     /**
521      * Tries operator evaluation, handles method resolution caching.
522      *
523      * @param node the node
524      * @param operator the operator
525      * @param args the operator arguments
526      * @return the operator evaluation result or TRY_FAILED
527      */
528     private Object tryEval(final JexlCache.Reference node, final JexlOperator operator, final Object...args) {
529         if (node != null) {
530             final Object cached = node.getCache();
531             if (cached instanceof JexlMethod) {
532                 // we found a method on previous call; try and reuse it (*1)
533                 final JexlMethod me = (JexlMethod) cached;
534                 final Object eval = me.tryInvoke(operator.getMethodName(), arithmetic, args);
535                 if (!me.tryFailed(eval)) {
536                     return eval;
537                 }
538             } else if (cached instanceof MethodKey) {
539                 // check for a fail-fast, we tried to find an overload before but could not (*2)
540                 final MethodKey cachedKey = (MethodKey) cached;
541                 final MethodKey key = new MethodKey(operator.getMethodName(), args);
542                 if (key.equals(cachedKey)) {
543                     return JexlEngine.TRY_FAILED;
544                 }
545             }
546         }
547         // trying to find an operator overload
548         JexlMethod vm = getOperator(operator, args);
549         // no direct overload, any special case ?
550         if (vm == null) {
551             vm = getAlternateOverload(operator, args);
552         }
553         // *1: found a method, try it and cache it if successful
554         if (vm != null) {
555             final Object result = vm.tryInvoke(operator.getMethodName(), arithmetic, args);
556             if (node != null && !vm.tryFailed(result)) {
557                 node.setCache(vm);
558             }
559             return result;
560         }
561         if (node != null) {
562             // *2: could not find an overload for this operator and arguments, keep track of the fail
563             final MethodKey key = new MethodKey(operator.getMethodName(), args);
564             node.setCache(key);
565         }
566         return JexlEngine.TRY_FAILED;
567     }
568 
569     /**
570      * Special handling of overloads where another attempt at finding a method may be attempted.
571      * <p>As of 3.5.0, only the comparison operators attempting to use compare() are handled.</p>
572      *
573      * @param operator the operator
574      * @param args the arguments
575      * @return an instance or null
576      */
577     private JexlMethod getAlternateOverload(final JexlOperator operator, final Object... args) {
578         // comparison operators may use the compare overload in derived arithmetic
579         if (CMP_OPS.contains(operator) && args.length == 2) {
580             JexlMethod cmp = getOperator(JexlOperator.COMPARE, args);
581             if (cmp != null) {
582                 return new Operator.CompareMethod(operator, cmp);
583             }
584             cmp = getOperator(JexlOperator.COMPARE, args[1], args[0]);
585             if (cmp != null) {
586                 return new Operator.AntiCompareMethod(operator, cmp);
587             }
588         }
589         return null;
590     }
591 
592     /**
593      * Delegates a comparison operator to a compare method.
594      * The expected signature of the derived JexlArithmetic method is:
595      * int compare(L left, R right);
596      */
597     private static class CompareMethod implements JexlMethod {
598         protected final JexlOperator operator;
599         protected final JexlMethod compare;
600 
601         CompareMethod(final JexlOperator op, final JexlMethod m) {
602             operator = op;
603             compare = m;
604         }
605 
606         @Override
607         public Class<?> getReturnType() {
608             return Boolean.TYPE;
609         }
610 
611         @Override
612         public Object invoke(final Object arithmetic, final Object... params) throws Exception {
613             return operate((int) compare.invoke(arithmetic, params));
614         }
615 
616         @Override
617         public boolean isCacheable() {
618             return true;
619         }
620 
621         @Override
622         public boolean tryFailed(final Object rval) {
623             return rval == JexlEngine.TRY_FAILED;
624         }
625 
626         @Override
627         public Object tryInvoke(final String name, final Object arithmetic, final Object... params) throws JexlException.TryFailed {
628             final Object cmp = compare.tryInvoke(JexlOperator.COMPARE.getMethodName(), arithmetic, params);
629             return cmp instanceof Integer? operate((int) cmp) : JexlEngine.TRY_FAILED;
630         }
631 
632         /**
633          * From compare() int result to operator boolean result.
634          *
635          * @param cmp the compare result
636          * @return the operator applied
637          */
638         protected boolean operate(final int cmp) {
639             switch(operator) {
640                 case EQ: return cmp == 0;
641                 case LT: return cmp < 0;
642                 case LTE: return cmp <= 0;
643                 case GT: return cmp > 0;
644                 case GTE: return cmp >= 0;
645                 default:
646                     throw new ArithmeticException("unexpected operator " + operator);
647             }
648         }
649     }
650 
651     /**
652      * The reverse compare swaps left and right arguments and exploits the symmetry
653      * of compare as in: compare(a, b) &lt=&gt -compare(b, a)
654      */
655     private static class AntiCompareMethod extends CompareMethod {
656         AntiCompareMethod(final JexlOperator op, final JexlMethod m) {
657             super(op, m);
658         }
659 
660         @Override
661         public Object invoke(final Object arithmetic, final Object... params) throws Exception {
662             return operate(-(int) compare.invoke(arithmetic, params[1], params[0]));
663         }
664 
665         @Override
666         public Object tryInvoke(final String name, final Object arithmetic, final Object... params) throws JexlException.TryFailed {
667             final Object cmp = compare.tryInvoke(JexlOperator.COMPARE.getMethodName(), arithmetic, params[1], params[0]);
668             return cmp instanceof Integer? operate(-Integer.signum((Integer) cmp)) : JexlEngine.TRY_FAILED;
669         }
670     }
671 }