JEXL-288: public class exposing annotated statement call, various final nitpicks
Task #JEXL-288 - Annotation can not be specified for a standalone statement
diff --git a/src/main/java/org/apache/commons/jexl3/internal/Frame.java b/src/main/java/org/apache/commons/jexl3/internal/Frame.java
index cb27170..9fe9ac8 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Frame.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Frame.java
@@ -28,7 +28,7 @@
     /** The actual stack frame. */
     private final Object[] stack;
     /** Number of curried parameters. */
-    private int curried = 0;
+    private final int curried;
 
     /**
      * Creates a new frame.
diff --git a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
index 10679ac..5b31987 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java
@@ -1816,6 +1816,57 @@
     }
 
     /**
+     * An annotated call.
+     */
+    public class AnnotatedCall implements Callable<Object> {
+        /** The statement. */
+        private final ASTAnnotatedStatement stmt;
+        /** The child index. */
+        private final int index;
+        /** The data. */
+        private final Object data;
+        /** Tracking whether we processed the annotation. */
+        private boolean processed = false;
+
+        /**
+         * Simple ctor.
+         * @param astmt the statement
+         * @param aindex the index
+         * @param adata the data
+         */
+        AnnotatedCall(final ASTAnnotatedStatement astmt, final int aindex, final Object adata) {
+            stmt = astmt;
+            index = aindex;
+            data = adata;
+        }
+
+
+        @Override
+        public Object call() throws Exception {
+            processed = true;
+            try {
+                return processAnnotation(stmt, index, data);
+            } catch (JexlException.Return | JexlException.Break | JexlException.Continue xreturn) {
+                return xreturn;
+            }
+        }
+        
+        /**
+         * @return whether the statement has been processed
+         */
+        public boolean isProcessed() {
+            return processed;
+        }
+        
+        /**
+         * @return the actual statement.
+         */
+        public Object getStatement() {
+            return stmt;
+        }
+    }
+    
+    /**
      * Processes an annotated statement.
      * @param stmt the statement
      * @param index the index of the current annotation being processed
@@ -1846,18 +1897,7 @@
             }
         }
         // tracking whether we processed the annotation
-        final boolean[] processed = new boolean[]{false};
-        final Callable<Object> jstmt = new Callable<Object>() {
-            @Override
-            public Object call() throws Exception {
-                processed[0] = true;
-                try {
-                    return processAnnotation(stmt, index + 1, data);
-                } catch(JexlException.Return | JexlException.Continue | JexlException.Break xreturn) {
-                    return xreturn;
-                }
-            }
-        };
+        final AnnotatedCall jstmt = new AnnotatedCall(stmt, index + 1, data);
         // the annotation node and name
         final ASTAnnotation anode = (ASTAnnotation) stmt.jjtGetChild(index);
         final String aname = anode.getName();
@@ -1869,7 +1909,7 @@
         try {
             result = processAnnotation(aname, argv, jstmt);
             // not processing an annotation is an error
-            if (!processed[0]) {
+            if (!jstmt.isProcessed()) {
                 return annotationError(anode, aname, null);
             }
         } catch (JexlException xany) {
@@ -1893,7 +1933,7 @@
      * @throws Exception if anything goes wrong
      */
     protected Object processAnnotation(String annotation, Object[] args, Callable<Object> stmt) throws Exception {
-        return context instanceof JexlContext.AnnotationProcessor
+                return context instanceof JexlContext.AnnotationProcessor
                 ? ((JexlContext.AnnotationProcessor) context).processAnnotation(annotation, args, stmt)
                 : stmt.call();
     }
diff --git a/src/main/java/org/apache/commons/jexl3/internal/Scope.java b/src/main/java/org/apache/commons/jexl3/internal/Scope.java
index 2a5b9b9..0434de2 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/Scope.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/Scope.java
@@ -47,7 +47,7 @@
     /**
      * The parent scope.
      */
-    private Scope parent = null;
+    private final Scope parent;
     /**
      * The number of parameters.
      */
diff --git a/src/test/java/org/apache/commons/jexl3/AnnotationTest.java b/src/test/java/org/apache/commons/jexl3/AnnotationTest.java
index b299179..e9ea1cb 100644
--- a/src/test/java/org/apache/commons/jexl3/AnnotationTest.java
+++ b/src/test/java/org/apache/commons/jexl3/AnnotationTest.java
@@ -19,6 +19,10 @@
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.jexl3.internal.Interpreter;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -30,6 +34,9 @@
 
 public class AnnotationTest extends JexlTestCase {
 
+    public final static int NUM_THREADS = 10;
+    public final static int NUM_ITERATIONS = 1000;
+    
     public AnnotationTest() {
         super("AnnotationTest");
     }
@@ -60,6 +67,21 @@
                 throw new IllegalArgumentException(args[0].toString());
             } else if ("unknown".equals(name)) {
                 return null;
+            } else if ("synchronized".equals(name)) {
+                if (statement instanceof Interpreter.AnnotatedCall) {
+                    Object sa = ((Interpreter.AnnotatedCall) statement).getStatement();
+                    if (sa != null) {
+                        synchronized (sa) {
+                            return statement.call();
+                        }
+                    }
+                }
+                final JexlEngine jexl = JexlEngine.getThreadEngine();
+                if (jexl != null) {
+                    synchronized (jexl) {
+                        return statement.call();
+                    }
+                }
             }
             return statement.call();
         }
@@ -291,4 +313,79 @@
             Assert.assertEquals(0, log.count("warn"));
         }
     }
+    
+    /**
+     * A counter whose inc method will misbehave if not mutex-ed.
+     */
+    public static class Counter {
+        private int value = 0;
+
+        public void inc() {
+            int v = value;
+            // introduce some concurency
+            for (int i = (int) System.currentTimeMillis() % 5; i >= 0; --i) {
+                Thread.yield();
+            }
+            value = v + 1;
+        }
+
+        public int getValue() {
+            return value;
+        }
+    }
+
+    /**
+     * Runs a counter test with n-thread in //.
+     */
+    public static class TestRunner {
+        public final Counter syncCounter = new Counter();
+        public final Counter concCounter = new Counter();
+
+        public void run(Runnable runnable) throws InterruptedException {
+            ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
+            for (int i = 0; i < NUM_THREADS; i++) {
+                executor.submit(runnable);
+            }
+            executor.shutdown();
+            executor.awaitTermination(5, TimeUnit.SECONDS);
+            Assert.assertEquals(NUM_THREADS * NUM_ITERATIONS, syncCounter.getValue());
+            Assert.assertNotEquals(NUM_THREADS * NUM_ITERATIONS, concCounter.getValue());
+        }
+    }    
+
+    @Test
+    /**
+     * A base test to ensure synchronized makes a difference. 
+     */
+    public void testSynchronized() throws InterruptedException {
+        final TestRunner tr = new TestRunner();
+        final Counter syncCounter = tr.syncCounter;
+        final Counter concCounter = tr.concCounter;
+        tr.run(() -> {
+            for (int i = 0; i < NUM_ITERATIONS; i++) {
+                synchronized (syncCounter) {
+                    syncCounter.inc();
+                }
+                concCounter.inc();
+            }
+        });
+    }
+
+    @Test
+    public void testJexlSynchronized0() throws InterruptedException {
+        final TestRunner tr = new TestRunner();
+        final AnnotationContext ctxt = new AnnotationContext();
+        final JexlScript script = JEXL.createScript(
+                "for(var i : 1..NUM_ITERATIONS) {"
+                + "@synchronized { syncCounter.inc(); }"
+                + "concCounter.inc();"
+                + "}",
+                "NUM_ITERATIONS",
+                "syncCounter",
+                "concCounter");
+        // will sync on syncCounter
+        tr.run(() -> {
+            script.execute(ctxt, NUM_ITERATIONS, tr.syncCounter, tr.concCounter);
+        });
+    }
 }