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  
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  public 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     void testSelfAssignOperators(final String text, final int x, final int y0, final int x0) {
591         //String text = "y.add(x++)";
592         final JexlEngine jexl = new JexlBuilder().safe(true).create();
593         final JexlScript script = jexl.createScript(text);
594         final JexlContext context = new MapContext();
595         context.set("x", x);
596         final List<Number> y = new ArrayList<>();
597         context.set("y", y);
598         final Object result = script.execute(context);
599         assertEquals(x0, context.get("x"), "x0");
600         assertEquals(y0, y.get(0), "y0");
601     }
602     @Test
603     void testStartsEndsWith() throws Exception {
604         asserter.setVariable("x", "foobar");
605         asserter.assertExpression("x =^ 'foo'", Boolean.TRUE);
606         asserter.assertExpression("x =$ 'foo'", Boolean.FALSE);
607         asserter.setVariable("x", "barfoo");
608         asserter.assertExpression("x =^ 'foo'", Boolean.FALSE);
609         asserter.assertExpression("x =$ 'foo'", Boolean.TRUE);
610 
611         final int[] ai = {2, 4, 42, 54};
612         final IterableContainer ic = new IterableContainer(ai);
613         asserter.setVariable("x", ic);
614         asserter.assertExpression("x =^ 2", Boolean.TRUE);
615         asserter.assertExpression("x =$ 54", Boolean.TRUE);
616         asserter.assertExpression("x =^ 4", Boolean.FALSE);
617         asserter.assertExpression("x =$ 42", Boolean.FALSE);
618         asserter.assertExpression("x =^ [2, 4]", Boolean.TRUE);
619         asserter.assertExpression("x =^ [42, 54]", Boolean.TRUE);
620     }
621     @Test
622     void testStartsEndsWithString() throws Exception {
623         asserter.setVariable("x", "foobar");
624         asserter.assertExpression("x =^ 'foo'", Boolean.TRUE);
625         asserter.assertExpression("x =$ 'foo'", Boolean.FALSE);
626         asserter.setVariable("x", "barfoo");
627         asserter.assertExpression("x =^ 'foo'", Boolean.FALSE);
628         asserter.assertExpression("x =$ 'foo'", Boolean.TRUE);
629     }
630 
631     @Test
632     void testStartsEndsWithStringBuilder() throws Exception {
633         asserter.setVariable("x", new StringBuilder("foobar"));
634         asserter.assertExpression("x =^ 'foo'", Boolean.TRUE);
635         asserter.assertExpression("x =$ 'foo'", Boolean.FALSE);
636         asserter.setVariable("x", new StringBuilder("barfoo"));
637         asserter.assertExpression("x =^ 'foo'", Boolean.FALSE);
638         asserter.assertExpression("x =$ 'foo'", Boolean.TRUE);
639     }
640 
641     @Test
642     void testStartsEndsWithStringDot() throws Exception {
643         asserter.setVariable("x.y", "foobar");
644         asserter.assertExpression("x.y =^ 'foo'", Boolean.TRUE);
645         asserter.assertExpression("x.y =$ 'foo'", Boolean.FALSE);
646         asserter.setVariable("x.y", "barfoo");
647         asserter.assertExpression("x.y =^ 'foo'", Boolean.FALSE);
648         asserter.assertExpression("x.y =$ 'foo'", Boolean.TRUE);
649     }
650 
651     /**
652      * A comparator using an evaluated expression on objects as comparison arguments.
653      * <p>Lifetime is the sort method; it is thus safe to encapsulate the context</p>
654      */
655      private static class PropertyComparator implements JexlCache.Reference, Comparator<Object> {
656         private final JexlContext context = JexlEngine.getThreadContext();
657         private final JexlArithmetic arithmetic;
658         private final JexlOperator.Uberspect operator;
659         private final JexlScript expr;
660         private Object cache;
661 
662         PropertyComparator(JexlArithmetic jexla, JexlScript expr) {
663             this.arithmetic = jexla;
664             this.operator = JexlEngine.getThreadEngine().getUberspect().getOperator(arithmetic);
665             this.expr = expr;
666         }
667         @Override
668         public int compare(Object o1, Object o2) {
669             final Object left = expr.execute(context, o1);
670             final Object right = expr.execute(context, o2);
671             Object result = operator.tryOverload(this, JexlOperator.COMPARE, left, right);
672             if (result instanceof Integer) {
673                 return (int) result;
674             }
675             return arithmetic.compare(left, right, JexlOperator.COMPARE);
676         }
677 
678         @Override
679         public Object getCache() {
680             return cache;
681         }
682 
683         @Override
684         public void setCache(Object cache) {
685             this.cache = cache;
686         }
687     }
688 
689     public static class SortingArithmetic extends JexlArithmetic {
690         public SortingArithmetic(boolean strict) {
691             this(strict, null, Integer.MIN_VALUE);
692         }
693 
694         private SortingArithmetic(boolean strict, MathContext context, int scale) {
695             super(strict, context, scale);
696         }
697 
698         public int compare(Integer left, Integer right) {
699             return left.compareTo(right);
700         }
701 
702         public int compare(String left, String right) {
703             return left.compareTo(right);
704         }
705 
706         /**
707          * Sorts an array using a script to evaluate the property used to compare elements.
708          *
709          * @param array the elements array
710          * @param expr  the property evaluation lambda
711          */
712         public void sort(final Object[] array, final JexlScript expr) {
713             Arrays.sort(array, new PropertyComparator(this, expr));
714         }
715     }
716 
717     @Test
718     void testSortArray() {
719         final JexlEngine jexl = new JexlBuilder()
720                 .cache(32)
721                 .arithmetic(new SortingArithmetic(true))
722                 .silent(false).create();
723         // test data, json like
724         final String src = "[{'id':1,'name':'John','type':9},{'id':2,'name':'Doe','type':7},{'id':3,'name':'Doe','type':10}]";
725         final Object a =  jexl.createExpression(src).evaluate(null);
726         assertNotNull(a);
727         // row 0 and 1 are not ordered
728         final Map[] m = (Map[]) a;
729         assertEquals(9, m[0].get("type"));
730         assertEquals(7, m[1].get("type"));
731         // sort the elements on the type
732         jexl.createScript("array.sort( e -> e.type )", "array").execute(null, a);
733         // row 0 and 1 are now ordered
734         assertEquals(7, m[0].get("type"));
735         assertEquals(9, m[1].get("type"));
736     }
737 
738     public static class MatchingArithmetic extends JexlArithmetic {
739         public MatchingArithmetic(final boolean astrict) {
740             super(astrict);
741         }
742 
743         public boolean contains(final Pattern[] container, final String str) {
744             for(final Pattern pattern : container) {
745                 if (pattern.matcher(str).matches()) {
746                     return true;
747                 }
748             }
749             return false;
750         }
751     }
752 
753     @Test
754     void testPatterns() {
755         final JexlEngine jexl = new JexlBuilder().arithmetic(new MatchingArithmetic(true)).create();
756         final JexlScript script = jexl.createScript("str =~ [~/abc.*/, ~/def.*/]", "str");
757         assertTrue((boolean) script.execute(null, "abcdef"));
758         assertTrue((boolean) script.execute(null, "defghi"));
759         assertFalse((boolean) script.execute(null, "ghijkl"));
760     }
761 
762     public static class Arithmetic428 extends JexlArithmetic {
763         public Arithmetic428(boolean strict) {
764             this(strict, null, Integer.MIN_VALUE);
765         }
766 
767         private Arithmetic428(boolean strict, MathContext context, int scale) {
768             super(strict, context, scale);
769         }
770 
771         public int compare(Instant lhs, String str) {
772             Instant rhs = Instant.parse(str);
773             return lhs.compareTo(rhs);
774         }
775     }
776 
777     static final List<Integer> LOOPS = new ArrayList<>(Arrays.asList(0, 1));
778 
779     @Test
780     void test428() {
781         // see JEXL-428
782         final JexlEngine jexl = new JexlBuilder().cache(32).arithmetic(new Arithmetic428(true)).create();
783         final String rhsstr ="2024-09-09T10:42:42.00Z";
784         final Instant rhs = Instant.parse(rhsstr);
785         final String lhs = "2020-09-09T01:24:24.00Z";
786         JexlScript script;
787         script = jexl.createScript("x < y", "x", "y");
788         final JexlScript s0 = script;
789         assertThrows(JexlException.class, () -> s0.execute(null, 42, rhs));
790         for(int i : LOOPS) { assertTrue((boolean) script.execute(null, lhs, rhs)); }
791         for(int i : LOOPS) { assertTrue((boolean) script.execute(null, lhs, rhs)); }
792         for(int i : LOOPS) { assertFalse((boolean) script.execute(null, rhs, lhs)); }
793         for(int i : LOOPS) { assertFalse((boolean) script.execute(null, rhs, lhs)); }
794         for(int i : LOOPS) { assertTrue((boolean) script.execute(null, lhs, rhs)); }
795         for(int i : LOOPS) { assertFalse((boolean) script.execute(null, rhs, lhs)); }
796 
797         script = jexl.createScript("x <= y", "x", "y");
798         final JexlScript s1 = script;
799         assertThrows(JexlException.class, () -> s1.execute(null, 42, rhs));
800         assertTrue((boolean) script.execute(null, lhs, rhs));
801         assertFalse((boolean) script.execute(null, rhs, lhs));
802 
803         script = jexl.createScript("x >= y", "x", "y");
804         final JexlScript s2 = script;
805         assertThrows(JexlException.class, () -> s2.execute(null, 42, rhs));
806         assertFalse((boolean) script.execute(null, lhs, rhs));
807         assertFalse((boolean) script.execute(null, lhs, rhs));
808         assertTrue((boolean) script.execute(null, rhs, lhs));
809         assertTrue((boolean) script.execute(null, rhs, lhs));
810         assertFalse((boolean) script.execute(null, lhs, rhs));
811         assertTrue((boolean) script.execute(null, rhs, lhs));
812 
813         script = jexl.createScript("x > y", "x", "y");
814         final JexlScript s3 = script;
815         assertThrows(JexlException.class, () -> s3.execute(null, 42, rhs));
816         assertFalse((boolean) script.execute(null, lhs, rhs));
817         assertTrue((boolean) script.execute(null, rhs, lhs));
818 
819         script = jexl.createScript("x == y", "x", "y");
820         assertFalse((boolean) script.execute(null, 42, rhs));
821         assertFalse((boolean) script.execute(null, lhs, rhs));
822         assertFalse((boolean) script.execute(null, lhs, rhs));
823         assertTrue((boolean) script.execute(null, rhs, rhsstr));
824         assertTrue((boolean) script.execute(null, rhsstr, rhs));
825         assertFalse((boolean) script.execute(null, lhs, rhs));
826 
827         script = jexl.createScript("x != y", "x", "y");
828         assertTrue((boolean) script.execute(null, 42, rhs));
829         assertTrue((boolean) script.execute(null, lhs, rhs));
830         assertFalse((boolean) script.execute(null, rhs, rhsstr));
831     }
832 
833     public static class Arithmetic429 extends JexlArithmetic {
834         public Arithmetic429(boolean astrict) {
835             super(astrict);
836         }
837 
838         public int compare(String lhs, Number rhs) {
839             return lhs.compareTo(rhs.toString());
840         }
841     }
842 
843     @Test
844     void test429a() {
845         final JexlEngine jexl = new JexlBuilder()
846                 .arithmetic(new Arithmetic429(true))
847                 .cache(32)
848                 .create();
849         String src;
850         JexlScript script;
851         src = "'1.1' > 0";
852         script = jexl.createScript(src);
853         assertTrue((boolean) script.execute(null));
854         src = "1.2 <= '1.20'";
855         script = jexl.createScript(src);
856         assertTrue((boolean) script.execute(null));
857         src = "1.2 >= '1.2'";
858         script = jexl.createScript(src);
859         assertTrue((boolean) script.execute(null));
860         src = "1.2 < '1.2'";
861         script = jexl.createScript(src);
862         assertFalse((boolean) script.execute(null));
863         src = "1.2 > '1.2'";
864         script = jexl.createScript(src);
865         assertFalse((boolean) script.execute(null));
866         src = "1.20 == 'a'";
867         script = jexl.createScript(src);
868         assertFalse((boolean) script.execute(null));
869     }
870 }