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 }