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}