001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.svl.functions;
018
019import java.math.*;
020import java.util.*;
021import java.util.regex.*;
022
023import org.apache.juneau.commons.svl.*;
024
025/**
026 * Reserved {@code $op.*} operator functions targeted by the compiled expression grammar.
027 *
028 * <p>
029 * Each operator is one {@link VarFunction} registered <i>only</i> under its reserved {@code $op.} name (never
030 * under a plain name).  All are deniable via an allowlist like any other function.  Arguments arrive already
031 * evaluated; short-circuiting ({@code and}, {@code or}, {@code elvis}, {@code ternary}) is the evaluator's job.
032 *
033 * <h2 class='section'>Coercion rules (SpEL-style):</h2>
034 * <ul>
035 *    <li>Numeric tower: {@code Byte/Short/Integer/Long} stay {@code long} (overflow detected with {@code Math.*Exact}
036 *       and promoted to {@link BigDecimal}); any {@code Double/Float} operand gives {@code double} (this wins over
037 *       {@code BigDecimal}); otherwise any {@code BigDecimal}/{@code BigInteger} gives {@link BigDecimal}.
038 *    <li>{@code add} concatenates when either operand is a {@link String}; a {@code null} operand renders as
039 *       {@code "null"}.  {@code sub/mul/div/mod/neg/pos} are numeric only.
040 *    <li>{@code div} of two integral values stays integral only when exact, otherwise {@code double}.  Integral (and
041 *       BigDecimal) division or modulo by zero is an error; by {@code 0.0} follows IEEE 754.
042 *    <li>{@code eq/ne} compare numbers numerically ({@code 1 == 1.0}), otherwise {@link Objects#equals}.
043 *    <li>{@code lt/le/gt/ge} compare numbers numerically or same-class {@link Comparable}s; anything else is an
044 *       error.  A {@code null} operand is an error in strict mode and {@code false} in lenient mode.
045 *    <li>{@code and/or/not/ternary} accept {@link Boolean} or the strings {@code "true"}/{@code "false"}, matched
046 *       case-insensitively (consistent with {@code BooleanFunctions}, whose parser also uses case-insensitive
047 *       matching but additionally accepts 1/yes/on and 0/no/off, which operators deliberately do not).  {@code and}
048 *       and {@code or} validate <i>both</i> operands before combining.
049 *    <li>{@code Long.MIN_VALUE / -1} overflows and is promoted to {@link BigDecimal}; {@code Long.MIN_VALUE % -1} is {@code 0}.
050 *    <li>NaN is unordered: {@code eq} is false, {@code ne} true, and all relational operators false.  {@code 0.0 == -0.0}.
051 *       Long-versus-double comparison is exact (no precision loss).
052 *    <li>{@code add} string concatenation renders doubles via {@link ArithmeticFunctions#render(double)} (integral
053 *       values drop {@code .0}) and BigDecimal via {@code toPlainString()}.
054 *    <li>{@code elvis} is null-only: {@code false}, {@code 0} and {@code ""} are kept.
055 *    <li>{@code matches} uses a bounded shared pattern cache.  There is <b>no match timeout</b>: ReDoS exposure is
056 *       bounded only by the configured limits and the function allowlist.
057 * </ul>
058 */
059@SuppressWarnings({
060   "java:S3776" // Numeric comparison and arithmetic dispatch over every numeric kind in one place
061})
062public final class OpFunctions {
063
064   private OpFunctions() {}
065
066   /** All function classes in this category. */
067   @SuppressWarnings({
068      "java:S2386", // ALL is an immutable compile-time registry; exposed as an array for the cross-package/varargs functions(...) API.
069      "unchecked" // Cast is safe: type verified by caller context.
070   })
071   public static final Class<? extends VarFunction>[] ALL = new Class[] {
072      Add.class, Sub.class, Mul.class, Div.class, Mod.class, Eq.class, Ne.class, Lt.class, Le.class, Gt.class, Ge.class, And.class, Or.class, Not.class, Neg.class, Pos.class, Elvis.class, Ternary.class, Matches.class, Select.class, Project.class
073   };
074
075   /** Base class for operator functions. */
076   abstract static class Op implements VarFunction {
077      private final String name;
078      private final int arity;
079
080      Op(String name, int arity) {
081         this.name = name;
082         this.arity = arity;
083      }
084
085      @Override public String name() { return name; }
086      @Override public int minArity() { return arity; }
087      @Override public int maxArity() { return arity; }
088
089      abstract Object apply(VarResolverSession s, Object a, Object b, Object c);
090
091      @Override
092      public Object invokeValue(VarResolverSession session, List<Object> args) {
093         if (args.size() != arity)
094            throw new VarResolverException("Operator '%s' requires %s argument(s) but got %s", name, arity, args.size());
095         return apply(session, args.get(0), arity > 1 ? args.get(1) : null, arity > 2 ? args.get(2) : null);
096      }
097
098      @Override
099      public String invoke(VarResolverSession session, List<Object> args) {
100         var r = invokeValue(session, args);
101         return r == null ? "" : r.toString();
102      }
103   }
104
105   //-----------------------------------------------------------------------------------------------------------------
106   // Helpers
107   //-----------------------------------------------------------------------------------------------------------------
108
109   private static String typeOf(Object o) {
110      return o == null ? "null" : o.getClass().getSimpleName();
111   }
112
113   private static boolean isLong(Object o) {
114      return o instanceof Long || o instanceof Integer || o instanceof Short || o instanceof Byte;
115   }
116
117   private static boolean isDouble(Object o) {
118      return o instanceof Double || o instanceof Float;
119   }
120
121   private static boolean isBig(Object o) {
122      return o instanceof BigDecimal || o instanceof BigInteger;
123   }
124
125   private static boolean isNum(Object o) {
126      return isLong(o) || isDouble(o) || isBig(o);
127   }
128
129   @SuppressWarnings({
130      "java:S2111" // The exact binary expansion of the double is intended here
131   })
132   private static BigDecimal big(Object o) {
133      if (o instanceof BigDecimal x) return x;
134      if (o instanceof BigInteger x) return new BigDecimal(x);
135      if (isDouble(o)) return new BigDecimal(((Number)o).doubleValue());  // exact binary expansion; callers exclude NaN/Infinity
136      return BigDecimal.valueOf(((Number)o).longValue());
137   }
138
139   private static Number num(String op, Object o) {
140      if (!isNum(o))
141         throw new VarResolverException("Operator '%s' requires a numeric operand but got %s", op, typeOf(o));
142      return (Number)o;
143   }
144
145   static Object add(Object a, Object b) {
146      if (a instanceof String || b instanceof String)
147         return str(a) + str(b);
148      return arith("+", a, b);
149   }
150
151   static Object arith(String op, Object a, Object b) {
152      var x = num(op, a);
153      var y = num(op, b);
154      if (isDouble(x) || isDouble(y))
155         return dbl(op, x.doubleValue(), y.doubleValue());
156      if (isLong(x) && isLong(y))
157         return lng(op, x.longValue(), y.longValue());
158      return bd(op, big(x), big(y));
159   }
160
161   private static Object dbl(String op, double x, double y) {
162      return switch (op) {
163         case "+" -> x + y;
164         case "-" -> x - y;
165         case "*" -> x * y;
166         case "/" -> x / y;
167         default -> x % y;
168      };
169   }
170
171   private static VarResolverException zero(String op) {
172      return new VarResolverException("Operator '%s' %s by zero", op, "/".equals(op) ? "division" : "modulo");
173   }
174
175   private static Object lng(String op, long x, long y) {
176      try {
177         switch (op) {
178            case "+": return Math.addExact(x, y);
179            case "-": return Math.subtractExact(x, y);
180            case "*": return Math.multiplyExact(x, y);
181            case "/":
182               if (y == 0)
183                  throw zero(op);
184               if (x == Long.MIN_VALUE && y == -1) throw new ArithmeticException();
185               return x / y;
186            default:
187               if (y == 0)
188                  throw zero(op);
189               return x % y;
190         }
191      } catch (ArithmeticException e) {
192         return bd(op, BigDecimal.valueOf(x), BigDecimal.valueOf(y));
193      }
194   }
195
196   private static Object bd(String op, BigDecimal x, BigDecimal y) {
197      switch (op) {
198         case "+": return (x.add(y));
199         case "-": return (x.subtract(y));
200         case "*": return (x.multiply(y));
201         case "/":
202            if (y.signum() == 0)
203               throw zero(op);
204            return (x.divide(y, MathContext.DECIMAL128));
205         default:
206            if (y.signum() == 0)
207               throw zero(op);
208            return (x.remainder(y));
209      }
210   }
211
212   static Object neg(Object a) {
213      var x = num("-", a);
214      if (isDouble(x)) return -x.doubleValue();
215      if (isLong(x)) {
216         var l = x.longValue();
217         return l == Long.MIN_VALUE ? BigDecimal.valueOf(l).negate() : (Object)(-l);
218      }
219      return big(x).negate();
220   }
221
222   /** Returned by {@link #numCmp} when either operand is NaN (no ordering exists). */
223   private static final int UNORDERED = 2;
224
225   /**
226    * Numeric comparison: -1, 0, 1, or {@link #UNORDERED}.
227    *
228    * <p>
229    * Two doubles compare with primitive semantics (so {@code 0.0 == -0.0}).  A double against an integral or
230    * BigDecimal value compares exactly through {@link BigDecimal} (no long-to-double precision loss);
231    * infinities are ordered by sign without conversion.
232    */
233   private static int numCmp(Number x, Number y) {
234      if (isDouble(x) && isDouble(y)) {
235         var dx = x.doubleValue();
236         var dy = y.doubleValue();
237         if (Double.isNaN(dx) || Double.isNaN(dy)) return UNORDERED;
238         if (dx < dy) return -1;
239         return dx > dy ? 1 : 0;
240      }
241      if (isDouble(x) || isDouble(y)) {
242         var d = (isDouble(x) ? x : y).doubleValue();
243         if (Double.isNaN(d)) return UNORDERED;
244         if (Double.isInfinite(d)) {
245            var sign = d > 0 ? 1 : -1;
246            return isDouble(x) ? sign : -sign;
247         }
248      }
249      if (isLong(x) && isLong(y))
250         return Long.compare(x.longValue(), y.longValue());
251      return big(x).compareTo(big(y));
252   }
253
254   static boolean eq(Object a, Object b) {
255      if (isNum(a) && isNum(b))
256         return numCmp((Number)a, (Number)b) == 0;
257      return Objects.equals(a, b);
258   }
259
260   @SuppressWarnings({"unchecked", "rawtypes"})
261   static boolean rel(VarResolverSession s, String op, Object a, Object b) {
262      if (a == null || b == null) {
263         if (s != null && s.isStrict())
264            throw new VarResolverException("Operator '%s' cannot compare null operand", op);
265         return false;
266      }
267      int c;
268      if (isNum(a) && isNum(b))
269         c = numCmp((Number)a, (Number)b);
270      else if (a.getClass() == b.getClass() && a instanceof Comparable x)
271         c = x.compareTo(b);
272      else
273         throw new VarResolverException("Operator '%s' cannot compare %s with %s", op, typeOf(a), typeOf(b));
274      if (c == UNORDERED)
275         return false;
276      return switch (op) {
277         case "<" -> c < 0;
278         case "<=" -> c <= 0;
279         case ">" -> c > 0;
280         default -> c >= 0;
281      };
282   }
283
284   static boolean bool(String op, Object o) {
285      if (o instanceof Boolean b) return b;
286      if (o instanceof String s) {
287         if ("true".equalsIgnoreCase(s)) return true;
288         if ("false".equalsIgnoreCase(s)) return false;
289      }
290      throw new VarResolverException("Operator '%s' requires a boolean operand but got %s", op, typeOf(o));
291   }
292
293   /** Validates both operands (no short-circuit; that is the evaluator's job) then combines them. */
294   static boolean both(String op, Object a, Object b, boolean and) {
295      var x = bool(op, a);
296      var y = bool(op, b);
297      return and ? x && y : x || y;
298   }
299
300   /** Scalar string form used by {@code add} concatenation; shares {@link ArithmeticFunctions#render(double)}. */
301   static String str(Object o) {
302      if (isDouble(o)) return ArithmeticFunctions.render(((Number)o).doubleValue());
303      if (o instanceof BigDecimal x) return x.toPlainString();
304      return String.valueOf(o);
305   }
306
307   static boolean matches(Object s, Object regex) {
308      if (!(s instanceof CharSequence cs))
309         throw new VarResolverException("Operator 'matches' requires a string subject but got %s", typeOf(s));
310      if (!(regex instanceof String r))
311         throw new VarResolverException("Operator 'matches' requires a string regex but got %s", typeOf(regex));
312      try {
313         return RegexFunctions.PATTERN_CACHE.get(r).matcher(cs).matches();
314      } catch (PatternSyntaxException e) {
315         throw new VarResolverException(e, "Operator 'matches' has an invalid regex '%s': %s", r, e.getDescription());
316      }
317   }
318
319   /** {@code $op.add}. */
320   public static class Add extends Op {
321      /** Constructor. */
322      public Add() { super("$op.add", 2); }
323      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return add(a, b); }
324   }
325
326   /** {@code $op.sub}. */
327   public static class Sub extends Op {
328      /** Constructor. */
329      public Sub() { super("$op.sub", 2); }
330      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return arith("-", a, b); }
331   }
332
333   /** {@code $op.mul}. */
334   public static class Mul extends Op {
335      /** Constructor. */
336      public Mul() { super("$op.mul", 2); }
337      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return arith("*", a, b); }
338   }
339
340   /** {@code $op.div}. */
341   public static class Div extends Op {
342      /** Constructor. */
343      public Div() { super("$op.div", 2); }
344      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return arith("/", a, b); }
345   }
346
347   /** {@code $op.mod}. */
348   public static class Mod extends Op {
349      /** Constructor. */
350      public Mod() { super("$op.mod", 2); }
351      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return arith("%", a, b); }
352   }
353
354   /** {@code $op.eq}. */
355   public static class Eq extends Op {
356      /** Constructor. */
357      public Eq() { super("$op.eq", 2); }
358      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return eq(a, b); }
359   }
360
361   /** {@code $op.ne}. */
362   public static class Ne extends Op {
363      /** Constructor. */
364      public Ne() { super("$op.ne", 2); }
365      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return !eq(a, b); }
366   }
367
368   /** {@code $op.lt}. */
369   public static class Lt extends Op {
370      /** Constructor. */
371      public Lt() { super("$op.lt", 2); }
372      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return rel(s, "<", a, b); }
373   }
374
375   /** {@code $op.le}. */
376   public static class Le extends Op {
377      /** Constructor. */
378      public Le() { super("$op.le", 2); }
379      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return rel(s, "<=", a, b); }
380   }
381
382   /** {@code $op.gt}. */
383   public static class Gt extends Op {
384      /** Constructor. */
385      public Gt() { super("$op.gt", 2); }
386      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return rel(s, ">", a, b); }
387   }
388
389   /** {@code $op.ge}. */
390   public static class Ge extends Op {
391      /** Constructor. */
392      public Ge() { super("$op.ge", 2); }
393      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return rel(s, ">=", a, b); }
394   }
395
396   /** {@code $op.and}. */
397   public static class And extends Op {
398      /** Constructor. */
399      public And() { super("$op.and", 2); }
400      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return both("and", a, b, true); }
401   }
402
403   /** {@code $op.or}. */
404   public static class Or extends Op {
405      /** Constructor. */
406      public Or() { super("$op.or", 2); }
407      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return both("or", a, b, false); }
408   }
409
410   /** {@code $op.not}. */
411   public static class Not extends Op {
412      /** Constructor. */
413      public Not() { super("$op.not", 1); }
414      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return !bool("not", a); }
415   }
416
417   /** {@code $op.neg}. */
418   public static class Neg extends Op {
419      /** Constructor. */
420      public Neg() { super("$op.neg", 1); }
421      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return neg(a); }
422   }
423
424   /** {@code $op.pos}. */
425   public static class Pos extends Op {
426      /** Constructor. */
427      public Pos() { super("$op.pos", 1); }
428      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return num("+", a); }
429   }
430
431   /** {@code $op.elvis}. */
432   public static class Elvis extends Op {
433      /** Constructor. */
434      public Elvis() { super("$op.elvis", 2); }
435      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return a != null ? a : b; }
436   }
437
438   /** {@code $op.ternary}. */
439   public static class Ternary extends Op {
440      /** Constructor. */
441      public Ternary() { super("$op.ternary", 3); }
442      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return bool("?:", a) ? b : c; }
443   }
444
445   /** {@code $op.matches}. */
446   public static class Matches extends Op {
447      /** Constructor. */
448      public Matches() { super("$op.matches", 2); }
449      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { return matches(a, b); }
450   }
451
452   /** Registered stub for the evaluator-only {@code $op.select} operator.
453    *
454    * <p>
455    * Exists so the name can be allowed or denied by an allowlist.  The evaluator must call
456    * {@code session.findFunction("$op.select")} and fail like a denied function before iterating; invoking it
457    * directly always throws {@link UnsupportedOperationException}. */
458   public static class Select extends Op {
459      /** Constructor. */
460      public Select() { super("$op.select", 2); }
461      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { throw new UnsupportedOperationException("evaluator-only"); }
462   }
463
464   /** Registered stub for the evaluator-only {@code $op.project} operator.
465    *
466    * <p>
467    * Exists so the name can be allowed or denied by an allowlist.  The evaluator must call
468    * {@code session.findFunction("$op.project")} and fail like a denied function before iterating; invoking it
469    * directly always throws {@link UnsupportedOperationException}. */
470   public static class Project extends Op {
471      /** Constructor. */
472      public Project() { super("$op.project", 2); }
473      @Override Object apply(VarResolverSession s, Object a, Object b, Object c) { throw new UnsupportedOperationException("evaluator-only"); }
474   }
475}