blob: af02a0bb0edfbd818fde192f1f6a7eff6d6e4e9f [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.jexl;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.jexl2.Expression;
import org.apache.commons.jexl2.JexlEngine;
import org.apache.commons.jexl2.Script;
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.env.AbstractBaseEvaluator;
import org.apache.commons.scxml2.env.EffectiveContextMap;
import org.apache.commons.scxml2.model.SCXML;
/**
* Evaluator implementation enabling use of JEXL expressions in
* SCXML documents.
* <P>
* This implementation itself is thread-safe, so you can keep singleton
* for efficiency of the internal <code>JexlEngine</code> member.
* </P>
*/
public class JexlEvaluator 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 = "jexl";
public static class JexlEvaluatorProvider implements EvaluatorProvider {
@Override
public String getSupportedDatamodel() {
return SUPPORTED_DATA_MODEL;
}
@Override
public Evaluator getEvaluator() {
return new JexlEvaluator();
}
@Override
public Evaluator getEvaluator(final SCXML document) {
return new JexlEvaluator();
}
}
/** Error message if evaluation context is not a JexlContext. */
private static final String ERR_CTX_TYPE = "Error evaluating JEXL "
+ "expression, Context must be a org.apache.commons.scxml2.env.jexl.JexlContext";
/** The internal JexlEngine instance to use. */
private transient volatile JexlEngine jexlEngine;
/** The current JexlEngine silent mode, stored locally to be reapplied after deserialization of the engine */
private boolean jexlEngineSilent;
/** The current JexlEngine strict mode, stored locally to be reapplied after deserialization of the engine */
private boolean jexlEngineStrict;
/** Constructor. */
public JexlEvaluator() {
super();
// create the internal JexlEngine initially
jexlEngine = createJexlEngine();
jexlEngineSilent = jexlEngine.isSilent();
jexlEngineStrict = jexlEngine.isStrict();
}
/**
* Checks whether the internal Jexl engine throws JexlException during evaluation.
* @return true if silent, false (default) otherwise
*/
public boolean isJexlEngineSilent() {
return jexlEngineSilent;
}
/**
* Delegate method for {@link JexlEngine#setSilent(boolean)} to set whether the engine throws JexlException during
* evaluation when an error is triggered.
* <p>This method should be called as an optional step of the JexlEngine
* initialization code before expression creation &amp; evaluation.</p>
* @param silent true means no JexlException will occur, false allows them
*/
public void setJexlEngineSilent(boolean silent) {
synchronized (this) {
JexlEngine engine = getJexlEngine();
engine.setSilent(silent);
this.jexlEngineSilent = silent;
}
}
/**
* Checks whether the internal Jexl engine behaves in strict or lenient mode.
* @return true for strict, false for lenient
*/
public boolean isJexlEngineStrict() {
return jexlEngineStrict;
}
/**
* Delegate method for {@link JexlEngine#setStrict(boolean)} to set whether it behaves in strict or lenient mode.
* <p>This method is should be called as an optional step of the JexlEngine
* initialization code before expression creation &amp; evaluation.</p>
* @param strict true for strict, false for lenient
*/
public void setJexlEngineStrict(boolean strict) {
synchronized (this) {
JexlEngine engine = getJexlEngine();
engine.setStrict(strict);
this.jexlEngineStrict = strict;
}
}
@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)
*/
public Object eval(final Context ctx, final String expr)
throws SCXMLExpressionException {
if (expr == null) {
return null;
}
if (!(ctx instanceof JexlContext)) {
throw new SCXMLExpressionException(ERR_CTX_TYPE);
}
try {
final JexlContext effective = getEffectiveContext((JexlContext)ctx);
Expression exp = getJexlEngine().createExpression(expr);
return exp.evaluate(effective);
} 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)
*/
public Boolean evalCond(final Context ctx, final String expr)
throws SCXMLExpressionException {
if (expr == null) {
return null;
}
if (!(ctx instanceof JexlContext)) {
throw new SCXMLExpressionException(ERR_CTX_TYPE);
}
try {
final JexlContext effective = getEffectiveContext((JexlContext)ctx);
Expression exp = getJexlEngine().createExpression(expr);
final Object result = exp.evaluate(effective);
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#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 {
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)
*/
public Object evalScript(final Context ctx, final String script)
throws SCXMLExpressionException {
if (script == null) {
return null;
}
if (!(ctx instanceof JexlContext)) {
throw new SCXMLExpressionException(ERR_CTX_TYPE);
}
try {
final JexlContext effective = getEffectiveContext((JexlContext) ctx);
final Script jexlScript = getJexlEngine().createScript(script);
return jexlScript.execute(effective);
} catch (Exception e) {
String exMessage = e.getMessage() != null ? e.getMessage() : e.getClass().getCanonicalName();
throw new SCXMLExpressionException("evalScript('" + script + "'): " + exMessage, e);
}
}
/**
* Create a new child context.
*
* @param parent parent context
* @return new child context
* @see Evaluator#newContext(Context)
*/
public Context newContext(final Context parent) {
return new JexlContext(parent);
}
/**
* Create the internal JexlEngine member during the initialization.
* This method can be overriden to specify more detailed options
* into the JexlEngine.
* @return new JexlEngine instance
*/
protected JexlEngine createJexlEngine() {
JexlEngine engine = new JexlEngine();
// With null prefix, define top-level user defined functions.
// See javadoc of org.apache.commons.jexl2.JexlEngine#setFunctions(Map<String,Object> funcs) for detail.
Map<String, Object> funcs = new HashMap<String, Object>();
funcs.put(null, JexlBuiltin.class);
engine.setFunctions(funcs);
engine.setCache(256);
return engine;
}
/**
* Returns the internal JexlEngine if existing.
* Otherwise, it creates a new engine by invoking {@link #createJexlEngine()}.
* <P>
* <EM>NOTE: The internal JexlEngine instance can be null when this is deserialized.</EM>
* </P>
* @return the current JexlEngine
*/
private JexlEngine getJexlEngine() {
JexlEngine engine = jexlEngine;
if (engine == null) {
synchronized (this) {
engine = jexlEngine;
if (engine == null) {
jexlEngine = engine = createJexlEngine();
jexlEngine.setSilent(jexlEngineSilent);
jexlEngine.setStrict(jexlEngineStrict);
}
}
}
return engine;
}
/**
* 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 JexlContext for this state.
* @return The effective JexlContext for the path leading up to
* document root.
*/
protected JexlContext getEffectiveContext(final JexlContext nodeCtx) {
return new JexlContext(nodeCtx, new EffectiveContextMap(nodeCtx));
}
}