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  package org.apache.commons.jexl3;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertNull;
21  import static org.junit.jupiter.api.Assertions.assertThrows;
22  import static org.junit.jupiter.api.Assertions.fail;
23  
24  import java.util.ArrayList;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.concurrent.Callable;
29  import java.util.concurrent.Future;
30  import java.util.concurrent.TimeUnit;
31  
32  import org.junit.jupiter.api.AfterEach;
33  import org.junit.jupiter.api.BeforeEach;
34  import org.junit.jupiter.api.Test;
35  
36  /**
37   * Verifies cache & tryExecute
38   */
39  @SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"})
40  class CacheTest extends JexlTestCase {
41      /**
42       * A task to check boolean assignment.
43       */
44      public static class AssignBooleanTask extends Task {
45          public AssignBooleanTask(final int loops) {
46              super(loops);
47          }
48  
49          @Override
50          public Integer call() throws Exception {
51              return runAssignBoolean(Boolean.TRUE);
52          }
53  
54          /** The actual test function. */
55          private Integer runAssignBoolean(final Boolean value) {
56              args.value = new Object[]{value};
57              final JexlExpression cacheGetValue = jexl.createExpression("cache.flag");
58              final JexlExpression cacheSetValue = jexl.createExpression("cache.flag = value");
59              Object result;
60  
61              for (int l = 0; l < loops; ++l) {
62                  final int px = (int) Thread.currentThread().getId();
63                  final int mix = MIX[(l + px) % MIX.length];
64  
65                  vars.put("cache", args.ca[mix]);
66                  vars.put("value", args.value[0]);
67                  result = cacheSetValue.evaluate(jc);
68                  assertEquals(args.value[0], result, cacheSetValue::toString);
69  
70                  result = cacheGetValue.evaluate(jc);
71                  assertEquals(args.value[0], result, cacheSetValue::toString);
72  
73              }
74  
75              return Integer.valueOf(loops);
76          }
77      }
78      /**
79       * A task to check list assignment.
80       */
81      public static class AssignListTask extends Task {
82          public AssignListTask(final int loops) {
83              super(loops);
84          }
85  
86          @Override
87          public Integer call() throws Exception {
88              return runAssignList();
89          }
90  
91          /** The actual test function. */
92          private Integer runAssignList() {
93              args.value = new Object[]{"foo"};
94              final java.util.ArrayList<String> c1 = new java.util.ArrayList<>(2);
95              c1.add("foo");
96              c1.add("bar");
97              args.ca = new Object[]{
98                  new String[]{"one", "two"},
99                  c1
100             };
101 
102             final JexlExpression cacheGetValue = jexl.createExpression("cache.0");
103             final JexlExpression cacheSetValue = jexl.createExpression("cache[0] = value");
104             Object result;
105 
106             for (int l = 0; l < loops; ++l) {
107                 final int px = (int) Thread.currentThread().getId();
108                 final int mix = MIX[(l + px) % MIX.length] % args.ca.length;
109 
110                 vars.put("cache", args.ca[mix]);
111                 vars.put("value", args.value[0]);
112                 result = cacheSetValue.evaluate(jc);
113                 assertEquals(args.value[0], result, cacheSetValue::toString);
114 
115                 result = cacheGetValue.evaluate(jc);
116                 assertEquals(args.value[0], result, cacheGetValue::toString);
117             }
118 
119             return Integer.valueOf(loops);
120         }
121     }
122     /**
123      * A task to check null assignment.
124      */
125     public static class AssignNullTask extends Task {
126         public AssignNullTask(final int loops) {
127             super(loops);
128         }
129 
130         @Override
131         public Integer call() throws Exception {
132             return runAssign(null);
133         }
134     }
135 
136     /**
137      * A task to check assignment.
138      */
139     public static class AssignTask extends Task {
140         public AssignTask(final int loops) {
141             super(loops);
142         }
143 
144         @Override
145         public Integer call() throws Exception {
146             return runAssign("foo");
147         }
148     }
149     /**
150      * A set of classes that define different getter/setter methods for the same properties.
151      * The goal is to verify that the cached JexlPropertyGet / JexlPropertySet in the AST Nodes are indeed
152      * volatile and do not generate errors even when multiple threads concurrently hammer them.
153      */
154     public static class Cached {
155         public static String COMPUTE(final int arg) {
156             return "CACHED@i#" + arg;
157         }
158 
159         public static String COMPUTE(final int arg0, final int arg1) {
160             return "CACHED@i#" + arg0 + ",i#" + arg1;
161         }
162 
163         public static String COMPUTE(String arg) {
164             if (arg == null) {
165                 arg = "na";
166             }
167             return "CACHED@s#" + arg;
168         }
169 
170         public static String COMPUTE(String arg0, String arg1) {
171             if (arg0 == null) {
172                 arg0 = "na";
173             }
174             if (arg1 == null) {
175                 arg1 = "na";
176             }
177             return "CACHED@s#" + arg0 + ",s#" + arg1;
178         }
179 
180         public String ambiguous(final int arg0, final Integer arg1) {
181             return getClass().getSimpleName() + "!i#" + arg0 + ",i#" + arg1;
182         }
183 
184         public String ambiguous(final Integer arg0, final int arg1) {
185             return getClass().getSimpleName() + "!i#" + arg0 + ",i#" + arg1;
186         }
187 
188         public String compute(final float arg) {
189             return getClass().getSimpleName() + "@f#" + arg;
190         }
191 
192         public String compute(final int arg0, final int arg1) {
193             return getClass().getSimpleName() + "@i#" + arg0 + ",i#" + arg1;
194         }
195 
196         public String compute(final Integer arg) {
197             return getClass().getSimpleName() + "@i#" + arg;
198         }
199 
200         public String compute(String arg) {
201             if (arg == null) {
202                 arg = "na";
203             }
204             return getClass().getSimpleName() + "@s#" + arg;
205         }
206 
207         public String compute(String arg0, String arg1) {
208             if (arg0 == null) {
209                 arg0 = "na";
210             }
211             if (arg1 == null) {
212                 arg1 = "na";
213             }
214             return getClass().getSimpleName() + "@s#" + arg0 + ",s#" + arg1;
215         }
216     }
217     public static class Cached0 extends Cached {
218         protected String value = "Cached0:new";
219         protected Boolean flag = Boolean.FALSE;
220 
221         public Cached0() {
222         }
223 
224         public String getValue() {
225             return value;
226         }
227 
228         public boolean isFlag() {
229             return flag;
230         }
231 
232         public void setFlag(final boolean b) {
233             flag = Boolean.valueOf(b);
234         }
235 
236         public void setValue(String arg) {
237             if (arg == null) {
238                 arg = "na";
239             }
240             value = "Cached0:" + arg;
241         }
242     }
243     public static class Cached1 extends Cached0 {
244         @Override
245         public void setValue(String arg) {
246             if (arg == null) {
247                 arg = "na";
248             }
249             value = "Cached1:" + arg;
250         }
251     }
252 
253     public static class Cached2 extends Cached {
254         boolean flag;
255         protected String value;
256 
257         public Cached2() {
258             value = "Cached2:new";
259         }
260 
261         public Object get(final String prop) {
262             if ("value".equals(prop)) {
263                 return value;
264             }
265             if ("flag".equals(prop)) {
266                 return Boolean.valueOf(flag);
267             }
268             throw new IllegalArgumentException("no such property");
269         }
270 
271         public void set(final String p, Object v) {
272             if (v == null) {
273                 v = "na";
274             }
275             if ("value".equals(p)) {
276                 value = getClass().getSimpleName() + ":" + v;
277             } else if ("flag".equals(p)) {
278                 flag = Boolean.parseBoolean(v.toString());
279             } else {
280                 throw new IllegalArgumentException("no such property");
281             }
282         }
283     }
284 
285     public static class Cached3 extends java.util.TreeMap<String, Object> {
286         private static final long serialVersionUID = 1L;
287         boolean flag;
288 
289         public Cached3() {
290             put("value", "Cached3:new");
291             put("flag", "false");
292         }
293 
294         @Override
295         public Object get(final Object key) {
296             return super.get(key.toString());
297         }
298 
299         public boolean isflag() {
300             return flag;
301         }
302 
303         @Override
304         public final Object put(final String key, Object arg) {
305             if (arg == null) {
306                 arg = "na";
307             }
308             arg = "Cached3:" + arg;
309             return super.put(key, arg);
310         }
311 
312         public void setflag(final boolean b) {
313             flag = b;
314         }
315     }
316 
317     public static class Cached4 extends java.util.ArrayList<String> {
318         private static final long serialVersionUID = 1L;
319 
320         public Cached4() {
321             super.add("Cached4:new");
322             super.add("false");
323         }
324 
325         public String getValue() {
326             return super.get(0);
327         }
328 
329         public boolean isflag() {
330             return Boolean.parseBoolean(super.get(1));
331         }
332 
333         public void setflag(final Boolean b) {
334             super.set(1, b.toString());
335         }
336 
337         public void setValue(String arg) {
338             if (arg == null) {
339                 arg = "na";
340             }
341             super.set(0, "Cached4:" + arg);
342         }
343     }
344 
345     /**
346      * A task to check method calls.
347      */
348     public static class ComputeTask extends Task {
349         public ComputeTask(final int loops) {
350             super(loops);
351         }
352 
353         @Override
354         public Integer call() throws Exception {
355             args.ca = new Object[]{args.c0, args.c1, args.c2};
356             args.value = new Object[]{Integer.valueOf(2), "quux"};
357             //jexl.setDebug(true);
358             final JexlExpression compute2 = jexl.createExpression("cache.compute(a0, a1)");
359             final JexlExpression compute1 = jexl.createExpression("cache.compute(a0)");
360             final JexlExpression compute1null = jexl.createExpression("cache.compute(a0)");
361             final JexlExpression ambiguous = jexl.createExpression("cache.ambiguous(a0, a1)");
362             //jexl.setDebug(false);
363 
364             Object result = null;
365             String expected = null;
366             for (int l = 0; l < loops; ++l) {
367                 final int mix = MIX[l % MIX.length] % args.ca.length;
368                 final Object value = args.value[l % args.value.length];
369 
370                 vars.put("cache", args.ca[mix]);
371                 if (value instanceof String) {
372                     vars.put("a0", "S0");
373                     vars.put("a1", "S1");
374                     expected = "Cached" + mix + "@s#S0,s#S1";
375                 } else if (value instanceof Integer) {
376                     vars.put("a0", Integer.valueOf(7));
377                     vars.put("a1", Integer.valueOf(9));
378                     expected = "Cached" + mix + "@i#7,i#9";
379                 } else {
380                     fail("unexpected value type");
381                 }
382                 result = compute2.evaluate(jc);
383                 assertEquals(expected, result, compute2::toString);
384 
385                 if (value instanceof Integer) {
386                     vars.put("a0", Short.valueOf((short) 17));
387                     vars.put("a1", Short.valueOf((short) 19));
388                     assertThrows(JexlException.class, () -> ambiguous.evaluate(jc));
389                 }
390 
391                 if (value instanceof String) {
392                     vars.put("a0", "X0");
393                     expected = "Cached" + mix + "@s#X0";
394                 } else if (value instanceof Integer) {
395                     vars.put("a0", Integer.valueOf(5));
396                     expected = "Cached" + mix + "@i#5";
397                 } else {
398                     fail("unexpected value type");
399                 }
400                 result = compute1.evaluate(jc);
401                 assertEquals(expected, result, compute1::toString);
402 
403                 vars.put("a0", null);
404                 final JexlException xany = assertThrows(JexlException.class, () -> compute1null.evaluate(jc));
405                 // throws due to ambiguous exception
406                 final String sany = xany.getMessage();
407                 final String tname = getClass().getName();
408                 if (!sany.startsWith(tname)) {
409                     fail("debug mode should carry caller information, "
410                             + sany + ", "
411                             + tname);
412                 }
413             }
414             return Integer.valueOf(loops);
415         }
416     }
417 
418     public static class JexlContextNS extends JexlEvalContext {
419         final Map<String, Object> funcs;
420 
421         JexlContextNS(final Map<String, Object> vars, final Map<String, Object> funcs) {
422             super(vars);
423             this.funcs = funcs;
424         }
425 
426         @Override
427         public Object resolveNamespace(final String name) {
428             return funcs.get(name);
429         }
430 
431     }
432 
433     /**
434      * The base class for MT tests.
435      */
436     public abstract static class Task implements Callable<Integer> {
437         final TestCacheArguments args = new TestCacheArguments();
438         final int loops;
439         final Map<String, Object> vars = new HashMap<>();
440         final JexlEvalContext jc = new JexlEvalContext(vars);
441 
442         Task(final int loops) {
443             this.loops = loops;
444         }
445 
446         @Override
447         public abstract Integer call() throws Exception;
448 
449         /**
450          * The actual test function; assigns and checks.
451          * <p>
452          * The expression will be evaluated against different classes in parallel.
453          * This verifies that neither the volatile cache in the AST nor the expression cache in the JEXL engine
454          * induce errors.</p>
455          * <p>
456          * Using it as a micro benchmark, it shows creating expression as the dominating cost; the expression
457          * cache takes care of this.
458          * By moving the expression creations out of the main loop, it also shows that the volatile cache speeds
459          * things up around 2x.
460          * </p>
461          * @param value the argument value to control
462          * @return the number of loops performed
463          */
464         public Integer runAssign(final Object value) {
465             args.value = new Object[]{value};
466             Object result;
467 
468             final JexlExpression cacheGetValue = jexl.createExpression("cache.value");
469             final JexlExpression cacheSetValue = jexl.createExpression("cache.value = value");
470             for (int l = 0; l < loops; ++l) {
471                 final int px = (int) Thread.currentThread().getId();
472                 final int mix = MIX[(l + px) % MIX.length];
473 
474                 vars.put("cache", args.ca[mix]);
475                 vars.put("value", args.value[0]);
476                 result = cacheSetValue.evaluate(jc);
477                 if (args.value[0] == null) {
478                     assertNull(result);
479                 } else {
480                     assertEquals(args.value[0], result, cacheSetValue::toString);
481                 }
482 
483                 result = cacheGetValue.evaluate(jc);
484                 if (args.value[0] == null) {
485                     assertEquals("Cached" + mix + ":na", result, cacheGetValue::toString);
486                 } else {
487                     assertEquals("Cached" + mix + ":" + args.value[0], result, cacheGetValue::toString);
488                 }
489 
490             }
491 
492             return Integer.valueOf(loops);
493         }
494     }
495 
496     /**
497      * A helper class to pass arguments in tests (instances of getter/setter exercising classes).
498      */
499     static class TestCacheArguments {
500         Cached0 c0 = new Cached0();
501         Cached1 c1 = new Cached1();
502         Cached2 c2 = new Cached2();
503         Cached3 c3 = new Cached3();
504         Cached4 c4 = new Cached4();
505         Object[] ca = {
506             c0, c1, c2, c3, c4
507         };
508         Object[] value;
509     }
510 
511     // LOOPS & THREADS
512     private static final int LOOPS = 4096;
513 
514     private static final int NTHREADS = 4;
515 
516     // A pseudo random mix of accessors
517     private static final int[] MIX = {
518         0, 0, 3, 3, 4, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 1, 1, 1, 2, 2, 2,
519         3, 3, 3, 4, 4, 4, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 2, 2, 3, 3, 0
520     };
521 
522     private static final JexlEngine jexlCache = new JexlBuilder()
523         .cache(1024)
524         .strict(true)
525         .create();
526 
527     private static final JexlEngine jexlNoCache = new JexlBuilder()
528         .cache(0)
529         .strict(true)
530         .create();
531 
532     private static JexlEngine jexl = jexlCache;
533 
534     public CacheTest() {
535         super("CacheTest", null);
536     }
537 
538     /**
539      * The remaining tests exercise the namespaced namespaces; not MT.
540      * @param x
541      * @param loops
542      * @param cache
543      * @throws Exception
544      */
545     void doCOMPUTE(final TestCacheArguments x, int loops, final boolean cache) throws Exception {
546         if (loops == 0) {
547             loops = MIX.length;
548         }
549         if (!cache) {
550             jexl.clearCache();
551         }
552         final Map<String, Object> vars = new HashMap<>();
553         final java.util.Map<String, Object> funcs = new java.util.HashMap<>();
554         final JexlEvalContext jc = new JexlContextNS(vars, funcs);
555         final JexlExpression compute2 = jexl.createExpression("cached:COMPUTE(a0, a1)");
556         final JexlExpression compute1 = jexl.createExpression("cached:COMPUTE(a0)");
557         Object result = null;
558         String expected = null;
559         for (int l = 0; l < loops; ++l) {
560             final int mix = MIX[l % MIX.length] % x.ca.length;
561             final Object value = x.value[l % x.value.length];
562 
563             funcs.put("cached", x.ca[mix]);
564             if (value instanceof String) {
565                 vars.put("a0", "S0");
566                 vars.put("a1", "S1");
567                 expected = "CACHED@s#S0,s#S1";
568             } else if (value instanceof Integer) {
569                 vars.put("a0", Integer.valueOf(7));
570                 vars.put("a1", Integer.valueOf(9));
571                 expected = "CACHED@i#7,i#9";
572             } else {
573                 fail("unexpected value type");
574             }
575             result = compute2.evaluate(jc);
576             assertEquals(expected, result, compute2::toString);
577 
578             if (value instanceof String) {
579                 vars.put("a0", "X0");
580                 expected = "CACHED@s#X0";
581             } else if (value instanceof Integer) {
582                 vars.put("a0", Integer.valueOf(5));
583                 expected = "CACHED@i#5";
584             } else {
585                 fail("unexpected value type");
586             }
587             result = compute1.evaluate(jc);
588             assertEquals(expected, result, compute1::toString);
589         }
590     }
591 
592     /**
593      * Run same test function in NTHREADS in parallel.
594      * @param ctask the task / test
595      * @param loops number of loops to perform
596      * @param cache whether jexl cache is used or not
597      * @throws Exception if anything goes wrong
598      */
599     @SuppressWarnings("boxing")
600     void runThreaded(final Class<? extends Task> ctask, int loops, final boolean cache) throws Exception {
601         if (loops == 0) {
602             loops = MIX.length;
603         }
604         if (!cache) {
605             jexl = jexlNoCache;
606         } else {
607             jexl = jexlCache;
608         }
609         final java.util.concurrent.ExecutorService execs = java.util.concurrent.Executors.newFixedThreadPool(NTHREADS);
610         final List<Callable<Integer>> tasks = new ArrayList<>(NTHREADS);
611         for (int t = 0; t < NTHREADS; ++t) {
612             tasks.add(jexl.newInstance(ctask, loops));
613         }
614         // let's not wait for more than a minute
615         final List<Future<Integer>> futures = execs.invokeAll(tasks, 60, TimeUnit.SECONDS);
616         // check that all returned loops
617         for (final Future<Integer> future : futures) {
618             assertEquals(Integer.valueOf(loops), future.get());
619         }
620     }
621 
622     @BeforeEach
623     @Override
624     public void setUp() throws Exception {
625         // ensure jul logging is only error to avoid warning in silent mode
626         java.util.logging.Logger.getLogger(JexlEngine.class.getName()).setLevel(java.util.logging.Level.SEVERE);
627     }
628 
629     @AfterEach
630     @Override
631     public void tearDown() throws Exception {
632         debuggerCheck(jexl);
633     }
634 
635     @Test
636     void testAssignBooleanCache() throws Exception {
637         runThreaded(AssignBooleanTask.class, LOOPS, true);
638     }
639 
640     @Test
641     void testAssignBooleanNoCache() throws Exception {
642         runThreaded(AssignBooleanTask.class, LOOPS, false);
643     }
644 
645     @Test
646     void testAssignCache() throws Exception {
647         runThreaded(AssignTask.class, LOOPS, true);
648     }
649 
650     @Test
651     void testAssignListCache() throws Exception {
652         runThreaded(AssignListTask.class, LOOPS, true);
653     }
654 
655     @Test
656     void testAssignListNoCache() throws Exception {
657         runThreaded(AssignListTask.class, LOOPS, false);
658     }
659 
660     @Test
661     void testAssignNoCache() throws Exception {
662         runThreaded(AssignTask.class, LOOPS, false);
663     }
664 
665     @Test
666     void testComputeCache() throws Exception {
667         runThreaded(ComputeTask.class, LOOPS, true);
668     }
669 
670     @Test
671     void testCOMPUTECache() throws Exception {
672         final TestCacheArguments args = new TestCacheArguments();
673         args.ca = new Object[]{
674             Cached.class, Cached1.class, Cached2.class
675         };
676         args.value = new Object[]{Integer.valueOf(2), "quux"};
677         doCOMPUTE(args, LOOPS, true);
678     }
679 
680     @Test
681     void testComputeNoCache() throws Exception {
682         runThreaded(ComputeTask.class, LOOPS, false);
683     }
684 
685     @Test
686     void testCOMPUTENoCache() throws Exception {
687         final TestCacheArguments args = new TestCacheArguments();
688         args.ca = new Object[]{
689             Cached.class, Cached1.class, Cached2.class
690         };
691         args.value = new Object[]{Integer.valueOf(2), "quux"};
692         doCOMPUTE(args, LOOPS, false);
693     }
694 
695     @Test
696     void testNullAssignCache() throws Exception {
697         runThreaded(AssignNullTask.class, LOOPS, true);
698     }
699 
700     @Test
701     void testNullAssignNoCache() throws Exception {
702         runThreaded(AssignNullTask.class, LOOPS, false);
703     }
704 }