Author: ate
Date: Fri Jan 17 15:18:35 2014
New Revision: 1559139
URL: http://svn.apache.org/r1559139
Log:
SCXML-186: Groovy Expression evaluator support
- completion of this feature:
- enhanced GroovyEvaluator using the GroovyEnhancedScriptCache:
- adding a convenient ScriptPreProcessor, allowing to use Groovy operator
aliases for XML unfriendly operators like < (alias: lt)
- optionally using a SCXML initial script as base class for following
scripts (allowing the intial script to be used to add extra functions for
example)
- enhanced GroovyContext with proper de-serialization support using the
Groovy class loader during readObject
- also explicitly disables (de)serialization of Groovy closure objects (by
removing them from the variables before serialization)
- a GroovyContextBinding to bridge between SCXML and Groovy contexts
- the original GroovyBinding no longer is needed and replaced by the
GroovySCXMLScript base class
Added:
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyContextBinding.java
- copied, changed from r1556820,
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyBinding.java
Removed:
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyBinding.java
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyBuiltin.java
Modified:
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyContext.java
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyEvaluator.java
Modified:
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyContext.java
URL:
http://svn.apache.org/viewvc/commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyContext.java?rev=1559139&r1=1559138&r2=1559139&view=diff
==============================================================================
---
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyContext.java
(original)
+++
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyContext.java
Fri Jan 17 15:18:35 2014
@@ -16,11 +16,22 @@
*/
package org.apache.commons.scxml2.env.groovy;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
+import java.util.Iterator;
import java.util.Map;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
import org.apache.commons.scxml2.Context;
import org.apache.commons.scxml2.env.SimpleContext;
+import groovy.lang.Closure;
+
/**
* Groovy Context implementation for Commons SCXML.
*/
@@ -28,12 +39,26 @@ public class GroovyContext extends Simpl
private static final long serialVersionUID = 1L;
+ private static final Log log = LogFactory.getLog(GroovyContext.class);
+
/**
* Internal flag to indicate whether it is to evaluate a location
* that returns a Node within an XML data tree.
*/
private boolean evaluatingLocation = false;
+ private String scriptBaseClass;
+ private GroovyEvaluator evaluator;
+ private GroovyContextBinding binding;
+ private Map<String, Object> vars;
+
+ GroovyContextBinding getBinding() {
+ if (binding == null) {
+ binding = new GroovyContextBinding(this);
+ }
+ return binding;
+ }
+
/**
* Constructor.
*/
@@ -46,8 +71,9 @@ public class GroovyContext extends Simpl
*
* @param initialVars The initial set of variables.
*/
- public GroovyContext(final Map<String, Object> initialVars) {
+ public GroovyContext(final Map<String, Object> initialVars,
GroovyEvaluator evaluator) {
super(initialVars);
+ this.evaluator = evaluator;
}
/**
@@ -55,8 +81,17 @@ public class GroovyContext extends Simpl
*
* @param parent The parent context.
*/
- public GroovyContext(final Context parent) {
+ public GroovyContext(final Context parent, GroovyEvaluator evaluator) {
super(parent);
+ this.evaluator = evaluator;
+ }
+
+ protected GroovyEvaluator getGroovyEvaluator() {
+ return evaluator;
+ }
+
+ protected void setGroovyEvaluator(GroovyEvaluator evaluator) {
+ this.evaluator = evaluator;
}
/**
@@ -75,4 +110,69 @@ public class GroovyContext extends Simpl
this.evaluatingLocation = evaluatingLocation;
}
+ @Override
+ public Map<String, Object> getVars() {
+ return vars;
+ }
+
+ @Override
+ protected void setVars(final Map<String, Object> vars) {
+ this.vars = vars;
+ }
+
+ protected void setScriptBaseClass(String scriptBaseClass) {
+ this.scriptBaseClass = scriptBaseClass;
+ }
+
+ protected String getScriptBaseClass() {
+ if (scriptBaseClass != null) {
+ return scriptBaseClass;
+ }
+ if (getParent() != null) {
+ return ((GroovyContext)getParent()).getScriptBaseClass();
+ }
+ return null;
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ boolean closureErased = false;
+ if (vars != null) {
+ Iterator<Map.Entry<String, Object>> iterator =
getVars().entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry<String, Object> entry = iterator.next();
+ if (entry.getValue() != null && entry.getValue() instanceof
Closure) {
+ iterator.remove();
+ closureErased = true;
+ }
+ }
+ if (closureErased) {
+ log.warn("Encountered and removed Groovy Closure(s) in the
GroovyContext during serialization: these are not supported for
(de)serialization");
+ }
+ }
+ out.writeObject(this.scriptBaseClass);
+ out.writeObject(this.evaluator);
+ out.writeObject(this.binding);
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ new ObjectOutputStream(bout).writeObject(this.vars);
+ out.writeObject(bout.toByteArray());
+ }
+
+ @SuppressWarnings("unchecked")
+ private void readObject(ObjectInputStream in) throws
IOException,ClassNotFoundException {
+ this.scriptBaseClass = (String)in.readObject();
+ this.evaluator = (GroovyEvaluator)in.readObject();
+ this.binding = (GroovyContextBinding)in.readObject();
+ byte[] bytes = (byte[])in.readObject();
+ if (evaluator != null) {
+ this.vars = (Map<String, Object>)
+ new ObjectInputStream(new ByteArrayInputStream(bytes)) {
+ protected Class resolveClass(ObjectStreamClass osc)
throws IOException, ClassNotFoundException {
+ return Class.forName(osc.getName(), true,
evaluator.getGroovyClassLoader());
+ }
+ }.readObject();
+ }
+ else {
+ this.vars = (Map<String, Object>)new ObjectInputStream(new
ByteArrayInputStream(bytes)).readObject();
+ }
+ }
}
Copied:
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyContextBinding.java
(from r1556820,
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyBinding.java)
URL:
http://svn.apache.org/viewvc/commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyContextBinding.java?p2=commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyContextBinding.java&p1=commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyBinding.java&r1=1556820&r2=1559139&rev=1559139&view=diff
==============================================================================
---
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyBinding.java
(original)
+++
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyContextBinding.java
Fri Jan 17 15:18:35 2014
@@ -17,57 +17,45 @@
package org.apache.commons.scxml2.env.groovy;
import groovy.lang.Binding;
+import groovy.lang.MissingPropertyException;
+import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
-import org.apache.commons.scxml2.Context;
-
/**
- * Wrapper class for the Groovy Binding class that extends the
- * wrapped Binding to search the SCXML context for variables and predefined
- * functions that do not exist in the wrapped Binding.
+ * Delegating Groovy Binding class which delegates all variables access to its
GroovyContext
*/
-public class GroovyBinding extends Binding {
+public class GroovyContextBinding extends Binding implements Serializable {
- private Context context;
- private Binding binding;
+ private static final long serialVersionUID = 1L;
- /**
- * Initialises the internal Bindings delegate and SCXML context.
- * @param context SCXML Context to use for script variables.
- * @param binding GroovyShell bindings for variables.
- * @throws IllegalArgumentException Thrown if either <code>context</code>
- * or <code>binding</code> is <code>null</code>.
- */
- public GroovyBinding(Context context, Binding binding) {
- if (context == null) {
- throw new IllegalArgumentException("Invalid SCXML context");
- }
+ private GroovyContext context;
- if (binding == null) {
- throw new IllegalArgumentException("Invalid GroovyShell Binding");
+ public GroovyContextBinding(GroovyContext context) {
+ if (context == null) {
+ throw new IllegalArgumentException("Parameter context may not be
null");
}
-
this.context = context;
- this.binding = binding;
+ }
+
+ GroovyContext getContext() {
+ return context;
}
@Override
public Object getVariable(String name) {
- if (context.has(name)) {
- return context.get(name);
+ Object result = context.get(name);
+ if (result == null && !context.has(name)) {
+ throw new MissingPropertyException(name, this.getClass());
}
-
- return binding.getVariable(name);
+ return result;
}
@Override
public void setVariable(String name, Object value) {
if (context.has(name)) {
context.set(name, value);
- } else if (binding.hasVariable(name)) {
- binding.setVariable(name, value);
} else {
context.setLocal(name, value);
}
@@ -75,29 +63,21 @@ public class GroovyBinding extends Bindi
@Override
public boolean hasVariable(String name) {
- if (context.has(name)) {
- return true;
- }
-
- return binding.hasVariable(name);
+ return context.has(name);
}
- @SuppressWarnings({ "unchecked", "rawtypes" })
@Override
- public Map getVariables() {
- Map<String, Object> variables = new LinkedHashMap<String,
Object>(binding.getVariables());
- variables.putAll(context.getVars());
- return variables;
+ public Map<String, Object> getVariables() {
+ return new LinkedHashMap<String, Object>(context.getVars());
}
@Override
public Object getProperty(String property) {
- return binding.getProperty(property);
+ return getVariable(property);
}
@Override
public void setProperty(String property, Object newValue) {
- binding.setProperty(property, newValue);
+ setVariable(property, newValue);
}
-
}
Modified:
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyEvaluator.java
URL:
http://svn.apache.org/viewvc/commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyEvaluator.java?rev=1559139&r1=1559138&r2=1559139&view=diff
==============================================================================
---
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyEvaluator.java
(original)
+++
commons/proper/scxml/trunk/src/main/java/org/apache/commons/scxml2/env/groovy/GroovyEvaluator.java
Fri Jan 17 15:18:35 2014
@@ -16,24 +16,23 @@
*/
package org.apache.commons.scxml2.env.groovy;
-import groovy.lang.Binding;
-import groovy.lang.GroovyShell;
+import groovy.lang.Script;
import java.io.Serializable;
-import java.util.AbstractMap;
-import java.util.HashSet;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
-import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import org.apache.commons.scxml2.Context;
import org.apache.commons.scxml2.Evaluator;
import org.apache.commons.scxml2.SCXMLExpressionException;
-import org.codehaus.groovy.runtime.MethodClosure;
+import org.apache.commons.scxml2.env.EffectiveContextMap;
import org.w3c.dom.Node;
/**
- * Evaluator implementation enabling use of Groovy expressions in
- * SCXML documents.
+ * Evaluator implementation enabling use of Groovy expressions in SCXML
documents.
* <P>
* This implementation itself is thread-safe, so you can keep singleton for
efficiency.
* </P>
@@ -47,11 +46,75 @@ public class GroovyEvaluator implements
private static final String ERR_CTX_TYPE = "Error evaluating Groovy "
+ "expression, Context must be a
org.apache.commons.scxml2.env.groovy.GroovyContext";
- /** Constructor. */
+ private static final GroovyExtendableScriptCache.ScriptPreProcessor
scriptPreProcessor = new GroovyExtendableScriptCache.ScriptPreProcessor () {
+
+ /**
+ * Pattern for case-sensitive matching of the Groovy operator aliases,
delimited by whitespace
+ */
+ private final Pattern GROOVY_OPERATOR_ALIASES_PATTERN =
Pattern.compile("(?<=\\s)(and|or|not|eq|lt|le|ne|gt|ge)(?=\\s)");
+
+ /**
+ * Groovy operator aliases mapped to their underlying Groovy operator
+ */
+ private final Map<String, String> GROOVY_OPERATOR_ALIASES =
Collections.unmodifiableMap(new HashMap<String, String>() {{
+ put("and", "&& "); put("or", "||"); put("not", " ! ");
+ put("eq", "=="); put("lt", "< "); put("le", "<=");
+ put("ne", "!="); put("gt", "> "); put("ge", ">=");
+ }});
+
+ @Override
+ public String preProcess(final String script) {
+ if (script == null || script.length() == 0) {
+ return script;
+ }
+ StringBuffer sb = null;
+ Matcher m = GROOVY_OPERATOR_ALIASES_PATTERN.matcher(script);
+ while (m.find()) {
+ if (sb == null) {
+ sb = new StringBuffer();
+ }
+ m.appendReplacement(sb,
GROOVY_OPERATOR_ALIASES.get(m.group()));
+ }
+ if (sb != null) {
+ m.appendTail(sb);
+ return sb.toString();
+ }
+ return script;
+ }
+ };
+
+ private final boolean useInitialScriptAsBaseScript;
+ private final GroovyExtendableScriptCache scriptCache;
+
public GroovyEvaluator() {
- super();
+ this(false);
+ }
+
+ public GroovyEvaluator(boolean useInitialScriptAsBaseScript) {
+ this.useInitialScriptAsBaseScript = useInitialScriptAsBaseScript;
+ this.scriptCache = new GroovyExtendableScriptCache();
+ scriptCache.setScriptPreProcessor(scriptPreProcessor);
+ scriptCache.setScriptBaseClass(GroovySCXMLScript.class.getName());
}
+ @SuppressWarnings("unchecked")
+ protected Script getScript(GroovyContext groovyContext, String
scriptBaseClassName, String scriptSource) {
+ Script script = scriptCache.getScript(scriptBaseClassName,
scriptSource);
+ script.setBinding(groovyContext.getBinding());
+ return script;
+ }
+
+ @SuppressWarnings("unused")
+ public void clearCache() {
+ scriptCache.clearCache();
+ }
+
+ public GroovyExtendableScriptCache.ScriptPreProcessor
getScriptPreProcessor() {
+ return scriptPreProcessor;
+ }
+
+ /* SCXMLEvaluator implementation methods */
+
/**
* Evaluate an expression.
*
@@ -61,23 +124,22 @@ public class GroovyEvaluator implements
* @throws SCXMLExpressionException For a malformed expression
* @see Evaluator#eval(Context, String)
*/
+ @Override
public Object eval(final Context ctx, final String expr) throws
SCXMLExpressionException {
if (expr == null) {
return null;
}
- GroovyContext groovyCtx = null;
-
- if (ctx instanceof GroovyContext) {
- groovyCtx = (GroovyContext) ctx;
- } else {
+ if (!(ctx instanceof GroovyContext)) {
throw new SCXMLExpressionException(ERR_CTX_TYPE);
}
+ GroovyContext groovyCtx = (GroovyContext) ctx;
+ if (groovyCtx.getGroovyEvaluator() == null) {
+ groovyCtx.setGroovyEvaluator(this);
+ }
try {
- final GroovyContext effective = getEffectiveContext(groovyCtx);
- GroovyShell shell = createGroovyShell(effective);
- return shell.evaluate(expr);
+ return getScript(getEffectiveContext(groovyCtx),
groovyCtx.getScriptBaseClass(), expr).run();
} catch (Exception e) {
throw new SCXMLExpressionException("eval('" + expr + "'):" +
e.getMessage(), e);
}
@@ -86,23 +148,22 @@ public class GroovyEvaluator implements
/**
* @see Evaluator#evalCond(Context, String)
*/
+ @Override
public Boolean evalCond(final Context ctx, final String expr) throws
SCXMLExpressionException {
if (expr == null) {
return null;
}
- GroovyContext groovyCtx = null;
-
- if (ctx instanceof GroovyContext) {
- groovyCtx = (GroovyContext) ctx;
- } else {
+ if (!(ctx instanceof GroovyContext)) {
throw new SCXMLExpressionException(ERR_CTX_TYPE);
}
+ GroovyContext groovyCtx = (GroovyContext) ctx;
+ if (groovyCtx.getGroovyEvaluator() == null) {
+ groovyCtx.setGroovyEvaluator(this);
+ }
try {
- final GroovyContext effective = getEffectiveContext(groovyCtx);
- GroovyShell shell = createGroovyShell(effective);
- return (Boolean) shell.evaluate(expr);
+ return (Boolean)getScript(getEffectiveContext(groovyCtx),
groovyCtx.getScriptBaseClass(), expr).run();
} catch (Exception e) {
throw new SCXMLExpressionException("evalCond('" + expr + "'):" +
e.getMessage(), e);
}
@@ -111,24 +172,24 @@ public class GroovyEvaluator implements
/**
* @see Evaluator#evalLocation(Context, String)
*/
+ @Override
public Node evalLocation(final Context ctx, final String expr) throws
SCXMLExpressionException {
if (expr == null) {
return null;
}
- GroovyContext groovyCtx = null;
-
- if (ctx instanceof GroovyContext) {
- groovyCtx = (GroovyContext) ctx;
- } else {
+ if (!(ctx instanceof GroovyContext)) {
throw new SCXMLExpressionException(ERR_CTX_TYPE);
}
+ GroovyContext groovyCtx = (GroovyContext) ctx;
+ if (groovyCtx.getGroovyEvaluator() == null) {
+ groovyCtx.setGroovyEvaluator(this);
+ }
try {
final GroovyContext effective = getEffectiveContext(groovyCtx);
effective.setEvaluatingLocation(true);
- GroovyShell shell = createGroovyShell(effective);
- return (Node) shell.evaluate(expr);
+ return (Node)getScript(effective, groovyCtx.getScriptBaseClass(),
expr).run();
} catch (Exception e) {
throw new SCXMLExpressionException("evalLocation('" + expr + "'):"
+ e.getMessage(), e);
}
@@ -137,29 +198,41 @@ public class GroovyEvaluator implements
/**
* @see Evaluator#evalScript(Context, String)
*/
- public Object evalScript(final Context ctx, final String script) throws
SCXMLExpressionException {
- if (script == null) {
+ @Override
+ public Object evalScript(final Context ctx, final String scriptSource)
throws SCXMLExpressionException {
+ if (scriptSource == null) {
return null;
}
- GroovyContext groovyCtx = null;
-
- if (ctx instanceof GroovyContext) {
- groovyCtx = (GroovyContext) ctx;
- } else {
+ if (!(ctx instanceof GroovyContext)) {
throw new SCXMLExpressionException(ERR_CTX_TYPE);
}
+ GroovyContext groovyCtx = (GroovyContext) ctx;
+ if (groovyCtx.getGroovyEvaluator() == null) {
+ groovyCtx.setGroovyEvaluator(this);
+ }
try {
final GroovyContext effective = getEffectiveContext(groovyCtx);
effective.setEvaluatingLocation(true);
- GroovyShell shell = createGroovyShell(effective);
- return shell.evaluate(script);
+ boolean isInitialScript = groovyCtx.getParent() != null &&
+ groovyCtx.getParent().getParent() == null &&
+ scriptCache.isEmpty();
+ Script script = getScript(effective,
groovyCtx.getScriptBaseClass(), scriptSource);
+ Object result = script.run();
+ if (isInitialScript && useInitialScriptAsBaseScript) {
+ groovyCtx.setScriptBaseClass(script.getClass().getName());
+ }
+ return result;
} catch (Exception e) {
- throw new SCXMLExpressionException("evalScript('" + script + "'):"
+ e.getMessage(), e);
+ throw new SCXMLExpressionException("evalScript('" + scriptSource +
"'):" + e.getMessage(), e);
}
}
+ protected ClassLoader getGroovyClassLoader() {
+ return scriptCache.getGroovyClassLoader();
+ }
+
/**
* Create a new child context.
*
@@ -167,25 +240,9 @@ public class GroovyEvaluator implements
* @return new child context
* @see Evaluator#newContext(Context)
*/
+ @Override
public Context newContext(final Context parent) {
- return new GroovyContext(parent);
- }
-
- /**
- * Create a GroovyShell instance.
- * @param context
- * @return
- */
- protected GroovyShell createGroovyShell(GroovyContext groovyContext) {
- Binding binding = new Binding();
- GroovyBuiltin builtin = new GroovyBuiltin(groovyContext);
- MethodClosure dataClosure = new MethodClosure(builtin, "Data");
- MethodClosure inClosure = new MethodClosure(builtin, "In");
- binding.setProperty("Data", dataClosure);
- binding.setProperty("In", inClosure);
- GroovyBinding groovyBinding = new GroovyBinding(groovyContext,
binding);
- GroovyShell shell = new GroovyShell(groovyBinding);
- return shell;
+ return new GroovyContext(parent, this);
}
/**
@@ -198,68 +255,6 @@ public class GroovyEvaluator implements
* document root.
*/
private GroovyContext getEffectiveContext(final GroovyContext nodeCtx) {
- return new GroovyContext(new EffectiveContextMap(nodeCtx));
- }
-
- /**
- * The map that will back the effective context for the
- * {@link GroovyEvaluator}. The effective context enables the chaining of
- * {@link Context}s all the way from the current state node to the root.
- *
- */
- private static final class EffectiveContextMap extends AbstractMap<String,
Object> {
-
- /** The {@link Context} for the current state. */
- private final Context leaf;
-
- /** Constructor. */
- public EffectiveContextMap(final GroovyContext ctx) {
- super();
- this.leaf = ctx;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Set<Map.Entry<String, Object>> entrySet() {
- Set<Map.Entry<String, Object>> entrySet = new
HashSet<Map.Entry<String, Object>>();
- Context current = leaf;
- while (current != null) {
- entrySet.addAll(current.getVars().entrySet());
- current = current.getParent();
- }
- return entrySet;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Object put(final String key, final Object value) {
- Object old = leaf.get(key);
- if (leaf.has(key)) {
- leaf.set(key, value);
- } else {
- leaf.setLocal(key, value);
- }
- return old;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Object get(final Object key) {
- Context current = leaf;
- while (current != null) {
- if (current.getVars().containsKey(key)) {
- return current.getVars().get(key);
- }
- current = current.getParent();
- }
- return null;
- }
+ return new GroovyContext(new EffectiveContextMap(nodeCtx), this);
}
-
}
\ No newline at end of file