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  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  public 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         .debug(true)
525         .strict(true)
526         .create();
527 
528     private static final JexlEngine jexlNoCache = new JexlBuilder()
529         .cache(0)
530         .debug(true)
531         .strict(true)
532         .create();
533 
534     private static JexlEngine jexl = jexlCache;
535 
536     public CacheTest() {
537         super("CacheTest", null);
538     }
539 
540     /**
541      * The remaining tests exercise the namespaced namespaces; not MT.
542      * @param x
543      * @param loops
544      * @param cache
545      * @throws Exception
546      */
547     void doCOMPUTE(final TestCacheArguments x, int loops, final boolean cache) throws Exception {
548         if (loops == 0) {
549             loops = MIX.length;
550         }
551         if (!cache) {
552             jexl.clearCache();
553         }
554         final Map<String, Object> vars = new HashMap<>();
555         final java.util.Map<String, Object> funcs = new java.util.HashMap<>();
556         final JexlEvalContext jc = new JexlContextNS(vars, funcs);
557         final JexlExpression compute2 = jexl.createExpression("cached:COMPUTE(a0, a1)");
558         final JexlExpression compute1 = jexl.createExpression("cached:COMPUTE(a0)");
559         Object result = null;
560         String expected = null;
561         for (int l = 0; l < loops; ++l) {
562             final int mix = MIX[l % MIX.length] % x.ca.length;
563             final Object value = x.value[l % x.value.length];
564 
565             funcs.put("cached", x.ca[mix]);
566             if (value instanceof String) {
567                 vars.put("a0", "S0");
568                 vars.put("a1", "S1");
569                 expected = "CACHED@s#S0,s#S1";
570             } else if (value instanceof Integer) {
571                 vars.put("a0", Integer.valueOf(7));
572                 vars.put("a1", Integer.valueOf(9));
573                 expected = "CACHED@i#7,i#9";
574             } else {
575                 fail("unexpected value type");
576             }
577             result = compute2.evaluate(jc);
578             assertEquals(expected, result, compute2::toString);
579 
580             if (value instanceof String) {
581                 vars.put("a0", "X0");
582                 expected = "CACHED@s#X0";
583             } else if (value instanceof Integer) {
584                 vars.put("a0", Integer.valueOf(5));
585                 expected = "CACHED@i#5";
586             } else {
587                 fail("unexpected value type");
588             }
589             result = compute1.evaluate(jc);
590             assertEquals(expected, result, compute1::toString);
591         }
592     }
593 
594     /**
595      * Run same test function in NTHREADS in parallel.
596      * @param ctask the task / test
597      * @param loops number of loops to perform
598      * @param cache whether jexl cache is used or not
599      * @throws Exception if anything goes wrong
600      */
601     @SuppressWarnings("boxing")
602     void runThreaded(final Class<? extends Task> ctask, int loops, final boolean cache) throws Exception {
603         if (loops == 0) {
604             loops = MIX.length;
605         }
606         if (!cache) {
607             jexl = jexlNoCache;
608         } else {
609             jexl = jexlCache;
610         }
611         final java.util.concurrent.ExecutorService execs = java.util.concurrent.Executors.newFixedThreadPool(NTHREADS);
612         final List<Callable<Integer>> tasks = new ArrayList<>(NTHREADS);
613         for (int t = 0; t < NTHREADS; ++t) {
614             tasks.add(jexl.newInstance(ctask, loops));
615         }
616         // let's not wait for more than a minute
617         final List<Future<Integer>> futures = execs.invokeAll(tasks, 60, TimeUnit.SECONDS);
618         // check that all returned loops
619         for (final Future<Integer> future : futures) {
620             assertEquals(Integer.valueOf(loops), future.get());
621         }
622     }
623 
624     @BeforeEach
625     @Override
626     public void setUp() throws Exception {
627         // ensure jul logging is only error to avoid warning in silent mode
628         java.util.logging.Logger.getLogger(JexlEngine.class.getName()).setLevel(java.util.logging.Level.SEVERE);
629     }
630 
631     @AfterEach
632     @Override
633     public void tearDown() throws Exception {
634         debuggerCheck(jexl);
635     }
636 
637     @Test
638     public void testAssignBooleanCache() throws Exception {
639         runThreaded(AssignBooleanTask.class, LOOPS, true);
640     }
641 
642     @Test
643     public void testAssignBooleanNoCache() throws Exception {
644         runThreaded(AssignBooleanTask.class, LOOPS, false);
645     }
646 
647     @Test
648     public void testAssignCache() throws Exception {
649         runThreaded(AssignTask.class, LOOPS, true);
650     }
651 
652     @Test
653     public void testAssignListCache() throws Exception {
654         runThreaded(AssignListTask.class, LOOPS, true);
655     }
656 
657     @Test
658     public void testAssignListNoCache() throws Exception {
659         runThreaded(AssignListTask.class, LOOPS, false);
660     }
661 
662     @Test
663     public void testAssignNoCache() throws Exception {
664         runThreaded(AssignTask.class, LOOPS, false);
665     }
666 
667     @Test
668     public void testComputeCache() throws Exception {
669         runThreaded(ComputeTask.class, LOOPS, true);
670     }
671 
672     @Test
673     public void testCOMPUTECache() throws Exception {
674         final TestCacheArguments args = new TestCacheArguments();
675         args.ca = new Object[]{
676             Cached.class, Cached1.class, Cached2.class
677         };
678         args.value = new Object[]{Integer.valueOf(2), "quux"};
679         doCOMPUTE(args, LOOPS, true);
680     }
681 
682     @Test
683     public void testComputeNoCache() throws Exception {
684         runThreaded(ComputeTask.class, LOOPS, false);
685     }
686 
687     @Test
688     public void testCOMPUTENoCache() throws Exception {
689         final TestCacheArguments args = new TestCacheArguments();
690         args.ca = new Object[]{
691             Cached.class, Cached1.class, Cached2.class
692         };
693         args.value = new Object[]{Integer.valueOf(2), "quux"};
694         doCOMPUTE(args, LOOPS, false);
695     }
696 
697     @Test
698     public void testNullAssignCache() throws Exception {
699         runThreaded(AssignNullTask.class, LOOPS, true);
700     }
701 
702     @Test
703     public void testNullAssignNoCache() throws Exception {
704         runThreaded(AssignNullTask.class, LOOPS, false);
705     }
706 }