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