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);
+ });
+ }
}