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("");
buffer.append(oldLocation[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("");
buffer.append(oldLocation[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:
*