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