001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.commons.jexl2;
018    
019    import java.util.List;
020    import java.util.Map;
021    import java.util.ArrayList;
022    import java.util.HashMap;
023    import java.util.concurrent.Callable;
024    import java.util.concurrent.Future;
025    import java.util.concurrent.TimeUnit;
026    
027    /**
028     * Verifies cache & tryExecute
029     */
030    public class CacheTest extends JexlTestCase {
031        public CacheTest(String testName) {
032            super(testName);
033        }
034        private static final JexlEngine jexl = createEngine(false);
035    
036        static {
037            jexl.setCache(512);
038            jexl.setSilent(false);
039        }
040    
041        @Override
042        public void setUp() throws Exception {
043            // ensure jul logging is only error to avoid warning in silent mode
044            java.util.logging.Logger.getLogger(JexlEngine.class.getName()).setLevel(java.util.logging.Level.SEVERE);
045        }
046    
047        // LOOPS & THREADS
048        private static final int LOOPS = 4096;
049        private static final int NTHREADS = 4;
050        // A pseudo random mix of accessors
051        private static final int[] MIX = {
052            0, 0, 3, 3, 4, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 1, 1, 1, 2, 2, 2,
053            3, 3, 3, 4, 4, 4, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 2, 2, 3, 3, 0
054        };
055    
056        @Override
057        protected void tearDown() throws Exception {
058            debuggerCheck(jexl);
059        }
060    
061        /**
062         * A set of classes that define different getter/setter methods for the same properties.
063         * The goal is to verify that the cached JexlPropertyGet / JexlPropertySet in the AST Nodes are indeed
064         * volatile and do not generate errors even when multiple threads concurently hammer them.
065         */
066        public static class Cached {
067            public String compute(String arg) {
068                if (arg == null) {
069                    arg = "na";
070                }
071                return getClass().getSimpleName() + "@s#" + arg;
072            }
073    
074            public String compute(String arg0, String arg1) {
075                if (arg0 == null) {
076                    arg0 = "na";
077                }
078                if (arg1 == null) {
079                    arg1 = "na";
080                }
081                return getClass().getSimpleName() + "@s#" + arg0 + ",s#" + arg1;
082            }
083    
084            public String compute(Integer arg) {
085                return getClass().getSimpleName() + "@i#" + arg;
086            }
087    
088            public String compute(float arg) {
089                return getClass().getSimpleName() + "@f#" + arg;
090            }
091    
092            public String compute(int arg0, int arg1) {
093                return getClass().getSimpleName() + "@i#" + arg0 + ",i#" + arg1;
094            }
095    
096            public String ambiguous(Integer arg0, int arg1) {
097                return getClass().getSimpleName() + "!i#" + arg0 + ",i#" + arg1;
098            }
099    
100            public String ambiguous(int arg0, Integer arg1) {
101                return getClass().getSimpleName() + "!i#" + arg0 + ",i#" + arg1;
102            }
103    
104            public static String COMPUTE(String arg) {
105                if (arg == null) {
106                    arg = "na";
107                }
108                return "CACHED@s#" + arg;
109            }
110    
111            public static String COMPUTE(String arg0, String arg1) {
112                if (arg0 == null) {
113                    arg0 = "na";
114                }
115                if (arg1 == null) {
116                    arg1 = "na";
117                }
118                return "CACHED@s#" + arg0 + ",s#" + arg1;
119            }
120    
121            public static String COMPUTE(int arg) {
122                return "CACHED@i#" + arg;
123            }
124    
125            public static String COMPUTE(int arg0, int arg1) {
126                return "CACHED@i#" + arg0 + ",i#" + arg1;
127            }
128        }
129    
130        public static class Cached0 extends Cached {
131            protected String value = "Cached0:new";
132            protected Boolean flag = Boolean.FALSE;
133    
134            public Cached0() {
135            }
136    
137            public String getValue() {
138                return value;
139            }
140    
141            public void setValue(String arg) {
142                if (arg == null) {
143                    arg = "na";
144                }
145                value = "Cached0:" + arg;
146            }
147    
148            public void setFlag(boolean b) {
149                flag = Boolean.valueOf(b);
150            }
151    
152            public boolean isFlag() {
153                return flag.booleanValue();
154            }
155        }
156    
157        public static class Cached1 extends Cached0 {
158            @Override
159            public void setValue(String arg) {
160                if (arg == null) {
161                    arg = "na";
162                }
163                value = "Cached1:" + arg;
164            }
165        }
166    
167        public static class Cached2 extends Cached {
168            boolean flag = false;
169            protected String value;
170    
171            public Cached2() {
172                value = "Cached2:new";
173            }
174    
175            public Object get(String prop) {
176                if ("value".equals(prop)) {
177                    return value;
178                } else if ("flag".equals(prop)) {
179                    return Boolean.valueOf(flag);
180                }
181                throw new RuntimeException("no such property");
182            }
183    
184            public void set(String p, Object v) {
185                if (v == null) {
186                    v = "na";
187                }
188                if ("value".equals(p)) {
189                    value = getClass().getSimpleName() + ":" + v;
190                } else if ("flag".equals(p)) {
191                    flag = Boolean.parseBoolean(v.toString());
192                } else {
193                    throw new RuntimeException("no such property");
194                }
195            }
196        }
197    
198        public static class Cached3 extends java.util.TreeMap<String, Object> {
199            private static final long serialVersionUID = 1L;
200            boolean flag = false;
201    
202            public Cached3() {
203                put("value", "Cached3:new");
204                put("flag", "false");
205            }
206    
207            @Override
208            public Object get(Object key) {
209                return super.get(key.toString());
210            }
211    
212            @Override
213            public Object put(String key, Object arg) {
214                if (arg == null) {
215                    arg = "na";
216                }
217                arg = "Cached3:" + arg;
218                return super.put(key, arg);
219            }
220    
221            public void setflag(boolean b) {
222                flag = b;
223            }
224    
225            public boolean isflag() {
226                return flag;
227            }
228        }
229    
230        public static class Cached4 extends java.util.ArrayList<String> {
231            private static final long serialVersionUID = 1L;
232    
233            public Cached4() {
234                super.add("Cached4:new");
235                super.add("false");
236            }
237    
238            public String getValue() {
239                return super.get(0);
240            }
241    
242            public void setValue(String arg) {
243                if (arg == null) {
244                    arg = "na";
245                }
246                super.set(0, "Cached4:" + arg);
247            }
248    
249            public void setflag(Boolean b) {
250                super.set(1, b.toString());
251            }
252    
253            public boolean isflag() {
254                return Boolean.parseBoolean(super.get(1));
255            }
256        }
257    
258        /**
259         * A helper class to pass arguments in tests (instances of getter/setter exercising classes).
260         */
261        static class TestCacheArguments {
262            Cached0 c0 = new Cached0();
263            Cached1 c1 = new Cached1();
264            Cached2 c2 = new Cached2();
265            Cached3 c3 = new Cached3();
266            Cached4 c4 = new Cached4();
267            Object[] ca = {
268                c0, c1, c2, c3, c4
269            };
270            Object[] value = null;
271        }
272    
273        /**
274         * Run same test function in NTHREADS in parallel.
275         * @param ctask the task / test
276         * @param loops number of loops to perform
277         * @param cache whether jexl cache is used or not
278         * @throws Exception if anything goes wrong
279         */
280        @SuppressWarnings("boxing")
281        void runThreaded(Class<? extends Task> ctask, int loops, boolean cache) throws Exception {
282            if (loops == 0) {
283                loops = MIX.length;
284            }
285            if (cache) {
286                jexl.setCache(32);
287            } else {
288                jexl.setCache(0);
289            }
290            java.util.concurrent.ExecutorService execs = java.util.concurrent.Executors.newFixedThreadPool(NTHREADS);
291            List<Callable<Integer>> tasks = new ArrayList<Callable<Integer>>(NTHREADS);
292            for(int t = 0; t < NTHREADS; ++t) {
293                tasks.add(jexl.newInstance(ctask, loops));
294            }
295            // let's not wait for more than a minute
296            List<Future<Integer>> futures = execs.invokeAll(tasks, 60, TimeUnit.SECONDS);
297            // check that all returned loops
298            for(Future<Integer> future : futures) {
299                assertEquals(Integer.valueOf(loops), future.get());
300            }
301        }
302    
303        /**
304         * The base class for MT tests.
305         */
306        public abstract static class Task implements Callable<Integer> {
307            final TestCacheArguments args = new TestCacheArguments();
308            final int loops;
309            final Map<String, Object> vars = new HashMap<String, Object>();
310            final JexlContext jc = new MapContext(vars);
311    
312            Task(int loops) {
313                this.loops = loops;
314            }
315    
316            public abstract Integer call() throws Exception;
317    
318            /**
319             * The actual test function; assigns and checks.
320             * <p>The expression will be evaluated against different classes in parallel.
321             * This verifies that neither the volatile cache in the AST nor the expression cache in the JEXL engine
322             * induce errors.</p>
323             * <p>
324             * Using it as a micro benchmark, it shows creating expression as the dominating cost; the expression
325             * cache takes care of this.
326             * By moving the expression creations out of the main loop, it also shows that the volatile cache speeds
327             * things up around 2x.
328             * </p>
329             * @param value the argument value to control
330             * @return the number of loops performed
331             */
332            public Integer runAssign(Object value) {
333                args.value = new Object[]{value};
334                Object result;
335    
336                Expression cacheGetValue = jexl.createExpression("cache.value");
337                Expression cacheSetValue = jexl.createExpression("cache.value = value");
338                for (int l = 0; l < loops; ++l) {
339                    int px = (int) Thread.currentThread().getId();
340                    int mix = MIX[(l + px) % MIX.length];
341    
342                    vars.put("cache", args.ca[mix]);
343                    vars.put("value", args.value[0]);
344                    result = cacheSetValue.evaluate(jc);
345                    if (args.value[0] == null) {
346                        assertNull(cacheSetValue.toString(), result);
347                    } else {
348                        assertEquals(cacheSetValue.toString(), args.value[0], result);
349                    }
350    
351                    result = cacheGetValue.evaluate(jc);
352                    if (args.value[0] == null) {
353                        assertEquals(cacheGetValue.toString(), "Cached" + mix + ":na", result);
354                    } else {
355                        assertEquals(cacheGetValue.toString(), "Cached" + mix + ":" + args.value[0], result);
356                    }
357    
358                }
359    
360                return Integer.valueOf(loops);
361            }
362        }
363    
364        /**
365         * A task to check assignment.
366         */
367        public static class AssignTask extends Task {
368            public AssignTask(int loops) {
369                super(loops);
370            }
371            @Override
372            public Integer call() throws Exception {
373                return runAssign("foo");
374            }
375        }
376    
377        /**
378         * A task to check null assignment.
379         */
380        public static class AssignNullTask extends Task {
381            public AssignNullTask(int loops) {
382                super(loops);
383            }
384            @Override
385            public Integer call() throws Exception {
386                return runAssign(null);
387            }
388        }
389    
390        /**
391         * A task to check boolean assignment.
392         */
393        public static class AssignBooleanTask extends Task {
394            public AssignBooleanTask(int loops) {
395                super(loops);
396            }
397            @Override
398            public Integer call() throws Exception {
399                return runAssignBoolean(Boolean.TRUE);
400            }
401    
402            /** The actual test function. */
403            private Integer runAssignBoolean(Boolean value) {
404                args.value = new Object[]{value};
405                Expression cacheGetValue = jexl.createExpression("cache.flag");
406                Expression cacheSetValue = jexl.createExpression("cache.flag = value");
407                Object result;
408    
409                for (int l = 0; l < loops; ++l) {
410                    int px = (int) Thread.currentThread().getId();
411                    int mix = MIX[(l + px) % MIX.length];
412    
413                    vars.put("cache", args.ca[mix]);
414                    vars.put("value", args.value[0]);
415                    result = cacheSetValue.evaluate(jc);
416                    assertEquals(cacheSetValue.toString(), args.value[0], result);
417    
418                    result = cacheGetValue.evaluate(jc);
419                    assertEquals(cacheGetValue.toString(), args.value[0], result);
420    
421                }
422    
423                return Integer.valueOf(loops);
424            }
425        }
426    
427        /**
428         * A task to check list assignment.
429         */
430        public static class AssignListTask extends Task {
431            public AssignListTask(int loops) {
432                super(loops);
433            }
434    
435            @Override
436            public Integer call() throws Exception {
437                return runAssignList();
438            }
439            /** The actual test function. */
440            private Integer runAssignList() {
441                args.value = new Object[]{"foo"};
442                java.util.ArrayList<String> c1 = new java.util.ArrayList<String>(2);
443                c1.add("foo");
444                c1.add("bar");
445                args.ca = new Object[]{
446                            new String[]{"one", "two"},
447                            c1
448                        };
449    
450                Expression cacheGetValue = jexl.createExpression("cache.0");
451                Expression cacheSetValue = jexl.createExpression("cache[0] = value");
452                Object result;
453    
454                for (int l = 0; l < loops; ++l) {
455                    int px = (int) Thread.currentThread().getId();
456                    int mix = MIX[(l + px) % MIX.length] % args.ca.length;
457    
458                    vars.put("cache", args.ca[mix]);
459                    vars.put("value", args.value[0]);
460                    result = cacheSetValue.evaluate(jc);
461                    assertEquals(cacheSetValue.toString(), args.value[0], result);
462    
463                    result = cacheGetValue.evaluate(jc);
464                    assertEquals(cacheGetValue.toString(), args.value[0], result);
465                }
466    
467                return Integer.valueOf(loops);
468            }
469        }
470    
471    
472        public void testNullAssignNoCache() throws Exception {
473            runThreaded(AssignNullTask.class, LOOPS, false);
474        }
475    
476        public void testNullAssignCache() throws Exception {
477            runThreaded(AssignNullTask.class, LOOPS, true);
478        }
479    
480        public void testAssignNoCache() throws Exception {
481            runThreaded(AssignTask.class, LOOPS, false);
482        }
483    
484        public void testAssignCache() throws Exception {
485            runThreaded(AssignTask.class, LOOPS, true);
486        }
487    
488        public void testAssignBooleanNoCache() throws Exception {
489            runThreaded(AssignBooleanTask.class, LOOPS, false);
490        }
491    
492        public void testAssignBooleanCache() throws Exception {
493            runThreaded(AssignBooleanTask.class, LOOPS, true);
494        }
495    
496        public void testAssignListNoCache() throws Exception {
497            runThreaded(AssignListTask.class, LOOPS, false);
498        }
499    
500        public void testAssignListCache() throws Exception {
501            runThreaded(AssignListTask.class, LOOPS, true);
502        }
503    
504        /**
505         * A task to check method calls.
506         */
507        public static class ComputeTask extends Task {
508            public ComputeTask(int loops) {
509                super(loops);
510            }
511    
512            @Override
513            public Integer call() throws Exception {
514                args.ca = new Object[]{args.c0, args.c1, args.c2};
515                args.value = new Object[]{new Integer(2), "quux"};
516                //jexl.setDebug(true);
517                Expression compute2 = jexl.createExpression("cache.compute(a0, a1)");
518                Expression compute1 = jexl.createExpression("cache.compute(a0)");
519                Expression compute1null = jexl.createExpression("cache.compute(a0)");
520                Expression ambiguous = jexl.createExpression("cache.ambiguous(a0, a1)");
521                //jexl.setDebug(false);
522    
523                Object result = null;
524                String expected = null;
525                for (int l = 0; l < loops; ++l) {
526                    int mix = MIX[l % MIX.length] % args.ca.length;
527                    Object value = args.value[l % args.value.length];
528    
529                    vars.put("cache", args.ca[mix]);
530                    if (value instanceof String) {
531                        vars.put("a0", "S0");
532                        vars.put("a1", "S1");
533                        expected = "Cached" + mix + "@s#S0,s#S1";
534                    } else if (value instanceof Integer) {
535                        vars.put("a0", Integer.valueOf(7));
536                        vars.put("a1", Integer.valueOf(9));
537                        expected = "Cached" + mix + "@i#7,i#9";
538                    } else {
539                        fail("unexpected value type");
540                    }
541                    result = compute2.evaluate(jc);
542                    assertEquals(compute2.toString(), expected, result);
543    
544                    if (value instanceof Integer) {
545                        try {
546                            vars.put("a0", Short.valueOf((short) 17));
547                            vars.put("a1", Short.valueOf((short) 19));
548                            result = ambiguous.evaluate(jc);
549                            fail("should have thrown an exception");
550                        } catch (JexlException xany) {
551                            // throws due to ambiguous exception
552                        }
553                    }
554    
555                    if (value instanceof String) {
556                        vars.put("a0", "X0");
557                        expected = "Cached" + mix + "@s#X0";
558                    } else if (value instanceof Integer) {
559                        vars.put("a0", Integer.valueOf(5));
560                        expected = "Cached" + mix + "@i#5";
561                    } else {
562                        fail("unexpected value type");
563                    }
564                    result = compute1.evaluate(jc);
565                    assertEquals(compute1.toString(), expected, result);
566    
567                    try {
568                        vars.put("a0", null);
569                        result = compute1null.evaluate(jc);
570                        fail("should have thrown an exception");
571                    } catch (JexlException xany) {
572                        // throws due to ambiguous exception
573                        String sany = xany.getMessage();
574                        String tname = getClass().getName();
575                        if (!sany.startsWith(tname)) {
576                            fail("debug mode should carry caller information, "
577                                    + sany + ", "
578                                    + tname);
579                        }
580                    }
581                }
582                return Integer.valueOf(loops);
583            }
584        }
585    
586        public void testComputeNoCache() throws Exception {
587            try {
588                jexl.setDebug(true);
589                runThreaded(ComputeTask.class, LOOPS, false);
590            } finally {
591                jexl.setDebug(false);
592            }
593        }
594    
595        public void testComputeCache() throws Exception {
596            try {
597                jexl.setDebug(true);
598                runThreaded(ComputeTask.class, LOOPS, true);
599            } finally {
600                jexl.setDebug(false);
601            }
602        }
603    
604        /**
605         * The remaining tests exercise the namespaced functions; not MT.
606         * @param x
607         * @param loops
608         * @param cache
609         * @throws Exception
610         */
611        void doCOMPUTE(TestCacheArguments x, int loops, boolean cache) throws Exception {
612            if (loops == 0) {
613                loops = MIX.length;
614            }
615            if (cache) {
616                jexl.setCache(32);
617            } else {
618                jexl.setCache(0);
619            }
620            Map<String, Object> vars = new HashMap<String, Object>();
621            JexlContext jc = new MapContext(vars);
622            java.util.Map<String, Object> funcs = new java.util.HashMap<String, Object>();
623            jexl.setFunctions(funcs);
624            Expression compute2 = jexl.createExpression("cached:COMPUTE(a0, a1)");
625            Expression compute1 = jexl.createExpression("cached:COMPUTE(a0)");
626            Object result = null;
627            String expected = null;
628            for (int l = 0; l < loops; ++l) {
629                int mix = MIX[l % MIX.length] % x.ca.length;
630                Object value = x.value[l % x.value.length];
631    
632                funcs.put("cached", x.ca[mix]);
633                if (value instanceof String) {
634                    vars.put("a0", "S0");
635                    vars.put("a1", "S1");
636                    expected = "CACHED@s#S0,s#S1";
637                } else if (value instanceof Integer) {
638                    vars.put("a0", Integer.valueOf(7));
639                    vars.put("a1", Integer.valueOf(9));
640                    expected = "CACHED@i#7,i#9";
641                } else {
642                    fail("unexpected value type");
643                }
644                result = compute2.evaluate(jc);
645                assertEquals(compute2.toString(), expected, result);
646    
647                if (value instanceof String) {
648                    vars.put("a0", "X0");
649                    expected = "CACHED@s#X0";
650                } else if (value instanceof Integer) {
651                    vars.put("a0", Integer.valueOf(5));
652                    expected = "CACHED@i#5";
653                } else {
654                    fail("unexpected value type");
655                }
656                result = compute1.evaluate(jc);
657                assertEquals(compute1.toString(), expected, result);
658            }
659        }
660    
661        public void testCOMPUTENoCache() throws Exception {
662            TestCacheArguments args = new TestCacheArguments();
663            args.ca = new Object[]{
664                        Cached.class, Cached1.class, Cached2.class
665                    };
666            args.value = new Object[]{new Integer(2), "quux"};
667            doCOMPUTE(args, LOOPS, false);
668        }
669    
670        public void testCOMPUTECache() throws Exception {
671            TestCacheArguments args = new TestCacheArguments();
672            args.ca = new Object[]{
673                        Cached.class, Cached1.class, Cached2.class
674                    };
675            args.value = new Object[]{new Integer(2), "quux"};
676            doCOMPUTE(args, LOOPS, true);
677        }
678    }