package edu.princeton.toy; import java.util.HashMap; import java.util.Map; import java.io.*; import javax.xml.parsers.*; import org.xml.sax.helpers.*; import org.xml.sax.*; /** * TConfigurationManager is a class that reads and stores the bindings passed to Visual X-TOY * through $USER_HOME/toy.conf. It will also write a HashMap into toy.conf file. * * @author btsang * @version 7.1 */ public class TConfigurationManager extends DefaultHandler implements Runnable { /** * The filename of the configuration file (to be found in the user's home directory). */ public static final String CONFIGURATION_FILE = "toy.conf"; private static final SAXParserFactory parserFactory = SAXParserFactory.newInstance(); private static final TConfigurationManager INSTANCE = new TConfigurationManager(); private Locator locator; private StringBuffer tags; private Thread runner; private HashMap bindings; private File configurationFile; /** * Constructs a TConfigurationManager which will attempt to load the * bindings from the specified applet. */ private TConfigurationManager() { bindings = new HashMap(50); String homePath = System.getProperty("user.home"); if (homePath == null) { System.err.println( "Configuration file not loaded: property user.home not found." ); } else { configurationFile = new File(homePath, CONFIGURATION_FILE); runner = new Thread(this); runner.start(); } } /** * Exports a set of properties to the toy.conf file. Note that the keys are expected to be * non-null strings. * * @param bindings A map of keys (of the form "tag1.tag2.tag3...tagN#param") to * values. */ public static boolean exportProperties(Map bindings) { if (bindings == null) throw new NullPointerException(); File configurationFile = INSTANCE.configurationFile; if (configurationFile == null) { System.err.println( "Configuration file not updated: property user.home not found." ); return false; } if (configurationFile.exists()) { if (!configurationFile.isFile()) { System.err.println( "Configuration file not updated: file \"" + configurationFile + "\" is not a file." ); return false; } if (!configurationFile.canWrite()) { System.err.println( "Configuration file not updated: file \"" + configurationFile + "\" could not be written to." ); return false; } } try { PrintStream printStream = new PrintStream(new FileOutputStream(configurationFile)); printStream.print( "\n" + "\n" ); // Get the keys Object keyObjects[] = bindings.keySet().toArray(); int keyCount = keyObjects.length; String keys[] = new String[keyCount]; int maxStringLength = 0; for (int ctr = 0; ctr < keyCount; ctr++) { keys[ctr] = (String)keyObjects[ctr]; int length = keys[ctr].length(); if (maxStringLength < length) maxStringLength = length; } // Sort the keys (since we don't expect humongous data sets, we'll do a selection sort) // Note how '.' and '#' were good choices of delimiters in that '#' < '.' for (int ctr = keyCount; ctr > 1; ctr--) { String largestString = keys[0]; int largestIndex = 0; // Locate the largest string for (int ctr2 = 1; ctr2 < ctr; ctr2++) { if (keys[ctr2].compareTo(largestString) > 0) { largestString = keys[ctr2]; largestIndex = ctr2; } } // Swap the largest string with the last string keys[largestIndex] = keys[ctr - 1]; keys[ctr - 1] = largestString; } // Write the XML file String oldLocation[] = new String[maxStringLength + 1]; int oldDepth = 0; String newLocation[] = new String[maxStringLength + 1]; int newDepth = 0; StringBuffer buffer = new StringBuffer(); int shareCount = 0; for (int ctr = 0; ctr < keyCount; ctr++) { // Parse the key String parameter = null; newDepth = 0; { char chars[] = keys[ctr].toCharArray(); int charCount = chars.length; int startCtr = 0, charCtr = 0; boolean encounteredPound = false; while (charCtr < charCount) { if (chars[charCtr] == '.') { if (encounteredPound || charCtr == startCtr) { throw new IllegalArgumentException( "Invalid key: \"" + keys[ctr] + "\"." ); } newLocation[newDepth] = new String(chars, startCtr, charCtr - startCtr); newDepth++; charCtr++; startCtr = charCtr; } else if (chars[charCtr] == '#') { if (encounteredPound || charCtr == startCtr) { throw new IllegalArgumentException( "Invalid key: \"" + keys[ctr] + "\"." ); } newLocation[newDepth] = new String(chars, startCtr, charCtr - startCtr); newDepth++; charCtr++; startCtr = charCtr; encounteredPound = true; } else { charCtr++; } } if (!encounteredPound || charCtr == startCtr) { throw new IllegalArgumentException("Invalid key: \"" + keys[ctr] + "\"."); } parameter = new String(chars, startCtr, charCtr - startCtr); } // Locate the common depth int commonDepth = 0; while (commonDepth < oldDepth && commonDepth < newDepth && oldLocation[commonDepth].equals(newLocation[commonDepth])) { commonDepth++; } // Break into cases if (commonDepth == newDepth) { if (commonDepth == oldDepth) { // Out parameter is in the same container as the previous parameter if (shareCount == 0) printStream.print('\n'); buffer.delete(0, buffer.length()); fillSpaces( buffer, 4 * (newDepth - 1) + 2 + newLocation[newDepth - 1].length() ); buffer.append(parameter); buffer.append(" = \""); buffer.append(bindings.get(keys[ctr])); buffer.append("\"\n"); printStream.print(buffer); shareCount++; } else { // Our parameter is in an ancestor of the previous parameter's container, // this is impossible due to the sorting! throw new RuntimeException("Unknown error"); } } else { // Our parameter is in a decendant of an ancestor of the previous parameter's // container if (commonDepth < oldDepth) { // Go up to the common ancestor buffer.delete(0, buffer.length()); if (shareCount > 0) { fillSpaces( buffer, 4 * (oldDepth - 1) + 2 + oldLocation[oldDepth - 1].length() ); } buffer.append("/>\n"); printStream.print(buffer); oldDepth--; while (commonDepth < oldDepth) { buffer.delete(0, buffer.length()); fillSpaces(buffer, 4 * (oldDepth - 1)); buffer.append("\n"); printStream.print(buffer); oldDepth--; } } else { if (oldDepth > 0) { // Close off this tag in preparation to move up buffer.delete(0, buffer.length()); if (shareCount > 0) { fillSpaces( buffer, 4 * (oldDepth - 1) + 2 + oldLocation[oldDepth - 1].length() ); } buffer.append(">\n"); printStream.print(buffer); } } // Go down to the container's parent while (commonDepth < newDepth - 1) { buffer.delete(0, buffer.length()); fillSpaces(buffer, 4 * commonDepth); buffer.append('<'); buffer.append(newLocation[commonDepth]); buffer.append(">\n"); printStream.print(buffer); commonDepth++; } buffer.delete(0, buffer.length()); fillSpaces(buffer, 4 * commonDepth); buffer.append('<'); buffer.append(newLocation[commonDepth]); buffer.append(' '); buffer.append(parameter); buffer.append(" = \""); buffer.append(bindings.get(keys[ctr])); buffer.append("\""); printStream.print(buffer); shareCount = 0; } // Swap the locations { oldDepth = newDepth; String location[] = newLocation; newLocation = oldLocation; oldLocation = location; } } // Go up to the root if (oldDepth > 0) { buffer.delete(0, buffer.length()); if (shareCount > 0) { fillSpaces( buffer, 4 * (oldDepth - 1) + 2 + oldLocation[oldDepth - 1].length() ); } buffer.append("/>\n"); printStream.print(buffer); oldDepth--; while (oldDepth > 0) { buffer.delete(0, buffer.length()); fillSpaces(buffer, 4 * (oldDepth - 1)); buffer.append("\n"); printStream.print(buffer); oldDepth--; } } printStream.close(); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * Fills a StringBuffer with n spaces. * * @param buffer The StringBuffer to fill. A NullPointerException will be thrown if the buffer * is null (unless n <= 0 as well). * @param n The number of spaces to fill the buffer with. If n <= 0, nothing will happen. */ private static void fillSpaces(StringBuffer buffer, int n) { for (int ctr = 0; ctr < n; ctr++) buffer.append(' '); } /** * Indicates wheter or not each example in the manifest either does not exist or has been loaded * and processed. * * @return true iff all the example programs have either been accounted for. */ public static boolean isReady() { return INSTANCE.runner == null; } /** * Returns the value of the requested parameter. Note on parameter naming: nested tags are * separated by '.'s and tags and their bindings are separated by '#'s. For example, suppose * we have the following (rather convoluted) toy.conf file:

* <toy>ignoredText
* <!-- ignored comment -->
* <tag1 param1="value1" param2="value2">ignoredText<tag2 param3="value3"/>
* ignoredText</tag1>
* ignoredText</toy>

* The following statements would be true: *
  1. TConfigurationManager.getParameter( "toy.tag1#param1").equals("value1"); *
  2. TConfigurationManager.getParameter( "toy.tag1#param2").equals("value2"); *
  3. TConfigurationManager.getParameter("toy.tag1.tag2#param3").equals("value3");
* * @param name The name of the requested parameter; this is case-sensitive. * @return The value of the requested parameter. Null is returned if the requested parameter * does not exist. */ public static String getProperty(String name) { return (String)INSTANCE.bindings.get(name); } /** * Implement the Runnable interface to parse the conf file in a separate thread. */ public void run() { try { if (!configurationFile.exists()) { System.err.println( "Configuration file not loaded: file \"" + configurationFile + "\" does not exist." ); return; } if (!configurationFile.isFile()) { System.err.println( "Configuration file not loaded: file \"" + configurationFile + "\" is not a file." ); return; } if (!configurationFile.canRead()) { System.err.println( "Configuration file not loaded: file \"" + configurationFile + "\" can not be read from." ); return; } try { InputStream inputStream = new BufferedInputStream( new FileInputStream(configurationFile) ); SAXParser parser = parserFactory.newSAXParser(); parser.parse(inputStream, this); } catch (Exception e) { e.printStackTrace(); } } finally { runner = null; } } /** * Overriding DefaultHandler's methods. */ public InputSource resolveEntity(String publicId, String systemId) { (new UnsupportedOperationException()).printStackTrace(); return null; } /** * Overriding DefaultHandler's methods. */ public void notationDecl(String name, String publicId, String systemId) { (new UnsupportedOperationException()).printStackTrace(); } /** * Overriding DefaultHandler's methods. */ public void unparsedEntityDecl(String name, String publicId, String systemId, String notationName) { (new UnsupportedOperationException()).printStackTrace(); } /** * Overriding DefaultHandler's methods. */ public void setDocumentLocator(Locator locator) { this.locator = locator; } /** * Overriding DefaultHandler's methods. */ public void startDocument() { tags = new StringBuffer(); if (locator == null) throw new NullPointerException("Document started without locator"); } /** * Overriding DefaultHandler's methods. */ public void endDocument() { locator = null; tags = null; } /** * Overriding DefaultHandler's methods. */ public void startPrefixMapping(String prefix, String uri) { (new UnsupportedOperationException()).printStackTrace(); } /** * Overriding DefaultHandler's methods. */ public void endPrefixMapping(String prefix) { (new UnsupportedOperationException()).printStackTrace(); } /** * Overriding DefaultHandler's methods. */ public void startElement(String uri, String localName, String qName, Attributes attributes) { if (tags.length() > 0) tags.append('.'); tags.append(qName); int length = attributes.getLength(); for (int ctr = 0; ctr < length; ctr++) { bindings.put(tags + "#" + attributes.getQName(ctr), attributes.getValue(ctr)); } } /** * Overriding DefaultHandler's methods. */ public void endElement(String uri, String localName, String qName) { int tagsLength = tags.length(); int qNameLength = qName.length(); if (tagsLength > qNameLength) { tags.delete(tagsLength - qNameLength - 1, tagsLength); } else { tags.delete(0, tagsLength); } } /** * Overriding DefaultHandler's methods. */ public void characters(char[] ch, int start, int length) { // Ignore } /** * Overriding DefaultHandler's methods. */ public void ignorableWhitespace(char[] ch, int start, int length) { // Ignore } /** * Overriding DefaultHandler's methods. */ public void processingInstruction(String target, String data) { (new UnsupportedOperationException()).printStackTrace(); } /** * Overriding DefaultHandler's methods. */ public void skippedEntity(String name) { (new UnsupportedOperationException()).printStackTrace(); } /** * Overriding DefaultHandler's methods. */ public void warning(SAXParseException e) { System.err.println( "Warning at toy.conf(" + locator.getLineNumber() + ", " + locator.getColumnNumber() + "):\n" + e.getMessage() ); } /** * Overriding DefaultHandler's methods. */ public void error(SAXParseException e) { System.err.println( "Error at toy.conf(" + locator.getLineNumber() + ", " + locator.getColumnNumber() + "):\n" + e.getMessage() ); } /** * Overriding DefaultHandler's methods. */ public void fatalError(SAXParseException e) { System.err.println( "Fatal error at toy.conf(" + locator.getLineNumber() + ", " + locator.getColumnNumber() + "):\n" + e.getMessage() ); } }