/* * Sun Public License * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is available at http://www.sun.com/ * * The Original Code is the SLAMD Distributed Load Generation Engine. * The Initial Developer of the Original Code is Neil A. Wilson. * Portions created by Neil A. Wilson are Copyright (C) 2004-2010. * Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): Neil A. Wilson */ package com.slamd.scripting.engine; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import com.slamd.common.Constants; import com.slamd.job.JobClass; import com.slamd.stat.StatTracker; import com.slamd.scripting.general.BooleanLiteral; import com.slamd.scripting.general.BooleanVariable; import com.slamd.scripting.general.CategoricalTrackerVariable; import com.slamd.scripting.general.FileURLVariable; import com.slamd.scripting.general.IncrementalTrackerVariable; import com.slamd.scripting.general.IntegerLiteral; import com.slamd.scripting.general.IntegerValueTrackerVariable; import com.slamd.scripting.general.IntegerVariable; import com.slamd.scripting.general.RateLimiterVariable; import com.slamd.scripting.general.ScriptVariable; import com.slamd.scripting.general.StringArrayVariable; import com.slamd.scripting.general.StringLiteral; import com.slamd.scripting.general.StringVariable; import com.slamd.scripting.general.TimeTrackerVariable; import com.slamd.scripting.general.ValuePatternVariable; /** * This class implements a mechanism for parsing and executing script files that * specify instructions for performing operations in a SLAMD job. The set of * reserved words understood by this parser is hard-coded, but the set of * data types that may be used is flexible and may be extended by the end user. * * * @author Neil A. Wilson */ public class ScriptParser { /** * The set of variable types that will be automatically included and available * to scripts without the need for an explicit "use" definition. */ public static final String[] AUTO_INCLUDED_VARIABLE_TYPES = new String[] { ScriptVariable.class.getName(), ScriptVariable.class.getName(), BooleanVariable.class.getName(), CategoricalTrackerVariable.class.getName(), FileURLVariable.class.getName(), IncrementalTrackerVariable.class.getName(), IntegerValueTrackerVariable.class.getName(), IntegerVariable.class.getName(), RateLimiterVariable.class.getName(), StringVariable.class.getName(), StringArrayVariable.class.getName(), TimeTrackerVariable.class.getName(), ValuePatternVariable.class.getName() }; /** * The Java class name that is the superclass of all variable types that may * be used in SLAMD scripts. */ public static final String VARIABLE_TYPE_SUPERCLASS_NAME = "com.slamd.scripting.engine.Variable"; /** * The reserved word that is used to define a new variable type for use in the * SLAMD script. */ public static final String RESERVED_WORD_USE = "use"; /** * The reserved word that is used to define a new variable instance for use * in the SLAMD script. */ public static final String RESERVED_WORD_VARIABLE = "variable"; /** * The reserved word that is used to indicate the beginning of an instruction * block. */ public static final String RESERVED_WORD_BEGIN = "begin"; /** * The reserved word that is used to indicate the end of an instruction block. */ public static final String RESERVED_WORD_END = "end"; /** * The reserved word that is used to execute an instruction if the associated * boolean value is true. */ public static final String RESERVED_WORD_IF = "if"; /** * The reserved word that is used to execute an instruction if the associated * boolean value is false. */ public static final String RESERVED_WORD_IF_NOT = "ifnot"; /** * The reserved word that is used to execute an instruction if the boolean * value associated with the immediately preceding if statement is false. */ public static final String RESERVED_WORD_ELSE = "else"; /** * The reserved word that is used to execute an instruction a specified number * of times. */ public static final String RESERVED_WORD_LOOP = "loop"; /** * The reserved word that is used to execute an instruction until a specified * condition becomes false. */ public static final String RESERVED_WORD_WHILE = "while"; /** * The reserved word that is used to execute an instruction until a specified * condition becomes true. */ public static final String RESERVED_WORD_WHILE_NOT = "whilenot"; /** * The reserved word that is used to indicate that execution of the current * loop should stop immediately and that execution should resume with the next * instruction immediately after the loop. */ public static final String RESERVED_WORD_BREAK = "break"; /** * The reserved word that is used to indicate that the execution of the * current loop iteration should stop immediately and that the next iteration * should begin if appropriate. */ public static final String RESERVED_WORD_CONTINUE = "continue"; // The map of script arguments. private HashMap<String,String> scriptArgumentHash; // The line number from which the last token was read. private int tokenLine; // The character in the line at which the last token started. private int tokenStartPos; // The character in the line at which the last token ended. private int tokenEndPos; // The lines contained in the script file being parsed. private char[][] lines; // The set of instructions that have been read from the script file. private ArrayList<Instruction> instructionList; // The superclass of all variable type definitions. private Class<?> variableTypeSuperclass; // The set of variables defined and used in the script. private HashMap<String,Variable> variableHash; // The correlation between variable type names and the Java class that // implements them. private HashMap<String,String> variableTypeHash; /** * Creates a new script parser that can be used to parse and execute a SLAMD * script. * * @throws ScriptException If there is a problem initializing the script * parser or loading the automatically included * variable types. */ public ScriptParser() throws ScriptException { variableHash = new HashMap<String,Variable>(); variableTypeHash = new HashMap<String,String>(); instructionList = new ArrayList<Instruction>(); tokenLine = 0; tokenStartPos = -1; tokenEndPos = -1; // Load the variable type superclass. try { variableTypeSuperclass = Constants.classForName(VARIABLE_TYPE_SUPERCLASS_NAME); } catch (Exception e) { throw new ScriptException("Could not find the variable type superclass " + VARIABLE_TYPE_SUPERCLASS_NAME, e); } // Load all the automatically-included variable type definitions. for (int i=0; i < AUTO_INCLUDED_VARIABLE_TYPES.length; i++) { registerVariableType(AUTO_INCLUDED_VARIABLE_TYPES[i]); } // Create a new script variable and add it to the variable hash. ScriptVariable scriptVariable = new ScriptVariable(); scriptVariable.setName("script"); variableHash.put(scriptVariable.getName(), scriptVariable); } /** * Registers a variable type definition for use in the SLAMD script. * * @param className The name of the Java class that provides the variable * type definition. * * @throws ScriptException If the specified class cannot be found, cannot * be instantiated, does not define a variable type, * or defines a variable type that is already in * use. */ public void registerVariableType(String className) throws ScriptException { Class<?> variableTypeClass; try { variableTypeClass = Constants.classForName(className); } catch (ClassNotFoundException cnfe) { if (tokenStartPos < 0) { throw new ScriptException("Could not find a Java class named " + className, cnfe); } else { throw new ScriptException(tokenLine, tokenStartPos, "Could not find a Java class named " + className, cnfe); } } // Make sure that the Java class is a variable type definition if (! variableTypeSuperclass.isAssignableFrom(variableTypeClass)) { if (tokenStartPos < 0) { throw new ScriptException("Java class " + className + " does not provide a valid variable type " + "definition."); } else { throw new ScriptException(tokenLine, tokenStartPos, "Java class " + className + " does not provide a valid variable type " + "definition."); } } // Instantiate the class and get the type name from it to add to the // variable type hash. try { Variable variableType = (Variable) variableTypeClass.newInstance(); // See if there is already a variable type defined with the same // variable type name String variableTypeName = variableType.getVariableTypeName().toLowerCase(); if (variableTypeHash.get(variableTypeName) != null) { String variableClassName = variableTypeHash.get(variableTypeName); if (! variableClassName.equals(className)) { if (tokenStartPos < 0) { throw new ScriptException("Variable type " + variableTypeName + " is already defined by class " + variableClassName); } else { throw new ScriptException(tokenLine, tokenStartPos, "Variable type " + variableTypeName + " is already defined by class " + variableClassName); } } } else { variableTypeHash.put(variableTypeName, className); } } catch (Exception e) { if (tokenStartPos < 0) { throw new ScriptException("Could not create an instance of Java " + "class " + className + ": " + e, e); } else { throw new ScriptException(tokenLine, tokenStartPos, "Could not create an instance of Java " + "class " + className + ": " + e, e); } } } /** * Reads the specified script file into memory. * * @param filename The path and name of the script file to be read. * * @throws IOException If there is a problem reading from the script file. */ public void read(String filename) throws IOException { FileInputStream inputStream = new FileInputStream(filename); read(inputStream); inputStream.close(); } /** * Reads the script file data from the specified input stream into memory. * * @param inputStream The input stream from which the script file data will * be read. * * @throws IOException If there is a problem reading the script file data. */ public void read(InputStream inputStream) throws IOException { ArrayList<String> lineList = new ArrayList<String>(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String line = reader.readLine(); while (line != null) { lineList.add(line); line = reader.readLine(); } reader.close(); lines = new char[lineList.size()][]; for (int i=0; i < lines.length; i++) { lines[i] = lineList.get(i).toCharArray(); } } /** * Specifies the lines to use for the script. * * @param lines The lines to use for the script. */ public void setScriptLines(String[] lines) { this.lines = new char[lines.length][]; for (int i=0; i < lines.length; i++) { this.lines[i] = lines[i].toCharArray(); } } /** * Specifies the script arguments to use with this script. * * @param scriptArgumentHash The set of arguments to use in conjunction with * this script. */ public void setScriptArguments(HashMap<String,String> scriptArgumentHash) { this.scriptArgumentHash = scriptArgumentHash; } /** * Retrieves the value of the specified script argument. * * @param argumentName The name of the script argument to retrieve. * * @return The value of the requested script argument, or <CODE>null</CODE> * if it was not defined. */ public String getScriptArgument(String argumentName) { return scriptArgumentHash.get(argumentName); } /** * Parses the script information and tokenizes the instructions so that they * may be more easily and efficiently executed. * * @throws ScriptException If the script cannot be parsed as a valid SLAMD * script (i.e., if the script contains one or more * syntax errors). */ public void parse() throws ScriptException { // Initialize the tokenizer. tokenLine = 0; tokenStartPos = 0; tokenEndPos = -1; // First, get any variable type definitions out of the way. String token = peekAtNextToken(); if (token == null) { return; } while (token.equals(RESERVED_WORD_USE)) { nextToken(); handleUseStatement(); token = peekAtNextToken(); if (token == null) { return; } } // Next, take care of the variable declarations. while (token.equals(RESERVED_WORD_VARIABLE)) { nextToken(); handleVariableDeclaration(); token = peekAtNextToken(); if (token == null) { return; } } // The remainder of the file should be all instructions. Instruction i = nextInstruction(); while (i != null) { instructionList.add(i); i = nextInstruction(); } } /** * Parses the next instruction from the script. * * @return The next instruction that has been parsed from the script. * * @throws ScriptException If the next instruction could not be parsed * because of a syntax error. */ private Instruction nextInstruction() throws ScriptException { String token = nextToken(); // If there are no more tokens, then we are at the end of the script. if (token == null) { return null; } if (token.equals(RESERVED_WORD_USE)) { throw new ScriptException(tokenLine, tokenStartPos, "Variable type definitions may only occur at " + "the beginning of the script."); } else if (token.equals(RESERVED_WORD_VARIABLE)) { throw new ScriptException(tokenLine, tokenStartPos, "Variable type definitions may only occur at " + "the beginning of the script."); } else if (token.equals(RESERVED_WORD_IF)) { return parseIfInstruction(); } else if (token.equals(RESERVED_WORD_IF_NOT)) { return parseIfNotInstruction(); } else if (token.equals(RESERVED_WORD_LOOP)) { return parseLoopInstruction(); } else if (token.equals(RESERVED_WORD_WHILE)) { return parseWhileInstruction(); } else if (token.equals(RESERVED_WORD_WHILE_NOT)) { return parseWhileNotInstruction(); } else if (token.equals(RESERVED_WORD_BREAK)) { String nextToken = nextToken(); if (! nextToken.equals(";")) { throw new ScriptException(tokenLine, tokenStartPos, "A break instruction must be followed by a " + "semicolon."); } return new BreakInstruction(tokenLine); } else if (token.equals(RESERVED_WORD_CONTINUE)) { String nextToken = nextToken(); if (! nextToken.equals(";")) { throw new ScriptException(tokenLine, tokenStartPos, "A continue instruction must be followed " + "by a semicolon."); } return new ContinueInstruction(tokenLine); } else { // This could either be an assignment instruction or a method call // instruction. First, verify that the token provided is a valid variable // and then look at the next token to see if it is a "=" (assignment // instruction) or a "." (method call instruction). Variable v = variableHash.get(token); if (v == null) { throw new ScriptException(tokenLine, tokenStartPos, '"' + token + "\" is not a defined variable name."); } String token2 = nextToken(); if (token2.equals("=")) { return parseAssignmentInstruction(v); } else if (token2.equals(".")) { return parseMethodCallInstruction(v); } else { throw new ScriptException(tokenLine, tokenStartPos, "Equal sign or period expected after " + "variable name " + token + '.'); } } } /** * Parses a line starting with the "use" reserved word and registers the * specified variable type. * * @throws ScriptException If there is a syntax error on the line, or if * there is a problem registering the variable type. */ private void handleUseStatement() throws ScriptException { // The reserved word "use" must be immediately followed by a Java class // name. String className = nextClassNameToken(); if (className == null) { throw new ScriptException(tokenLine, tokenStartPos, "The use reserved word must be followed " + "by a fully-qualified Java class name"); } // Make sure that the Java class exists Class<?> variableTypeClass; try { variableTypeClass = Constants.classForName(className); } catch (ClassNotFoundException cnfe) { throw new ScriptException(tokenLine, tokenStartPos, "Could not find a Java class named " + className, cnfe); } // Make sure that the Java class is a variable type definition if (! variableTypeSuperclass.isAssignableFrom(variableTypeClass)) { throw new ScriptException(tokenLine, tokenStartPos, "Java class " + className + " does not provide a valid variable type " + "definition."); } // Instantiate the class and get the type name from it to add to the // variable type hash. try { Variable variableType = (Variable) variableTypeClass.newInstance(); // See if there is already a variable type defined with the same // variable type name String variableTypeName = variableType.getVariableTypeName().toLowerCase(); if (variableTypeHash.get(variableTypeName) != null) { String existingName = variableTypeHash.get(variableTypeName); if (! existingName.equals(className)) { throw new ScriptException(tokenLine, tokenStartPos, "Variable type " + variableTypeName + " is already defined by class " + existingName); } } else { variableTypeHash.put(variableTypeName, className); } } catch (Exception e) { throw new ScriptException(tokenLine, tokenStartPos, "Could not create an instance of Java " + "class " + className + ": " + e, e); } // The next token must be a semicolon to end the variable type // definition. String token = nextToken(); if ((token == null) || (! token.equals(";"))) { throw new ScriptException(tokenLine, tokenStartPos, "Semicolon expected after variable type " + "definition."); } } /** * Parses a line starting with the "variable" reserved word and defines the * specified variable. * * @throws ScriptException If there is a syntax error on the line, if the * variable type is not known, or if there is * already a variable defined with the specified * name. */ private void handleVariableDeclaration() throws ScriptException { // The next token must be the variable type. String variableType = nextToken(); if (variableType == null) { throw new ScriptException(tokenLine, tokenStartPos, "Variable type expected."); } // Make sure that the variable type has been defined. String className = variableTypeHash.get(variableType); if (className == null) { throw new ScriptException(tokenLine, tokenStartPos, "Undefined variable type " + variableType); } // The next token must be the variable name. String variableName = nextToken(); if (variableName == null) { throw new ScriptException(tokenLine, tokenStartPos, "Variable name expected."); } // Make sure the variable name is a valid identifier. if (! Variable.isValidIdentifier(variableName)) { throw new ScriptException(tokenLine, tokenStartPos, "Is not a valid identifier for a variable " + "name."); } // Make sure the variable name is not already in use. if (variableHash.get(variableName) != null) { throw new ScriptException(tokenLine, tokenStartPos, "There is already a variable defined " + "with the name of " + variableName); } // The variable declaration must end with a semicolon String token = nextToken(); if ((token == null) || (! token.equals(";"))) { throw new ScriptException(tokenLine, tokenStartPos, "A variable declaration must end with a " + "semicolon"); } // Create the new variable and add it into the variable hash try { Class<?> variableClass = Constants.classForName(className); Variable v = (Variable) variableClass.newInstance(); v.setName(variableName); variableHash.put(variableName, v); } catch (Exception e) { throw new ScriptException(tokenLine, tokenStartPos, "Unable to create variable " + variableName + ": " + e, e); } } /** * Parses an argument from the script. An argument may be an argument to a * method, a condition in an if or while statement, or the number of * iterations to perform in a loop statement. * * @return The argument that was parsed from the script. * * @throws ScriptException If the argument could not be parsed for some * reason. */ private Argument parseArgument() throws ScriptException { // Read the first token and look at the first character of that token. String token = nextToken(); if ((token == null) || (token.length() == 0)) { return null; } char c = token.charAt(0); // Look at the first character of the token and determine what kind of token // If the token starts with a quotation mark, then it's a string literal. if (c == '"') { // Strip the opening and closing quotes. return new StringLiteral(token.substring(1, token.length() - 1)); } // If the token starts with a dash or a digit, then it's an integer literal. else if ((c == '-') || ((c >= '0') && (c <= '9'))) { try { return new IntegerLiteral(Integer.parseInt(token)); } catch (NumberFormatException nfe) { throw new ScriptException(tokenLine, tokenStartPos, "Unable to parse \"" + token + "\" as an integer literal.", nfe); } } // If the token is "true" or "false", then it's a Boolean literal else if (token.equals(BooleanLiteral.BOOLEAN_TRUE_VALUE)) { return new BooleanLiteral(true); } else if (token.equals(BooleanLiteral.BOOLEAN_FALSE_VALUE)) { return new BooleanLiteral(false); } // Otherwise, the token must be the name of a variable. else { // Make sure it is a valid variable name. int lineNumber = tokenLine; Variable v = variableHash.get(token); if (v == null) { throw new ScriptException(tokenLine, tokenStartPos, "Unrecognized argument " + token); } // Look at the next token. If it is a period, then this is actually a // method call argument. Otherwise, the argument is just the variable. token = peekAtNextToken(); if ((token != null) && token.equals(".")) { // It is a method call, so figure out what the method name is. nextToken(); String methodName = nextToken(); if (token == null) { throw new ScriptException(tokenLine, tokenStartPos, "Expected a method name after reference " + "to variable " + v.getName()); } // The next token must be an opening parenthesis. token = nextToken(); if ((token == null) || (! token.equals("("))) { throw new ScriptException(tokenLine, tokenStartPos, "Expected an opening parenthesis in " + "call to method " + v.getName() + '.' + methodName); } // Parse the argument list. The next token must be either an argument // or a closing parenthesis. ArrayList<Argument> argumentList = new ArrayList<Argument>(); token = peekAtNextToken(); if (token == null) { throw new ScriptException(tokenLine, tokenStartPos, "Unterminated argument list detected in " + "call to " + v.getName() + '.' + methodName + "()"); } if (! token.equals(")")) { while (! token.equals(")")) { argumentList.add(parseArgument()); // The next token must be a comma or a close parenthesis. token = nextToken(); if ((token == null) || (! (token.equals(",") || token.equals(")")))) { throw new ScriptException(tokenLine, tokenStartPos, "Comma or closing parenthesis " + "expected in argument list for " + "method call " + v.getName() + '.' + methodName); } } } else { nextToken(); } // Create an array of the arguments Argument[] arguments = new Argument[argumentList.size()]; argumentList.toArray(arguments); // Create an array of the argument types. String[] argumentTypes = new String[argumentList.size()]; for (int i=0; i < argumentTypes.length; i++) { argumentTypes[i] = argumentList.get(i).getArgumentType(); } // If the variable does not have a method with the specified signature, // then return an error. int methodNumber = v.getMethodNumber(methodName, argumentTypes); if (methodNumber < 0) { String message = "Variable " + v.getName() + " does not have a method " + methodName + '('; String separator = ""; for (int i=0; i < argumentTypes.length; i++) { message += separator + argumentTypes[i]; separator = ","; } message += ")"; throw new ScriptException(tokenLine, tokenStartPos, message); } // It is a valid method call, so return the method call argument type. return new MethodCallInstruction(lineNumber, v, methodName, methodNumber, arguments); } else { return v; } } } /** * Parses an "if" instruction, including the optional else clause if it is * present. * * @return The if instruction parsed from the script. * * @throws ScriptException If any problem occurs while parsing the if * instruction. */ private Instruction parseIfInstruction() throws ScriptException { // The line number on which this if instruction starts. int ifLineNumber = tokenLine; // The first thing that comes in an if instruction is the argument. Argument condition = parseArgument(); // The argument that we have parsed must be of a Boolean type. if ((condition == null) || (! condition.getArgumentType().equals( BooleanVariable.BOOLEAN_VARIABLE_TYPE))) { throw new ScriptException(tokenLine, tokenStartPos, "An if instruction must be followed by an " + "argument that has a Boolean value."); } // Next, we need to determine if there is a single instruction to execute // or a set of instructions. Instruction ifInstruction; String token = peekAtNextToken(); if (token == null) { throw new ScriptException(tokenLine, tokenStartPos, "Instruction expected in if statement."); } else if (token.equals(RESERVED_WORD_BEGIN)) { // There is a set of instructions. Read past the begin, and then keep // going until we reach the end. nextToken(); int beginLineNumber = tokenLine; token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } ArrayList<Instruction> ifInstructionList = new ArrayList<Instruction>(); while (! token.equals(RESERVED_WORD_END)) { ifInstructionList.add(nextInstruction()); token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } } // If we've gotten here, then we found the end. Read it, and convert the // set of instructions that have been read into an instruction block. nextToken(); Instruction[] instructions = new Instruction[ifInstructionList.size()]; ifInstructionList.toArray(instructions); ifInstruction = new InstructionBlock(beginLineNumber, instructions); // There must be a semicolon after the end. Make sure it's there. token = nextToken(); if ((token == null) || (! token.equals(";"))) { throw new ScriptException(tokenLine, tokenStartPos, "Semicolon required after end."); } } else { // There should only be a single instruction, so read it. ifInstruction = nextInstruction(); } // Now check to see if there is an else clause. Instruction elseInstruction = null; token = peekAtNextToken(); if (token.equals(RESERVED_WORD_ELSE)) { // There is an else clause. See if it is a single instruction or if it // is a set of instructions. nextToken(); token = peekAtNextToken(); if (token == null) { throw new ScriptException(tokenLine, tokenStartPos, "Instruction expected in else statement."); } else if (token.equals(RESERVED_WORD_BEGIN)) { nextToken(); int beginLineNumber = tokenLine; token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } ArrayList<Instruction> elseInstructionList = new ArrayList<Instruction>(); while (! token.equals(RESERVED_WORD_END)) { elseInstructionList.add(nextInstruction()); token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } } nextToken(); Instruction[] instructions = new Instruction[elseInstructionList.size()]; elseInstructionList.toArray(instructions); elseInstruction = new InstructionBlock(beginLineNumber, instructions); // There must be a semicolon after the end. Make sure it's there. token = nextToken(); if ((token == null) || (! token.equals(";"))) { throw new ScriptException(tokenLine, tokenStartPos, "Semicolon required after end."); } } else { elseInstruction = nextInstruction(); } } return new IfElseInstruction(ifLineNumber, condition, ifInstruction, elseInstruction); } /** * Parses an "ifnot" instruction, including the optional else clause if it is * present. * * @return The ifnot instruction parsed from the script. * * @throws ScriptException If any problem occurs while parsing the ifnot * instruction. */ private Instruction parseIfNotInstruction() throws ScriptException { // The line number on which this ifnot instruction starts. int ifNotLineNumber = tokenLine; // The first thing that comes in an ifnot instruction is the argument. Argument condition = parseArgument(); // The argument that we have parsed must be of a Boolean type. if ((condition == null) || (! condition.getArgumentType().equals( BooleanVariable.BOOLEAN_VARIABLE_TYPE))) { throw new ScriptException(tokenLine, tokenStartPos, "An ifnot instruction must be followed by an " + "argument that has a Boolean value."); } // Next, we need to determine if there is a single instruction to execute // or a set of instructions. Instruction ifNotInstruction; String token = peekAtNextToken(); if (token == null) { throw new ScriptException(tokenLine, tokenStartPos, "Instruction expected in ifnot statement."); } else if (token.equals(RESERVED_WORD_BEGIN)) { // There is a set of instructions. Read past the begin, and then keep // going until we reach the end. nextToken(); int beginLineNumber = tokenLine; token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } ArrayList<Instruction> ifNotInstructionList = new ArrayList<Instruction>(); while (! token.equals(RESERVED_WORD_END)) { ifNotInstructionList.add(nextInstruction()); token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } } // If we've gotten here, then we found the end. Read it, and convert the // set of instructions that have been read into an instruction block. nextToken(); Instruction[] instructions = new Instruction[ifNotInstructionList.size()]; ifNotInstructionList.toArray(instructions); ifNotInstruction = new InstructionBlock(beginLineNumber, instructions); // There must be a semicolon after the end. Make sure it's there. token = nextToken(); if ((token == null) || (! token.equals(";"))) { throw new ScriptException(tokenLine, tokenStartPos, "Semicolon required after end."); } } else { // There should only be a single instruction, so read it. ifNotInstruction = nextInstruction(); } // Now check to see if there is an else clause. Instruction elseInstruction = null; token = peekAtNextToken(); if (token.equals(RESERVED_WORD_ELSE)) { // There is an else clause. See if it is a single instruction or if it // is a set of instructions. nextToken(); token = peekAtNextToken(); if (token == null) { throw new ScriptException(tokenLine, tokenStartPos, "Instruction expected in else statement."); } else if (token.equals(RESERVED_WORD_BEGIN)) { nextToken(); int beginLineNumber = tokenLine; token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } ArrayList<Instruction> elseInstructionList = new ArrayList<Instruction>(); while (! token.equals(RESERVED_WORD_END)) { elseInstructionList.add(nextInstruction()); token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } } nextToken(); Instruction[] instructions = new Instruction[elseInstructionList.size()]; elseInstructionList.toArray(instructions); elseInstruction = new InstructionBlock(beginLineNumber, instructions); // There must be a semicolon after the end. Make sure it's there. token = nextToken(); if ((token == null) || (! token.equals(";"))) { throw new ScriptException(tokenLine, tokenStartPos, "Semicolon required after end."); } } else { elseInstruction = nextInstruction(); } } return new IfNotInstruction(ifNotLineNumber, condition, ifNotInstruction, elseInstruction); } /** * Parses a "loop" instruction from the script. * * @return The loop instruction parsed from the script. * * @throws ScriptException If any problem occurs while parsing the loop * instruction. */ private Instruction parseLoopInstruction() throws ScriptException { // The line number on which this loop instruction starts. int loopLineNumber = tokenLine; // The first thing that comes in a loop instruction is the argument. Argument iterations = parseArgument(); // The argument that we have parsed must be of an integer type. if ((iterations == null) || (! iterations.getArgumentType().equals( IntegerVariable.INTEGER_VARIABLE_TYPE))) { throw new ScriptException(tokenLine, tokenStartPos, "A loop instruction must be followed by an " + "argument that has an integer value."); } // Next, we need to determine if there is a single instruction to execute // or a set of instructions. Instruction loopInstruction; String token = peekAtNextToken(); if (token == null) { throw new ScriptException(tokenLine, tokenStartPos, "Instruction expected in loop statement."); } else if (token.equals(RESERVED_WORD_BEGIN)) { // There is a set of instructions. Read past the begin, and then keep // going until we reach the end. nextToken(); int beginLineNumber = tokenLine; token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } ArrayList<Instruction> loopInstructionList = new ArrayList<Instruction>(); while (! token.equals(RESERVED_WORD_END)) { loopInstructionList.add(nextInstruction()); token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } } // If we've gotten here, then we found the end. Read it, and convert the // set of instructions that have been read into an instruction block. nextToken(); Instruction[] instructions = new Instruction[loopInstructionList.size()]; loopInstructionList.toArray(instructions); loopInstruction = new InstructionBlock(beginLineNumber, instructions); // There must be a semicolon after the end. Make sure it's there. token = nextToken(); if ((token == null) || (! token.equals(";"))) { throw new ScriptException(tokenLine, tokenStartPos, "Semicolon required after end."); } } else { // There should only be a single instruction, so read it. loopInstruction = nextInstruction(); } return new LoopInstruction(loopLineNumber, iterations, loopInstruction); } /** * Parses a "while" instruction from the script. * * @return The while instruction parsed from the script. * * @throws ScriptException If any problem occurs while parsing the while * instruction. */ private Instruction parseWhileInstruction() throws ScriptException { // The line number on which this while instruction starts. int whileLineNumber = tokenLine; // The first thing that comes in a while instruction is the argument. Argument condition = parseArgument(); // The argument that we have parsed must be of a Boolean type. if ((condition == null) || (! condition.getArgumentType().equals( BooleanVariable.BOOLEAN_VARIABLE_TYPE))) { throw new ScriptException(tokenLine, tokenStartPos, "A while instruction must be followed by an " + "argument that has a Boolean value."); } // Next, we need to determine if there is a single instruction to execute // or a set of instructions. Instruction whileInstruction; String token = peekAtNextToken(); if (token == null) { throw new ScriptException(tokenLine, tokenStartPos, "Instruction expected in loop statement."); } else if (token.equals(RESERVED_WORD_BEGIN)) { // There is a set of instructions. Read past the begin, and then keep // going until we reach the end. nextToken(); int beginLineNumber = tokenLine; token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } ArrayList<Instruction> whileInstructionList = new ArrayList<Instruction>(); while (! token.equals(RESERVED_WORD_END)) { whileInstructionList.add(nextInstruction()); token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } } // If we've gotten here, then we found the end. Read it, and convert the // set of instructions that have been read into an instruction block. nextToken(); Instruction[] instructions = new Instruction[whileInstructionList.size()]; whileInstructionList.toArray(instructions); whileInstruction = new InstructionBlock(beginLineNumber, instructions); // There must be a semicolon after the end. Make sure it's there. token = nextToken(); if ((token == null) || (! token.equals(";"))) { throw new ScriptException(tokenLine, tokenStartPos, "Semicolon required after end."); } } else { // There should only be a single instruction, so read it. whileInstruction = nextInstruction(); } return new WhileInstruction(whileLineNumber, condition, whileInstruction); } /** * Parses a "whilenot" instruction from the script. * * @return The whilenot instruction parsed from the script. * * @throws ScriptException If any problem occurs while parsing the whilenot * instruction. */ private Instruction parseWhileNotInstruction() throws ScriptException { // The line number on which this while instruction starts. int whileLineNumber = tokenLine; // The first thing that comes in a while instruction is the argument. Argument condition = parseArgument(); // The argument that we have parsed must be of a Boolean type. if ((condition == null) || (! condition.getArgumentType().equals( BooleanVariable.BOOLEAN_VARIABLE_TYPE))) { throw new ScriptException(tokenLine, tokenStartPos, "A while instruction must be followed by an " + "argument that has a Boolean value."); } // Next, we need to determine if there is a single instruction to execute // or a set of instructions. Instruction whileInstruction; String token = peekAtNextToken(); if (token == null) { throw new ScriptException(tokenLine, tokenStartPos, "Instruction expected in loop statement."); } else if (token.equals(RESERVED_WORD_BEGIN)) { // There is a set of instructions. Read past the begin, and then keep // going until we reach the end. nextToken(); int beginLineNumber = tokenLine; token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } ArrayList<Instruction> whileInstructionList = new ArrayList<Instruction>(); while (! token.equals(RESERVED_WORD_END)) { whileInstructionList.add(nextInstruction()); token = peekAtNextToken(); if (token == null) { throw new ScriptException(beginLineNumber, "Unterminated begin block detected."); } } // If we've gotten here, then we found the end. Read it, and convert the // set of instructions that have been read into an instruction block. nextToken(); Instruction[] instructions = new Instruction[whileInstructionList.size()]; whileInstructionList.toArray(instructions); whileInstruction = new InstructionBlock(beginLineNumber, instructions); // There must be a semicolon after the end. Make sure it's there. token = nextToken(); if ((token == null) || (! token.equals(";"))) { throw new ScriptException(tokenLine, tokenStartPos, "Semicolon required after end."); } } else { // There should only be a single instruction, so read it. whileInstruction = nextInstruction(); } return new WhileNotInstruction(whileLineNumber, condition, whileInstruction); } /** * Parses an assignment from the script. * * @param variable The variable that is being assigned. * * @return The assignment instruction parsed from the script. * * @throws ScriptException If a problem occurred while parsing the * assignment. */ private Instruction parseAssignmentInstruction(Variable variable) throws ScriptException { int lineNumber = tokenLine; Argument argument = parseArgument(); // Make sure that the argument type is the same as the variable type. if (! argument.getArgumentType().equals(variable.getVariableTypeName())) { throw new ScriptException(tokenLine, tokenStartPos, "Cannot assign an argument of type " + argument.getArgumentType() + " to a variable of type " + variable.getVariableTypeName()); } // There must be a semicolon after the argument. Make sure it's there. String token = nextToken(); if ((token == null) || (! token.equals(";"))) { throw new ScriptException(tokenLine, tokenStartPos, "Semicolon required after an assignment."); } return new AssignmentInstruction(lineNumber, variable, argument); } /** * Parses a method call from the script. * * @param variable The variable on which the action is to be taken. * * @return The method call instruction parsed from the script. * * @throws ScriptException If a problem occurred while parsing the method * call. */ private Instruction parseMethodCallInstruction(Variable variable) throws ScriptException { // Make sure that it is an actual variable name. int lineNumber = tokenLine; String variableName = variable.getName(); // The next token must be the name of the method. String methodName = nextToken(); if (methodName == null) { throw new ScriptException(tokenLine, tokenStartPos, "Method name expected after reference to " + "variable " + variableName); } // Make sure that it is a valid method for the specified variable. if (! variable.hasMethod(methodName)) { throw new ScriptException(tokenLine, tokenStartPos, '"' + methodName + "\" is not a valid method name for " + variableName + " variables."); } // The next token must be an opening parenthesis to start the argument list. String token = nextToken(); if ((token == null) || (! token.equals("("))) { throw new ScriptException(tokenLine, tokenStartPos, "Expected an opening parenthesis in " + "call to method " + variableName + '.' + methodName); } // Parse the argument list. The next token must be either an argument // or a closing parenthesis. ArrayList<Argument> argumentList = new ArrayList<Argument>(); token = peekAtNextToken(); if (token == null) { throw new ScriptException(tokenLine, tokenStartPos, "Unterminated argument list detected in " + "call to " + variableName + '.' + methodName + "()"); } if (! token.equals(")")) { while (! token.equals(")")) { argumentList.add(parseArgument()); // The next token must be a comma or a close parenthesis. token = nextToken(); if ((token == null) || (! (token.equals(",") || token.equals(")")))) { throw new ScriptException(tokenLine, tokenStartPos, "Comma or closing parenthesis " + "expected in argument list for " + "method call " + variableName + '.' + methodName); } } } else { nextToken(); } // Create an array of the arguments Argument[] arguments = new Argument[argumentList.size()]; argumentList.toArray(arguments); // Create an array of the argument types. String[] argumentTypes = new String[argumentList.size()]; for (int i=0; i < argumentTypes.length; i++) { argumentTypes[i] = argumentList.get(i).getArgumentType(); } // If the variable does not have a method with the specified signature, // then return an error. int methodNumber = variable.getMethodNumber(methodName, argumentTypes); if (methodNumber < 0) { String message = "Variable " + variableName + " does not have a method " + methodName + '('; String separator = ""; for (int i=0; i < argumentTypes.length; i++) { message += separator + argumentTypes[i]; separator = ","; } message += ")"; throw new ScriptException(tokenLine, tokenStartPos, message); } // The variable instruction must end with a semicolon. token = nextToken(); if ((token == null) || (! token.equals(";"))) { throw new ScriptException(tokenLine, tokenStartPos, "Semicolon expected after call to " + variableName + '.' + methodName); } // It is a valid method call, so return the method call argument type. return new MethodCallInstruction(lineNumber, variable, methodName, methodNumber, arguments); } /** * Retrieves the next token from the script but preserves the internal markers * used to keep track of the position in the script. That is, it retrieves * the next token that will be returned by the call to * <CODE>nextToken()</CODE>. * * @return The next token that will be returned by <CODE>nextToken()</CODE>. * * @throws ScriptException If there is a problem while parsing the next * token. */ public String peekAtNextToken() throws ScriptException { int saveLine = tokenLine; int saveStartPos = tokenStartPos; int saveEndPos = tokenEndPos; String token = nextToken(); this.tokenLine = saveLine; this.tokenStartPos = saveStartPos; this.tokenEndPos = saveEndPos; return token; } /** * Reads the next token from the script. A token is classified as any single * element that is not a comment or whitespace, and will be one of the * following: * * <UL> * <LI>An alphabetic character followed by a set of alphabetic characters, * numeric digits, or underscore characters in any combination. All * alphabetic characters will be converted to lowercase.</LI> * <LI>A set of numeric digits, optionally beginning with a dash</LI> * <LI>A period</LI> * <LI>A semicolon</LI> * <LI>A comma</LI> * <LI>An opening or closing parenthesis</LI> * <LI>A quotation mark, followed by a sequence of any characters and * ending with another quotation mark that is not preceded immediately * by a backslash. The closing quotation mark must occur on the same * line as the opening quotation mark. All capitalization will be * preserved.</LI> * </UL> * * @return The next token read from the script. * * @throws ScriptException If a problem occurs while attempting to parse the * next token. */ private String nextToken() throws ScriptException { // Set the point at which to start looking. tokenStartPos = tokenEndPos + 1; // If we're at the beginning of a line, make sure it is not blank or a // comment. if (tokenStartPos == 0) { while ((tokenLine < lines.length) && ((lines[tokenLine].length == 0) || (lines[tokenLine][0] == '#') || (new String(lines[tokenLine]).trim().startsWith("#")))) { tokenLine++; } if (tokenLine >= lines.length) { return null; } } // If the point at which to start looking is past the end of the line, then // start the next line. if (tokenStartPos >= lines[tokenLine].length) { tokenStartPos = 0; // Keep going until we find a line that is not blank and not a comment. do { tokenLine++; // If we were on the last line, then return null to indicate there are // no more tokens. if (tokenLine >= lines.length) { return null; } } while ((lines[tokenLine].length == 0) || (lines[tokenLine][0] == '#') || (new String(lines[tokenLine]).trim().length() == 0) || (new String(lines[tokenLine]).trim().startsWith("#"))); } // Now read until we find something other than whitespace. char c = lines[tokenLine][tokenStartPos]; while ((c == ' ') || (c == '\t')) { tokenStartPos++; // If we've reached the end of the line, then it must be an entire line // of just whitespace. If that happens, then find the next line that's // not blank and not a comment. if (tokenStartPos >= lines[tokenLine].length) { tokenStartPos = 0; // Keep going until we find a line that is not blank and not a comment. do { tokenLine++; // If we were on the last line, then return null to indicate there are // no more tokens. if (tokenLine >= lines.length) { return null; } } while ((lines[tokenLine].length == 0) || (lines[tokenLine][0] == '#')); } c = lines[tokenLine][tokenStartPos]; } // Finally, something other than whitespace. See what the character is. switch (lines[tokenLine][tokenStartPos]) { // Check for the single characters that we return by themselves. case '.': case '=': case ';': case ',': case '(': case ')': tokenEndPos = tokenStartPos; return new String(new char[] { lines[tokenLine][tokenStartPos] }); // Check for a numeric value. If it is a numeric value, then it must be // followed only by other numeric digits and ended with a space, a tab, // a comma, a close parenthesis, or the end of the line. case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': int i; for (i=tokenStartPos+1; i < lines[tokenLine].length; i++) { c = lines[tokenLine][i]; if ((c == ' ') || (c == ';') || (c == '\t') || (c == ',') || (c == ')')) { break; } else if (! ((c >= '0') && (c <= '9'))) { throw new ScriptException(tokenLine, i, "Unexpected character '" + c + "' while parsing an integer value"); } } tokenEndPos = i-1; String s = new String(lines[tokenLine], tokenStartPos, (i-tokenStartPos)); // If the token is only a single dash character, then it's invalid. if ((lines[tokenLine][tokenStartPos] == '-') && (s.length() == 1)) { throw new ScriptException(tokenLine, tokenStartPos, "The dash character may only appear when " + "when followed immediately by a numeric " + "value to indicate a negative integer"); } return s; // Check for a quotation mark. If we find one, then look for the next // unescaped quotation mark and return the token as a string literal. case '"': for (i=tokenStartPos+1; i < lines[tokenLine].length; i++) { c = lines[tokenLine][i]; if (c == '"') { if (lines[tokenLine][i-1] != '\\') { break; } } } if ((i > tokenStartPos) && (i < lines[tokenLine].length) && (lines[tokenLine][i] == '"') && (lines[tokenLine][i-1] != '\\')) { tokenEndPos = i; s = new String(lines[tokenLine], tokenStartPos, (i+1-tokenStartPos)); return s; } else { throw new ScriptException(tokenLine, tokenStartPos, "Detected an unterminated string literal."); } // It must be a reserved word, variable name, method name, or a Boolean // literal. Make sure that it only starts with a letter and only // contains letters, digits, and underscores. default: c = lines[tokenLine][tokenStartPos]; // Make sure it starts with a letter. if (! (((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')))) { throw new ScriptException(tokenLine, tokenStartPos, "Unexpected character '" + c + "' at the beginning of the token."); } // Now loop until we find something other than a letter, a number, or // an underscore. for (i=tokenStartPos+1; i < lines[tokenLine].length; i++) { c = lines[tokenLine][i]; if (! (((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || ((c >= '0') && (c <= '9')) || (c == '_'))) { tokenEndPos = i-1; break; } } if (i >= lines[tokenLine].length) { tokenEndPos = lines[tokenLine].length - 1; } s = new String(lines[tokenLine], tokenStartPos, (i - tokenStartPos)); return s.toLowerCase(); } } /** * Reads the next token from the script as a Java class name. This has * special requirements (periods are not delimiters, and only alphabetic, * numeric, and underscore characters are permitted between periods) that * makes it appropriate to handle this separately. * * @return The next Java class name read from the script. * * @throws ScriptException If a problem occurs while reading the next token. */ private String nextClassNameToken() throws ScriptException { // Set the point at which to start looking. tokenStartPos = tokenEndPos + 1; // If we're at the beginning of a line, make sure it is not blank or a // comment. if (tokenStartPos == 0) { while ((tokenLine < lines.length) && ((lines[tokenLine].length == 0) || (lines[tokenLine][0] == '#'))) { tokenLine++; } if (tokenLine >= lines.length) { return null; } } // If the point at which to start looking is past the end of the line, then // start the next line. if (tokenStartPos >= lines[tokenLine].length) { tokenStartPos = 0; // Keep going until we find a line that is not blank and not a comment. do { tokenLine++; // If we were on the last line, then return null to indicate there are // no more tokens. if (tokenLine >= lines.length) { return null; } } while ((lines[tokenLine].length == 0) || (lines[tokenLine][0] == '#')); } // Now read until we find something other than whitespace. char c = lines[tokenLine][tokenStartPos]; while ((c == ' ') || (c == '\t')) { tokenStartPos++; // If we've reached the end of the line, then it must be an entire line // of just whitespace. If that happens, then find the next line that's // not blank and not a comment. if (tokenStartPos >= lines[tokenLine].length) { tokenStartPos = 0; // Keep going until we find a line that is not blank and not a comment. do { tokenLine++; // If we were on the last line, then return null to indicate there are // no more tokens. if (tokenLine >= lines.length) { return null; } } while ((lines[tokenLine].length == 0) || (lines[tokenLine][0] == '#')); } c = lines[tokenLine][tokenStartPos]; } // Finally, something other than whitespace. For a class name, the first // character must be alphabetic. if (! (((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')))) { throw new ScriptException(tokenLine, tokenStartPos, "A variable type class name must begin with " + "an alphabetic character"); } // Now keep reading until we find either whitespace or a semicolon. Until // then, the only characters allowed must be alphabetic, numeric, or // periods. int i; for (i=tokenStartPos+1; i < lines[tokenLine].length; i++) { c = lines[tokenLine][i]; if ((c == ' ') || (c == '\t') || (c == ';')) { break; } else if (! (((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) || (c == '.') || (c == '_'))) { throw new ScriptException(tokenLine, i, "Unexpected character '" + c + "' while parsing Java class name."); } } tokenEndPos = i - 1; return new String(lines[tokenLine], tokenStartPos, (i - tokenStartPos)); } /** * Indicates whether the specified token is one of the reserved words in the * SLAMD scripting environment. It is expected that the token will be in all * lowercase characters. * * @param token The token for which to make the determination. * * @return <CODE>true</CODE> if the specified token is one of the reserved * words, or <CODE>false</CODE> if not. */ public static boolean isReservedWord(String token) { return (token.equals(RESERVED_WORD_USE) || token.equals(RESERVED_WORD_VARIABLE) || token.equals(RESERVED_WORD_IF) || token.equals(RESERVED_WORD_IF_NOT) || token.equals(RESERVED_WORD_ELSE) || token.equals(RESERVED_WORD_LOOP) || token.equals(RESERVED_WORD_WHILE) || token.equals(RESERVED_WORD_WHILE_NOT) || token.equals(RESERVED_WORD_BEGIN) || token.equals(RESERVED_WORD_END)); } /** * Executes the script. The script must have already been read in and parsed * into tokens. * * @param jobThread The job thread that will be executing the script. * * @throws ScriptException If a problem occurs while executing the script. */ public void execute(JobClass jobThread) throws ScriptException { // Make sure to associate the script variable with the job thread. ScriptVariable scriptVariable = (ScriptVariable) variableHash.get("script"); scriptVariable.setJobThread(jobThread); scriptVariable.setParser(this); // Start all of the stat trackers. for (Variable v : variableHash.values()) { v.startStatTrackers(jobThread); } // Execute each instruction in the instruction set. try { for (int i=0; i < instructionList.size(); i++) { Instruction instruction = instructionList.get(i); instruction.execute(jobThread); } } catch (StopRunningException sre) { jobThread.logMessage(sre.getMessage()); } // Stop all of the stat trackers. for (Variable v : variableHash.values()) { v.stopStatTrackers(); } } /** * Executes the script in debug mode, sending debug information to the * client's message writer. The script must have already been read in and * parsed into tokens. * * @param jobThread The job thread that will be executing the script. * * @throws ScriptException If a problem occurs while executing the script. */ public void debugExecute(JobClass jobThread) throws ScriptException { // Make sure to associate the script variable with the job thread. ScriptVariable scriptVariable = (ScriptVariable) variableHash.get("script"); scriptVariable.setJobThread(jobThread); scriptVariable.setParser(this); // Start all of the stat trackers. for (Variable v : variableHash.values()) { v.startStatTrackers(jobThread); } // Execute each instruction in the instruction set. try { for (int i=0; i < instructionList.size(); i++) { Instruction instruction = instructionList.get(i); instruction.debugExecute(jobThread); } } catch (StopRunningException sre) { jobThread.logMessage(sre.getMessage()); } // Stop all of the stat trackers. for (Variable v : variableHash.values()) { v.stopStatTrackers(); } } /** * Retrieves the set of statistics gathered while running this script. * * @return The set of statistics gathered while running this script. */ public StatTracker[] getStatTrackers() { ArrayList<StatTracker> trackerList = new ArrayList<StatTracker>(); for (Variable v : variableHash.values()) { StatTracker[] trackers = v.getStatTrackers(); if ((trackers != null) && (trackers.length > 0)) { for (int i=0; i < trackers.length; i++) { trackerList.add(trackers[i]); } } } StatTracker[] trackers = new StatTracker[trackerList.size()]; trackerList.toArray(trackers); return trackers; } }