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