View Javadoc
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   * &lt;assign location="Data(hotelbooking,'hotel/rooms')" expr="2" /&gt;
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 }