Java tutorial
/* * This software copyright by various authors including the RPTools.net * development team, and licensed under the LGPL Version 3 or, at your option, * any later version. * * Portions of this software were originally covered under the Apache Software * License, Version 1.1 or Version 2.0. * * See the file LICENSE elsewhere in this distribution for license details. */ package net.rptools.maptool.client; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.rptools.common.expression.ExpressionParser; import net.rptools.common.expression.Result; import net.rptools.maptool.client.functions.*; import net.rptools.maptool.client.functions.AbortFunction.AbortFunctionException; import net.rptools.maptool.client.functions.AssertFunction.AssertFunctionException; import net.rptools.maptool.client.ui.htmlframe.HTMLFrameFactory; import net.rptools.maptool.client.ui.macrobuttons.buttons.MacroButtonPrefs; import net.rptools.maptool.client.ui.zone.ZoneRenderer; import net.rptools.maptool.language.I18N; import net.rptools.maptool.model.MacroButtonProperties; import net.rptools.maptool.model.Player; import net.rptools.maptool.model.Token; import net.rptools.maptool.model.Zone; import net.rptools.parser.ParserException; import net.rptools.parser.VariableResolver; import net.rptools.parser.function.Function; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; public class MapToolLineParser { // Logger for this class. private static final Logger log = Logger.getLogger(MapToolLineParser.class); /** MapTool functions to add to the parser. */ private static final Function[] mapToolParserFunctions = { AbortFunction.getInstance(), AssertFunction.getInstance(), AddAllToInitiativeFunction.getInstance(), ChatFunction.getInstance(), CurrentInitiativeFunction.getInstance(), DefineMacroFunction.getInstance(), EvalMacroFunctions.getInstance(), FindTokenFunctions.getInstance(), HasImpersonated.getInstance(), InitiativeRoundFunction.getInstance(), InputFunction.getInstance(), IsTrustedFunction.getInstance(), JSONMacroFunctions.getInstance(), LookupTableFunction.getInstance(), MacroArgsFunctions.getInstance(), MacroDialogFunctions.getInstance(), MacroFunctions.getInstance(), MacroLinkFunction.getInstance(), MapFunctions.getInstance(), MiscInitiativeFunction.getInstance(), PlayerFunctions.getInstance(), RemoveAllFromInitiativeFunction.getInstance(), StateImageFunction.getInstance(), StringFunctions.getInstance(), StrListFunctions.getInstance(), StrPropFunctions.getInstance(), SwitchTokenFunction.getInstance(), TokenAddToInitiativeFunction.getInstance(), TokenBarFunction.getInstance(), TokenCopyDeleteFunctions.getInstance(), TokenGMNameFunction.getInstance(), TokenHaloFunction.getInstance(), TokenImage.getInstance(), TokenInitFunction.getInstance(), TokenInitHoldFunction.getInstance(), TokenLabelFunction.getInstance(), TokenLightFunctions.getInstance(), TokenLocationFunctions.getInstance(), TokenNameFunction.getInstance(), TokenPropertyFunctions.getInstance(), TokenRemoveFromInitiativeFunction.getInstance(), TokenSelectionFunctions.getInstance(), TokenSightFunctions.getInstance(), TokenSpeechFunctions.getInstance(), TokenStateFunction.getInstance(), TokenVisibleFunction.getInstance(), UserDefinedMacroFunctions.getInstance(), isVisibleFunction.getInstance(), getInfoFunction.getInstance(), TokenMoveFunctions.getInstance(), FogOfWarFunctions.getInstance(), VBL_Functions.getInstance(), ZoomFunctions.getInstance(), ParserPropertyFunctions.getInstance(), MathFunctions.getInstance() }; /** Name and Source or macros that come from chat. */ public static final String CHAT_INPUT = "chat"; /** Name of macro to divert calls to unknown macros on a lib macro to. */ public static final String UNKNOWN_LIB_MACRO = "!!unknown-macro!!"; /** Stack that holds our contexts. */ private final Stack<MapToolMacroContext> contextStack = new Stack<MapToolMacroContext>(); /** Was every context we entered during the macro trusted. */ private volatile boolean macroPathTrusted = false; /** * The macro button index of the button on the impersonated token that * called us. */ private volatile int macroButtonIndex = -1; /** The default value for maximum macro recursion. */ private static final int DEFAULT_MAX_RECURSIVE_DEPTH = 150; /** The current parser recursion depth. */ private int parserRecurseDepth = 0; /** The current macro recursive depth. */ private int macroRecurseDepth = 0; /** The maximum parser and macro recursive depth. */ private int maxRecursionDepth = DEFAULT_MAX_RECURSIVE_DEPTH; /** The default maximum loop iterations. */ private static final int DEFAULT_MAX_LOOP_ITERATIONS = 10000; /** The maximum amount of loop iterations. */ private int maxLoopIterations = DEFAULT_MAX_LOOP_ITERATIONS; private enum Output { // Mutually exclusive output formats NONE, RESULT, TOOLTIP, EXPANDED, UNFORMATTED, } private enum LoopType { // Mutually exclusive looping options NO_LOOP, COUNT, FOR, WHILE, FOREACH, } private enum BranchType { // Mutually exclusive branching options NO_BRANCH, IF, SWITCH, } private enum CodeType { // Mutually exclusive code-execution options NO_CODE, MACRO, CODEBLOCK, } private enum OutputLoc { // Mutually exclusive output location CHAT, DIALOG, FRAME } private enum ScanState { SEARCHING_FOR_ROLL, SEARCHING_FOR_QUOTE, SEARCHING_FOR_CLOSE_BRACKET, SKIP_NEXT_CHAR } // Class to hold the inline rolls and where they start and end. private static class InlineRollMatch { final int start; final int end; final String match; final int optEnd; @SuppressWarnings("unused") InlineRollMatch(int start, int end, String match) { this.start = start; this.end = end; this.match = match; this.optEnd = -1; } InlineRollMatch(int start, int end, String match, int optEnd) { this.start = start; this.end = end; this.match = match; this.optEnd = optEnd; } public int getStart() { return start; } public int getEnd() { return end; } public String getMatch() { return match; } @SuppressWarnings("unused") public int getOptEnd() { return optEnd; } public String getOpt() { if (optEnd > 0) { return match.substring(1, optEnd - start); } else { return ""; } } public String getRoll() { if (optEnd > 0) { return match.substring(optEnd + 1 - start, end - start); } else { return match.substring(1, end - start); } } } /***************************************************************************** * OptionType - defines roll options, including values for default * parameters. *****************************************************************************/ // These items are only used in the enum below, but have to be declared out here // because they must appear before being used in the enum definitions. private static final String defaultLoopSep = "\", \""; private static final Object nullParam = null; /* * In order to add a new roll option, follow the instructions in the "todo" * comments in this file. */ private enum OptionType { /* * TODO: If you're adding a new option, make an entry in this table */ // The format is: // NAME (nameRegex, minParams, maxParams, defaultValues...) // // You must provide (maxParams - minParams) default values (BigDecimal or String types). // // If maxParams is -1, unlimited params may be passed in. NO_OPTION("", 0, 0), // output formats EXPANDED("e|expanded", 0, 0), HIDDEN("h|hidden|hide", 0, 0), RESULT("r|result", 0, 0), UNFORMATTED("u|unformatted", 0, 0), TOOLTIP("t|tooltip", 0, 1, nullParam), // visibility GM("g|gm", 0, 0), SELF("s|self", 0, 0), WHISPER("w|whisper", 1, -1), // tooltip visibility GMTT("gt|gmtt", 0, 0), SELFTT("st|selftt", 0, 0), // WHISPER ("wt|whispertt", 1, -1), // loops COUNT("c|count", 1, 2, defaultLoopSep), FOR("for", 3, 5, BigDecimal.ONE, defaultLoopSep), FOREACH("foreach", 2, 4, defaultLoopSep, ","), WHILE("while", 1, 2, defaultLoopSep), // branches IF("if", 1, 1), SWITCH("switch", 1, 1), // code CODE("code", 0, 0), MACRO("macro", 1, 1), // HTML Dockable Frame FRAME("frame", 1, 2, "\"\""), // HTML Dialog DIALOG("dialog", 1, 2, "\"\""), // Run for another token TOKEN("token", 1, 1); protected final String nameRegex; protected final int minParams, maxParams; protected final Object[] defaultParams; OptionType(String nameRegex, int minParams, int maxParams, Object... defaultParams) { this.nameRegex = nameRegex; this.minParams = minParams; this.maxParams = maxParams; if (defaultParams == null) { // The Java 5 varargs facility has a small hack which we must work around. // If you pass a single null argument, Java doesn't know whether you wanted a single variable arg of null, // or if you meant to say that the variable argument array itself should be null. // Java chooses the latter, but we want the former. this.defaultParams = new Object[1]; this.defaultParams[0] = null; } else { this.defaultParams = defaultParams; } if (maxParams != -1 && this.defaultParams.length != (maxParams - minParams)) { log.error(String.format( "Internal error: roll option %s specifies wrong number of default parameters", name())); } } /** * Obtain one of the enum values, or null if <code>strName</code> * doesn't match any of them. */ protected static OptionType optionTypeFromName(String strName) { for (OptionType rot : OptionType.values()) { if (Pattern.compile("^\\s*" + rot.getNameRegex() + "\\s*$", Pattern.CASE_INSENSITIVE) .matcher(strName).matches()) { return rot; } } return null; } /** Returns the regex that matches all valid names for this option. */ public String getNameRegex() { return nameRegex; } public int getMinParams() { return minParams; } public int getMaxParams() { return maxParams; } /** Returns a copy of the default params array for this option type. */ public Object[] getDefaultParams() { if (maxParams == -1) return null; Object[] retval = new Object[maxParams]; for (int i = minParams; i < maxParams; i++) { retval[i] = defaultParams[i - minParams]; } return retval; } @Override public String toString() { String retval = name() + ", default params: ["; boolean first = true; for (Object p : defaultParams) { if (first) { first = false; } else { retval += ", "; } if (p == null) retval += "null"; else if (p instanceof String) retval += "\"" + p + "\""; else retval += p.toString(); } retval += "]"; return retval; } } /********************************************************************************** * OptionInfo class - holds extracted name and parameters for a roll option. **********************************************************************************/ private class OptionInfo { private OptionType optionType; private String optionName; private final int optionStart; private int optionEnd; private final String srcString; private Object[] params; /** * Attempts to create an OptionInfo object by parsing the text in * <code>optionString</code> beginning at position <code>start</code>. */ public OptionInfo(String optionString, int start) throws RollOptionException { srcString = optionString; optionStart = start; parseOptionString(optionString, start); } /** * Parses a roll option and sets the RollOptionType and parameters. <br> * Missing optional parameters are set to the default for the type. * * @param optionString * The string containing the option * @param start * Where in the string to begin parsing from * @throws RollOptionException * if the option string can't be parsed. */ private void parseOptionString(String optionString, int start) throws RollOptionException { boolean paramsFound; // does the option string have a "(" after the name? int endOfString = optionString.length(); // Find the name Pattern pattern = Pattern.compile("^\\s*(?:(\\w+)\\s*\\(|(\\w+))"); // matches "abcd(" or "abcd" Matcher matcher = pattern.matcher(optionString); matcher.region(start, endOfString); if (!matcher.find()) { throw new RollOptionException(I18N.getText("lineParser.badRollOpt", optionString)); } paramsFound = (matcher.group(1) != null); String name = paramsFound ? matcher.group(1).trim() : matcher.group(2).trim(); start = matcher.end(); matcher.region(start, endOfString); // Get the option type and default params from the name optionType = OptionType.optionTypeFromName(name); if (optionType == null) { throw new RollOptionException(I18N.getText("lineParser.unknownOptionName", name)); } optionName = name; params = optionType.getDefaultParams(); // begin with default values for optional params // If no params found (i.e. no "(" after option name), we're done if (!paramsFound) { if (optionType.getMinParams() == 0) { optionEnd = start; return; } else { throw new RollOptionException( I18N.getText("lineParser.optRequiresParam", optionName, optionType.getMaxParams())); } } // Otherwise, match the individual parameters one at a time pattern = Pattern.compile( "^(?:((?:[^()\"',]|\"[^\"]*\"|'[^']*'|\\((?:[^()\"']|\"[^\"]*\"|'[^']*')*\\))+)(,|\\))){1}?"); matcher = pattern.matcher(optionString); matcher.region(start, endOfString); List<String> paramList = new ArrayList<String>(); boolean lastItem = false; // true if last match ended in ")" while (!lastItem) { if (matcher.find()) { String param = matcher.group(1).trim(); paramList.add(param); lastItem = matcher.group(2).equalsIgnoreCase(")"); start = matcher.end(); matcher.region(start, endOfString); } else { throw new RollOptionException( I18N.getText("lineParser.optBadParam", optionName, optionType.getMaxParams())); } } // Error checking int min = optionType.getMinParams(), max = optionType.getMaxParams(); int numParamsFound = paramList.size(); if (numParamsFound < min || (max != -1 && numParamsFound > max)) { throw new RollOptionException( I18N.getText("lineParser.optWrongParam", optionName, min, max, numParamsFound, srcString)); } // Fill in the found parameters, converting to BigDecimal if possible. if (params == null) params = new Object[numParamsFound]; for (int i = 0; i < numParamsFound; i++) { params[i] = toNumIfPossible(paramList.get(i)); } optionEnd = start; return; } /** * Converts a String to a BigDecimal if possible, otherwise returns * original String. */ private Object toNumIfPossible(String s) { Object retval = s; try { retval = new BigDecimal(Integer.decode(s)); } catch (NumberFormatException nfe) { // Do nothing } return retval; } @SuppressWarnings("unused") public String getName() { return optionName; } @SuppressWarnings("unused") public int getStart() { return optionStart; } public int getEnd() { return optionEnd; } /** Returns the number of options passed in */ public int getParamCount() { return params.length; } /** Gets a parameter (Object type). */ public Object getObjectParam(int index) { return params[index]; } /** Gets the text of a parameter. */ public String getStringParam(int index) { Object o = params[index]; return (o == null) ? null : o.toString(); } /** * Gets the text of a parameter if it is a valid identifier. * * @throws ParserException * if the parameter text is not a valid identifier. */ public String getIdentifierParam(int index) throws ParserException { String s = params[index].toString(); if (!s.matches("[a-zA-Z]\\w*")) { // MapTool doesn't allow variable names to start with '_' throw new ParserException(I18N.getText("lineParser.notValidVariableName", s)); } return s; } /** Gets a parameter, casting it to BigDecimal. */ public BigDecimal getNumericParam(int index) { return (BigDecimal) params[index]; } /** Gets the integer value of a parameter. */ @SuppressWarnings("unused") public int getIntParam(int index) { return getNumericParam(index).intValue(); } /** Returns a param, parsing it as an expression if it is a string. */ public Object getParsedParam(int index, MapToolVariableResolver res, Token tokenInContext) throws ParserException { Object retval = params[index]; // No parsing is done if the param isn't a String (e.g. it's already a BigDecimal) if (params[index] instanceof String) { Result result = parseExpression(res, tokenInContext, (String) params[index]); retval = result.getValue(); } return retval; } /** * Returns a param as int, parsing it as an expression if it is a * string. */ public int getParsedIntParam(int index, MapToolVariableResolver res, Token tokenInContext) throws ParserException { Object retval = getParsedParam(index, res, tokenInContext); if (!(retval instanceof BigDecimal)) throw new ParserException(I18N.getText("lineParser.notValidNumber", retval.toString())); return ((BigDecimal) retval).intValue(); } @Override public String toString() { String retval = optionName + ": params: ("; boolean first = true; for (Object p : params) { if (first) { first = false; } else { retval += ", "; } if (p == null) retval += "null"; else if (p instanceof String) retval += "\"" + p + "\""; else retval += p.toString(); } retval += ")"; return retval; } } ///////////////////// end of OptionInfo class /** Thrown when a roll option can't be parsed. */ @SuppressWarnings("serial") public class RollOptionException extends Exception { public String msg; public RollOptionException(String msg) { this.msg = msg; } } /** * Scans a string of options and builds OptionInfo objects for each option * found. * * @param optionString * A string containing a comma-delimited list of roll options. * @throws RollOptionException * if any of the options are unknown or don't match the template * for that option type. */ private List<OptionInfo> getRollOptionList(String optionString) throws RollOptionException { if (optionString == null) return null; List<OptionInfo> list = new ArrayList<OptionInfo>(); optionString = optionString.trim(); int start = 0; int endOfString = optionString.length(); boolean atEnd = false; Pattern commaPattern = Pattern.compile("^\\s*,\\s*(?!$)"); while (start < endOfString) { OptionInfo roi; if (atEnd) { // If last param didn't end with ",", there shouldn't have been another option throw new RollOptionException(I18N.getText("lineParser.rollOptionComma")); } // Eat the next option from string, and add parsed option to list roi = new OptionInfo(optionString, start); list.add(roi); start = roi.getEnd(); // Eat any "," sitting between options Matcher matcher = commaPattern.matcher(optionString); matcher.region(start, endOfString); if (matcher.find()) { start = matcher.end(); atEnd = false; } else { atEnd = true; } } return list; } public String parseLine(String line) throws ParserException { return parseLine(null, line); } public String parseLine(Token tokenInContext, String line) throws ParserException { return parseLine(tokenInContext, line, null); } public String parseLine(Token tokenInContext, String line, MapToolMacroContext context) throws ParserException { return parseLine(null, tokenInContext, line, context); } public String parseLine(MapToolVariableResolver res, Token tokenInContext, String line) throws ParserException { return parseLine(res, tokenInContext, line, null); } public String parseLine(MapToolVariableResolver res, Token tokenInContext, String line, MapToolMacroContext context) throws ParserException { if (line == null) { return ""; } line = line.trim(); if (line.length() == 0) { return ""; } Stack<Token> contextTokenStack = new Stack<Token>(); enterContext(context); MapToolVariableResolver resolver = null; boolean resolverInitialized = false; String opts = null; String roll = null; try { // Keep the same variable context for this line resolver = (res == null) ? new MapToolVariableResolver(tokenInContext) : res; resolverInitialized = resolver.initialize(); StringBuilder builder = new StringBuilder(); int start = 0; List<InlineRollMatch> matches = this.locateInlineRolls(line); for (InlineRollMatch match : matches) { builder.append(line.substring(start, match.getStart())); // add everything before the roll start = match.getEnd() + 1; // These variables will hold data extracted from the roll options. Output output; if (MapTool.useToolTipsForUnformatedRolls()) { output = Output.TOOLTIP; } else { output = Output.EXPANDED; } String text = null; // used by the T option HashSet<String> outputOpts = new HashSet<String>(); OutputLoc outputTo = OutputLoc.CHAT; LoopType loopType = LoopType.NO_LOOP; int loopStart = 0, loopEnd = 0, loopStep = 1; int loopCount = 0; String loopSep = null; String loopVar = null, loopCondition = null; List<String> foreachList = new ArrayList<String>(); BranchType branchType = BranchType.NO_BRANCH; Object branchCondition = null; CodeType codeType = CodeType.NO_CODE; String macroName = null; String frameName = null; String frameOpts = null; if (match.getMatch().startsWith("[")) { opts = match.getOpt(); roll = match.getRoll(); if (opts != null) { // Turn the opts string into a list of OptionInfo objects. List<OptionInfo> optionList = null; try { optionList = getRollOptionList(opts); } catch (RollOptionException roe) { throw doError(roe.msg, opts, roll); } // Scan the roll options and prepare variables for later use for (OptionInfo option : optionList) { String error = null; /* * TODO: If you're adding a new option, add a new * case here to collect info from the parameters. If * your option uses parameters, use the * option.getXxxParam() methods to get the text or * parsed values of the parameters. */ switch (option.optionType) { /////////////////////////////////////////////////// // OUTPUT FORMAT OPTIONS /////////////////////////////////////////////////// case HIDDEN: output = Output.NONE; break; case RESULT: output = Output.RESULT; break; case EXPANDED: output = Output.EXPANDED; break; case UNFORMATTED: output = Output.UNFORMATTED; outputOpts.add("u"); break; case TOOLTIP: // T(display_text) output = Output.TOOLTIP; text = option.getStringParam(0); break; /////////////////////////////////////////////////// // VISIBILITY OPTIONS /////////////////////////////////////////////////// case GM: outputOpts.add("g"); break; case SELF: outputOpts.add("s"); break; case WHISPER: outputOpts.add("w"); for (int i = 0; i < option.getParamCount(); i++) { String arg = parseExpression(resolver, tokenInContext, option.getStringParam(i)) .getValue().toString(); if (arg.trim().startsWith("[")) { Object json = JSONMacroFunctions.convertToJSON(arg); if (json instanceof JSONArray) { for (Object name : (JSONArray) json) { outputOpts.add("w:" + name.toString().toLowerCase()); } } } else outputOpts.add("w:" + arg.toLowerCase()); } break; /////////////////////////////////////////////////// // TOOLTIP VISIBILITY OPTIONS /////////////////////////////////////////////////// case GMTT: outputOpts.add("gt"); break; case SELFTT: outputOpts.add("st"); break; /////////////////////////////////////////////////// // LOOP OPTIONS /////////////////////////////////////////////////// case COUNT: // COUNT(num [, sep]) loopType = LoopType.COUNT; error = null; try { loopCount = option.getParsedIntParam(0, resolver, tokenInContext); if (loopCount < 0) error = I18N.getText("lineParser.countNonNeg", loopCount); } catch (ParserException pe) { error = I18N.getText("lineParser.errorProcessingOpt", "COUNT", pe.getMessage()); } loopSep = option.getStringParam(1); if (error != null) throw doError(error, opts, roll); break; case FOR: // FOR(var, start, end [, step [, sep]]) loopType = LoopType.FOR; error = null; try { loopVar = option.getIdentifierParam(0); loopStart = option.getParsedIntParam(1, resolver, tokenInContext); loopEnd = option.getParsedIntParam(2, resolver, tokenInContext); try { loopStep = option.getParsedIntParam(3, resolver, tokenInContext); } catch (ParserException pe) { // Build a more informative error message for this common mistake String msg = pe.getMessage(); msg = msg + " " + I18N.getText("lineParser.nonDefLoopSep"); throw new ParserException(msg); } loopSep = option.getStringParam(4); if (loopStep != 0) loopCount = Math.max(1, (int) Math.ceil( Math.abs((double) (loopEnd - loopStart) / (double) loopStep))); if (loopVar.equalsIgnoreCase("")) error = I18N.getText("lineParser.forVarMissing"); if (loopStep == 0) error = I18N.getText("lineParser.forNoZeroStep"); if ((loopEnd <= loopStart && loopStep > 0) || (loopEnd >= loopStart && loopStep < 0)) loopCount = 0; } catch (ParserException pe) { error = I18N.getText("lineParser.errorProcessingOpt", "FOR", pe.getMessage()); } if (error != null) throw doError(error, opts, roll); break; case FOREACH: // FOREACH(var, list [, outputDelim [, inputDelim]]) loopType = LoopType.FOREACH; error = null; try { loopVar = option.getIdentifierParam(0); String listString = option.getParsedParam(1, resolver, tokenInContext) .toString(); loopSep = option.getStringParam(2); String listDelim = option.getStringParam(3); if (listDelim.trim().startsWith("\"")) { listDelim = parseExpression(resolver, tokenInContext, listDelim).getValue() .toString(); } foreachList = null; if (listString.trim().startsWith("{") || listString.trim().startsWith("[")) { // if String starts with [ or { it is JSON -- try to treat it as a JSON String Object obj = JSONMacroFunctions.convertToJSON(listString); if (obj != null) { foreachList = new ArrayList<String>(); if (obj instanceof JSONArray) { for (Object o : ((JSONArray) obj).toArray()) { foreachList.add(o.toString()); } } else { @SuppressWarnings("unchecked") Set<String> keySet = ((JSONObject) obj).keySet(); foreachList.addAll(keySet); } } } // If we still dont have a list treat it list a string list if (foreachList == null) { foreachList = new ArrayList<String>(); StrListFunctions.parse(listString, foreachList, listDelim); } loopCount = foreachList.size(); if (loopVar.equalsIgnoreCase("")) error = I18N.getText("lineParser.foreachVarMissing"); } catch (ParserException pe) { error = I18N.getText("lineParser.errorProcessingOpt", "FOREACH", pe.getMessage()); } if (error != null) throw doError(error, opts, roll); break; case WHILE: // WHILE(cond [, sep]) loopType = LoopType.WHILE; loopCondition = option.getStringParam(0); loopSep = option.getStringParam(1); break; /////////////////////////////////////////////////// // BRANCH OPTIONS /////////////////////////////////////////////////// case IF: // IF(condition) branchType = BranchType.IF; branchCondition = option.getStringParam(0); break; case SWITCH: // SWITCH(condition) branchType = BranchType.SWITCH; branchCondition = option.getObjectParam(0); break; /////////////////////////////////////////////////// // DIALOG AND FRAME OPTIONS /////////////////////////////////////////////////// case FRAME: codeType = CodeType.CODEBLOCK; frameName = option.getParsedParam(0, resolver, tokenInContext).toString(); frameOpts = option.getParsedParam(1, resolver, tokenInContext).toString(); outputTo = OutputLoc.FRAME; break; case DIALOG: codeType = CodeType.CODEBLOCK; frameName = option.getParsedParam(0, resolver, tokenInContext).toString(); frameOpts = option.getParsedParam(1, resolver, tokenInContext).toString(); outputTo = OutputLoc.DIALOG; break; /////////////////////////////////////////////////// // CODE OPTIONS /////////////////////////////////////////////////// case MACRO: // MACRO("macroName@location") codeType = CodeType.MACRO; macroName = option.getStringParam(0); break; case CODE: codeType = CodeType.CODEBLOCK; break; /////////////////////////////////////////////////// // MISC OPTIONS /////////////////////////////////////////////////// case TOKEN: if (!isMacroTrusted()) { throw new ParserException(I18N.getText("macro.function.roll.noPerm")); } Token newToken = MapTool.getFrame().getCurrentZoneRenderer().getZone().resolveToken( option.getParsedParam(0, resolver, tokenInContext).toString()); if (newToken != null) { contextTokenStack.push(resolver.getTokenInContext()); resolver.setTokenIncontext(newToken); } break; default: // should never happen log.error(I18N.getText("lineParser.badOptionFound", opts, roll)); throw doError("lineParser.badOptionFound", opts, roll); } } } // Now that the options have been dealt with, process the body of the roll. // We deal with looping first, then branching, then deliver the output. StringBuilder expressionBuilder = new StringBuilder(); int iteration = 0; boolean doLoop = true; while (doLoop) { int loopConditionValue; Integer branchConditionValue = null; Object branchConditionParsed = null; // Process loop settings if (iteration > maxLoopIterations) { throw doError("lineParser.tooManyLoops", opts, roll); } if (loopType != LoopType.NO_LOOP) { // We only update roll.count in a loop statement. This allows simple nested // statements to inherit roll.count from the outer statement. resolver.setVariable("roll.count", iteration); } switch (loopType) { /* * TODO: If you're adding a new looping option, add a * new case to handle the iteration */ case NO_LOOP: if (iteration > 0) { // stop after first iteration doLoop = false; } break; case COUNT: if (iteration == loopCount) { doLoop = false; } break; case FOR: if (iteration != loopCount) { resolver.setVariable(loopVar, new BigDecimal(loopStart + loopStep * iteration)); } else { doLoop = false; resolver.setVariable(loopVar, null); } break; case FOREACH: if (iteration != loopCount) { String item = foreachList.get(iteration); resolver.setVariable(loopVar, item); } else { doLoop = false; resolver.setVariable(loopVar, null); } break; case WHILE: // This is a hack to get around a bug with the parser's comparison operators. // The InlineTreeFormatter class in the parser chokes on comparison operators, because they're // not listed in the operator precedence table. // // The workaround is that "non-deterministic" functions fully evaluate their arguments, // so the comparison operators are reduced to a number by the time the buggy code is reached. // The if() function defined in dicelib is such a function, so we use it here to eat // any comparison operators. String hackCondition = (loopCondition == null) ? null : String.format("if(%s, 1, 0)", loopCondition); // Stop loop if the while condition is false try { Result result = parseExpression(resolver, tokenInContext, hackCondition); loopConditionValue = ((Number) result.getValue()).intValue(); if (loopConditionValue == 0) { doLoop = false; } } catch (Exception e) { throw doError(I18N.getText("lineParser.invalidWhile", loopCondition), opts, roll); } break; } // Output the loop separator if (doLoop && iteration != 0 && output != Output.NONE) { expressionBuilder.append(parseExpression(resolver, tokenInContext, loopSep).getValue()); } if (!doLoop) { break; } iteration++; // Extract the appropriate branch to evaluate. // Evaluate the branch condition/expression if (branchCondition != null) { // This is a similar hack to the one used for the loopCondition above. String hackCondition = (branchCondition == null) ? null : branchCondition.toString(); if (branchType == BranchType.IF) { hackCondition = (hackCondition == null) ? null : String.format("if(%s, 1, 0)", hackCondition); } Result result = null; try { result = parseExpression(resolver, tokenInContext, hackCondition); } catch (Exception e) { throw doError(I18N.getText("lineParser.invalidCondition", branchType.toString(), branchCondition.toString()), opts, roll); } branchConditionParsed = result.getValue(); if (branchConditionParsed instanceof Number) { branchConditionValue = ((Number) branchConditionParsed).intValue(); } } // Set up regexes for scanning through the branches. // branchRegex then defines one matcher group for the parseable content of the branch. String rollBranch = roll; String branchRegex, branchSepRegex, branchLastSepRegex; if (codeType != CodeType.CODEBLOCK) { // matches any text not containing a ";" (skipping over strings) String noCodeRegex = "((?:[^\";]|\"[^\"]*\"|'[^']*')*)"; branchRegex = noCodeRegex; branchSepRegex = ";"; branchLastSepRegex = ";?"; // The last clause doesn't have to end with a separator } else { // matches text inside braces "{...}", skipping over strings (one level of {} nesting allowed) String codeRegex = "\\{((?:[^{}\"]|\"[^\"]*\"|'[^']*'|\\{(?:[^}\"]|\"[^\"]*\"|'[^']*')*})*)}"; branchRegex = codeRegex; branchSepRegex = ";"; branchLastSepRegex = ";?"; // The last clause doesn't have to end with a separator } // Extract the branch to use switch (branchType) { /* * TODO: If you're adding a new branching option, add a * new case to extract the branch text */ case NO_BRANCH: { // There's only one branch, so our regex is very simple String testRegex = String.format("^\\s*%s\\s*$", branchRegex); Matcher testMatcher = Pattern.compile(testRegex).matcher(roll); if (testMatcher.find()) { rollBranch = testMatcher.group(1); } else { throw doError("lineParser.errorBodyRoll", opts, roll); } break; } case IF: { // IF can have one or two branches. // When there's only one branch and the condition is false, there's no output. if (branchConditionValue == null) { throw doError(I18N.getText("lineParser.invalidIfCond", branchCondition, branchConditionParsed.toString()), opts, roll); } int whichBranch = (branchConditionValue != 0) ? 0 : 1; String testRegex = String.format("^\\s*%s\\s*(?:%s\\s*%s\\s*%s)?\\s*$", branchRegex, branchSepRegex, branchRegex, branchLastSepRegex); Matcher testMatcher = Pattern.compile(testRegex).matcher(roll); if (testMatcher.find()) { // verifies that roll body is well-formed rollBranch = testMatcher.group(1 + whichBranch); if (rollBranch == null) rollBranch = "''"; // quick-and-dirty way to get no output rollBranch = rollBranch.trim(); } else { throw doError("lineParser.ifError", opts, roll); } break; } case SWITCH: { // We augment the branch regex to detect the "case xxx:" or "default:" prefixes, // and search for a match. An error is thrown if no case match is found. // Regex matches 'default', 'case 123:', 'case "123":', 'case "abc":', but not 'case abc:' branchRegex = "(?:case\\s*\"?((?<!\")(?:\\+|-)?[\\d]+(?!\")|(?<=\")[^\"]*(?=\"))\"?|(default))\\s*:\\s*" + branchRegex; String caseTarget = branchConditionParsed.toString(); String testRegex = String.format("^(?:\\s*%s\\s*%s\\s*)*\\s*%s\\s*%s\\s*$", branchRegex, branchSepRegex, branchRegex, branchLastSepRegex); Matcher testMatcher = Pattern.compile(testRegex).matcher(roll); if (testMatcher.find()) { // verifies that roll body is well-formed String scanRegex = String.format("\\s*%s\\s*(?:%s)?", branchRegex, branchSepRegex); Matcher scanMatcher = Pattern.compile(scanRegex).matcher(roll); boolean foundMatch = false; while (!foundMatch && scanMatcher.find()) { String caseLabel = scanMatcher.group(1); // "case (xxx):" String def = scanMatcher.group(2); // "(default):" String branch = scanMatcher.group(3); if (def != null) { rollBranch = branch.trim(); foundMatch = true; ; } if (caseLabel != null && caseLabel.matches(caseTarget)) { rollBranch = branch.trim(); foundMatch = true; } } if (!foundMatch) { doError(I18N.getText("lineParser.switchNoMatch", caseTarget), opts, roll); } } else { doError("lineParser.switchError", opts, roll); } break; } } // end of switch(branchType) statement // Construct the output. // If a MACRO or CODE block is being used, we default to bare output as in the RESULT style. // The output style NONE is also allowed in these cases. Result result; String output_text; switch (codeType) { case NO_CODE: // If none of the code options are active, any of the formatting options can be used. switch (output) { /* * TODO: If you're adding a new formatting option, * add a new case to build the output */ case NONE: parseExpression(resolver, tokenInContext, rollBranch); break; case RESULT: result = parseExpression(resolver, tokenInContext, rollBranch); output_text = result != null ? result.getValue().toString() : ""; if (!this.isMacroTrusted()) { output_text = output_text.replaceAll( "\u00AB|\u00BB|«|»|«|»|\036|\037", ""); } if (outputOpts.isEmpty()) { expressionBuilder.append(output_text); } else { outputOpts.add("r"); expressionBuilder.append(rollString(outputOpts, output_text)); } break; case TOOLTIP: String tooltip = rollBranch + " = "; output_text = null; result = parseExpression(resolver, tokenInContext, rollBranch); tooltip += result.getDetailExpression(); if (text == null) { output_text = result.getValue().toString(); } else { if (!result.getDetailExpression().equals(result.getValue().toString())) { tooltip += " = " + result.getValue(); } resolver.setVariable("roll.result", result.getValue()); output_text = parseExpression(resolver, tokenInContext, text).getValue() .toString(); } tooltip = tooltip.replaceAll("'", "'"); expressionBuilder.append( output_text != null ? rollString(outputOpts, tooltip, output_text) : ""); break; case EXPANDED: expressionBuilder.append(rollString(outputOpts, rollBranch + " = " + expandRoll(resolver, tokenInContext, rollBranch))); break; case UNFORMATTED: output_text = rollBranch + " = " + expandRoll(resolver, tokenInContext, rollBranch); // Escape quotes so that the result can be used in a title attribute output_text = output_text.replaceAll("'", "'"); output_text = output_text.replaceAll("\"", """); expressionBuilder.append(rollString(outputOpts, output_text)); } // end of switch(output) statement break; // end of case NO_CODE in switch(codeType) statement /* * TODO: If you're adding a new code option, add a new * case to execute the code */ case MACRO: // [MACRO("macroName@location"): args] result = parseExpression(resolver, tokenInContext, macroName); String callName = result.getValue().toString(); result = parseExpression(resolver, tokenInContext, rollBranch); String macroArgs = result.getValue().toString(); output_text = runMacro(resolver, tokenInContext, callName, macroArgs); if (output != Output.NONE) { expressionBuilder.append(output_text); } resolver.setVariable("roll.count", iteration); // reset this because called code might change it break; case CODEBLOCK: output_text = runMacroBlock(resolver, tokenInContext, rollBranch); resolver.setVariable("roll.count", iteration); // reset this because called code might change it if (output != Output.NONE) { expressionBuilder.append(output_text); } break; } } switch (outputTo) { case FRAME: HTMLFrameFactory.show(frameName, true, frameOpts, expressionBuilder.toString()); break; case DIALOG: HTMLFrameFactory.show(frameName, false, frameOpts, expressionBuilder.toString()); break; case CHAT: builder.append(expressionBuilder); break; } // Revert to our previous token if [token(): ] was used if (contextTokenStack.size() > 0) { resolver.setTokenIncontext(contextTokenStack.pop()); } } else if (match.getMatch().startsWith("{")) { roll = match.getRoll(); Result result = parseExpression(resolver, tokenInContext, roll); if (isMacroTrusted()) { builder.append(result != null ? result.getValue().toString() : ""); } else { builder.append(result != null ? result.getValue().toString() .replaceAll("\u00AB|\u00BB|«|»|«|»|\036|\037", "") : ""); } } } builder.append(line.substring(start)); return builder.toString(); } catch (AbortFunctionException e) { // do nothing; this exception will never generate any output // throw doError("macroExecutionAbort", opts == null ? "" : opts, roll == null ? line : roll); throw e; } catch (AssertFunctionException e) { // do nothing; this exception will never generate any output // throw doError("macroExecutionAssert", opts == null ? "" : opts, roll == null ? line : roll); throw e; } catch (ParserException e) { // do nothing, jut pass message back up throw e; } catch (Exception e) { log.info(line, e); throw doError("lineParser.errorBodyRoll", opts == null ? "" : opts, roll == null ? line : roll); } finally { exitContext(); if (resolverInitialized) { // This is the top level call, time to clean up resolver.flush(); } // If we have exited the last context let the html frame we have (potentially) // updated a token. if (contextStackEmpty()) { HTMLFrameFactory.tokenChanged(tokenInContext); } MapTool.getFrame().refresh(); // Repaint incase macros changed anything. } } public Result parseExpression(String expression) throws ParserException { return parseExpression(null, expression); } public Result parseExpression(Token tokenInContext, String expression) throws ParserException { return parseExpression(new MapToolVariableResolver(tokenInContext), tokenInContext, expression); } public Result parseExpression(VariableResolver resolver, Token tokenInContext, String expression) throws ParserException { if (parserRecurseDepth > maxRecursionDepth) { parserRecurseDepth = 0; macroRecurseDepth = 0; throw new ParserException(I18N.getText("lineParser.maxRecursion")); } try { parserRecurseDepth++; if (log.isDebugEnabled()) { StringBuilder b = new StringBuilder(); for (int i = 1; i < parserRecurseDepth; i++) { b.append(' '); } b.append(expression); log.debug(b.toString()); } return createParser(resolver, tokenInContext == null ? false : true).evaluate(expression); } catch (AbortFunctionException e) { log.debug(e); throw e; } catch (AssertFunctionException afe) { log.debug(afe); throw afe; } catch (Exception e) { if (e.getCause() instanceof ParserException) { log.debug(e.getCause()); throw (ParserException) e.getCause(); } if (e instanceof ParserException) { log.debug(e); throw (ParserException) e; } log.debug(e); throw new ParserException( I18N.getText("lineParser.errorExecutingExpression", e.toString(), expression)); } finally { parserRecurseDepth--; } } public String expandRoll(String roll) throws ParserException { return expandRoll(null, roll); } public String expandRoll(Token tokenInContext, String roll) throws ParserException { return expandRoll(new MapToolVariableResolver(tokenInContext), tokenInContext, roll); } public String expandRoll(MapToolVariableResolver resolver, Token tokenInContext, String roll) throws ParserException { try { Result result = parseExpression(resolver, tokenInContext, roll); StringBuilder sb = new StringBuilder(); if (result.getDetailExpression().equals(result.getValue().toString())) { sb.append(result.getDetailExpression()); } else { sb.append(result.getDetailExpression()).append(" = ").append(result.getValue()); } return sb.toString(); } catch (AbortFunctionException ae) { throw ae; } catch (AssertFunctionException afe) { throw afe; } catch (ParserException e) { throw e; } catch (Exception e) { return I18N.getText("lineParser.invalidExpr", roll); } } public String runMacro(MapToolVariableResolver resolver, Token tokenInContext, String qMacroName, String args) throws ParserException { return runMacro(resolver, tokenInContext, qMacroName, args, true); } /** Runs a macro from a specified location. */ public String runMacro(MapToolVariableResolver resolver, Token tokenInContext, String qMacroName, String args, boolean createNewVariableContext) throws ParserException { MapToolMacroContext macroContext; String macroBody = null; String[] macroParts = qMacroName.split("@", 2); String macroLocation; String macroName = macroParts[0]; if (macroParts.length == 1) { macroLocation = null; } else { macroLocation = macroParts[1]; } // For convenience to macro authors, no error on a blank macro name if (macroName.equalsIgnoreCase("")) return ""; // IF the macro is a @this, then we get the location of the current macro and use that. if (macroLocation != null && macroLocation.equalsIgnoreCase("this")) { macroLocation = getMacroSource(); if (macroLocation.equals(CHAT_INPUT) || macroLocation.toLowerCase().startsWith("token:")) { macroLocation = "TOKEN"; } } if (macroLocation == null || macroLocation.length() == 0 || macroLocation.equals(CHAT_INPUT)) { // Unqualified names are not allowed. throw new ParserException(I18N.getText("lineParser.invalidMacroLoc", macroName)); } else if (macroLocation.equalsIgnoreCase("TOKEN")) { macroContext = new MapToolMacroContext(macroName, "token", MapTool.getPlayer().isGM()); // Search token for the macro if (tokenInContext != null) { MacroButtonProperties buttonProps = tokenInContext.getMacro(macroName, false); if (buttonProps == null) { throw new ParserException(I18N.getText("lineParser.atTokenNotFound", macroName)); } macroBody = buttonProps.getCommand(); } } else if (macroLocation.equalsIgnoreCase("CAMPAIGN")) { MacroButtonProperties mbp = null; for (MacroButtonProperties m : MapTool.getCampaign().getMacroButtonPropertiesArray()) { if (m.getLabel().equals(macroName)) { mbp = m; break; } } if (mbp == null) { throw new ParserException(I18N.getText("lineParser.unknownCampaignMacro", macroName)); } macroBody = mbp.getCommand(); macroContext = new MapToolMacroContext(macroName, "campaign", !mbp.getAllowPlayerEdits()); } else if (macroLocation.equalsIgnoreCase("GLOBAL")) { macroContext = new MapToolMacroContext(macroName, "global", MapTool.getPlayer().isGM()); MacroButtonProperties mbp = null; for (MacroButtonProperties m : MacroButtonPrefs.getButtonProperties()) { if (m.getLabel().equals(macroName)) { mbp = m; break; } } if (mbp == null) { throw new ParserException(I18N.getText("lineParser.unknownGlobalMacro", macroName)); } macroBody = mbp.getCommand(); } else { // Search for a token called macroLocation (must start with "Lib:") macroBody = getTokenLibMacro(macroName, macroLocation); Token token = getTokenMacroLib(macroLocation); if (macroBody == null || token == null) { throw new ParserException(I18N.getText("lineParser.unknownMacro", macroName)); } boolean secure = isSecure(macroName, token); macroContext = new MapToolMacroContext(macroName, macroLocation, secure); } // Error if macro not found if (macroBody == null) { throw new ParserException(I18N.getText("lineParser.unknownMacro", macroName)); } MapToolVariableResolver macroResolver; if (createNewVariableContext) { macroResolver = new MapToolVariableResolver(tokenInContext); } else { macroResolver = resolver; } macroResolver.setVariable("macro.args", args); Object obj = JSONMacroFunctions.convertToJSON(args); if (obj instanceof JSONArray) { JSONArray jarr = (JSONArray) obj; macroResolver.setVariable("macro.args.num", BigDecimal.valueOf(jarr.size())); for (int i = 0; i < jarr.size(); i++) { macroResolver.setVariable("macro.args." + i, jarr.get(i)); } } else { macroResolver.setVariable("macro.args.num", BigDecimal.ZERO); } macroResolver.setVariable("macro.return", ""); // Call the macro macroRecurseDepth++; if (macroRecurseDepth > maxRecursionDepth) { parserRecurseDepth = 0; macroRecurseDepth = 0; throw new ParserException(I18N.getText("lineParser.maxRecursion")); } try { String macroOutput = runMacroBlock(macroResolver, tokenInContext, macroBody, macroContext); // Copy the return value of the macro into our current variable scope. resolver.setVariable("macro.return", macroResolver.getVariable("macro.return")); if (macroOutput != null) { // Note! Its important that trim is not used to replace the following two lines. // If you use String.trim() you may inadvertnatly remove the special characters // used to mark rolls. macroOutput = macroOutput.replaceAll("^\\s+", ""); macroOutput = macroOutput.replaceAll("\\s+$", ""); } return macroOutput; } finally { // exitContext(); macroRecurseDepth--; } } /** * Returns if the specified macro on the token is secure, that is player * would not be able to edit it. * * @param macroName * The name of the macro. * @param token * the token. * @return true if it is secure. */ private boolean isSecure(String macroName, Token token) { MacroButtonProperties mbp = token.getMacro(macroName, false); // Macro button may be null as we could be running the unknown macro if (mbp != null && !mbp.getAllowPlayerEdits()) { return true; } if (token.isOwnedByAll()) { return false; } else { Set<String> gmPlayers = new HashSet<String>(); for (Object o : MapTool.getPlayerList()) { Player p = (Player) o; if (p.isGM()) { gmPlayers.add(p.getName()); } } for (String owner : token.getOwners()) { if (!gmPlayers.contains(owner)) { return false; } } } return true; } /** Executes a string as a block of macro code. */ String runMacroBlock(MapToolVariableResolver resolver, Token tokenInContext, String macroBody) throws ParserException { return runMacroBlock(resolver, tokenInContext, macroBody, null); } /** Executes a string as a block of macro code. */ String runMacroBlock(MapToolVariableResolver resolver, Token tokenInContext, String macroBody, MapToolMacroContext context) throws ParserException { String macroOutput = parseLine(resolver, tokenInContext, macroBody, context); return macroOutput; } /** * Searches all maps for a token and returns the body of the requested * macro. * * @param macro * The name of the macro to fetch. * @param location * The name of the token containing the macro. Must begin with * "lib:". * @return The body of the requested macro. * @throws ParserException * if the token name is illegal, the token appears multiple * times, or if the caller doesn't have access to the token. */ public String getTokenLibMacro(String macro, String location) throws ParserException { Token token = getTokenMacroLib(location); if (token == null) { throw new ParserException(I18N.getText("lineParser.unknownLibToken", location)); } MacroButtonProperties buttonProps = token.getMacro(macro, false); if (buttonProps == null) { // Try the "unknown macro" buttonProps = token.getMacro(UNKNOWN_LIB_MACRO, false); if (buttonProps == null) { throw new ParserException(I18N.getText("lineParser.unknownMacro", macro + "@" + location)); } } return buttonProps.getCommand(); } /** * Searches all maps for a token and returns the the requested lib: macro. * * @return The token which holds the library. * @throws ParserException * if the token name is illegal, the token appears multiple * times, or if the caller doesn't have access to the token. */ public Token getTokenMacroLib(String location) throws ParserException { if (!location.matches("(?i)^lib:.*")) { throw new ParserException(I18N.getText("lineParser.notALibToken")); } final String libTokenName = location; Token libToken = null; if (libTokenName != null && libTokenName.length() > 0) { List<ZoneRenderer> zrenderers = MapTool.getFrame().getZoneRenderers(); for (ZoneRenderer zr : zrenderers) { List<Token> tokenList = zr.getZone().getTokensFiltered(new Zone.Filter() { public boolean matchToken(Token t) { return t.getName().equalsIgnoreCase(libTokenName); } }); for (Token token : tokenList) { // If we are not the GM and the token is not visible to players then we don't // let them get functions from it. if (!MapTool.getPlayer().isGM() && !token.isVisible()) { throw new ParserException(I18N.getText("lineParser.libUnableToExec", libTokenName)); } if (libToken != null) { throw new ParserException(I18N.getText("lineParser.duplicateLibTokens", libTokenName)); } libToken = token; } } return libToken; } return null; } /** * Searches all maps for a token and returns the zone that the lib: macro is * in. * * @return The zone which holds the library. * @throws ParserException * if the token name is illegal, the token appears multiple * times, or if the caller doesn't have access to the token. */ public Zone getTokenMacroLibZone(String location) throws ParserException { if (!location.matches("(?i)^lib:.*")) { throw new ParserException(I18N.getText("lineParser.notALibToken")); } final String libTokenName = location; Zone libTokenZone = null; if (libTokenName != null && libTokenName.length() > 0) { List<ZoneRenderer> zrenderers = MapTool.getFrame().getZoneRenderers(); for (ZoneRenderer zr : zrenderers) { List<Token> tokenList = zr.getZone().getTokensFiltered(new Zone.Filter() { public boolean matchToken(Token t) { return t.getName().equalsIgnoreCase(libTokenName); } }); for (Token token : tokenList) { // If we are not the GM and the token is not visible to players then we don't // let them get functions from it. if (!MapTool.getPlayer().isGM() && !token.isVisible()) { throw new ParserException(I18N.getText("lineParser.libUnableToExec", libTokenName)); } if (libTokenZone != null) { throw new ParserException(I18N.getText("lineParser.duplicateLibTokens", libTokenName)); } libTokenZone = zr.getZone(); } } return libTokenZone; } return null; } /** * Throws a helpful ParserException that shows the roll options and body. * * @param msg * The message * @param opts * The roll options * @param roll * The roll body */ private ParserException doError(String msg, String opts, String roll) { return new ParserException(errorString(msg, opts, roll)); } /** Builds a formatted string showing the roll options and roll body. */ String errorString(String msg, String opts, String roll) { String retval = "<br> " + I18N.getText(msg); retval += "<br> <u>" + I18N.getText("lineParser.errorStmtOpts") + "</u>: " + opts; if (roll.length() <= 200) { retval += "<br> <u>" + I18N.getText("lineParser.errorStmtBody") + "</u>: " + roll; } else { retval += "<br> <u>" + I18N.getText("lineParser.errorStmtBodyFirst200") + "</u>: " + roll.substring(0, 199); } return retval; } private ExpressionParser createParser(VariableResolver resolver, boolean hasTokenInContext) { ExpressionParser parser = new ExpressionParser(resolver); parser.getParser().addFunctions(mapToolParserFunctions); return parser; } private String rollString(Collection<String> options, String text) { return rollString(options, null, text); } private String rollString(Collection<String> options, String tooltip, String text) { StringBuilder s = new StringBuilder("\036"); if (options != null) s.append("\001" + StringUtils.join(options, ",") + "\002"); if (tooltip != null) { tooltip = tooltip.replaceAll("'", "'"); s.append(tooltip + "\037"); } s.append(text + "\036"); return s.toString(); } /** * Enters a new context for the macro. * * @param context * The context for the macro. If context is null and there is a * current context it is reentered. If context is null and there * is no current context then a new top level context is created. */ public void enterContext(MapToolMacroContext context) { // First time through set our trusted path to same as first context. // Any subsequent trips through we only change trusted path if conext // is not trusted (if context == null on subsequent calls we dont change // anything as trusted context will remain the same as it was before the call). if (contextStack.size() == 0) { macroPathTrusted = context == null ? false : context.isTrusted(); macroButtonIndex = context == null ? -1 : context.getMacroButtonIndex(); } else if (context != null) { if (!context.isTrusted()) { macroPathTrusted = false; } } if (context == null) { if (contextStack.size() == 0) { context = new MapToolMacroContext(MapToolLineParser.CHAT_INPUT, MapToolLineParser.CHAT_INPUT, MapTool.getPlayer().isGM()); } else { context = contextStack.peek(); } } contextStack.push(context); } /** * Leaves the current context reverting to the previous context. * * @return The context that you leave. */ public MapToolMacroContext exitContext() { return contextStack.pop(); } /** * Convenience method to enter a new trusted context. * * @param name * The name of the macro. * @param source * Where the macro comes from. */ public void enterTrustedContext(String name, String source) { enterContext(new MapToolMacroContext(name, source, true)); } /** * Gets the current context for the execution. It is save to enter a context * a second time. * * @return the current context. */ public MapToolMacroContext getContext() { return contextStack.peek(); } /** * Convenience method to enter a new insecure context. * * @param name * The name of the macro. * @param source * Where the macro comes from. */ public void enterUntrustedContext(String name, String source) { enterContext(new MapToolMacroContext(name, source, false)); } /** * Convenience method to enter a new context. * * @param name * The name of the macro. * @param source * Where the macro comes from. * @param secure * Is the context secure or not. */ public void enterContext(String name, String source, boolean secure) { enterContext(new MapToolMacroContext(name, source, secure)); } /** * Returns if the context stack is empty. * * @return true if the context stack is empty. */ private boolean contextStackEmpty() { return contextStack.size() == 0; } /** * Gets the macro name for the current context. * * @return The macro name for the current context. */ public String getMacroName() { return contextStack.peek().getName(); } /** * Gets the name of the source for where the macro resides. * * @return The name of the source for where the macro resides. */ public String getMacroSource() { return contextStack.peek().getSource(); } /** * Gets if the macro context is trusted or not. An empty context stack * returns false. * * @return if the macro context is trusted or not. */ public boolean isMacroTrusted() { return !contextStack.isEmpty() && contextStack.peek().isTrusted(); } /** * Locate the inline rolls within the input line. * * @param line * The line to search for the rolls in. * @return A list of the rolls. */ private List<InlineRollMatch> locateInlineRolls(String line) { List<InlineRollMatch> matches = new ArrayList<InlineRollMatch>(); ScanState scanState = ScanState.SEARCHING_FOR_ROLL; int startMatch = 0; int bracketLevel = 0; char quoteChar = ' '; char bracketChar = ' '; ScanState savedState = null; int optEnd = -1; for (int i = 0, strMax = line.length(); i < strMax; i++) { char c = line.charAt(i); switch (scanState) { case SEARCHING_FOR_ROLL: if (c == '{' || c == '[') { startMatch = i; scanState = ScanState.SEARCHING_FOR_CLOSE_BRACKET; bracketChar = c; bracketLevel++; optEnd = -1; } break; case SEARCHING_FOR_CLOSE_BRACKET: if (c == bracketChar) { bracketLevel++; } else if (bracketChar == '[' && c == ']') { bracketLevel--; if (bracketLevel == 0) { matches.add(new InlineRollMatch(startMatch, i, line.substring(startMatch, i + 1), optEnd)); scanState = ScanState.SEARCHING_FOR_ROLL; } } else if (bracketChar == '{' && c == '}') { bracketLevel--; if (bracketLevel == 0) { matches.add(new InlineRollMatch(startMatch, i, line.substring(startMatch, i + 1), optEnd)); scanState = ScanState.SEARCHING_FOR_ROLL; } } else if (c == '"' || c == '\'') { quoteChar = c; scanState = ScanState.SEARCHING_FOR_QUOTE; } else if (c == '\\') { savedState = scanState; scanState = ScanState.SKIP_NEXT_CHAR; } else if (bracketChar == '[' && optEnd == -1 && c == ':') { optEnd = i; } break; case SEARCHING_FOR_QUOTE: if (c == quoteChar) { scanState = ScanState.SEARCHING_FOR_CLOSE_BRACKET; } else if (c == '\\') { savedState = scanState; scanState = ScanState.SKIP_NEXT_CHAR; } break; case SKIP_NEXT_CHAR: scanState = savedState; break; } } return matches; } /** * Gets if the whole of the macro path up to this point has been running in * a trusted context. * * @return true if the whole of the macro path is running in trusted * context. */ public boolean isMacroPathTrusted() { return macroPathTrusted; } /** * Gets the index of the macro button on the index token. * * @return the index of the macro button. */ public int getMacroButtonIndex() { return macroButtonIndex; } /** * Gets the maximum number of iterations for a loop in a loop. * @return the maximum number of macro loop iterations. */ public int getMaxLoopIterations() { return maxLoopIterations; } /** * Sets the maximum number of iterations allowed in a macro. * Note: this will not set the value smaller than the initial starting value. * * @param loopIterations The maximum number of iterations allowed. */ public void setMaxLoopIterations(int loopIterations) { maxLoopIterations = Math.max(loopIterations, DEFAULT_MAX_LOOP_ITERATIONS); } /** * Gets the maximum recursive depth allowed for macros. * @return The maxiumum recursive depth allowed for macros. */ public int getMaxRecursionDepth() { return maxRecursionDepth; } /** * Sets the maximum recursive depth for macros. * Note: this will not set the value smaller than the initial starting value. * * @param recursionDepth The maximum recursive depth allowed. */ public void setMaxRecursionDepth(int recursionDepth) { maxRecursionDepth = Math.max(recursionDepth, DEFAULT_MAX_RECURSIVE_DEPTH); } /** * Gets the current recursive depth. This will be the macro or parser recursive depth which ever is greater. * @return the current recursive depth. */ public int getRecursionDepth() { return Math.max(parserRecurseDepth, macroRecurseDepth); } public int getContextStackSize() { return contextStack.size(); } }