1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.scxml2.env.javascript;
19
20 import java.util.UUID;
21 import java.util.regex.Pattern;
22
23 import javax.script.Bindings;
24 import javax.script.ScriptContext;
25 import javax.script.ScriptEngine;
26 import javax.script.ScriptEngineManager;
27
28 import org.apache.commons.scxml2.Context;
29 import org.apache.commons.scxml2.Evaluator;
30 import org.apache.commons.scxml2.EvaluatorProvider;
31 import org.apache.commons.scxml2.SCXMLExpressionException;
32 import org.apache.commons.scxml2.XPathBuiltin;
33 import org.apache.commons.scxml2.env.EffectiveContextMap;
34 import org.apache.commons.scxml2.model.SCXML;
35
36 /**
37 * Embedded JavaScript expression evaluator for SCXML expressions. This
38 * implementation is a just a 'thin' wrapper around the Javascript engine in
39 * JDK 6 (based on on Mozilla Rhino 1.6.2).
40 * <p>
41 * Mozilla Rhino 1.6.2 does not support E4X so accessing the SCXML data model
42 * is implemented in the same way as the JEXL expression evaluator i.e. using
43 * the Data() function, for example,
44 * <assign location="Data(hotelbooking,'hotel/rooms')" expr="2" />
45 * <p>
46 */
47
48 public class JSEvaluator implements Evaluator {
49
50 /**
51 * Unique context variable name used for temporary reference to assign data (thus must be a valid variable name)
52 */
53 private static final String ASSIGN_VARIABLE_NAME = "a"+UUID.randomUUID().toString().replace('-','x');
54
55 public static final String SUPPORTED_DATA_MODEL = Evaluator.ECMASCRIPT_DATA_MODEL;
56
57 public static class JSEvaluatorProvider implements EvaluatorProvider {
58
59 @Override
60 public String getSupportedDatamodel() {
61 return SUPPORTED_DATA_MODEL;
62 }
63
64 @Override
65 public Evaluator getEvaluator() {
66 return new JSEvaluator();
67 }
68
69 @Override
70 public Evaluator getEvaluator(final SCXML document) {
71 return new JSEvaluator();
72 }
73 }
74
75 /** Error message if evaluation context is not a JexlContext. */
76 private static final String ERR_CTX_TYPE = "Error evaluating JavaScript "
77 + "expression, Context must be a org.apache.commons.scxml2.env.javascript.JSContext";
78
79 /** Pattern for recognizing the SCXML In() special predicate. */
80 private static final Pattern IN_FN = Pattern.compile("In\\(");
81 /** Pattern for recognizing the Commons SCXML Data() builtin function. */
82 private static final Pattern DATA_FN = Pattern.compile("Data\\(");
83 /** Pattern for recognizing the Commons SCXML Location() builtin function. */
84 private static final Pattern LOCATION_FN = Pattern.compile("Location\\(");
85
86 // INSTANCE VARIABLES
87
88 private ScriptEngineManager factory;
89
90 // CONSTRUCTORS
91
92 /**
93 * Initialises the internal Javascript engine factory.
94 */
95 public JSEvaluator() {
96 factory = new ScriptEngineManager();
97 }
98
99 // 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 }