/* * JaamSim Discrete Event Simulation * Copyright (C) 2009-2011 Ausenco Engineering Canada Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jaamsim.input; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map.Entry; import com.jaamsim.StringProviders.StringProvider; import com.jaamsim.basicsim.Entity; import com.jaamsim.basicsim.ErrorException; import com.jaamsim.basicsim.FileEntity; import com.jaamsim.basicsim.Group; import com.jaamsim.basicsim.ObjectType; import com.jaamsim.basicsim.Simulation; import com.jaamsim.datatypes.DoubleVector; import com.jaamsim.events.EventManager; import com.jaamsim.math.Vec3d; import com.jaamsim.ui.GUIFrame; import com.jaamsim.ui.LogBox; import com.jaamsim.units.Unit; public class InputAgent { private static final String recordEditsMarker = "RecordEdits"; private static int numErrors = 0; private static int numWarnings = 0; private static FileEntity logFile; private static long lastTickForTrace; private static File configFile; // present configuration file private static boolean batchRun; private static boolean scriptMode; // TRUE if script mode (command line) is specified private static boolean sessionEdited; // TRUE if any inputs have been changed after loading a configuration file private static boolean recordEditsFound; // TRUE if the "RecordEdits" marker is found in the configuration file private static boolean recordEdits; // TRUE if input changes are to be marked as edited. private static final String INP_ERR_DEFINEUSED = "The name: %s has already been used and is a %s"; private static final String[] EARLY_KEYWORDS = {"AttributeDefinitionList", "UnitType", "UnitTypeList", "TickLength", "CustomOutputList"}; private static File reportDir; private static FileEntity reportFile; // file to which the output report will be written private static PrintStream outStream; // location where the selected outputs will be written private static long preDefinedEntityCount; // Number of Entities after loading autoload.cfg static { recordEditsFound = false; sessionEdited = false; batchRun = false; configFile = null; reportDir = null; reportFile = null; outStream = null; lastTickForTrace = -1l; } /** * Clears the InputAgent prior to loading a new model. */ public static void clear() { logFile = null; numErrors = 0; numWarnings = 0; recordEditsFound = false; sessionEdited = false; configFile = null; reportDir = null; lastTickForTrace = -1l; setReportDirectory(null); stop(); } /** * Resets the InputAgent when a run is stopped and reset to zero simulation time. */ public static void stop() { if (reportFile != null) { reportFile.close(); reportFile = null; } if (outStream != null) { outStream.close(); outStream = null; } } public static void setPreDefinedEntityCount(long count) { preDefinedEntityCount = count; } private static String getReportDirectory() { if (reportDir != null) return reportDir.getPath() + File.separator; if (configFile != null) return configFile.getParentFile().getPath() + File.separator; return null; } public static String getReportFileName(String name) { return getReportDirectory() + name; } public static void setReportDirectory(File dir) { reportDir = dir; if (reportDir == null) return; if (!reportDir.exists() && !reportDir.mkdirs()) throw new InputErrorException("Was unable to create the Report Directory: %s", reportDir.toString()); } public static void prepareReportDirectory() { if (reportDir != null) reportDir.mkdirs(); } /** * Sets the present configuration file. * * @param file - the present configuration file. */ public static void setConfigFile(File file) { configFile = file; } /** * Returns the present configuration file. * <p> * Null is returned if no configuration file has been loaded or saved yet. * <p> * @return the present configuration file. */ public static File getConfigFile() { return configFile; } /** * Returns the name of the simulation run. * <p> * For example, if the configuration file name is "case1.cfg", then the * run name is "case1". * <p> * @return the name of simulation run. */ public static String getRunName() { if( InputAgent.getConfigFile() == null ) return ""; String name = InputAgent.getConfigFile().getName(); int index = name.lastIndexOf('.'); if( index == -1 ) return name; return name.substring( 0, index ); } /** * Specifies whether a RecordEdits marker was found in the present configuration file. * * @param bool - TRUE if a RecordEdits marker was found. */ public static void setRecordEditsFound(boolean bool) { recordEditsFound = bool; } /** * Indicates whether a RecordEdits marker was found in the present configuration file. * * @return - TRUE if a RecordEdits marker was found. */ public static boolean getRecordEditsFound() { return recordEditsFound; } /** * Returns the "RecordEdits" mode for the InputAgent. * <p> * When RecordEdits is TRUE, any model inputs that are changed and any objects that * are defined are marked as "edited". When FALSE, model inputs and object * definitions are marked as "unedited". * <p> * RecordEdits mode is used to determine the way JaamSim saves a configuration file * through the graphical user interface. Object definitions and model inputs * that are marked as unedited will be copied exactly as they appear in the original * configuration file that was first loaded. Object definitions and model inputs * that are marked as edited will be generated automatically by the program. * * @return the RecordEdits mode for the InputAgent. */ public static boolean recordEdits() { return recordEdits; } /** * Sets the "RecordEdits" mode for the InputAgent. * <p> * When RecordEdits is TRUE, any model inputs that are changed and any objects that * are defined are marked as "edited". When FALSE, model inputs and object * definitions are marked as "unedited". * <p> * RecordEdits mode is used to determine the way JaamSim saves a configuration file * through the graphical user interface. Object definitions and model inputs * that are marked as unedited will be copied exactly as they appear in the original * configuration file that was first loaded. Object definitions and model inputs * that are marked as edited will be generated automatically by the program. * * @param b - boolean value for the RecordEdits mode */ public static void setRecordEdits(boolean b) { recordEdits = b; } public static boolean isSessionEdited() { return sessionEdited; } public static void setBatch(boolean batch) { batchRun = batch; } public static boolean getBatch() { return batchRun; } public static void setScriptMode(boolean bool) { scriptMode = bool; } public static boolean isScriptMode() { return scriptMode; } private static int getBraceDepth(ArrayList<String> tokens, int startingBraceDepth, int startingIndex) { int braceDepth = startingBraceDepth; for (int i = startingIndex; i < tokens.size(); i++) { String token = tokens.get(i); if (token.equals("{")) braceDepth++; if (token.equals("}")) braceDepth--; if (braceDepth < 0) { InputAgent.logBadInput(tokens, "Extra closing braces found"); tokens.clear(); } if (braceDepth > 3) { InputAgent.logBadInput(tokens, "Maximum brace depth (3) exceeded"); tokens.clear(); } } return braceDepth; } private static URI resRoot; private static final String res = "/resources/"; static { try { // locate the resource folder, and create resRoot = InputAgent.class.getResource(res).toURI(); } catch (URISyntaxException e) {} } private static void rethrowWrapped(Exception ex) { StringBuilder causedStack = new StringBuilder(); for (StackTraceElement elm : ex.getStackTrace()) causedStack.append(elm.toString()).append("\n"); throw new InputErrorException("Caught exception: %s", ex.getMessage() + "\n" + causedStack.toString()); } public static final void readResource(String res) { if (res == null) return; try { readStream(null, null, res); } catch (URISyntaxException ex) { rethrowWrapped(ex); } } public static final boolean readStream(String root, URI path, String file) throws URISyntaxException { URI resolved = getFileURI(path, file, root); URL url = null; try { url = resolved.normalize().toURL(); } catch (MalformedURLException e) { rethrowWrapped(e); } if (url == null) { InputAgent.logError("Unable to resolve path %s%s - %s", root, path.toString(), file); return false; } BufferedReader buf = null; try { InputStream in = url.openStream(); buf = new BufferedReader(new InputStreamReader(in)); } catch (IOException e) { InputAgent.logError("Could not read from %s", url.toString()); return false; } InputAgent.readBufferedStream(buf, resolved, root); return true; } public static final void readBufferedStream(BufferedReader buf, URI resolved, String root) { try { ArrayList<String> record = new ArrayList<>(); int braceDepth = 0; ParseContext pc = new ParseContext(resolved, root); while (true) { String line = buf.readLine(); // end of file, stop reading if (line == null) break; int previousRecordSize = record.size(); Parser.tokenize(record, line, true); braceDepth = InputAgent.getBraceDepth(record, braceDepth, previousRecordSize); if( braceDepth != 0 ) continue; if (record.size() == 0) continue; InputAgent.echoInputRecord(record); if ("DEFINE".equalsIgnoreCase(record.get(0))) { InputAgent.processDefineRecord(record); record.clear(); continue; } if ("INCLUDE".equalsIgnoreCase(record.get(0))) { try { InputAgent.processIncludeRecord(pc, record); } catch (URISyntaxException ex) { rethrowWrapped(ex); } record.clear(); continue; } if ("RECORDEDITS".equalsIgnoreCase(record.get(0))) { InputAgent.setRecordEditsFound(true); InputAgent.setRecordEdits(true); record.clear(); continue; } // Otherwise assume it is a Keyword record InputAgent.processKeywordRecord(record, pc); record.clear(); } // Leftover Input at end of file if (record.size() > 0) InputAgent.logBadInput(record, "Leftover input at end of file"); buf.close(); } catch (IOException e) { // Make best effort to ensure it closes try { buf.close(); } catch (IOException e2) {} } } private static void processIncludeRecord(ParseContext pc, ArrayList<String> record) throws URISyntaxException { if (record.size() != 2) { InputAgent.logError("Bad Include record, should be: Include <File>"); return; } InputAgent.readStream(pc.jail, pc.context, record.get(1).replaceAll("\\\\", "/")); } private static void processDefineRecord(ArrayList<String> record) { if (record.size() < 5 || !record.get(2).equals("{") || !record.get(record.size() - 1).equals("}")) { InputAgent.logError("Bad Define record, should be: Define <Type> { <names>... }"); return; } Class<? extends Entity> proto = null; try { if( record.get( 1 ).equalsIgnoreCase( "ObjectType" ) ) { proto = ObjectType.class; } else { proto = Input.parseEntityType(record.get(1)); } } catch (InputErrorException e) { InputAgent.logError("%s", e.getMessage()); return; } // Loop over all the new Entity names for (int i = 3; i < record.size() - 1; i++) { InputAgent.defineEntity(proto, record.get(i), InputAgent.recordEdits()); } } public static <T extends Entity> T generateEntityWithName(Class<T> proto, String key) { if (key != null && !isValidName(key)) { InputAgent.logError("Entity names cannot contain spaces, tabs, { or }: %s", key); return null; } T ent = null; try { ent = proto.newInstance(); ent.setFlag(Entity.FLAG_GENERATED); if (key != null) ent.setName(key); else ent.setName(proto.getSimpleName() + "-" + ent.getEntityNumber()); } catch (InstantiationException e) {} catch (IllegalAccessException e) {} finally { if (ent == null) { InputAgent.logError("Could not create new Entity: %s", key); return null; } } return ent; } /** * Like defineEntity(), but will generate a unique name if a name collision exists * @param proto * @param key * @param sep * @param addedEntity * @return */ public static <T extends Entity> T defineEntityWithUniqueName(Class<T> proto, String key, String sep, boolean addedEntity) { // Has the provided name been used already? if (Entity.getNamedEntity(key) == null) { return defineEntity(proto, key, addedEntity); } // Try the provided name plus "1", "2", etc. until an unused name is found int entityNum = 1; while(true) { String name = String.format("%s%s%d", key, sep, entityNum); if (Entity.getNamedEntity(name) == null) { return defineEntity(proto, name, addedEntity); } entityNum++; } } private static boolean isValidName(String key) { for (int i = 0; i < key.length(); ++i) { final char c = key.charAt(i); if (c == ' ' || c == '\t' || c == '{' || c == '}') return false; } return true; } /** * if addedEntity is true then this is an entity defined * by user interaction or after added record flag is found; * otherwise, it is from an input file define statement * before the model is configured * @param proto * @param key * @param addedEntity */ private static <T extends Entity> T defineEntity(Class<T> proto, String key, boolean addedEntity) { Entity existingEnt = Input.tryParseEntity(key, Entity.class); if (existingEnt != null) { InputAgent.logError(INP_ERR_DEFINEUSED, key, existingEnt.getClass().getSimpleName()); return null; } if (!isValidName(key)) { InputAgent.logError("Entity names cannot contain spaces, tabs, { or }: %s", key); return null; } T ent = null; try { ent = proto.newInstance(); if (addedEntity) { ent.setFlag(Entity.FLAG_ADDED); sessionEdited = true; } } catch (InstantiationException e) {} catch (IllegalAccessException e) {} finally { if (ent == null) { InputAgent.logError("Could not create new Entity: %s", key); return null; } } ent.setName(key); return ent; } /** * Assigns a new name to the given entity. * @param ent - entity to be renamed * @param newName - new name for the entity */ public static void renameEntity(Entity ent, String newName) { // If the name has not changed, do nothing if (ent.getName().equals(newName)) return; // Check that the entity was defined AFTER the RecordEdits command if (!ent.testFlag(Entity.FLAG_ADDED)) throw new ErrorException("Cannot rename an entity that was defined before the RecordEdits command."); // Check that the new name is valid if (newName.contains(" ") || newName.contains("\t") || newName.contains("{") || newName.contains("}")) throw new ErrorException("Entity names cannot contain spaces, tabs, or braces ({})."); // Check that the name has not been used already Entity existingEnt = Input.tryParseEntity(newName, Entity.class); if (existingEnt != null && existingEnt != ent) throw new ErrorException("Entity name: %s is already in use.", newName); // Rename the entity ent.setName(newName); } public static void processKeywordRecord(ArrayList<String> record, ParseContext context) { Entity ent = Input.tryParseEntity(record.get(0), Entity.class); if (ent == null) { InputAgent.logError("Could not find Entity: %s", record.get(0)); return; } // Validate the tokens have the Entity Keyword { Args... } Keyword { Args... } ArrayList<KeywordIndex> words = InputAgent.getKeywords(record, context); for (KeywordIndex keyword : words) { try { InputAgent.processKeyword(ent, keyword); } catch (Throwable e) { InputAgent.logInpError("Entity: %s, Keyword: %s - %s", ent.getName(), keyword.keyword, e.getMessage()); if (e.getMessage() == null) { for (StackTraceElement each : e.getStackTrace()) InputAgent.logMessage(each.toString()); } } } } private static ArrayList<KeywordIndex> getKeywords(ArrayList<String> input, ParseContext context) { ArrayList<KeywordIndex> ret = new ArrayList<>(); int braceDepth = 0; int keyWordIdx = 1; for (int i = 1; i < input.size(); i++) { String tok = input.get(i); if ("{".equals(tok)) { braceDepth++; continue; } if ("}".equals(tok)) { braceDepth--; if (braceDepth == 0) { // validate keyword form String keyword = input.get(keyWordIdx); if (keyword.equals("{") || keyword.equals("}") || !input.get(keyWordIdx + 1).equals("{")) throw new InputErrorException("The input for a keyword must be enclosed by braces. Should be <keyword> { <args> }"); ret.add(new KeywordIndex(keyword, input, keyWordIdx + 2, i, context)); keyWordIdx = i + 1; continue; } } } if (keyWordIdx != input.size()) throw new InputErrorException("The input for a keyword must be enclosed by braces. Should be <keyword> { <args> }"); return ret; } // Load the run file public static void loadConfigurationFile( File file) throws URISyntaxException { String inputTraceFileName = InputAgent.getRunName() + ".log"; // Initializing the tracing for the model URI logURI = null; try { LogBox.logLine( "Creating trace file" ); URI confURI = file.toURI(); logURI = confURI.resolve(new URI(null, inputTraceFileName, null)); // The new URI here effectively escapes the file name // Set and open the input trace file name logFile = new FileEntity( logURI.getPath()); } catch( Exception e ) { InputAgent.logWarning("Could not create trace file"); } URI dirURI = file.getParentFile().toURI(); InputAgent.readStream("", dirURI, file.getName()); // The session is not considered to be edited after loading a configuration file sessionEdited = false; // Save and close the input trace file if (logFile != null) { if (InputAgent.numWarnings == 0 && InputAgent.numErrors == 0) { logFile.close(); logFile.delete(); if (logURI != null) logFile = new FileEntity( logURI.getPath() ); } } // Check for found errors if( InputAgent.numErrors > 0 ) throw new InputErrorException("%d input errors and %d warnings found", InputAgent.numErrors, InputAgent.numWarnings); if (Simulation.getPrintInputReport()) InputAgent.printInputFileKeywords(); } /** * Prepares the keyword and input value for processing. * * @param ent - the entity whose keyword and value has been entered. * @param keyword - the keyword. * @param value - the input value String for the keyword. */ public static void applyArgs(Entity ent, String keyword, String... args){ // Keyword ArrayList<String> tokens = new ArrayList<>(args.length); for (String each : args) tokens.add(each); // Parse the keyword inputs KeywordIndex kw = new KeywordIndex(keyword, tokens, null); InputAgent.apply(ent, kw); } public static final void apply(Entity ent, KeywordIndex kw) { Input<?> in = ent.getInput(kw.keyword); if (in == null) { InputAgent.logError("Keyword %s could not be found for Entity %s.", kw.keyword, ent.getName()); return; } InputAgent.apply(ent, in, kw); GUIFrame.updateUI(); } public static final void apply(Entity ent, Input<?> in, KeywordIndex kw) { // If the input value is blank, restore the default if (kw.numArgs() == 0) { in.reset(); } else { in.parse(kw); in.setTokens(kw); } // Only mark the keyword edited if we have finished initial configuration if (InputAgent.recordEdits()) { in.setEdited(true); ent.setFlag(Entity.FLAG_EDITED); if (!ent.testFlag(Entity.FLAG_GENERATED) && in.isPromptReqd()) sessionEdited = true; } ent.updateForInput(in); } public static void processKeyword(Entity entity, KeywordIndex key) { if (entity.testFlag(Entity.FLAG_LOCKED)) throw new InputErrorException("Entity: %s is locked and cannot be modified", entity.getName()); Input<?> input = entity.getInput( key.keyword ); if (input != null) { InputAgent.apply(entity, input, key); GUIFrame.updateUI(); return; } if (!(entity instanceof Group)) throw new InputErrorException("Not a valid keyword"); Group grp = (Group)entity; grp.saveGroupKeyword(key); // Store the keyword data for use in the edit table for( int i = 0; i < grp.getList().size(); i++ ) { Entity ent = grp.getList().get( i ); InputAgent.apply(ent, key); } } /* * write input file keywords and values * * input file format: * Define Group { <Group names> } * Define <Object> { <Object names> } * * <Object name> <Keyword> { < values > } * */ public static void printInputFileKeywords() { // Create report file for the inputs String inputReportFileName = InputAgent.getReportFileName(InputAgent.getRunName() + ".inp"); FileEntity inputReportFile = new FileEntity( inputReportFileName); inputReportFile.flush(); ArrayList<ObjectType> objectTypes = new ArrayList<>(); for (ObjectType type : ObjectType.getAll()) objectTypes.add( type ); // Sort ObjectTypes by Units, Simulation, and then alphabetically by palette name Collections.sort(objectTypes, new Comparator<ObjectType>() { @Override public int compare(ObjectType a, ObjectType b) { // Put Unit classes first if (Unit.class.isAssignableFrom(a.getJavaClass())) { if (Unit.class.isAssignableFrom(b.getJavaClass())) return 0; else return -1; } if (Unit.class.isAssignableFrom(b.getJavaClass())) { return 1; } // Put Simulation classes second if (Simulation.class.isAssignableFrom(a.getJavaClass())) { if (Simulation.class.isAssignableFrom(b.getJavaClass())) return 0; else return -1; } if (Simulation.class.isAssignableFrom(b.getJavaClass())) { return 1; } // Sort the rest alphabetically by palette name return a.getPaletteName().compareTo(b.getPaletteName()); } }); // Loop through the entity classes printing Define statements for (ObjectType type : objectTypes) { Class<? extends Entity> each = type.getJavaClass(); // Loop through the instances for this entity class int count = 0; for (Entity ent : Entity.getInstanceIterator(each)) { if (ent.getEntityNumber() <= preDefinedEntityCount) continue; count++; String entityName = ent.getName(); if ((count - 1) % 5 == 0) { inputReportFile.write("Define"); inputReportFile.write("\t"); inputReportFile.write(type.getName()); inputReportFile.write("\t"); inputReportFile.write("{ " + entityName); inputReportFile.write("\t"); } else if ((count - 1) % 5 == 4) { inputReportFile.write(entityName + " }"); inputReportFile.newLine(); } else { inputReportFile.write(entityName); inputReportFile.write("\t"); } } if (count % 5 != 0) { inputReportFile.write(" }"); inputReportFile.newLine(); } if (count > 0) inputReportFile.newLine(); } for (ObjectType type : objectTypes) { Class<? extends Entity> each = type.getJavaClass(); // Get the list of instances for this entity class // sort the list alphabetically ArrayList<Entity> cloneList = new ArrayList<>(); for (Entity ent : Entity.getInstanceIterator(each)) { if (ent.getEntityNumber() <= preDefinedEntityCount) { if (! (ent instanceof Simulation) ) { continue; } } cloneList.add(ent); } // Print the entity class name to the report (in the form of a comment) if (cloneList.size() > 0) { inputReportFile.write("\" " + each.getSimpleName() + " \""); inputReportFile.newLine(); inputReportFile.newLine(); // blank line below the class name heading } Collections.sort(cloneList, new Comparator<Entity>() { @Override public int compare(Entity a, Entity b) { return a.getName().compareTo(b.getName()); } }); // Loop through the instances for this entity class for (int j = 0; j < cloneList.size(); j++) { // Make sure the clone is an instance of the class (and not an instance of a subclass) if (cloneList.get(j).getClass() != each) continue; Entity ent = cloneList.get(j); String entityName = ent.getName(); boolean hasinput = false; // Loop through the editable Key Inputs for this instance for (Input<?> in : ent.getEditableInputs()) { if (in.isSynonym()) continue; // If the keyword has been used, then add a record to the report String valueString = in.getValueString(); if (valueString.length() == 0) continue; if (! in.getCategory().contains("Key Inputs")) continue; hasinput = true; inputReportFile.write("\t"); inputReportFile.write(entityName); inputReportFile.write("\t"); inputReportFile.write(in.getKeyword()); inputReportFile.write("\t"); if (valueString.lastIndexOf('{') > 10) { String[] item1Array; item1Array = valueString.trim().split(" }"); inputReportFile.write("{ " + item1Array[0] + " }"); for (int l = 1; l < (item1Array.length); l++) { inputReportFile.newLine(); inputReportFile.write("\t\t\t\t\t"); inputReportFile.write(item1Array[l] + " } "); } inputReportFile.write(" }"); } else { inputReportFile.write("{ " + valueString + " }"); } inputReportFile.newLine(); } // Loop through the editable keywords // (except for Key Inputs) for this instance for (Input<?> in : ent.getEditableInputs()) { if (in.isSynonym()) continue; // If the keyword has been used, then add a record to the report String valueString = in.getValueString(); if (valueString.length() == 0) continue; if (in.getCategory().contains("Key Inputs")) continue; hasinput = true; inputReportFile.write("\t"); inputReportFile.write(entityName); inputReportFile.write("\t"); inputReportFile.write(in.getKeyword()); inputReportFile.write("\t"); if (valueString.lastIndexOf('{') > 10) { String[] item1Array; item1Array = valueString.trim().split(" }"); inputReportFile.write("{ " + item1Array[0] + " }"); for (int l = 1; l < (item1Array.length); l++) { inputReportFile.newLine(); inputReportFile.write("\t\t\t\t\t"); inputReportFile.write(item1Array[l] + " } "); } inputReportFile.write(" }"); } else { inputReportFile.write("{ " + valueString + " }"); } inputReportFile.newLine(); } // Put a blank line after each instance if (hasinput) { inputReportFile.newLine(); } } } // Close out the report inputReportFile.flush(); inputReportFile.close(); } public static void closeLogFile() { if (logFile == null) return; logFile.flush(); logFile.close(); if (numErrors ==0 && numWarnings == 0) { logFile.delete(); } logFile = null; } private static final String errPrefix = "*** ERROR *** %s%n"; private static final String inpErrPrefix = "*** INPUT ERROR *** %s%n"; private static final String wrnPrefix = "***WARNING*** %s%n"; public static int numErrors() { return numErrors; } public static int numWarnings() { return numWarnings; } private static void echoInputRecord(ArrayList<String> tokens) { if (logFile == null) return; boolean beginLine = true; for (int i = 0; i < tokens.size(); i++) { if (!beginLine) logFile.write(Input.SEPARATOR); String tok = tokens.get(i); logFile.write(tok); beginLine = false; if (tok.startsWith("\"")) { logFile.newLine(); beginLine = true; } } // If there were any leftover string written out, make sure the line gets terminated if (!beginLine) logFile.newLine(); logFile.flush(); } private static void logBadInput(ArrayList<String> tokens, String msg) { InputAgent.echoInputRecord(tokens); InputAgent.logError("%s", msg); } /** * Writes an error or warning message to standard error, the Log Viewer, and the Log File. * @param fmt - format for the message * @param args - objects to be printed in the message */ public static void logMessage(String fmt, Object... args) { String msg = String.format(fmt, args); LogBox.logLine(msg); System.err.println(msg); if (logFile == null) return; logFile.write(msg); logFile.newLine(); logFile.flush(); } /** * Writes a stack trace to standard error, the Log Viewer, and the Log File. * @param e - exception to be traced */ public static void logStackTrace(Throwable t) { for (StackTraceElement each : t.getStackTrace()) { InputAgent.logMessage(each.toString()); } } public static final void trace(int indent, Entity ent, String fmt, Object... args) { // Print a TIME header every time time has advanced long traceTick = EventManager.simTicks(); if (lastTickForTrace != traceTick) { System.out.format(" \nTIME = %.6f\n", EventManager.current().ticksToSeconds(traceTick)); lastTickForTrace = traceTick; } // Create an indent string to space the lines StringBuilder str = new StringBuilder(""); for (int i = 0; i < indent; i++) str.append(" "); // Append the Entity name if provided if (ent != null) str.append(ent.toString()).append("."); str.append(String.format(fmt, args)); System.out.println(str.toString()); System.out.flush(); } /** * Writes a warning message to standard error, the Log Viewer, and the Log File. * @param fmt - format string for the warning message * @param args - objects used by the format string */ public static void logWarning(String fmt, Object... args) { numWarnings++; String msg = String.format(fmt, args); InputAgent.logMessage(wrnPrefix, msg); } /** * Writes an error message to standard error, the Log Viewer, and the Log File. * @param fmt - format string for the error message * @param args - objects used by the format string */ public static void logError(String fmt, Object... args) { numErrors++; String msg = String.format(fmt, args); InputAgent.logMessage(errPrefix, msg); } /** * Writes a input error message to standard error, the Log Viewer, and the Log File. * @param fmt - format string for the error message * @param args - objects used by the format string */ public static void logInpError(String fmt, Object... args) { numErrors++; String msg = String.format(fmt, args); InputAgent.logMessage(inpErrPrefix, msg); } /** * Prints the present state of the model to a new configuration file. * * @param fileName - the full path and file name for the new configuration file. */ public static void printNewConfigurationFileWithName( String fileName ) { // 1) WRITE LINES FROM THE ORIGINAL CONFIGURATION FILE // Copy the original configuration file up to the "RecordEdits" marker (if present) // Temporary storage for the copied lines is needed in case the original file is to be overwritten ArrayList<String> preAddedRecordLines = new ArrayList<>(); if( InputAgent.getConfigFile() != null ) { try { BufferedReader in = new BufferedReader( new FileReader(InputAgent.getConfigFile()) ); String line; while ( ( line = in.readLine() ) != null ) { preAddedRecordLines.add( line ); if ( line.startsWith( recordEditsMarker ) ) { break; } } in.close(); } catch ( Exception e ) { throw new ErrorException( e ); } } // Create the new configuration file and copy the saved lines FileEntity file = new FileEntity( fileName); for( int i=0; i < preAddedRecordLines.size(); i++ ) { file.format("%s%n", preAddedRecordLines.get( i )); } // If not already present, insert the "RecordEdits" marker at the end of the original configuration file if( ! InputAgent.getRecordEditsFound() ) { file.format("%n%s%n", recordEditsMarker); InputAgent.setRecordEditsFound(true); } // 2) WRITE THE DEFINITION STATEMENTS FOR NEW OBJECTS // Prepare a sorted list of all the entities that were added to the model ArrayList<Entity> newEntities = new ArrayList<>(); for (Entity ent : Entity.getClonesOfIterator(Entity.class)) { if (!ent.testFlag(Entity.FLAG_ADDED) || ent.testFlag(Entity.FLAG_GENERATED)) continue; newEntities.add(ent); } Collections.sort(newEntities, Input.uiSortOrder); // Prepare a sorted list of all the new classes that were created ArrayList<Class<? extends Entity>> newClasses = new ArrayList<>(); for (Entity ent : newEntities) { if (!newClasses.contains(ent.getClass())) newClasses.add(ent.getClass()); } Collections.sort(newClasses, uiClassSortOrder); // Add a blank line before the first object definition if( !newClasses.isEmpty() ) file.format("%n"); // Print the first part of the "Define" statement for this object type for( Class<? extends Entity> newClass : newClasses ) { ObjectType o = ObjectType.getObjectTypeForClass(newClass); if (o == null) throw new ErrorException("Cannot find object type for class: " + newClass.getName()); file.format("Define %s {", o.getName()); // Print the new instances that were defined for (Entity ent : newEntities) { if (ent.getClass() == newClass) file.format(" %s ", ent.getName()); } // Close the define statement file.format("}%n"); } // 3) WRITE THE INPUTS FOR SPECIAL KEYWORDS THAT MUST COME BEFORE THE OTHERS // Prepare a sorted list of all the entities that were edited ArrayList<Entity> entityList = new ArrayList<>(); for (Entity ent : Entity.getClonesOfIterator(Entity.class)) { if (!ent.testFlag(Entity.FLAG_EDITED) || ent.testFlag(Entity.FLAG_GENERATED)) continue; entityList.add(ent); } Collections.sort(entityList, uiEntitySortOrder); // Loop through the early keywords for (int i = 0; i < EARLY_KEYWORDS.length; i++) { // Loop through the entities boolean blankLinePrinted = false; for (Entity ent : entityList) { // Print an entry for each entity that used this keyword final Input<?> in = ent.getInput(EARLY_KEYWORDS[i]); if (in != null && in.isEdited()) { if (!blankLinePrinted) { file.format("%n"); blankLinePrinted = true; } writeInputOnFile_ForEntity(file, ent, in); } } } // 4) WRITE THE INPUTS FOR THE REMAINING KEYWORDS // Identify the entities whose inputs were edited for (Entity ent : entityList) { file.format("%n"); ArrayList<Input<?>> deferredInputs = new ArrayList<>(); // Print the key inputs first for (Input<?> in : ent.getEditableInputs()) { if (in.isSynonym()) continue; if (!in.isEdited() || matchesKey(in.getKeyword(), EARLY_KEYWORDS)) continue; // defer all inputs outside the Key Inputs category if (!"Key Inputs".equals(in.getCategory())) { deferredInputs.add(in); continue; } writeInputOnFile_ForEntity(file, ent, in); } for (Input<?> in : deferredInputs) { writeInputOnFile_ForEntity(file, ent, in); } } // Close the new configuration file file.flush(); file.close(); sessionEdited = false; } private static boolean matchesKey(String key, String[] keys) { for (int i=0; i<keys.length; i++) { if (keys[i].equals(key)) return true; } return false; } static void writeInputOnFile_ForEntity(FileEntity file, Entity ent, Input<?> in) { file.format("%s %s { %s }%n", ent.getName(), in.getKeyword(), in.getValueString()); } /** * Prints selected outputs for the simulation run to stdout or a file. * @param simTime - simulation time at which the outputs are printed. */ public static void printRunOutputs(double simTime) { // Set up the custom outputs if (outStream == null) { // Select either standard out or a file for the outputs outStream = System.out; if (!InputAgent.isScriptMode()) { StringBuilder sb = new StringBuilder(); sb.append(InputAgent.getReportFileName(InputAgent.getRunName())); sb.append(".dat"); try { outStream = new PrintStream(sb.toString()); } catch (FileNotFoundException e) { throw new InputErrorException( "FileNotFoundException thrown trying to open PrintStream: " + e ); } catch (SecurityException e) { throw new InputErrorException( "SecurityException thrown trying to open PrintStream: " + e ); } } // Write the header line for the expressions StringBuilder sb = new StringBuilder(); ArrayList<String> toks = new ArrayList<>(); Simulation.getRunOutputList().getValueTokens(toks); boolean first = true; for (String str : toks) { if (str.equals("{") || str.equals("}")) continue; if (first) first = false; else sb.append("\t"); sb.append(str); } outStream.println(sb.toString()); // Write the header line for the units sb = new StringBuilder(); for (int i=0; i<Simulation.getRunOutputList().getListSize(); i++) { Class<? extends Unit> ut = Simulation.getRunOutputList().getUnitType(i); String unit = Unit.getDisplayedUnit(ut); if (i > 0) sb.append("\t"); sb.append(unit); } outStream.println(sb.toString()); } // Write the selected outputs StringBuilder sb = new StringBuilder(); for (int i=0; i<Simulation.getRunOutputList().getListSize(); i++) { StringProvider samp = Simulation.getRunOutputList().getValue().get(i); Class<? extends Unit> ut = Simulation.getRunOutputList().getUnitType(i); double factor = Unit.getDisplayedUnitFactor(ut); String str; try { str = samp.getNextString(simTime, "%s", factor); } catch (Exception e) { str = e.getMessage(); } if (i > 0) sb.append("\t"); sb.append(str); } outStream.println(sb.toString()); // Terminate the outputs if (Simulation.isLastRun()) { outStream.close(); outStream = null; } } /** * Prints the output report for the simulation run. * @param simTime - simulation time at which the report is printed. */ public static void printReport(double simTime) { // Create the report file if (reportFile == null) { StringBuilder tmp = new StringBuilder(""); tmp.append(InputAgent.getReportFileName(InputAgent.getRunName())); tmp.append(".rep"); reportFile = new FileEntity(tmp.toString()); } // Print run number header when multiple runs are to be performed if (Simulation.isMultipleRuns()) reportFile.format("%s%n%n", Simulation.getRunHeader()); // Identify the classes that were used in the model ArrayList<Class<? extends Entity>> newClasses = new ArrayList<>(); for (Entity ent : Entity.getClonesOfIterator(Entity.class)) { if (ent.testFlag(Entity.FLAG_GENERATED)) continue; if (!ent.isReportable()) continue; if (!newClasses.contains(ent.getClass())) newClasses.add(ent.getClass()); } // Sort the classes by the names of their object types, except for Simulation // which is first on the list Collections.sort(newClasses, uiClassSortOrder); // Loop through the classes and identify the instances for (Class<? extends Entity> newClass : newClasses) { ArrayList<Entity> entList = new ArrayList<>(); for (Entity ent : Entity.getClonesOfIterator(Entity.class)) { if (ent.testFlag(Entity.FLAG_GENERATED)) continue; if (ent.getClass() == newClass) entList.add(ent); } // Sort the entities alphabetically by their names Collections.sort(entList, Input.uiSortOrder); // Print a header for this class if (newClass != Simulation.class) reportFile.format("*** %s ***%n%n", ObjectType.getObjectTypeForClass(newClass)); // Print each entity to the output report for (Entity ent : entList) { ent.printReport(reportFile, simTime); reportFile.format("%n"); } } // Close the report file if (Simulation.isLastRun()) { reportFile.close(); reportFile = null; } } private static class ClassComparator implements Comparator<Class<? extends Entity>> { @Override public int compare(Class<? extends Entity> class0, Class<? extends Entity> class1) { // Place the Simulation class in the first position if (class0 == Simulation.class && class1 == Simulation.class) return 0; if (class0 == Simulation.class && class1 != Simulation.class) return -1; if (class0 != Simulation.class && class1 == Simulation.class) return 1; // Sort alphabetically by Object Type name ObjectType ot0 = ObjectType.getObjectTypeForClass(class0); ObjectType ot1 = ObjectType.getObjectTypeForClass(class1); return Input.uiSortOrder.compare(ot0, ot1); } } public static final Comparator<Class<? extends Entity>> uiClassSortOrder = new ClassComparator(); private static class EntityComparator implements Comparator<Entity> { @Override public int compare(Entity ent0, Entity ent1) { // Place the Simulation entity in the first position Class<? extends Entity> class0 = ent0.getClass(); Class<? extends Entity> class1 = ent1.getClass(); if (class0 == Simulation.class && class1 == Simulation.class) return 0; if (class0 == Simulation.class && class1 != Simulation.class) return -1; if (class0 != Simulation.class && class1 == Simulation.class) return 1; // Otherwise, sort in natural order return Input.uiSortOrder.compare(ent0, ent1); } } public static final Comparator<Entity> uiEntitySortOrder = new EntityComparator(); /** * Returns a formated string for the specified output. * @param out - output * @param simTime - present simulation time * @param floatFmt - format string for numerical values * @param factor - divisor to be applied to numerical values * @return formated string for the output */ public static String getValueAsString(OutputHandle out, double simTime, String floatFmt, double factor) { StringBuilder sb = new StringBuilder(); String str; String COMMA_SEPARATOR = ", "; Class<?> retType = out.getReturnType(); // Numeric outputs if (out.isNumericValue()) { double val = out.getValueAsDouble(simTime, Double.NaN); return String.format(floatFmt, val/factor); } // Vec3d outputs if (retType == Vec3d.class) { Vec3d vec = out.getValue(simTime, Vec3d.class); sb.append(vec.x/factor); sb.append(Input.SEPARATOR).append(vec.y/factor); sb.append(Input.SEPARATOR).append(vec.z/factor); return sb.toString(); } // DoubleVector output if (retType == DoubleVector.class) { sb.append("{"); DoubleVector vec = out.getValue(simTime, DoubleVector.class); for (int i=0; i<vec.size(); i++) { str = String.format(floatFmt, vec.get(i)/factor); sb.append(str); if (i < vec.size()-1) { sb.append(COMMA_SEPARATOR); } } sb.append("}"); return sb.toString(); } // ArrayList output if (retType == ArrayList.class) { sb.append("{"); ArrayList<?> array = out.getValue(simTime, ArrayList.class); for (int i=0; i<array.size(); i++) { Object obj = array.get(i); if (obj instanceof Double) { double val = (Double)obj; str = String.format(floatFmt, val/factor); } else { str = String.format("%s", obj); } sb.append(str); if (i < array.size()-1) { sb.append(COMMA_SEPARATOR); } } sb.append("}"); return sb.toString(); } // Keyed outputs if (retType == LinkedHashMap.class) { sb.append("{"); LinkedHashMap<?, ?> map = out.getValue(simTime, LinkedHashMap.class); for (Entry<?, ?> mapEntry : map.entrySet()) { sb.append(String.format("%s=", mapEntry.getKey())); Object obj = mapEntry.getValue(); if (obj instanceof Double) { double val = (Double)obj; str = String.format(floatFmt, val/factor); } else { str = String.format("%s", obj); } sb.append(str).append(COMMA_SEPARATOR); } if (sb.length() > 1) sb.replace(sb.length()-2, sb.length()-1, "}"); else sb.append("}"); return sb.toString(); } if (out.getReturnType() == ExpResult.class) { ExpResult result = out.getValue(simTime, ExpResult.class); switch (result.type) { case STRING: sb.append(result.stringVal); break; case ENTITY: if (result.entVal == null) sb.append("null"); else sb.append("[").append(result.entVal.getName()).append("]"); break; case NUMBER: sb.append(String.format(floatFmt, result.value/factor)); break; case COLLECTION: sb.append(result.colVal.getOutputString()); break; default: assert(false); sb.append("???"); break; } return sb.toString(); } // All other outputs return out.getValue(simTime, retType).toString(); } /** * Returns the relative file path for the specified URI. * <p> * The path can start from either the folder containing the present * configuration file or from the resources folder. * <p> * @param uri - the URI to be relativized. * @return the relative file path. */ static public String getRelativeFilePath(URI uri) { // Relativize the file path against the resources folder String resString = resRoot.toString(); String inputString = uri.toString(); if (inputString.startsWith(resString)) { return String.format("<res>/%s", inputString.substring(resString.length())); } // Relativize the file path against the configuration file try { URI configDirURI = InputAgent.getConfigFile().getParentFile().toURI(); return String.format("%s", configDirURI.relativize(uri).getPath()); } catch (Exception ex) { return String.format("%s", uri.getPath()); } } /** * Loads the default configuration file. */ public static void loadDefault() { // Read the default configuration file InputAgent.readResource("<res>/inputs/default.cfg"); // A RecordEdits marker in the default configuration must be ignored InputAgent.setRecordEditsFound(false); // Set the model state to unedited sessionEdited = false; } public static KeywordIndex formatPointsInputs(String keyword, ArrayList<Vec3d> points, Vec3d offset) { ArrayList<String> tokens = new ArrayList<>(points.size() * 6); for (Vec3d v : points) { tokens.add("{"); tokens.add(String.format((Locale)null, "%.3f", v.x + offset.x)); tokens.add(String.format((Locale)null, "%.3f", v.y + offset.y)); tokens.add(String.format((Locale)null, "%.3f", v.z + offset.z)); tokens.add("m"); tokens.add("}"); } // Parse the keyword inputs return new KeywordIndex(keyword, tokens, null); } public static KeywordIndex formatPointInputs(String keyword, Vec3d point, String unit) { ArrayList<String> tokens = new ArrayList<>(4); tokens.add(String.format((Locale)null, "%.6f", point.x)); tokens.add(String.format((Locale)null, "%.6f", point.y)); tokens.add(String.format((Locale)null, "%.6f", point.z)); if (unit != null) tokens.add(unit); // Parse the keyword inputs return new KeywordIndex(keyword, tokens, null); } /** * Split an input (list of strings) down to a single level of nested braces, this may then be called again for * further nesting. * @param input * @return */ public static ArrayList<ArrayList<String>> splitForNestedBraces(List<String> input) { ArrayList<ArrayList<String>> inputs = new ArrayList<>(); int braceDepth = 0; ArrayList<String> currentLine = null; for (int i = 0; i < input.size(); i++) { if (currentLine == null) currentLine = new ArrayList<>(); currentLine.add(input.get(i)); if (input.get(i).equals("{")) { braceDepth++; continue; } if (input.get(i).equals("}")) { braceDepth--; if (braceDepth == 0) { inputs.add(currentLine); currentLine = null; continue; } } } return inputs; } /** * Converts a file path String to a URI. * <p> * The specified file path can be either relative or absolute. In the case * of a relative file path, a 'context' folder must be specified. A context * of null indicates an absolute file path. * <p> * To avoid bad input accessing an inappropriate file, a 'jail' folder can * be specified. The URI to be returned must include the jail folder for it * to be valid. * <p> * @param context - full file path for the folder that is the reference for relative file paths. * @param filePath - string to be resolved to a URI. * @param jailPrefix - file path to a base folder from which a relative cannot escape. * @return the URI corresponding to the context and filePath. */ public static URI getFileURI(URI context, String filePath, String jailPrefix) throws URISyntaxException { // Replace all backslashes with slashes String path = filePath.replaceAll("\\\\", "/"); int colon = path.indexOf(':'); int openBrace = path.indexOf('<'); int closeBrace = path.indexOf('>'); int firstSlash = path.indexOf('/'); // Add a leading slash if needed to convert from Windows format (e.g. from "C:" to "/C:") if (colon == 1) path = String.format("/%s", path); // 1) File path starts with a tagged folder, using the syntax "<tagName>/" URI ret = null; if (openBrace == 0 && closeBrace != -1 && firstSlash == closeBrace + 1) { String specPath = path.substring(openBrace + 1, closeBrace); // Resources folder in the Jar file if (specPath.equals("res")) { ret = new URI(resRoot.getScheme(), resRoot.getSchemeSpecificPart() + path.substring(closeBrace+2), null).normalize(); } } // 2) Normal file path else { URI pathURI = new URI(null, path, null).normalize(); if (context != null) { if (context.isOpaque()) { // Things are going to get messy in here URI schemeless = new URI(null, context.getSchemeSpecificPart(), null); URI resolved = schemeless.resolve(pathURI).normalize(); // Note: we are using the one argument constructor here because the 'resolved' URI is already encoded // and we do not want to double-encode (and schemes should never need encoding, I hope) ret = new URI(context.getScheme() + ":" + resolved.toString()); } else { ret = context.resolve(pathURI).normalize(); } } else { // We have no context, so append a 'file' scheme if necessary if (pathURI.getScheme() == null) { ret = new URI("file", pathURI.getPath(), null); } else { ret = pathURI; } } } // Check that the file path includes the jail folder if (jailPrefix != null && ret.toString().indexOf(jailPrefix) != 0) { InputAgent.logMessage("Failed jail test: %s\n" + "jail: %s\n" + "context: %s\n", ret.toString(), jailPrefix, context.toString()); return null; // This resolved URI is not in our jail } return ret; } /** * Determines whether or not a file exists. * <p> * @param filePath - URI for the file to be tested. * @return true if the file exists, false if it does not. */ public static boolean fileExists(URI filePath) { try { InputStream in = filePath.toURL().openStream(); in.close(); return true; } catch (MalformedURLException ex) { return false; } catch (IOException ex) { return false; } } }