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