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