/* * $Id$ * * Copyright (c) 2008-2009 by Brent Easton * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, copies are available * at http://www.opensource.org. */ package VASSAL.script; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import VASSAL.build.GameModule; import VASSAL.build.module.Map; import VASSAL.build.module.properties.PropertySource; import VASSAL.counters.GamePiece; import VASSAL.counters.Stack; import VASSAL.script.expression.ExpressionException; import VASSAL.tools.WarningDialog; import VASSAL.tools.io.IOUtils; import bsh.BeanShellExpressionValidator; import bsh.EvalError; import bsh.NameSpace; /** * * A BeanShell Interpreter customised to evaluate a single Vassal * expression containing Vassal property references. * All traits with the same expression will share the same Interpreter * * Each ExpressionInterpreter has 2 levels of NameSpace: * 1. Top level is a single global NameSpace that contains utility methods * available to all ExpressionInterpreters. It is the parent of all * level 2 NameSpaces. * 2. Level 2 is a NameSpace for each unique expression that contains the * parsed expression. All expressions in all traits that are the same * will use the one Expression NameSpace. * */ public class ExpressionInterpreter extends AbstractInterpreter { private static final long serialVersionUID = 1L; private static final Logger logger = LoggerFactory.getLogger(ExpressionInterpreter.class); protected static final String INIT_SCRIPT = "/VASSAL/script/init_expression.bsh"; protected static final String THIS = "_interp"; protected static final String SOURCE = "_source"; protected static final String MAGIC1 = "_xyzzy"; protected static final String MAGIC2 = "_plugh"; protected static final String MAGIC3 = "_plover"; // Top-level static NameSpace shared between all ExpressionInterpreters // Loaded with utility methods available to all interpreters protected static NameSpace topLevelNameSpace; protected NameSpace expressionNameSpace; //protected NameSpace localNameSpace; protected String expression; protected PropertySource source; protected List<String> variables = new ArrayList<String>(); // Maintain a cache of all generated Interpreters. All Expressions // with the same Expression use the same Interpreter. protected static HashMap<String, ExpressionInterpreter> cache = new HashMap<String, ExpressionInterpreter>(); public static ExpressionInterpreter createInterpreter (String expr) throws ExpressionException { final String e = expr == null ? "" : strip(expr); ExpressionInterpreter interpreter = cache.get(e); if (interpreter == null) { interpreter = new ExpressionInterpreter(e); cache.put(e, interpreter); } return interpreter; } protected static String strip(String expr) { final String s = expr.trim(); if (s.startsWith("{") && s.endsWith("}")) { return s.substring(1, s.length()-1); } return expr; } /** * Private constructor to build an ExpressionInterpreter. Interpreters * can only be created by createInterpreter. * * @param expr Expression * @throws MalformedExpressionException */ private ExpressionInterpreter(String expr) throws ExpressionException { super(); expression = expr; // Install the Vassal Class loader so that bsh can find Vassal classes this.setClassLoader(this.getClass().getClassLoader()); // Initialise the top-level name space if this is the first // expression to be created if (topLevelNameSpace == null) { initialiseStatic(); } // Create the Expression level namespace as a child of the // top level namespace expressionNameSpace = new NameSpace(topLevelNameSpace, "expression"); // Get a list of any variables used in the expression. These are // property names that will need to be evaluated at expression // evaluation time variables = new BeanShellExpressionValidator(expression).getVariables(); // Build a method enclosing the expression. This saves the results // of the expression parsing, improving performance. Force return // value to a String as this is what Vassal is expecting. setNameSpace(expressionNameSpace); if (expression.length() > 0) { try { eval("String "+MAGIC2+"() { "+MAGIC3+"=" + expression + "; return "+MAGIC3+".toString();}"); } catch (EvalError e) { throw new ExpressionException(getExpression()); } } // Add a link to this Interpreter into the new NameSpace for callbacks from // BeanShell back to us setVar(THIS, this); } /** * Initialise the static elements of this class. Create a Top Level * NameSpace using the Vassal class loader, load useful classes and * read and process the init_expression.bsh file to load scripted * methods available to expressions. */ protected void initialiseStatic() { topLevelNameSpace = new NameSpace((NameSpace) null, getClassManager(), "topLevel"); setNameSpace(topLevelNameSpace); getNameSpace().importClass("VASSAL.build.module.properties.PropertySource"); getNameSpace().importClass("VASSAL.script.ExpressionInterpreter"); // Read the Expression initialisation script into the top level namespace URL ini = getClass().getResource(INIT_SCRIPT); logger.info("Attempting to load "+INIT_SCRIPT+" URI generated="+ini.toString()); BufferedReader in = null; try { in = new BufferedReader( new InputStreamReader( ini.openStream())); try { eval(in); } catch (EvalError e) { logger.error("Error trying to read init script: "+ini.toString()); WarningDialog.show(e, ""); } } catch (IOException e) { logger.error("Error trying to read init script: "+ini.toString()); WarningDialog.show(e, ""); } finally { IOUtils.closeQuietly(in); } } /** * Return the current expression * @return expression */ public String getExpression() { return expression; } /** * Evaluate the expression, setting the value of any undefined * values to the matching Vassal property value. Primitives must * be wrapped. * * @return result */ public String evaluate(PropertySource ps) throws ExpressionException { return evaluate(ps, false); } public String evaluate(PropertySource ps, boolean localized) throws ExpressionException { if (getExpression().length() == 0) { return ""; } // Default to the GameModule to satisfy properties if no // GamePiece supplied. source = ps == null ? GameModule.getGameModule() : ps; setNameSpace(expressionNameSpace); // Bind each undeclared variable with the value of the // corresponding Vassal property. Allow for old-style $variable$ references for (String var : variables) { String name = var; if (name.length() > 2 && name.startsWith("$") && name.endsWith("$")) { name = name.substring(1, name.length()-1); } Object prop = localized ? source.getLocalizedProperty(name) : source.getProperty(name); String value = prop == null ? "" : prop.toString(); if (value == null) { setVar(var, ""); } else if ("true".equals(value)) { setVar(var, true); } else if ("false".equals(value)) { setVar(var, false); } else { try { setVar(var, Integer.valueOf(value).intValue()); } catch (NumberFormatException e) { try { setVar(var, Float.valueOf(value).floatValue()); } catch (NumberFormatException e1) { setVar(var, value); } } } } // Re-evaluate the pre-parsed expression now that the undefined variables have // been bound to their Vassal property values. setVar(THIS, this); setVar(SOURCE, source); String result = ""; try { eval(MAGIC1+"="+MAGIC2+"()"); result = get(MAGIC1).toString(); } catch (EvalError e) { final String s = e.getRawMessage(); final String search = MAGIC2+"();'' : "; final int pos = s.indexOf(search); throw new ExpressionException(getExpression(), s.substring(pos+search.length())); } return result; } public String evaluate() throws ExpressionException { return getExpression().length() == 0 ? "" : evaluate(GameModule.getGameModule()); } /** * Convert a String value into a wrapped primitive object if possible. * Note this is a non-static copy of BeanShell.wrap(). Callbacks from * beanshell (e.g. getProperty) fail if an attempt is made to call a static method. * * @param value * @return wrapped value */ public Object wrap (String value) { if (value == null) { return ""; } else if ("true".equals(value)) { return Boolean.TRUE; } else if ("false".equals(value)) { return Boolean.FALSE; } else { try { return Integer.valueOf(value); } catch (NumberFormatException e) { return value; } } } /***************************************************************** * Callbacks from BeanShell Expressions to Vassal **/ public Object getProperty(String name) { final Object value = source.getProperty(name); return value == null ? "" : wrap(value.toString()); } public Object getLocalizedProperty(String name) { final Object value = source.getLocalizedProperty(name); return value == null ? "" : wrap(value.toString()); } /** * SumStack(property) function * Total the value of the named property in all counters in the * same stack as the specified piece. * * @param property Property Name * @param ps GamePiece * @return total */ public Object sumStack(String property, PropertySource ps) { int result = 0; if (ps instanceof GamePiece) { Stack s = ((GamePiece) ps).getParent(); if (s != null) { for (int i = 0; i < s.getPieceCount(); i++) { try { result += Integer.valueOf(s.getPieceAt(i).getProperty(property).toString()); } catch (Exception e) { // Anything at all goes wrong trying to add the property, just ignore it and treat as 0 } } } } return result; } /** * SumLocation(property) function * Total the value of the named property in all counters in the * same location as the specified piece. * * * WARNING * This WILL be inneficient as the number of counters on the * map increases. * * @param property Property Name * @param ps GamePiece * @return total */ public Object sumLocation(String property, PropertySource ps) { int result = 0; if (ps instanceof GamePiece) { GamePiece p = (GamePiece) ps; Map m = p.getMap(); if (m != null) { String here = m.locationName(p.getPosition()); GamePiece[] pieces = m.getPieces(); for (int i = 0; i < pieces.length; i++) { if (here.equals(m.locationName(pieces[i].getPosition()))) { if (pieces[i] instanceof Stack) { Stack s = (Stack) pieces[i]; for (int j = 0; j < s.getPieceCount(); j++) { try { result += Integer.valueOf(s.getPieceAt(j).getProperty(property).toString()); } catch (NumberFormatException e) { // } } } else { try { result += Integer.valueOf(pieces[i].getProperty(property).toString()); } catch (NumberFormatException e) { // } } } } } } return result; } }