blob: c82d2d7b49cc7582fd552cc4a07856b4c700a266 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.scxml2.env.groovy;
import groovy.lang.Script;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
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.EvaluatorProvider;
import org.apache.commons.scxml2.SCXMLExpressionException;
import org.apache.commons.scxml2.SCXMLSystemContext;
import org.apache.commons.scxml2.env.AbstractBaseEvaluator;
import org.apache.commons.scxml2.env.EffectiveContextMap;
import org.apache.commons.scxml2.model.SCXML;
/**
* 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>
*/
public class GroovyEvaluator extends AbstractBaseEvaluator {
/** Serial version UID. */
private static final long serialVersionUID = 1L;
/**
* Unique context variable name used for temporary reference to assign data (thus must be a valid variable name)
*/
private static final String ASSIGN_VARIABLE_NAME = "a"+UUID.randomUUID().toString().replace('-','x');
public static final String SUPPORTED_DATA_MODEL = "groovy";
public static class GroovyEvaluatorProvider implements EvaluatorProvider {
@Override
public String getSupportedDatamodel() {
return SUPPORTED_DATA_MODEL;
}
@Override
public Evaluator getEvaluator() {
return new GroovyEvaluator();
}
@Override
public Evaluator getEvaluator(final SCXML document) {
return new GroovyEvaluator();
}
}
/** Error message if evaluation context is not a GroovyContext. */
private static final String ERR_CTX_TYPE = "Error evaluating Groovy "
+ "expression, Context must be a org.apache.commons.scxml2.env.groovy.GroovyContext";
protected static final GroovyExtendableScriptCache.ScriptPreProcessor scriptPreProcessor = new GroovyExtendableScriptCache.ScriptPreProcessor () {
/**
* Pattern for case-sensitive matching of the Groovy operator aliases, delimited by whitespace
*/
public 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
*/
public 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() {
this(false);
}
public GroovyEvaluator(boolean useInitialScriptAsBaseScript) {
this.useInitialScriptAsBaseScript = useInitialScriptAsBaseScript;
this.scriptCache = newScriptCache();
}
/**
* Overridable factory method to create the GroovyExtendableScriptCache for this GroovyEvaluator.
* <p>
* The default implementation configures the scriptCache to use the {@link #scriptPreProcessor GroovyEvaluator scriptPreProcessor}
* and the {@link GroovySCXMLScript} as script base class.
* </p>
*
* @return GroovyExtendableScriptCache for this GroovyEvaluator
*/
protected GroovyExtendableScriptCache newScriptCache() {
GroovyExtendableScriptCache scriptCache = new GroovyExtendableScriptCache();
scriptCache.setScriptPreProcessor(getScriptPreProcessor());
scriptCache.setScriptBaseClass(GroovySCXMLScript.class.getName());
return scriptCache;
}
@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 */
@Override
public String getSupportedDatamodel() {
return SUPPORTED_DATA_MODEL;
}
/**
* Evaluate an expression.
*
* @param ctx variable context
* @param expr expression
* @return a result of the evaluation
* @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;
}
if (!(ctx instanceof GroovyContext)) {
throw new SCXMLExpressionException(ERR_CTX_TYPE);
}
final GroovyContext groovyCtx = (GroovyContext) ctx;
if (groovyCtx.getGroovyEvaluator() == null) {
groovyCtx.setGroovyEvaluator(this);
}
try {
return getScript(getEffectiveContext(groovyCtx), groovyCtx.getScriptBaseClass(), expr).run();
}
catch (Exception e) {
String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
throw new SCXMLExpressionException("eval('" + expr + "'): " + exMessage, e);
}
}
/**
* @see Evaluator#evalCond(Context, String)
*/
@Override
public Boolean evalCond(final Context ctx, final String expr) throws SCXMLExpressionException {
if (expr == null) {
return null;
}
if (!(ctx instanceof GroovyContext)) {
throw new SCXMLExpressionException(ERR_CTX_TYPE);
}
final GroovyContext groovyCtx = (GroovyContext) ctx;
if (groovyCtx.getGroovyEvaluator() == null) {
groovyCtx.setGroovyEvaluator(this);
}
try {
final Object result = getScript(getEffectiveContext(groovyCtx), groovyCtx.getScriptBaseClass(), expr).run();
return result == null ? Boolean.FALSE : (Boolean)result;
} catch (Exception e) {
String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
throw new SCXMLExpressionException("evalCond('" + expr + "'): " + exMessage, e);
}
}
/**
* @see Evaluator#evalLocation(Context, String)
*/
@Override
public Object evalLocation(final Context ctx, final String expr) throws SCXMLExpressionException {
if (expr == null) {
return null;
}
else if (ctx.has(expr)) {
return expr;
}
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);
return getScript(effective, groovyCtx.getScriptBaseClass(), expr).run();
} catch (Exception e) {
String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
throw new SCXMLExpressionException("evalLocation('" + expr + "'): " + exMessage, e);
}
}
/**
* @see Evaluator#evalAssign(Context, String, Object, AssignType, String)
*/
public void evalAssign(final Context ctx, final String location, final Object data, final AssignType type,
final String attr) throws SCXMLExpressionException {
final StringBuilder sb = new StringBuilder(location).append("=").append(ASSIGN_VARIABLE_NAME);
try {
ctx.getVars().put(ASSIGN_VARIABLE_NAME, data);
eval(ctx, sb.toString());
}
finally {
ctx.getVars().remove(ASSIGN_VARIABLE_NAME);
}
}
/**
* @see Evaluator#evalScript(Context, String)
*/
@Override
public Object evalScript(final Context ctx, final String scriptSource) throws SCXMLExpressionException {
if (scriptSource == null) {
return null;
}
if (!(ctx instanceof GroovyContext)) {
throw new SCXMLExpressionException(ERR_CTX_TYPE);
}
final GroovyContext groovyCtx = (GroovyContext) ctx;
if (groovyCtx.getGroovyEvaluator() == null) {
groovyCtx.setGroovyEvaluator(this);
}
try {
final GroovyContext effective = getEffectiveContext(groovyCtx);
final boolean inGlobalContext = groovyCtx.getParent() instanceof SCXMLSystemContext;
final Script script = getScript(effective, groovyCtx.getScriptBaseClass(), scriptSource);
final Object result = script.run();
if (inGlobalContext && useInitialScriptAsBaseScript) {
groovyCtx.setScriptBaseClass(script.getClass().getName());
}
return result;
} catch (Exception e) {
final String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
throw new SCXMLExpressionException("evalScript('" + scriptSource + "'): " + exMessage, e);
}
}
protected ClassLoader getGroovyClassLoader() {
return scriptCache.getGroovyClassLoader();
}
/**
* Create a new child context.
*
* @param parent parent context
* @return new child context
* @see Evaluator#newContext(Context)
*/
@Override
public Context newContext(final Context parent) {
return new GroovyContext(parent, this);
}
/**
* Create a new context which is the summation of contexts from the
* current state to document root, child has priority over parent
* in scoping rules.
*
* @param nodeCtx The GroovyContext for this state.
* @return The effective GroovyContext for the path leading up to
* document root.
*/
protected GroovyContext getEffectiveContext(final GroovyContext nodeCtx) {
return new GroovyContext(nodeCtx, new EffectiveContextMap(nodeCtx), this);
}
}