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  
18  package org.apache.commons.jexl3;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertFalse;
22  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
23  import static org.junit.jupiter.api.Assertions.assertNotNull;
24  import static org.junit.jupiter.api.Assertions.assertThrows;
25  import static org.junit.jupiter.api.Assertions.assertTrue;
26  import static org.junit.jupiter.api.Assertions.fail;
27  
28  import java.io.StringWriter;
29  import java.math.MathContext;
30  import java.text.DecimalFormat;
31  import java.text.SimpleDateFormat;
32  import java.time.Instant;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.Calendar;
36  import java.util.Comparator;
37  import java.util.Date;
38  import java.util.HashMap;
39  import java.util.HashSet;
40  import java.util.Iterator;
41  import java.util.List;
42  import java.util.Locale;
43  import java.util.Map;
44  import java.util.Set;
45  import java.util.SortedSet;
46  import java.util.TimeZone;
47  import java.util.TreeSet;
48  import java.util.regex.Pattern;
49  
50  import org.apache.commons.jexl3.junit.Asserter;
51  import org.junit.jupiter.api.BeforeEach;
52  import org.junit.jupiter.api.Test;
53  
54  /**
55   * Tests for the startsWith, endsWith, match and range operators.
56   */
57  @SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"})
58  class ArithmeticOperatorTest extends JexlTestCase {
59      public static class Aggregate {
60          public static int sum(final Iterable<Integer> ii) {
61              int sum = 0;
62              for(final Integer i : ii) {
63                  sum += i;
64              }
65              return sum;
66          }
67          private Aggregate() {}
68      }
69      public static class DateArithmetic extends JexlArithmetic {
70          DateArithmetic(final boolean flag) {
71              super(flag);
72          }
73  
74          public Object arrayGet(final Date date, final String identifier) {
75              return getDateValue(date, identifier);
76          }
77  
78          public Object arraySet(final Date date, final String identifier, final Object value) throws Exception {
79              return setDateValue(date, identifier, value);
80          }
81  
82          protected Object getDateValue(final Date date, final String key) {
83              try {
84                  final Calendar cal = Calendar.getInstance(UTC);
85                  cal.setTime(date);
86                  switch (key) {
87                  case "yyyy":
88                      return cal.get(Calendar.YEAR);
89                  case "MM":
90                      return cal.get(Calendar.MONTH) + 1;
91                  case "dd":
92                      return cal.get(Calendar.DAY_OF_MONTH);
93                  default:
94                      break;
95                  }
96                  // Otherwise treat as format mask
97                  final SimpleDateFormat df = new SimpleDateFormat(key); //, dfs);
98                  return df.format(date);
99  
100             } catch (final Exception ex) {
101                 return null;
102             }
103         }
104 
105         public Date multiply(final Date d0, final Date d1) {
106             throw new ArithmeticException("unsupported");
107         }
108 
109         public Date now() {
110             return new Date(System.currentTimeMillis());
111         }
112 
113         public Object propertyGet(final Date date, final String identifier) {
114             return getDateValue(date, identifier);
115         }
116 
117         public Object propertySet(final Date date, final String identifier, final Object value) throws Exception {
118             return setDateValue(date, identifier, value);
119         }
120 
121         protected Object setDateValue(final Date date, final String key, final Object value) {
122             final Calendar cal = Calendar.getInstance(UTC);
123             cal.setTime(date);
124             switch (key) {
125             case "yyyy":
126                 cal.set(Calendar.YEAR, toInteger(value));
127                 break;
128             case "MM":
129                 cal.set(Calendar.MONTH, toInteger(value) - 1);
130                 break;
131             case "dd":
132                 cal.set(Calendar.DAY_OF_MONTH, toInteger(value));
133                 break;
134             default:
135                 break;
136             }
137             date.setTime(cal.getTimeInMillis());
138             return date;
139         }
140     }
141 
142     public static class DateContext extends MapContext {
143         private Locale locale = Locale.US;
144 
145         public String format(final Date date, final String fmt) {
146             final SimpleDateFormat sdf = new SimpleDateFormat(fmt, locale);
147             sdf.setTimeZone(UTC);
148             return sdf.format(date);
149         }
150 
151         public String format(final Number number, final String fmt) {
152             return new DecimalFormat(fmt).format(number);
153         }
154 
155         void setLocale(final Locale l10n) {
156             this.locale = l10n;
157         }
158     }
159 
160     public static class IterableContainer implements Iterable<Integer> {
161         private final SortedSet<Integer> values;
162 
163         public IterableContainer(final int[] is) {
164             values = new TreeSet<>();
165             for (final int value : is) {
166                 values.add(value);
167             }
168         }
169 
170         public boolean contains(final int i) {
171             return values.contains(i);
172         }
173 
174         public boolean contains(final int[] i) {
175             for(final int ii : i) {
176                 if (!values.contains(ii)) {
177                     return false;
178                 }
179             }
180             return true;
181         }
182 
183         public boolean endsWith(final int i) {
184             return values.last().equals(i);
185         }
186 
187         public boolean endsWith(final int[] i) {
188             final SortedSet<Integer> sw =  values.tailSet(values.size() - i.length);
189             int n = 0;
190             for (final Integer value : sw) {
191                 if (!value.equals(i[n++])) {
192                     return false;
193                 }
194             }
195             return true;
196         }
197 
198         @Override
199         public Iterator<Integer> iterator() {
200             return values.iterator();
201         }
202 
203         public boolean startsWith(final int i) {
204             return values.first().equals(i);
205         }
206         public boolean startsWith(final int[] i) {
207             final SortedSet<Integer> sw = values.headSet(i.length);
208             int n = 0;
209             for (final Integer value : sw) {
210                 if (!value.equals(i[n++])) {
211                     return false;
212                 }
213             }
214             return true;
215         }
216     }
217 
218     public static class MatchingContainer {
219         private final Set<Integer> values;
220 
221         public MatchingContainer(final int[] is) {
222             values = new HashSet<>();
223             for (final int value : is) {
224                 values.add(value);
225             }
226         }
227 
228         public boolean contains(final int value) {
229             return values.contains(value);
230         }
231     }
232 
233     private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
234 
235     private Asserter asserter;
236 
237     /**
238      * Create the named test.
239      */
240     public ArithmeticOperatorTest() {
241         super("ArithmeticOperatorTest");
242     }
243 
244     @BeforeEach
245     @Override
246     public void setUp() {
247         asserter = new Asserter(JEXL);
248         asserter.setStrict(false);
249     }
250 
251     @Test void test373a() {
252         testSelfAssignOperators("y.add(x++)", 42, 42, 43);
253     }
254 
255     @Test void test373b() {
256         testSelfAssignOperators("y.add(++x)", 42, 43, 43);
257     }
258 
259     @Test void test373c() {
260         testSelfAssignOperators("y.add(x--)", 42, 42, 41);
261     }
262 
263     @Test void test373d() {
264         testSelfAssignOperators("y.add(--x)", 42, 41, 41);
265     }
266 
267     @Test
268     void test391() throws Exception {
269         // with literals
270         for(final String src : Arrays.asList(
271                 "2 =~ [1, 2, 3, 4]",
272                 "[2, 3] =~ [1, 2, 3, 4]",
273                 "[2, 3,...] =~ [1, 2, 3, 4]",
274                 "3 =~ [1, 2, 3, 4,...]",
275                 "[2, 3] =~ [1, 2, 3, 4,...]",
276                 "[2, 3,...] =~ [1, 2, 3, 4,...]")) {
277             asserter.assertExpression(src, Boolean.TRUE);
278         }
279         // with variables
280         final int[] ic = {1, 2,  3, 4};
281         final List<Integer> iic = new ArrayList<>();
282         for(final int v : ic) { iic.add(v); }
283         final int[] iv = {2, 3};
284         final List<Integer> iiv = new ArrayList<>();
285         for(final int v : iv) { iiv.add(v); }
286         final String src = "(x,y) -> x =~ y ";
287         for(final Object v : Arrays.asList(iv, iiv, 2)) {
288             for(final Object c : Arrays.asList(ic, iic)) {
289                 asserter.assertExpression(src, Boolean.TRUE, v, c);
290             }
291         }
292     }
293 
294     @Test
295     void testDateArithmetic() {
296         final Date d = new Date();
297         final JexlContext jc = new MapContext();
298         final JexlEngine jexl = new JexlBuilder().cache(32).arithmetic(new DateArithmetic(true)).create();
299         final JexlScript expr0 = jexl.createScript("date.yyyy = 1969; date.MM=7; date.dd=20; ", "date");
300         Object value0 = expr0.execute(jc, d);
301         assertNotNull(value0);
302         value0 = d;
303         //d = new Date();
304         assertEquals(1969, jexl.createScript("date.yyyy", "date").execute(jc, value0));
305         assertEquals(7, jexl.createScript("date.MM", "date").execute(jc, value0));
306         assertEquals(20, jexl.createScript("date.dd", "date").execute(jc, value0));
307     }
308 
309     @Test
310     void testFormatArithmetic() {
311         final Calendar cal = Calendar.getInstance(UTC);
312         cal.set(1969, Calendar.AUGUST, 20);
313         final Date x0 = cal.getTime();
314         final String y0 =  "MM/yy/dd";
315         final Number x1 = 42.12345;
316         final String y1 = "##0.##";
317         final DateContext jc = new DateContext();
318         final JexlEngine jexl = new JexlBuilder().cache(32).arithmetic(new DateArithmetic(true)).create();
319         final JexlScript expr0 = jexl.createScript("x.format(y)", "x", "y");
320         Object value10 = expr0.execute(jc, x0, y0);
321         final Object value20 = expr0.execute(jc, x0, y0);
322         assertEquals(value10, value20);
323         Object value11 = expr0.execute(jc, x1, y1);
324         final Object value21 = expr0.execute(jc, x1, y1);
325         assertEquals(value11, value21);
326         value10 = expr0.execute(jc, x0, y0);
327         assertEquals(value10, value20);
328         value11 = expr0.execute(jc, x1, y1);
329         assertEquals(value11, value21);
330         value10 = expr0.execute(jc, x0, y0);
331         assertEquals(value10, value20);
332         value11 = expr0.execute(jc, x1, y1);
333         assertEquals(value11, value21);
334 
335         JexlScript expr1 = jexl.createScript("format(x, y)", "x", "y");
336         value10 = expr1.execute(jc, x0, y0);
337         assertEquals(value10, value20);
338         Object s0 = expr1.execute(jc, x0, "EEE dd MMM yyyy");
339         assertEquals("Wed 20 Aug 1969", s0);
340         jc.setLocale(Locale.FRANCE);
341         s0 = expr1.execute(jc, x0, "EEE dd MMM yyyy");
342         assertEquals("mer. 20 ao\u00fbt 1969", s0);
343 
344         expr1 = jexl.createScript("format(now(), y)", "y");
345         final Object n0 = expr1.execute(jc, y0);
346         assertNotNull(n0);
347         expr1 = jexl.createScript("now().format(y)", "y");
348         final Object n1 = expr1.execute(jc, y0);
349         assertNotNull(n0);
350         assertEquals(n0, n1);
351     }
352 
353     @Test
354     void testFormatArithmeticJxlt() throws Exception {
355         final Map<String, Object> ns = new HashMap<>();
356         ns.put("calc", Aggregate.class);
357         final Calendar cal = Calendar.getInstance(UTC);
358         cal.set(1969, Calendar.AUGUST, 20);
359         final Date x0 = cal.getTime();
360         final String y0 =  "yyyy-MM-dd";
361         final DateContext jc = new DateContext();
362         final JexlEngine jexl = new JexlBuilder().cache(32).namespaces(ns).arithmetic(new DateArithmetic(true)).create();
363         final JxltEngine jxlt = jexl.createJxltEngine();
364 
365         JxltEngine.Template expr0 = jxlt.createTemplate("${x.format(y)}", "x", "y");
366         StringWriter strw = new StringWriter();
367         expr0.evaluate(jc, strw, x0, y0);
368         String strws = strw.toString();
369         assertEquals("1969-08-20", strws);
370 
371         expr0 = jxlt.createTemplate("${calc:sum(x .. y)}", "x", "y");
372         strw = new StringWriter();
373         expr0.evaluate(jc, strw, 1, 3);
374         strws = strw.toString();
375         assertEquals("6", strws);
376 
377         final JxltEngine.Template expr1 = jxlt.createTemplate("${jexl:include(s, x, y)}", "s", "x", "y");
378         strw = new StringWriter();
379         expr1.evaluate(jc, strw, expr0, 1, 3);
380         strws = strw.toString();
381         assertEquals("6", strws);
382 
383         expr0 = jxlt.createTemplate("${now().format(y)}", "y");
384         strw = new StringWriter();
385         expr0.evaluate(jc, strw, y0);
386         strws = strw.toString();
387         assertNotNull(strws);
388     }
389 
390     @Test
391     void testIncrementOperatorOnNull() {
392         final JexlEngine jexl = new JexlBuilder().strict(false).create();
393         JexlScript script;
394         Object result;
395         script = jexl.createScript("var i = null; ++i");
396         result = script.execute(null);
397         assertEquals(1, result);
398         script = jexl.createScript("var i = null; --i");
399         result = script.execute(null);
400         assertEquals(-1, result);
401     }
402 
403     @Test
404     @SuppressWarnings("unchecked")
405     void testInterval() {
406         final Map<String, Object> ns = new HashMap<>();
407         ns.put("calc", Aggregate.class);
408         final JexlEngine jexl = new JexlBuilder().namespaces(ns).create();
409         JexlScript script;
410         Object result;
411 
412         script = jexl.createScript("1 .. 3");
413         result = script.execute(null);
414         assertInstanceOf(Iterable.class, result);
415         Iterator<Integer> ii = ((Iterable<Integer>) result).iterator();
416         assertEquals(Integer.valueOf(1), ii.next());
417         assertEquals(Integer.valueOf(2), ii.next());
418         assertEquals(Integer.valueOf(3), ii.next());
419 
420         script = jexl.createScript("(4 - 3) .. (9 / 3)");
421         result = script.execute(null);
422         assertInstanceOf(Iterable.class, result);
423         ii = ((Iterable<Integer>) result).iterator();
424         assertEquals(Integer.valueOf(1), ii.next());
425         assertEquals(Integer.valueOf(2), ii.next());
426         assertEquals(Integer.valueOf(3), ii.next());
427 
428         // sum of 1, 2, 3
429         script = jexl.createScript("var x = 0; for(var y : ((5 - 4) .. (12 / 4))) { x = x + y }; x");
430         result = script.execute(null);
431         assertEquals(Integer.valueOf(6), result);
432 
433         script = jexl.createScript("calc:sum(1 .. 3)");
434         result = script.execute(null);
435         assertEquals(Integer.valueOf(6), result);
436 
437         script = jexl.createScript("calc:sum(-3 .. 3)");
438         result = script.execute(null);
439         assertEquals(Integer.valueOf(0), result);
440     }
441 
442     @Test
443     void testMatch() throws Exception {
444         // check in/not-in on array, list, map, set and duck-type collection
445         final int[] ai = {2, 4, 42, 54};
446         final List<Integer> al = new ArrayList<>();
447         for (final int i : ai) {
448             al.add(i);
449         }
450         final Map<Integer, String> am = new HashMap<>();
451         am.put(2, "two");
452         am.put(4, "four");
453         am.put(42, "forty-two");
454         am.put(54, "fifty-four");
455         final MatchingContainer ad = new MatchingContainer(ai);
456         final IterableContainer ic = new IterableContainer(ai);
457         final Set<Integer> as = ad.values;
458         final Object[] vars = {ai, al, am, ad, as, ic};
459 
460         for (final Object variable : vars) {
461             asserter.setVariable("container", variable);
462             for (final int x : ai) {
463                 asserter.setVariable("x", x);
464                 asserter.assertExpression("x =~ container", Boolean.TRUE);
465             }
466             asserter.setVariable("x", 169);
467             asserter.assertExpression("x !~ container", Boolean.TRUE);
468         }
469     }
470 
471     @Test
472     void testNotStartsEndsWith() throws Exception {
473         asserter.setVariable("x", "foobar");
474         asserter.assertExpression("x !^ 'foo'", Boolean.FALSE);
475         asserter.assertExpression("x !$ 'foo'", Boolean.TRUE);
476         asserter.setVariable("x", "barfoo");
477         asserter.assertExpression("x !^ 'foo'", Boolean.TRUE);
478         asserter.assertExpression("x !$ 'foo'", Boolean.FALSE);
479 
480         final int[] ai = {2, 4, 42, 54};
481         final IterableContainer ic = new IterableContainer(ai);
482         asserter.setVariable("x", ic);
483         asserter.assertExpression("x !^ 2", Boolean.FALSE);
484         asserter.assertExpression("x !$ 54", Boolean.FALSE);
485         asserter.assertExpression("x !^ 4", Boolean.TRUE);
486         asserter.assertExpression("x !$ 42", Boolean.TRUE);
487         asserter.assertExpression("x !^ [2, 4]", Boolean.FALSE);
488         asserter.assertExpression("x !^ [42, 54]", Boolean.FALSE);
489     }
490 
491     @Test
492     void testNotStartsEndsWithString() throws Exception {
493         asserter.setVariable("x", "foobar");
494         asserter.assertExpression("x !^ 'foo'", Boolean.FALSE);
495         asserter.assertExpression("x !$ 'foo'", Boolean.TRUE);
496         asserter.setVariable("x", "barfoo");
497         asserter.assertExpression("x !^ 'foo'", Boolean.TRUE);
498         asserter.assertExpression("x !$ 'foo'", Boolean.FALSE);
499     }
500 
501     @Test
502     void testNotStartsEndsWithStringBuilder() throws Exception {
503         asserter.setVariable("x", new StringBuilder("foobar"));
504         asserter.assertExpression("x !^ 'foo'", Boolean.FALSE);
505         asserter.assertExpression("x !$ 'foo'", Boolean.TRUE);
506         asserter.setVariable("x", new StringBuilder("barfoo"));
507         asserter.assertExpression("x !^ 'foo'", Boolean.TRUE);
508         asserter.assertExpression("x !$ 'foo'", Boolean.FALSE);
509     }
510 
511     @Test
512     void testNotStartsEndsWithStringDot() throws Exception {
513         asserter.setVariable("x.y", "foobar");
514         asserter.assertExpression("x.y !^ 'foo'", Boolean.FALSE);
515         asserter.assertExpression("x.y !$ 'foo'", Boolean.TRUE);
516         asserter.setVariable("x.y", "barfoo");
517         asserter.assertExpression("x.y !^ 'foo'", Boolean.TRUE);
518         asserter.assertExpression("x.y !$ 'foo'", Boolean.FALSE);
519     }
520 
521     @Test
522     void testOperatorError() throws Exception {
523         runOperatorError(true);
524         runOperatorError(false);
525     }
526 
527     private void runOperatorError(final boolean silent) {
528         final CaptureLog log = new CaptureLog();
529         final DateContext jc = new DateContext();
530         final Date d = new Date();
531         final JexlEngine jexl = new JexlBuilder().logger(log).strict(true).silent(silent).cache(32)
532                                            .arithmetic(new DateArithmetic(true)).create();
533         final JexlScript expr0 = jexl.createScript("date * date", "date");
534         try {
535             final Object value0 = expr0.execute(jc, d);
536             if (!silent) {
537                 fail("should have failed");
538             } else {
539                 assertEquals(1, log.count("warn"));
540             }
541         } catch (final JexlException.Operator xop) {
542             assertEquals("*", xop.getSymbol());
543         }
544         if (!silent) {
545             assertEquals(0, log.count("warn"));
546         }
547     }
548 
549     @Test
550     void testRegexp() throws Exception {
551         asserter.setVariable("str", "abc456");
552         asserter.assertExpression("str =~ '.*456'", Boolean.TRUE);
553         asserter.assertExpression("str !~ 'ABC.*'", Boolean.TRUE);
554         asserter.setVariable("match", "abc.*");
555         asserter.setVariable("nomatch", ".*123");
556         asserter.assertExpression("str =~ match", Boolean.TRUE);
557         asserter.assertExpression("str !~ match", Boolean.FALSE);
558         asserter.assertExpression("str !~ nomatch", Boolean.TRUE);
559         asserter.assertExpression("str =~ nomatch", Boolean.FALSE);
560         asserter.setVariable("match", new StringBuilder("abc.*"));
561         asserter.setVariable("nomatch", new StringBuilder(".*123"));
562         asserter.assertExpression("str =~ match", Boolean.TRUE);
563         asserter.assertExpression("str !~ match", Boolean.FALSE);
564         asserter.assertExpression("str !~ nomatch", Boolean.TRUE);
565         asserter.assertExpression("str =~ nomatch", Boolean.FALSE);
566         asserter.setVariable("match", java.util.regex.Pattern.compile("abc.*"));
567         asserter.setVariable("nomatch", java.util.regex.Pattern.compile(".*123"));
568         asserter.assertExpression("str =~ match", Boolean.TRUE);
569         asserter.assertExpression("str !~ match", Boolean.FALSE);
570         asserter.assertExpression("str !~ nomatch", Boolean.TRUE);
571         asserter.assertExpression("str =~ nomatch", Boolean.FALSE);
572         // check the in/not-in variant
573         asserter.assertExpression("'a' =~ ['a','b','c','d','e','f']", Boolean.TRUE);
574         asserter.assertExpression("'a' !~ ['a','b','c','d','e','f']", Boolean.FALSE);
575         asserter.assertExpression("'z' =~ ['a','b','c','d','e','f']", Boolean.FALSE);
576         asserter.assertExpression("'z' !~ ['a','b','c','d','e','f']", Boolean.TRUE);
577     }
578 
579     @Test
580     void testRegexp2() throws Exception {
581         asserter.setVariable("str", "abc456");
582         asserter.assertExpression("str =~ ~/.*456/", Boolean.TRUE);
583         asserter.assertExpression("str !~ ~/ABC.*/", Boolean.TRUE);
584         asserter.assertExpression("str =~ ~/abc\\d{3}/", Boolean.TRUE);
585         // legacy, deprecated
586         asserter.assertExpression("matches(str, ~/.*456/)", Boolean.TRUE);
587         asserter.setVariable("str", "4/6");
588         asserter.assertExpression("str =~ ~/\\d\\/\\d/", Boolean.TRUE);
589     }
590 
591     /**
592      * Regex escapes such as {@code \b} (word boundary) must be passed through verbatim to
593      * {@link Pattern}, not interpreted as string-literal escapes that would corrupt them into a
594      * backspace character (JEXL-security f019).
595      */
596     @Test
597     void testRegexEscapesPassThrough() throws Exception {
598         asserter.setVariable("str", "a cat b");
599         asserter.assertExpression("str =~ ~/.*\\bcat\\b.*/", Boolean.TRUE);
600         asserter.setVariable("str", "category");
601         asserter.assertExpression("str =~ ~/.*\\bcat\\b.*/", Boolean.FALSE);
602         // \d (digit) and \/ (literal slash) still behave as before
603         asserter.setVariable("str", "4/6");
604         asserter.assertExpression("str =~ ~/\\d\\/\\d/", Boolean.TRUE);
605     }
606 
607     /**
608      * A malformed regex literal must surface as a parse error rather than leaking a raw
609      * {@link java.util.regex.PatternSyntaxException} (JEXL-security f019).
610      */
611     @Test
612     void testRegexInvalidPattern() {
613         final JexlEngine jexl = new JexlBuilder().create();
614         final JexlException.Parsing xparse = assertThrows(JexlException.Parsing.class,
615             () -> jexl.createScript("x =~ ~/(unbalanced/"));
616         assertNotNull(xparse.getMessage());
617     }
618     void testSelfAssignOperators(final String text, final int x, final int y0, final int x0) {
619         //String text = "y.add(x++)";
620         final JexlEngine jexl = new JexlBuilder().safe(true).create();
621         final JexlScript script = jexl.createScript(text);
622         final JexlContext context = new MapContext();
623         context.set("x", x);
624         final List<Number> y = new ArrayList<>();
625         context.set("y", y);
626         final Object result = script.execute(context);
627         assertEquals(x0, context.get("x"), "x0");
628         assertEquals(y0, y.get(0), "y0");
629     }
630     @Test
631     void testStartsEndsWith() throws Exception {
632         asserter.setVariable("x", "foobar");
633         asserter.assertExpression("x =^ 'foo'", Boolean.TRUE);
634         asserter.assertExpression("x =$ 'foo'", Boolean.FALSE);
635         asserter.setVariable("x", "barfoo");
636         asserter.assertExpression("x =^ 'foo'", Boolean.FALSE);
637         asserter.assertExpression("x =$ 'foo'", Boolean.TRUE);
638 
639         final int[] ai = {2, 4, 42, 54};
640         final IterableContainer ic = new IterableContainer(ai);
641         asserter.setVariable("x", ic);
642         asserter.assertExpression("x =^ 2", Boolean.TRUE);
643         asserter.assertExpression("x =$ 54", Boolean.TRUE);
644         asserter.assertExpression("x =^ 4", Boolean.FALSE);
645         asserter.assertExpression("x =$ 42", Boolean.FALSE);
646         asserter.assertExpression("x =^ [2, 4]", Boolean.TRUE);
647         asserter.assertExpression("x =^ [42, 54]", Boolean.TRUE);
648     }
649     @Test
650     void testStartsEndsWithString() throws Exception {
651         asserter.setVariable("x", "foobar");
652         asserter.assertExpression("x =^ 'foo'", Boolean.TRUE);
653         asserter.assertExpression("x =$ 'foo'", Boolean.FALSE);
654         asserter.setVariable("x", "barfoo");
655         asserter.assertExpression("x =^ 'foo'", Boolean.FALSE);
656         asserter.assertExpression("x =$ 'foo'", Boolean.TRUE);
657     }
658 
659     @Test
660     void testStartsEndsWithStringBuilder() throws Exception {
661         asserter.setVariable("x", new StringBuilder("foobar"));
662         asserter.assertExpression("x =^ 'foo'", Boolean.TRUE);
663         asserter.assertExpression("x =$ 'foo'", Boolean.FALSE);
664         asserter.setVariable("x", new StringBuilder("barfoo"));
665         asserter.assertExpression("x =^ 'foo'", Boolean.FALSE);
666         asserter.assertExpression("x =$ 'foo'", Boolean.TRUE);
667     }
668 
669     @Test
670     void testStartsEndsWithStringDot() throws Exception {
671         asserter.setVariable("x.y", "foobar");
672         asserter.assertExpression("x.y =^ 'foo'", Boolean.TRUE);
673         asserter.assertExpression("x.y =$ 'foo'", Boolean.FALSE);
674         asserter.setVariable("x.y", "barfoo");
675         asserter.assertExpression("x.y =^ 'foo'", Boolean.FALSE);
676         asserter.assertExpression("x.y =$ 'foo'", Boolean.TRUE);
677     }
678 
679     /**
680      * A comparator using an evaluated expression on objects as comparison arguments.
681      * <p>Lifetime is the sort method; it is thus safe to encapsulate the context</p>
682      */
683      private static class PropertyComparator implements JexlCache.Reference, Comparator<Object> {
684         private final JexlContext context = JexlEngine.getThreadContext();
685         private final JexlArithmetic arithmetic;
686         private final JexlOperator.Uberspect operator;
687         private final JexlScript expr;
688         private Object cache;
689 
690         PropertyComparator(final JexlArithmetic jexla, final JexlScript expr) {
691             this.arithmetic = jexla;
692             this.operator = JexlEngine.getThreadEngine().getUberspect().getOperator(arithmetic);
693             this.expr = expr;
694         }
695         @Override
696         public int compare(final Object o1, final Object o2) {
697             final Object left = expr.execute(context, o1);
698             final Object right = expr.execute(context, o2);
699             final Object result = operator.tryOverload(this, JexlOperator.COMPARE, left, right);
700             if (result instanceof Integer) {
701                 return (int) result;
702             }
703             return arithmetic.compare(left, right, JexlOperator.COMPARE);
704         }
705 
706         @Override
707         public Object getCache() {
708             return cache;
709         }
710 
711         @Override
712         public void setCache(final Object cache) {
713             this.cache = cache;
714         }
715     }
716 
717     public static class SortingArithmetic extends JexlArithmetic {
718         public SortingArithmetic(final boolean strict) {
719             this(strict, null, Integer.MIN_VALUE);
720         }
721 
722         private SortingArithmetic(final boolean strict, final MathContext context, final int scale) {
723             super(strict, context, scale);
724         }
725 
726         public int compare(final Integer left, final Integer right) {
727             return left.compareTo(right);
728         }
729 
730         public int compare(final String left, final String right) {
731             return left.compareTo(right);
732         }
733 
734         /**
735          * Sorts an array using a script to evaluate the property used to compare elements.
736          *
737          * @param array The elements array
738          * @param expr  The property evaluation lambda
739          */
740         public void sort(final Object[] array, final JexlScript expr) {
741             Arrays.sort(array, new PropertyComparator(this, expr));
742         }
743     }
744 
745     @Test
746     void testSortArray() {
747         final JexlEngine jexl = new JexlBuilder()
748                 .cache(32)
749                 .arithmetic(new SortingArithmetic(true))
750                 .silent(false).create();
751         // test data, json like
752         final String src = "[{'id':1,'name':'John','type':9},{'id':2,'name':'Doe','type':7},{'id':3,'name':'Doe','type':10}]";
753         final Object a =  jexl.createExpression(src).evaluate(null);
754         assertNotNull(a);
755         // row 0 and 1 are not ordered
756         final Map[] m = (Map[]) a;
757         assertEquals(9, m[0].get("type"));
758         assertEquals(7, m[1].get("type"));
759         // sort the elements on the type
760         jexl.createScript("array.sort( e -> e.type )", "array").execute(null, a);
761         // row 0 and 1 are now ordered
762         assertEquals(7, m[0].get("type"));
763         assertEquals(9, m[1].get("type"));
764     }
765 
766     public static class MatchingArithmetic extends JexlArithmetic {
767         public MatchingArithmetic(final boolean astrict) {
768             super(astrict);
769         }
770 
771         public boolean contains(final Pattern[] container, final String str) {
772             for(final Pattern pattern : container) {
773                 if (pattern.matcher(str).matches()) {
774                     return true;
775                 }
776             }
777             return false;
778         }
779     }
780 
781     @Test
782     void testPatterns() {
783         final JexlEngine jexl = new JexlBuilder().arithmetic(new MatchingArithmetic(true)).create();
784         final JexlScript script = jexl.createScript("str =~ [~/abc.*/, ~/def.*/]", "str");
785         assertTrue((boolean) script.execute(null, "abcdef"));
786         assertTrue((boolean) script.execute(null, "defghi"));
787         assertFalse((boolean) script.execute(null, "ghijkl"));
788     }
789 
790     public static class Arithmetic428 extends JexlArithmetic {
791         public Arithmetic428(final boolean strict) {
792             this(strict, null, Integer.MIN_VALUE);
793         }
794 
795         private Arithmetic428(final boolean strict, final MathContext context, final int scale) {
796             super(strict, context, scale);
797         }
798 
799         public int compare(final Instant lhs, final String str) {
800             final Instant rhs = Instant.parse(str);
801             return lhs.compareTo(rhs);
802         }
803     }
804 
805     static final List<Integer> LOOPS = new ArrayList<>(Arrays.asList(0, 1));
806 
807     @Test
808     void test428() {
809         // see JEXL-428
810         final JexlEngine jexl = new JexlBuilder().cache(32).arithmetic(new Arithmetic428(true)).create();
811         final String rhsstr ="2024-09-09T10:42:42.00Z";
812         final Instant rhs = Instant.parse(rhsstr);
813         final String lhs = "2020-09-09T01:24:24.00Z";
814         JexlScript script;
815         script = jexl.createScript("x < y", "x", "y");
816         final JexlScript s0 = script;
817         assertThrows(JexlException.class, () -> s0.execute(null, 42, rhs));
818         for(final int i : LOOPS) { assertTrue((boolean) script.execute(null, lhs, rhs)); }
819         for(final int i : LOOPS) { assertTrue((boolean) script.execute(null, lhs, rhs)); }
820         for(final int i : LOOPS) { assertFalse((boolean) script.execute(null, rhs, lhs)); }
821         for(final int i : LOOPS) { assertFalse((boolean) script.execute(null, rhs, lhs)); }
822         for(final int i : LOOPS) { assertTrue((boolean) script.execute(null, lhs, rhs)); }
823         for(final int i : LOOPS) { assertFalse((boolean) script.execute(null, rhs, lhs)); }
824 
825         script = jexl.createScript("x <= y", "x", "y");
826         final JexlScript s1 = script;
827         assertThrows(JexlException.class, () -> s1.execute(null, 42, rhs));
828         assertTrue((boolean) script.execute(null, lhs, rhs));
829         assertFalse((boolean) script.execute(null, rhs, lhs));
830 
831         script = jexl.createScript("x >= y", "x", "y");
832         final JexlScript s2 = script;
833         assertThrows(JexlException.class, () -> s2.execute(null, 42, rhs));
834         assertFalse((boolean) script.execute(null, lhs, rhs));
835         assertFalse((boolean) script.execute(null, lhs, rhs));
836         assertTrue((boolean) script.execute(null, rhs, lhs));
837         assertTrue((boolean) script.execute(null, rhs, lhs));
838         assertFalse((boolean) script.execute(null, lhs, rhs));
839         assertTrue((boolean) script.execute(null, rhs, lhs));
840 
841         script = jexl.createScript("x > y", "x", "y");
842         final JexlScript s3 = script;
843         assertThrows(JexlException.class, () -> s3.execute(null, 42, rhs));
844         assertFalse((boolean) script.execute(null, lhs, rhs));
845         assertTrue((boolean) script.execute(null, rhs, lhs));
846 
847         script = jexl.createScript("x == y", "x", "y");
848         assertFalse((boolean) script.execute(null, 42, rhs));
849         assertFalse((boolean) script.execute(null, lhs, rhs));
850         assertFalse((boolean) script.execute(null, lhs, rhs));
851         assertTrue((boolean) script.execute(null, rhs, rhsstr));
852         assertTrue((boolean) script.execute(null, rhsstr, rhs));
853         assertFalse((boolean) script.execute(null, lhs, rhs));
854 
855         script = jexl.createScript("x != y", "x", "y");
856         assertTrue((boolean) script.execute(null, 42, rhs));
857         assertTrue((boolean) script.execute(null, lhs, rhs));
858         assertFalse((boolean) script.execute(null, rhs, rhsstr));
859     }
860 
861     public static class Arithmetic429 extends JexlArithmetic {
862         public Arithmetic429(final boolean astrict) {
863             super(astrict);
864         }
865 
866         public int compare(final String lhs, final Number rhs) {
867             return lhs.compareTo(rhs.toString());
868         }
869     }
870 
871     @Test
872     void test429a() {
873         final JexlEngine jexl = new JexlBuilder()
874                 .arithmetic(new Arithmetic429(true))
875                 .cache(32)
876                 .create();
877         String src;
878         JexlScript script;
879         src = "'1.1' > 0";
880         script = jexl.createScript(src);
881         assertTrue((boolean) script.execute(null));
882         src = "1.2 <= '1.20'";
883         script = jexl.createScript(src);
884         assertTrue((boolean) script.execute(null));
885         src = "1.2 >= '1.2'";
886         script = jexl.createScript(src);
887         assertTrue((boolean) script.execute(null));
888         src = "1.2 < '1.2'";
889         script = jexl.createScript(src);
890         assertFalse((boolean) script.execute(null));
891         src = "1.2 > '1.2'";
892         script = jexl.createScript(src);
893         assertFalse((boolean) script.execute(null));
894         src = "1.20 == 'a'";
895         script = jexl.createScript(src);
896         assertFalse((boolean) script.execute(null));
897     }
898 }