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.Set;
20  import java.util.TreeSet;
21  import java.util.concurrent.Callable;
22  import java.util.concurrent.ExecutorService;
23  import java.util.concurrent.Executors;
24  import java.util.concurrent.TimeUnit;
25  import org.apache.commons.jexl3.internal.Interpreter;
26  import org.junit.Assert;
27  import org.junit.Test;
28  
29  /**
30   * Test cases for annotations.
31   * @since 3.1
32   */
33  @SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"})
34  
35  public class AnnotationTest extends JexlTestCase {
36  
37      public final static int NUM_THREADS = 10;
38      public final static int NUM_ITERATIONS = 1000;
39  
40      public AnnotationTest() {
41          super("AnnotationTest");
42      }
43  
44      @Test
45      public void test197a() throws Exception {
46          final JexlContext jc = new MapContext();
47          final JexlScript e = JEXL.createScript("@synchronized { return 42; }");
48          final Object r = e.execute(jc);
49          Assert.assertEquals(42, r);
50      }
51  
52      public static class AnnotationContext extends MapContext implements JexlContext.AnnotationProcessor {
53          private int count = 0;
54          private final Set<String> names = new TreeSet<>();
55  
56          @Override
57          public Object processAnnotation(final String name, final Object[] args, final Callable<Object> statement) throws Exception {
58              count += 1;
59              names.add(name);
60              if ("one".equals(name)) {
61                  names.add(args[0].toString());
62              } else if ("two".equals(name)) {
63                  names.add(args[0].toString());
64                  names.add(args[1].toString());
65              } else if ("error".equals(name)) {
66                  names.add(args[0].toString());
67                  throw new IllegalArgumentException(args[0].toString());
68              } else if ("unknown".equals(name)) {
69                  return null;
70              } else if ("synchronized".equals(name)) {
71                  if (statement instanceof Interpreter.AnnotatedCall) {
72                      final Object sa = ((Interpreter.AnnotatedCall) statement).getStatement();
73                      if (sa != null) {
74                          synchronized (sa) {
75                              return statement.call();
76                          }
77                      }
78                  }
79                  final JexlEngine jexl = JexlEngine.getThreadEngine();
80                  if (jexl != null) {
81                      synchronized (jexl) {
82                          return statement.call();
83                      }
84                  }
85              }
86              return statement.call();
87          }
88  
89          public int getCount() {
90              return count;
91          }
92  
93          public Set<String> getNames() {
94              return names;
95          }
96      }
97  
98      public static class OptAnnotationContext extends JexlEvalContext implements JexlContext.AnnotationProcessor {
99          @Override
100         public Object processAnnotation(final String name, final Object[] args, final Callable<Object> statement) throws Exception {
101             final JexlOptions options = this.getEngineOptions();
102             // transient side effect for strict
103             if ("strict".equals(name)) {
104                 final boolean s = (Boolean) args[0];
105                 final boolean b = options.isStrict();
106                 options.setStrict(s);
107                 final Object r = statement.call();
108                 options.setStrict(b);
109                 return r;
110             }
111             // transient side effect for silent
112             if ("silent".equals(name)) {
113                 if ((args != null) && (args.length != 0)) {
114                     final boolean s = (Boolean) args[0];
115                     final boolean b = options.isSilent();
116                     options.setSilent(s);
117                     Assert.assertEquals(s, options.isSilent());
118                     final Object r = statement.call();
119                     options.setSilent(b);
120                     return r;
121                 }
122                 final boolean b = options.isSilent();
123                 try {
124                     return statement.call();
125                 } catch(final JexlException xjexl) {
126                     return null;
127                 } finally {
128                     options.setSilent(b);
129                 }
130             }
131             // durable side effect for scale
132             if ("scale".equals(name)) {
133                 options.setMathScale((Integer) args[0]);
134                 return statement.call();
135             }
136             return statement.call();
137         }
138     }
139 
140     @Test
141     public void testVarStmt() throws Exception {
142         final OptAnnotationContext jc = new OptAnnotationContext();
143         final JexlOptions options = jc.getEngineOptions();
144         jc.getEngineOptions().set(JEXL);
145         options.setSharedInstance(true);
146         JexlScript e;
147         Object r;
148         e = JEXL.createScript("(s, v)->{ @strict(s) @silent(v) var x = y ; 42; }");
149 
150         // wont make an error
151         try {
152             r = e.execute(jc, false, true);
153             Assert.assertEquals(42, r);
154         } catch (final JexlException.Variable xjexl) {
155             Assert.fail("should not have thrown");
156         }
157 
158         r = null;
159         // will make an error and throw
160         options.setSafe(false);
161         try {
162             r = e.execute(jc, true, false);
163             Assert.fail("should have thrown");
164         } catch (final JexlException.Variable xjexl) {
165             Assert.assertNull(r);
166         }
167 
168         r = null;
169         // will make an error and will not throw but result is null
170         try {
171             r = e.execute(jc, true, true);
172             Assert.assertNull(r);
173         } catch (final JexlException.Variable xjexl) {
174             Assert.fail("should not have thrown");
175         }
176         options.setSafe(true);
177 
178         r = null;
179         // will not make an error and will not throw
180         try {
181             r = e.execute(jc, false, false);
182             Assert.assertEquals(42, r);
183         } catch (final JexlException.Variable xjexl) {
184             Assert.fail("should not have thrown");
185         }
186         //Assert.assertEquals(42, r);
187         Assert.assertTrue(options.isStrict());
188         e = JEXL.createScript("@scale(5) 42;");
189         r = e.execute(jc);
190         Assert.assertEquals(42, r);
191         Assert.assertTrue(options.isStrict());
192         Assert.assertEquals(5, options.getMathScale());
193     }
194 
195     @Test
196     public void testNoArg() throws Exception {
197         final AnnotationContext jc = new AnnotationContext();
198         final JexlScript e = JEXL.createScript("@synchronized { return 42; }");
199         final Object r = e.execute(jc);
200         Assert.assertEquals(42, r);
201         Assert.assertEquals(1, jc.getCount());
202         Assert.assertTrue(jc.getNames().contains("synchronized"));
203     }
204 
205     @Test
206     public void testNoArgExpression() throws Exception {
207         final AnnotationContext jc = new AnnotationContext();
208         final JexlScript e = JEXL.createScript("@synchronized 42");
209         final Object r = e.execute(jc);
210         Assert.assertEquals(42, r);
211         Assert.assertEquals(1, jc.getCount());
212         Assert.assertTrue(jc.getNames().contains("synchronized"));
213     }
214 
215     @Test
216     public void testNoArgStatement() throws Exception {
217         final AnnotationContext jc = new AnnotationContext();
218         final JexlScript e = JEXL.createScript("@synchronized if (true) 2 * 3 * 7; else -42;");
219         final Object r = e.execute(jc);
220         Assert.assertEquals(42, r);
221         Assert.assertEquals(1, jc.getCount());
222         Assert.assertTrue(jc.getNames().contains("synchronized"));
223     }
224 
225     @Test
226     public void testHoistingStatement() throws Exception {
227         final AnnotationContext jc = new AnnotationContext();
228         final JexlScript e = JEXL.createScript("var t = 1; @synchronized for(var x : [2,3,7]) t *= x; t");
229         final Object r = e.execute(jc);
230         Assert.assertEquals(42, r);
231         Assert.assertEquals(1, jc.getCount());
232         Assert.assertTrue(jc.getNames().contains("synchronized"));
233     }
234 
235     @Test
236     public void testOneArg() throws Exception {
237         final AnnotationContext jc = new AnnotationContext();
238         final JexlScript e = JEXL.createScript("@one(1) { return 42; }");
239         final Object r = e.execute(jc);
240         Assert.assertEquals(42, r);
241         Assert.assertEquals(1, jc.getCount());
242         Assert.assertTrue(jc.getNames().contains("one"));
243         Assert.assertTrue(jc.getNames().contains("1"));
244     }
245 
246     @Test
247     public void testMultiple() throws Exception {
248         final AnnotationContext jc = new AnnotationContext();
249         final JexlScript e = JEXL.createScript("@one(1) @synchronized { return 42; }");
250         final Object r = e.execute(jc);
251         Assert.assertEquals(42, r);
252         Assert.assertEquals(2, jc.getCount());
253         Assert.assertTrue(jc.getNames().contains("synchronized"));
254         Assert.assertTrue(jc.getNames().contains("one"));
255         Assert.assertTrue(jc.getNames().contains("1"));
256     }
257 
258     @Test
259     public void testError() throws Exception {
260         testError(true);
261         testError(false);
262     }
263 
264     private void testError(final boolean silent) throws Exception {
265         final CaptureLog log = new CaptureLog();
266         final AnnotationContext jc = new AnnotationContext();
267         final JexlEngine jexl = new JexlBuilder().logger(log).strict(true).silent(silent).create();
268         final JexlScript e = jexl.createScript("@error('42') { return 42; }");
269         try {
270             final Object r = e.execute(jc);
271             if (!silent) {
272                 Assert.fail("should have failed");
273             } else {
274                 Assert.assertEquals(1, log.count("warn"));
275             }
276         } catch (final JexlException.Annotation xjexl) {
277             Assert.assertEquals("error", xjexl.getAnnotation());
278         }
279         Assert.assertEquals(1, jc.getCount());
280         Assert.assertTrue(jc.getNames().contains("error"));
281         Assert.assertTrue(jc.getNames().contains("42"));
282         if (!silent) {
283             Assert.assertEquals(0, log.count("warn"));
284         }
285     }
286 
287     @Test
288     public void testUnknown() throws Exception {
289         testUnknown(true);
290         testUnknown(false);
291     }
292 
293     private void testUnknown(final boolean silent) throws Exception {
294         final CaptureLog log = new CaptureLog();
295         final AnnotationContext jc = new AnnotationContext();
296         final JexlEngine jexl = new JexlBuilder().logger(log).strict(true).silent(silent).create();
297         final JexlScript e = jexl.createScript("@unknown('42') { return 42; }");
298         try {
299             final Object r = e.execute(jc);
300             if (!silent) {
301                 Assert.fail("should have failed");
302             } else {
303                 Assert.assertEquals(1, log.count("warn"));
304             }
305         } catch (final JexlException.Annotation xjexl) {
306             Assert.assertEquals("unknown", xjexl.getAnnotation());
307         }
308         Assert.assertEquals(1, jc.getCount());
309         Assert.assertTrue(jc.getNames().contains("unknown"));
310         Assert.assertFalse(jc.getNames().contains("42"));
311         if (!silent) {
312             Assert.assertEquals(0, log.count("warn"));
313         }
314     }
315 
316     /**
317      * A counter whose inc method will misbehave if not mutex-ed.
318      */
319     public static class Counter {
320         private int value = 0;
321 
322         public void inc() {
323             final int v = value;
324             // introduce some concurency
325             for (int i = (int) System.currentTimeMillis() % 5; i >= 0; --i) {
326                 Thread.yield();
327             }
328             value = v + 1;
329         }
330 
331         public int getValue() {
332             return value;
333         }
334     }
335 
336     /**
337      * Runs a counter test with n-thread in //.
338      */
339     public static class TestRunner {
340         public final Counter syncCounter = new Counter();
341         public final Counter concCounter = new Counter();
342 
343         public void run(final Runnable runnable) throws InterruptedException {
344             final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
345             for (int i = 0; i < NUM_THREADS; i++) {
346                 executor.submit(runnable);
347             }
348             executor.shutdown();
349             executor.awaitTermination(5, TimeUnit.SECONDS);
350             // this may succeed concurrently if there is only one 'real' thread
351             // during execution; we can only prove the 'synchronized' if the unsync-ed
352             // version fails...
353             if (NUM_THREADS * NUM_ITERATIONS != concCounter.getValue()) {
354                 Assert.assertEquals(NUM_THREADS * NUM_ITERATIONS, syncCounter.getValue());
355             }
356         }
357     }
358 
359     @Test
360     /**
361      * A base test to ensure synchronized makes a difference.
362      */
363     public void testSynchronized() throws InterruptedException {
364         final TestRunner tr = new TestRunner();
365         final Counter syncCounter = tr.syncCounter;
366         final Counter concCounter = tr.concCounter;
367         tr.run(() -> {
368             for (int i = 0; i < NUM_ITERATIONS; i++) {
369                 synchronized (syncCounter) {
370                     syncCounter.inc();
371                 }
372                 concCounter.inc();
373             }
374         });
375     }
376 
377     @Test
378     public void testJexlSynchronized0() throws InterruptedException {
379         final TestRunner tr = new TestRunner();
380         final AnnotationContext ctxt = new AnnotationContext();
381         final JexlScript script = JEXL.createScript(
382                 "for(var i : 1..NUM_ITERATIONS) {"
383                 + "@synchronized { syncCounter.inc(); }"
384                 + "concCounter.inc();"
385                 + "}",
386                 "NUM_ITERATIONS",
387                 "syncCounter",
388                 "concCounter");
389         // will sync on syncCounter
390         tr.run(() -> {
391             script.execute(ctxt, NUM_ITERATIONS, tr.syncCounter, tr.concCounter);
392         });
393     }
394 }