package context.arch.enactor; import java.util.Map; import javax.script.Bindings; import javax.script.Compilable; import javax.script.CompiledScript; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import context.arch.discoverer.ComponentDescription; import context.arch.storage.Attribute; import context.arch.storage.AttributeNameValue; /** * Parser to parse a Javascript expression, and evaluate it with the Javascript eval() function * to assign a value to an {@link Attribute}. This is used by {@link EnactorXmlParser} to parse * expressions for outcome values for References. * @author Brian Y. Lim * * @param <T> */ public class AttributeEvalParser<T extends Comparable<? super T>> { private Attribute<?> attribute; private Class<T> type; private ScriptEngineManager manager; private ScriptEngine engine; private String scriptText; private CompiledScript script; private Bindings bindings; /** * @see #instance(Attribute, String, Map) * @param attribute * @param scriptText * @param constVars */ private AttributeEvalParser( Attribute<T> attribute, String scriptText, Map<String, Comparable<?>> constVars) { this.attribute = attribute; this.type = (Class<T>) attribute.getType(); this.scriptText = scriptText; manager = new ScriptEngineManager(); engine = manager.getEngineByName("js"); // javascript // compile to make script run faster on each execution try { script = ((Compilable) engine).compile(scriptText); } catch (ScriptException e) { e.printStackTrace(); } // bindings for vars bindings = engine.createBindings(); // add vars from constants bindings.putAll(constVars); } /** * Create an instance of the parser for an attribute from a Javascript expression. * Rather than calling a static parse method, we need to instantiate the parser to * configure it appropriately, and invoke the parsing later during runtime. This also * allows for retaining information about the original script expression. * * @param <T> the inferred type of the attribute to set * @param attribute the Attribute to assign to from this script expression * @param script Javascript expression as a string * @param constVars map of constant variables (and their values) that may be referenced in the script expression. * These will be bound as variables to the script engine. * * @return an instance of the parser that can parse the script expression to an Attribute. */ public static <T extends Comparable<? super T>> AttributeEvalParser<T> instance( Attribute<T> attribute, String script, Map<String, Comparable<?>> constVars) { return new AttributeEvalParser<T>(attribute, script, constVars); } /** * Get the name of the attribute that this parser will create on parsing. * @return */ public String getAttributeName() { return attribute.getName(); } /** * Pseudonym for {@link #eval(ComponentDescription)} * @param inWidgetStub from which to obtain values of non-constant attributes that may be references in the script. * @return Javascript eval() result converted to string so that it can be forced back to the appropriate type. * @see #eval(ComponentDescription) */ public T getAttributeValue(ComponentDescription inWidgetStub) { Object value = eval(inWidgetStub); return cast(value, type); } /** * Javascript eval() result converted to string so that it can be forced back to the appropriate type. * @param inWidgetStub from which to obtain values of non-constant attributes that may be references in the script. * @return */ protected String eval(ComponentDescription inWidgetStub) { try { // extract non-constant attribute values, and put into script for (Attribute<?> a : inWidgetStub.getNonConstantAttributes().values()) { if (a instanceof AttributeNameValue<?>) { AttributeNameValue<?> att = (AttributeNameValue<?>) a; String name = att.getName(); Object value = att.getValue(); bindings.put(name, value); } } engine.setBindings(bindings, ScriptContext.GLOBAL_SCOPE); if (script != null) { return script.eval().toString(); } // compiled version: faster else { return engine.eval(scriptText).toString(); } // uncompiled version: slower } catch (ScriptException e) { e.printStackTrace(); return null; } } /** * Convenience method to cast because javascript eval() would return values as String. * @param <T> type to cast to * @param value returned from the javascript eval() method * @param type to cast to * @return the value in the correct type cast */ @SuppressWarnings("unchecked") private T cast(Object value, Class<T> type) { if (type.equals(String.class)) { return (T) value; } // is number type, so would be mapped to double if (Number.class.isAssignableFrom(type)) { //double number = (Double) value; // actually, it seems eval() returns string double number = new Double((String) value); //System.out.println("number = " + number); if (type.equals(Double.class)) { return (T) value; } else if (type.equals(Integer.class)) { return (T) new Integer((int) number); } else if (type.equals(Float.class)) { return (T) new Float((float) number); } else if (type.equals(Short.class)) { return (T) new Short((short) number); } else if (type.equals(Byte.class)) { return (T) new Byte((byte) number); } else { return (T) value; } // unknown } else if (type.equals(Boolean.class)) { return (T) new Boolean((String) value); } // type is some proprietary class // use reflection to extract from String else { String strValue = (String) value; T tValue = null; try { tValue = (T) type.getMethod("valueOf", String.class) .invoke(null, strValue); } catch (Exception e) { e.printStackTrace(); } return tValue; } } }