package org.uscxml.datamodel.ecmascript; import java.lang.reflect.Method; import org.mozilla.javascript.Callable; import org.mozilla.javascript.Context; import org.mozilla.javascript.EvaluatorException; import org.mozilla.javascript.FunctionObject; import org.mozilla.javascript.NativeJSON; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.Undefined; import org.uscxml.Data; import org.uscxml.DataModel; import org.uscxml.Event; import org.uscxml.Interpreter; import org.uscxml.StringList; import org.uscxml.StringVector; public class ECMAScriptDataModel extends DataModel { public static boolean debug = true; private class NullCallable implements Callable { @Override public Object call(Context context, Scriptable scope, Scriptable holdable, Object[] objects) { return objects[1]; } } public Context ctx; public Scriptable scope; public Interpreter interpreter; public Data getScriptableAsData(Object object) { Data data = new Data(); Scriptable s; try { s = (Scriptable) object; String className = s.getClassName(); // ECMA class name if (className.toLowerCase().equals("object")) { ScriptableObject obj = (ScriptableObject) Context.toObject(s, scope); for (Object key : obj.getIds()) { data.put(Context.toString(key), getScriptableAsData(obj.get(key))); } } } catch (ClassCastException e) { if (object instanceof Boolean) { data.setAtom(Context.toBoolean(object) ? "true" : "false"); data.setType(Data.Type.INTERPRETED); } else if (object instanceof String) { data.setAtom((String) object); data.setType(Data.Type.VERBATIM); } else if (object instanceof Integer) { data.setAtom(((Integer) object).toString()); data.setType(Data.Type.INTERPRETED); } else { throw new RuntimeException("Unhandled ECMA type " + object.getClass().getName()); } } return data; } public ScriptableObject getDataAsScriptable(Data data) { throw new UnsupportedOperationException("Not implemented"); } public static boolean jsIn(String stateName) { return true; } @Override public DataModel create(Interpreter interpreter) { /** * Called when an SCXML interpreter wants an instance of this datamodel * Be careful to instantiate attributes of instance returned and not * *this* */ ECMAScriptDataModel newDM = new ECMAScriptDataModel(); newDM.interpreter = interpreter; newDM.ctx = Context.enter(); try { newDM.scope = newDM.ctx.initStandardObjects(); } catch (Exception e) { System.err.println(e); } newDM.scope.put("_name", newDM.scope, interpreter.getName()); newDM.scope.put("_sessionid", newDM.scope, interpreter.getSessionId()); // ioProcessors { Data ioProcs = new Data(); StringVector keys = interpreter.getIOProcessorKeys(); for (int i = 0; i < keys.size(); i++) { ioProcs.put(keys.get(i), interpreter .getIOProcessors().get(keys.get(i)) .getDataModelVariables()); } newDM.scope .put("_ioprocessors", newDM.scope, new ECMAData(ioProcs)); } // invokers { Data invokers = new Data(); StringVector keys = interpreter.getInvokerKeys(); for (int i = 0; i < keys.size(); i++) { invokers.put(keys.get(i), interpreter .getInvokers().get(keys.get(i)) .getDataModelVariables()); } newDM.scope .put("_ioprocessors", newDM.scope, new ECMAData(invokers)); } // In predicate (not working as static is required) see: // http://stackoverflow.com/questions/3441947/how-do-i-call-a-method-of-a-java-instance-from-javascript/16479685#16479685 try { Class[] parameters = new Class[] { String.class }; Method inMethod = ECMAScriptDataModel.class.getMethod("jsIn", parameters); FunctionObject inFunc = new FunctionObject("In", inMethod, newDM.scope); newDM.scope.put("In", newDM.scope, inFunc); } catch (SecurityException e) { System.err.println(e); } catch (NoSuchMethodException e) { System.err.println(e); } return newDM; } @Override public StringList getNames() { /** * Register with the following names for the datamodel attribute at the * scxml element. */ StringList ss = new StringList(); ss.add("ecmascript"); return ss; } @Override public boolean validate(String location, String schema) { /** * Validate the datamodel. This make more sense for XML datamodels and * is pretty much unused but required as per draft. */ return true; } @Override public void setEvent(Event event) { if (debug) { System.out.println(interpreter.getName() + " setEvent"); } /** * Make the current event available as the variable _event in the * datamodel. */ ECMAEvent ecmaEvent = new ECMAEvent(event); scope.put("_event", scope, ecmaEvent); } @Override public Data getStringAsData(String content) { if (debug) { System.out.println(interpreter.getName() + " getStringAsData"); } /** * Evaluate the string as a value expression and transform it into a * JSON-like Data structure */ if (content.length() == 0) { return new Data(); } // is it a json expression? try { Object json = NativeJSON.parse(ctx, scope, content, new NullCallable()); if (json != NativeJSON.NOT_FOUND) { return getScriptableAsData(json); } } catch (org.mozilla.javascript.EcmaError e) { System.err.println(e); } // is it a function call or variable? Object x = ctx.evaluateString(scope, content, "uscxml", 0, null); if (x == Undefined.instance) { // maybe a literal string? x = ctx.evaluateString(scope, '"' + content + '"', "uscxml", 0, null); } return getScriptableAsData(x); } @Override public long getLength(String expr) { if (debug) { System.out.println(interpreter.getName() + " getLength"); } /** * Return the length of the expression if it were an array, used by * foreach element. */ Object x = scope.get(expr, scope); if (x == Undefined.instance) { return 0; } Scriptable result = Context.toObject(x, scope); if (result.has("length", result)) { return (long) Context.toNumber(result.get("length", result)); } return 0; } @Override public void setForeach(String item, String array, String index, long iteration) { if (debug) { System.out.println(interpreter.getName() + " setForeach"); } /** * Prepare an iteration of the foreach element, by setting the variable * in index to the current iteration and setting the variable in item to * the current item from array. */ try { // get the array object Scriptable arr = (Scriptable) scope.get(array, scope); if (arr.has((int) iteration, arr)) { ctx.evaluateString(scope, item + '=' + array + '[' + iteration + ']', "uscxml", 1, null); if (index.length() > 0) { ctx.evaluateString(scope, index + '=' + iteration, "uscxml", 1, null); } } else { handleException(""); } } catch (ClassCastException e) { System.err.println(e); } } @Override public void eval(String scriptElem, String expr) { if (debug) { System.out.println(interpreter.getName() + " eval"); } /** * Evaluate the given expression in the datamodel. This is used foremost * with script elements. */ ctx.evaluateString(scope, expr, "uscxml", 1, null); } @Override public String evalAsString(String expr) { if (debug) { System.out.println(interpreter.getName() + " evalAsString: " + expr); } /** * Evaluate the expression as a string e.g. for the log element. */ if (!ctx.stringIsCompilableUnit(expr)) { handleException(""); return ""; } try { Object result = ctx.evaluateString(scope, expr, "uscxml", 1, null); return Context.toString(result); } catch (IllegalStateException e) { System.err.println(e); handleException(""); } catch (EvaluatorException e) { System.err.println(e); handleException(""); } return ""; } @Override public boolean evalAsBool(String elem, String expr) { if (debug) { System.out.println(interpreter.getName() + " evalAsBool"); } /** * Evaluate the expression as a boolean for cond attributes in if and * transition elements. */ Object result = ctx.evaluateString(scope, expr, "uscxml", 1, null); return Context.toBoolean(result); } @Override public boolean isDeclared(String expr) { if (debug) { System.out.println(interpreter.getName() + " isDeclared"); } /** * The interpreter is supposed to raise an error if we assign to an * undeclared variable. This method is used to check whether a location * from assign is declared. */ Object x = scope.get(expr, scope); return x != Scriptable.NOT_FOUND; } @Override public void init(String dataElem, String location, String content) { if (debug) { System.out.println(interpreter.getName() + " init"); } /** * Called when we pass data elements. */ if (("null").equals(location)) return; if (("null").equals(content) || content.length() == 0) { scope.put(location, scope, Context.getUndefinedValue()); return; } try { Object json = NativeJSON.parse(ctx, scope, content, new NullCallable()); if (json != NativeJSON.NOT_FOUND) { scope.put(location, scope, json); } else { scope.put(location, scope, content); } } catch (org.mozilla.javascript.EcmaError e) { scope.put(location, scope, content); } } @Override public void assign(String assignElem, String location, String content) { if (debug) { System.out.println(interpreter.getName() + " assign"); } /** * Called when we evaluate assign elements */ if (("null").equals(location)) return; if (("null").equals(content) || content.length() == 0) { scope.put(location, scope, Context.getUndefinedValue()); return; } String expr = location + "=" + content; ctx.evaluateString(scope, expr, "uscxml", 1, null); } public void handleException(String cause) { Event exceptionEvent = new Event(); exceptionEvent.setName("error.execution"); exceptionEvent.setEventType(Event.Type.PLATFORM); interpreter.receiveInternal(exceptionEvent); } }