001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.scxml2.env.javascript;
019
020import java.util.UUID;
021import java.util.regex.Pattern;
022
023import javax.script.Bindings;
024import javax.script.ScriptContext;
025import javax.script.ScriptEngine;
026import javax.script.ScriptEngineManager;
027
028import org.apache.commons.scxml2.Context;
029import org.apache.commons.scxml2.Evaluator;
030import org.apache.commons.scxml2.EvaluatorProvider;
031import org.apache.commons.scxml2.SCXMLExpressionException;
032import org.apache.commons.scxml2.XPathBuiltin;
033import org.apache.commons.scxml2.env.EffectiveContextMap;
034import org.apache.commons.scxml2.model.SCXML;
035
036/**
037 * Embedded JavaScript expression evaluator for SCXML expressions. This
038 * implementation is a just a 'thin' wrapper around the Javascript engine in
039 * JDK 6 (based on on Mozilla Rhino 1.6.2).
040 * <p>
041 * Mozilla Rhino 1.6.2 does not support E4X so accessing the SCXML data model
042 * is implemented in the same way as the JEXL expression evaluator i.e. using
043 * the Data() function, for example,
044 * &lt;assign location="Data(hotelbooking,'hotel/rooms')" expr="2" /&gt;
045 * <p>
046 */
047
048public class JSEvaluator implements Evaluator {
049
050    /**
051     * Unique context variable name used for temporary reference to assign data (thus must be a valid variable name)
052     */
053    private static final String ASSIGN_VARIABLE_NAME = "a"+UUID.randomUUID().toString().replace('-','x');
054
055    public static final String SUPPORTED_DATA_MODEL = Evaluator.ECMASCRIPT_DATA_MODEL;
056
057    public static class JSEvaluatorProvider implements EvaluatorProvider {
058
059        @Override
060        public String getSupportedDatamodel() {
061            return SUPPORTED_DATA_MODEL;
062        }
063
064        @Override
065        public Evaluator getEvaluator() {
066            return new JSEvaluator();
067        }
068
069        @Override
070        public Evaluator getEvaluator(final SCXML document) {
071            return new JSEvaluator();
072        }
073    }
074
075    /** Error message if evaluation context is not a JexlContext. */
076    private static final String ERR_CTX_TYPE = "Error evaluating JavaScript "
077        + "expression, Context must be a org.apache.commons.scxml2.env.javascript.JSContext";
078
079    /** Pattern for recognizing the SCXML In() special predicate. */
080    private static final Pattern IN_FN = Pattern.compile("In\\(");
081    /** Pattern for recognizing the Commons SCXML Data() builtin function. */
082    private static final Pattern DATA_FN = Pattern.compile("Data\\(");
083    /** Pattern for recognizing the Commons SCXML Location() builtin function. */
084    private static final Pattern LOCATION_FN = Pattern.compile("Location\\(");
085
086    // INSTANCE VARIABLES
087
088    private ScriptEngineManager factory;
089
090    // CONSTRUCTORS
091
092    /**
093     * Initialises the internal Javascript engine factory.
094     */
095    public JSEvaluator() {
096        factory = new ScriptEngineManager();
097    }
098
099    // INSTANCE METHODS
100
101    @Override
102    public String getSupportedDatamodel() {
103        return SUPPORTED_DATA_MODEL;
104    }
105
106    /**
107     * Creates a child context.
108     *
109     * @return Returns a new child JSContext.
110     *
111     */
112    @Override
113    public Context newContext(Context parent) {
114        return new JSContext(parent);
115    }
116
117    /**
118     * Evaluates the expression using a new Javascript engine obtained from
119     * factory instantiated in the constructor. The engine is supplied with
120     * a new JSBindings that includes the SCXML Context and
121     * <code>Data()</code> functions are replaced with an equivalent internal
122     * Javascript function.
123     *
124     * @param context    SCXML context.
125     * @param expression Expression to evaluate.
126     *
127     * @return Result of expression evaluation or <code>null</code>.
128     *
129     * @throws SCXMLExpressionException Thrown if the expression was invalid.
130     */
131    @Override
132    public Object eval(Context context, String expression) throws SCXMLExpressionException {
133        if (expression == null) {
134            return null;
135        }
136
137        if (!(context instanceof JSContext)) {
138            throw new SCXMLExpressionException(ERR_CTX_TYPE);
139        }
140
141        try {
142            JSContext effectiveContext = getEffectiveContext((JSContext) context);
143
144            // ... initialize
145            ScriptEngine engine = factory.getEngineByName("JavaScript");
146            Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
147
148            // ... replace built-in functions
149            String jsExpression = IN_FN.matcher(expression).replaceAll("_builtin.In(");
150            jsExpression = DATA_FN.matcher(jsExpression).replaceAll("_builtin.Data(");
151            jsExpression = LOCATION_FN.matcher(jsExpression).replaceAll("_builtin.Location(");
152
153            // ... evaluate
154            JSBindings jsBindings = new JSBindings(effectiveContext, bindings);
155            jsBindings.put("_builtin", new JSFunctions(effectiveContext));
156
157            Object ret = engine.eval(jsExpression, jsBindings);
158
159            // copy global bindings attributes to context, so callers may get access to the evaluated variables.
160            copyGlobalBindingsToContext(jsBindings, (JSContext) effectiveContext);
161
162            return ret;
163
164        } catch (Exception x) {
165            throw new SCXMLExpressionException("Error evaluating ['" + expression + "'] " + x);
166        }
167    }
168
169    /**
170     * Evaluates a conditional expression using the <code>eval()</code> method and
171     * casting the result to a Boolean.
172     *
173     * @param context    SCXML context.
174     * @param expression Expression to evaluate.
175     *
176     * @return Boolean or <code>null</code>.
177     *
178     * @throws SCXMLExpressionException Thrown if the expression was invalid or did
179     *                                  not return a boolean.
180     */
181    @Override
182    public Boolean evalCond(Context context, String expression) throws SCXMLExpressionException {
183        final Object result = eval(context, expression);
184
185        if (result == null) {
186            return Boolean.FALSE;
187        }
188
189        if (result instanceof Boolean) {
190            return (Boolean)result;
191        }
192
193        throw new SCXMLExpressionException("Invalid boolean expression: " + expression);
194    }
195
196    /**
197     * Evaluates a location expression using a new Javascript engine obtained from
198     * factory instantiated in the constructor. The engine is supplied with
199     * a new JSBindings that includes the SCXML Context and
200     * <code>Data()</code> functions are replaced with an equivalent internal
201     * Javascript function.
202     *
203     * @param context    FSM context.
204     * @param expression Expression to evaluate.
205     *
206     * @throws SCXMLExpressionException Thrown if the expression was invalid.
207     */
208    @Override
209    public Object evalLocation(Context context, String expression) throws SCXMLExpressionException {
210        if (expression == null) {
211            return null;
212        } else if (context.has(expression)) {
213            return expression;
214        }
215
216        return eval(context, expression);
217    }
218
219    /**
220     * @see Evaluator#evalAssign(Context, String, Object, AssignType, String)
221     */
222    public void evalAssign(final Context ctx, final String location, final Object data, final AssignType type,
223                           final String attr) throws SCXMLExpressionException {
224
225        Object loc = evalLocation(ctx, location);
226
227        if (loc != null) {
228            if (XPathBuiltin.isXPathLocation(ctx, loc)) {
229                XPathBuiltin.assign(ctx, loc, data, type, attr);
230            } else {
231                StringBuilder sb = new StringBuilder(location).append("=").append(ASSIGN_VARIABLE_NAME);
232
233                try {
234                    ctx.getVars().put(ASSIGN_VARIABLE_NAME, data);
235                    eval(ctx, sb.toString());
236                } finally {
237                    ctx.getVars().remove(ASSIGN_VARIABLE_NAME);
238                }
239            }
240        } else {
241            throw new SCXMLExpressionException("evalAssign - cannot resolve location: '" + location + "'");
242        }
243    }
244
245    /**
246     * Executes the script using a new Javascript engine obtained from
247     * factory instantiated in the constructor. The engine is supplied with
248     * a new JSBindings that includes the SCXML Context and
249     * <code>Data()</code> functions are replaced with an equivalent internal
250     * Javascript function.
251     *
252     * @param ctx    SCXML context.
253     * @param script Script to execute.
254     *
255     * @return Result of script execution or <code>null</code>.
256     *
257     * @throws SCXMLExpressionException Thrown if the script was invalid.
258     */
259    @Override
260    public Object evalScript(Context ctx, String script) throws SCXMLExpressionException {
261        return eval(ctx, script);
262    }
263
264    /**
265     * Create a new context which is the summation of contexts from the
266     * current state to document root, child has priority over parent
267     * in scoping rules.
268     *
269     * @param nodeCtx The JexlContext for this state.
270     * @return The effective JexlContext for the path leading up to
271     *         document root.
272     */
273    protected JSContext getEffectiveContext(final JSContext nodeCtx) {
274        return new JSContext(nodeCtx, new EffectiveContextMap(nodeCtx));
275    }
276
277    /**
278     * Copy the global Bindings (i.e. nashorn Global instance) attributes to {@code jsContext}
279     * in order to make sure all the new global variables set by the JavaScript engine after evaluation
280     * available from {@link JSContext} instance as well.
281     * @param jsBindings
282     * @param jsContext
283     */
284    private void copyGlobalBindingsToContext(final JSBindings jsBindings, final JSContext jsContext) {
285        Bindings globalBindings = jsBindings.getGlobalBindings();
286
287        if (globalBindings != null) {
288            for (String key : globalBindings.keySet()) {
289                jsContext.set(key, globalBindings.get(key));
290            }
291        }
292    }
293}