/* * This file is part of NodeBox. * * Copyright (C) 2008 Frederik De Bleser (frederik@pandora.be) * * NodeBox is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * NodeBox is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with NodeBox. If not, see . */ package nodebox.node; import nodebox.graphics.Color; import nodebox.util.waves.AbstractWave; import org.mvel2.CompileException; import org.mvel2.MVEL; import org.mvel2.ParserContext; import org.mvel2.UnresolveablePropertyException; import org.mvel2.integration.VariableResolver; import org.mvel2.integration.impl.BaseVariableResolverFactory; import org.mvel2.integration.impl.SimpleValueResolver; import org.mvel2.optimizers.OptimizerFactory; import org.python.google.common.collect.ImmutableMap; import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; public class Expression { static ParserContext parserContext = new ParserContext(); private static ImmutableMap defaultResolvers; static { // Initialize MVEL. // The dynamic optimizer crashes for some reason, so we use the "safe reflective" one. // Although "safe" sounds slower, this optimizer actually seems *faster* // than the dynamic one. Don't change this unless you want to go digging for weird // reflective constructor errors. OptimizerFactory.setDefaultOptimizer(OptimizerFactory.SAFE_REFLECTIVE); // Add "built-in" methods to the expression context. parserContext = new ParserContext(); try { // MVEL has a bug where it accepts methods with varargs, but only executes the method with // non-varargs. So in our ExpressionHelper we have both the varargs and non-varargs methods. // We lookup the varargs version here, but only the non-varargs will get called. parserContext.addImport("random", ExpressionHelper.class.getMethod("random", Object.class, double[].class)); parserContext.addImport("randint", ExpressionHelper.class.getMethod("randint", Object.class, int.class, int.class)); parserContext.addImport("clamp", ExpressionHelper.class.getMethod("clamp", double.class, double.class, double.class)); parserContext.addImport("color", ExpressionHelper.class.getMethod("color", double[].class)); parserContext.addImport("rgb", ExpressionHelper.class.getMethod("color", double[].class)); parserContext.addImport("hsb", ExpressionHelper.class.getMethod("hsb", double[].class)); parserContext.addImport("stamp", ExpressionHelper.class.getMethod("stamp", String.class, Object.class)); parserContext.addImport("int", ExpressionHelper.class.getMethod("toInt", double.class)); parserContext.addImport("float", ExpressionHelper.class.getMethod("toFloat", int.class)); parserContext.addImport("hold", ExpressionHelper.class.getMethod("hold", double.class, double.class, double[].class)); parserContext.addImport("schedule", ExpressionHelper.class.getMethod("schedule", double.class, double.class, double.class, double[].class)); parserContext.addImport("timeloop", ExpressionHelper.class.getMethod("timeloop", double.class, List.class)); parserContext.addImport("timeloop", ExpressionHelper.class.getMethod("timeloop", double.class, List.class, double.class)); parserContext.addImport("wave", ExpressionHelper.class.getMethod("wave")); parserContext.addImport("wave", ExpressionHelper.class.getMethod("wave", AbstractWave.Type.class, double[].class)); parserContext.addImport("math", Math.class); } catch (NoSuchMethodException e) { throw new AssertionError("Unknown static method for expression." + e); } ImmutableMap.Builder resolvers = ImmutableMap.builder(); for (AbstractWave.Type wave : AbstractWave.Type.values()) resolvers.put(wave.name(), new SimpleValueResolver(wave)); defaultResolvers = resolvers.build(); } private final Parameter parameter; private final String expression; private transient Throwable error; private transient Serializable compiledExpression; private Set> markedParameterReferences; /** * Construct and set the expression. *

* The expression is accepted as is, and no errors will be thrown even if the expression is invalid. * Only during compilation or evaluation will this result in an error. * * @param parameter * @param expression */ public Expression(Parameter parameter, String expression) { assert parameter != null; // We need the current parameter for stamp expressions. this.parameter = parameter; this.expression = expression; markedParameterReferences = null; compiledExpression = null; } //// Attribute access //// public String getExpression() { return expression; } public boolean hasError() { return error != null; } public Throwable getError() { return error; } /* package private */ void setError(Exception error) { // This method is called from Parameter to set an error for cyclic dependencies. this.error = error; } public Parameter getParameter() { return parameter; } //// Values //// public boolean asBoolean() throws ExpressionError { Object value = evaluate(); if (value instanceof Boolean) { return (Boolean) value; } else { throw new ExpressionError("Value \"" + value + "\" for expression \"" + expression + "\" is not a boolean."); } } public int asInt() throws ExpressionError { Object value = evaluate(); if (value instanceof Number) { return (Integer) value; } else { throw new ExpressionError("Value \"" + value + "\" for expression \"" + expression + "\" is not an integer."); } } public double asFloat() throws ExpressionError { Object value = evaluate(); if (value instanceof Number) { return (Double) value; } else { throw new ExpressionError("Value \"" + value + "\" for expression \"" + expression + "\" is not a floating-point value."); } } public String asString() throws ExpressionError { Object value = evaluate(); if (value instanceof String) { return (String) value; } else { throw new ExpressionError("Value \"" + value + "\" for expression \"" + expression + "\" is not a string."); } } public Color asColor() throws ExpressionError { Object value = evaluate(); if (value instanceof Color) { return (Color) value; } else { throw new ExpressionError("Value \"" + value + "\" for expression \"" + expression + "\" is not a color."); } } //// Evaluation //// /** * Compile the expression. * * @throws ExpressionError if the compilation fails. * @see #getError() */ public void compile() throws ExpressionError { try { this.compiledExpression = MVEL.compileExpression(expression, parserContext); error = null; } catch (Exception e) { error = e; throw new ExpressionError("Cannot compile expression '" + expression + "' on " + getParameter().getAbsolutePath() + ": " + e.getMessage(), e); } } /** * Evaluate the expression and return the result. * * @return the result of the expression * @throws ExpressionError if an error occurs whilst evaluating the expression. */ public Object evaluate() throws ExpressionError { return evaluate(new ProcessingContext(parameter.getNode())); } /** * Evaluate the expression and return the result. *

* Throw an exception if an error occurs. You can retrieve this exception by calling getError(). * * @param context the context wherein evaluation happens. * @return the result of the expression * @throws ExpressionError if an error occurs whilst evaluating the expression. * @see #getError() */ public Object evaluate(ProcessingContext context) throws ExpressionError { // If there was an error with the expression, throw it before doing anything. if (hasError()) { throw new ExpressionError("Cannot compile expression '" + expression + "' on " + getParameter().getAbsolutePath() + ": " + getError().getMessage(), getError()); } // If the expression was not compiled, compile it first. // This can throw an ExpressionError, which will be forwarded to the caller. if (compiledExpression == null) { compile(); } // Set up state variables in the expression utilities class. // TODO: This is not thread-safe. ExpressionHelper.currentContext = context; ExpressionHelper.currentParameter = parameter; // Marked parameter references are used to find which parameters this expression references. markedParameterReferences = new HashSet>(); ProxyResolverFactory prf = new ProxyResolverFactory(parameter.getNode(), context, markedParameterReferences); try { error = null; return MVEL.executeExpression(compiledExpression, prf); } catch (Exception e) { error = e; throw new ExpressionError("Cannot evaluate expression '" + expression + "' on " + getParameter().getAbsolutePath() + ": " + e.getMessage(), e); } } /** * Returns all parameters this expression depends on *

* If the expression contains an error, this method will return an empty set. * * @return a set of parameters */ public Set getDependencies() { if (markedParameterReferences == null) { try { evaluate(); } catch (ExpressionError expressionError) { return new HashSet(0); } } HashSet dependencies = new HashSet(markedParameterReferences.size()); for (WeakReference ref : markedParameterReferences) { Parameter p = ref.get(); if (p != null) dependencies.add(p); } return dependencies; } class ProxyResolverFactory extends BaseVariableResolverFactory { private Node node; private NodeAccessProxy proxy; private ProcessingContext context; public ProxyResolverFactory(Node node, ProcessingContext context) { this.node = node; proxy = new NodeAccessProxy(node); this.context = context; variableResolvers.putAll(defaultResolvers); } public ProxyResolverFactory(Node node, ProcessingContext context, Set> markedParameterReferences) { this.node = node; proxy = new NodeAccessProxy(node, markedParameterReferences); this.context = context; variableResolvers.putAll(defaultResolvers); } public Node getNode() { return node; } public NodeAccessProxy getProxy() { return proxy; } public VariableResolver createVariable(String name, Object value) { throw new CompileException("Variable assignment is not supported."); } public VariableResolver createVariable(String name, Object value, Class type) { throw new CompileException("Variable assignment is not supported."); } @Override public VariableResolver getVariableResolver(String name) { if (variableResolvers == null) { variableResolvers = new HashMap(); variableResolvers.putAll(defaultResolvers); } VariableResolver vr = variableResolvers.get(name); if (vr != null) { return vr; } else if (proxy.containsKey(name)) { vr = new ProxyResolver(proxy, proxy.get(name)); variableResolvers.put(name, vr); return vr; } else if (context.containsKey(name)) { vr = new ProcessingContextResolver(context, name); variableResolvers.put(name, vr); return vr; } else if (nextFactory != null) { return nextFactory.getVariableResolver(name); } throw new UnresolveablePropertyException("unable to resolve variable '" + name + "'"); } public boolean isResolveable(String name) { return (variableResolvers != null && variableResolvers.containsKey(name)) || (proxy.containsKey(name)) || (context.containsKey(name)) || (nextFactory != null && nextFactory.isResolveable(name)); } public boolean isTarget(String name) { return variableResolvers != null && variableResolvers.containsKey(name); } @Override public Set getKnownVariables() { Set knownVariables = new HashSet(); knownVariables.addAll(proxy.keySet()); knownVariables.addAll(context.keySet()); return knownVariables; } } class ProxyResolver implements VariableResolver { private NodeAccessProxy proxy; private Object value; public ProxyResolver(NodeAccessProxy proxy, Object value) { this.proxy = proxy; this.value = value; } public NodeAccessProxy getProxy() { return proxy; } public Node getNode() { return proxy.getNode(); } public String getName() { return proxy.getNode().getName(); } public Class getType() { return Object.class; } public void setStaticType(Class type) { throw new RuntimeException("Not implemented"); } public int getFlags() { return 0; } public Object getValue() { return value; } public void setValue(Object value) { throw new CompileException("Parameter values cannot be changed through expressions."); } } class ProcessingContextResolver implements VariableResolver { private String name; private ProcessingContext context; ProcessingContextResolver(ProcessingContext context, String name) { this.context = context; this.name = name; } public String getName() { return name; } public Class getType() { return Object.class; } public void setStaticType(Class aClass) { throw new RuntimeException("Not implemented"); } public int getFlags() { return 0; } public Object getValue() { return context.get(name); } public void setValue(Object o) { throw new CompileException("You cannot change the value of a constant."); } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Expression other = (Expression) o; return expression.equals(other.expression); } @Override public int hashCode() { return expression.hashCode(); } }