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