import java.util.List;
import java.util.Map;
import java.util.Scanner;
/**
* The way to use this class with your code:
*
* - Your code will somewhere call "Entry.buildWorld" (see documentation);
* that method reads the wiki (or a data file), and returns a room, which you
* can use as you see fit (perhaps as the starting room?).
*
* - How does this class call the other code from your project? Well,
* + createRoom calls the constructor for Room;
* + createTreasure calls the constructor for Treasure;
* + createConnection calls connectTo and biconnectTo for appropriate rooms.
* If your constructors take different arguments, or you named your methods/Classes
* something different than I did, then you'll need to modify those three methods.
* No other tinkering should be needed.
*
* - If there are errors in the data file, hopefully they'll be caught
* by buildWorld, and the error message will point you in the right direction.
* But there might be errors in the data file which cause the program to quit.
* If so, mail the bad data file to me (ibarland@).
*
*
* ===========================================================================
* class Entry represents one object (a "paragraph") taken from a gRUe-database.
* See http://ru-itec120.pbwiki.com/grue-data-2006fall
* for a gRUe-database file (which include comments
* explaining its syntax).
*
* If you want to modify class Entry, here is some info on how it's organized:
*
* Vocabulary:
* Entry -- A "paragraph" of the database;
* it consists of a type (the first line),
* and a bunch of attributes (each remaining line).
* Attribute -- a single line of the database like "description = fa la la";
* the general form of the line is "[key] = [value]".
* Type of an Entry -- one of {@VALUE VALID_ENTRY_TYPES}.
*
* You can ask an Entry what its type is, and you can also give it a
* key (a String, like "description") and have it return
* the associated value (another string, like "fa la la").
* This lookup is implemented by java.util.Map (built just for such tasks).
*
*
* - The static method getEntrie returns a list of "Entry" objects.
* If you like, you can provide a filename to read, otherwise
* it reads from {@value DEFAULT_DATABASE_FILENAME}
* - For each Entry, you can use "getType()" to ask it
* what type of thing it represents:
* "room", "treasure", "connection", or "biconnection".
* - For each entry, you can repeatedly ask it to "get(.)"
* attributes. For instance:
*
* Map stuffFromFile = new java.util.HashMap();
* List allStuff = getEntries();
* Entry e3 = allStuff.get(3);
* if (e3.getType().equals("treasure")) {
* double wt = Double.valueOf( e3.get("weight") );
* Treasure t = new Treasure( e3.get("name"), e3.get("description"), wt );
* stuffFromFile.put( e3.get("id"), t ); // I may want to look up this Treasure later via its id.
* }
*
*
*
* @author Ian Barland
* @version 2006.Dec.03,06
*/
public class Entry {
/** A URL or filename for reading the data from, if no other file/URL provided: */
private static final String DEFAULT_DATABASE_FILENAME = "http://ru-itec120.pbwiki.com/grue-data-2007spring";
/** Text which only occurs in a line immediately before the actual data, in a URL/file. */
private static final String BEGIN_GRUE_DATA_MARKER = "BEGIN GRUE DATA";
/** Text which only occurs in a line immediately after the actual data, in a URL/file. */
private static final String END_GRUE_DATA_MARKER = "END GRUE DATA";
/** a total hack -- a field to help hasNextGrueDatum. */
static boolean seenEndOfData;
/** The list of valid types. */
private static final List VALID_ENTRY_TYPES
= java.util.Arrays.asList("room", "treasure", "connection", "biconnection");
// VALID_ENTRY_TYPES must contain Strings in lower-case.
/** The type of this entry. One of the strings in {@value VALID_ENTRY_TYPES}. */
private String type;
private Map attrs
= new java.util.HashMap();
/** Return all the Entries read from a file.
* @param filename The name of the gRUe database to read from.
* See http://ru-itec120.pbwiki.com/grue-data-2006fall
* for the syntax of a gRUe-database file.
* @return a list of all Entries.
*/
public static List getEntries( String filenameOrURL ) {
Scanner theInput;
try {
if (filenameOrURL.startsWith("http:")) {
theInput = new Scanner( new java.net.URL( filenameOrURL ).openStream() );
}
else {
theInput = new Scanner( new java.io.File( filenameOrURL ) );
}
}
catch (Exception err) {
// We'll report the error, and continue the program...
System.err.println( err.toString() );
theInput = new Scanner( "" ); // Read from the empty string.
}
// Read past all the opening cruft:
boolean foundFirstLine = false;
while (!foundFirstLine && theInput.hasNext()) {
foundFirstLine = theInput.nextLine().contains( BEGIN_GRUE_DATA_MARKER );
}
return Entry.getEntries(theInput);
}
/** A 0-arg version reads from @value{DEFAULT_DATABASE_FILENAME}.
*/
public static List getEntries() {
return Entry.getEntries(DEFAULT_DATABASE_FILENAME);
}
/** Return all the Entries read from a Scanner;
* this is the workhorse, called by getEntries(String).
*
* @param in The scanner to read from.
* See http://ru-itec120.pbwiki.com/grue-data-2006fall
* for the syntax of a gRUe-database file.
* @return a list of all Entries.
*/
private static List getEntries( Scanner in ) {
seenEndOfData = false; // HACK -- a helper flag for hasNextGrueDatum.
Entry.skipComments( in, true );
List entries = new java.util.LinkedList();
while (Entry.hasNextGrueDatum(in)) {
entries.add( new Entry(in) );
Entry.skipComments( in, true );
}
return entries;
}
/** Check for common errors in an Entry,
* printing to System.err.
* @return true if and only if the Entry had no errors (that we noticed).
*/
private boolean validateSyntax() {
String errMsg = "";
if (!VALID_ENTRY_TYPES.contains(this.type)) {
errMsg += "* Unknown type \"" + this.getType() + "\"; "
+ "it must be one of " + VALID_ENTRY_TYPES.toString() + ".\n";
}
if (attrs.isEmpty()) {
errMsg += "* No attributes at all.\n";
}
else if ( (!this.containsKey("id"))
&& (this.getType().equals("room") || this.getType().equals("treasure")) ) {
errMsg += "* No \"id\" attribute.\n";
}
if ((this.containsKey("id") && (this.get("id").split(" ").length != 1))) {
errMsg += "* The \"id\" attribute should be one word.\n";
}
if ((this.getType().equals("room") && this.containsKey("treasure"))) {
errMsg += "* Rooms should have an attribute \"treasures\", not \"treasure\".\n";
}
if (!errMsg.equals("")) {
System.err.println( "Syntax error in an Entry:\n" + errMsg
+ "The offending Entry:\n" + this.toString() );
}
return errMsg.equals("");
}
/** Constructor
* @param in The Scanner to read our info from.
* A runtime exception is thrown (by nextLine()) if there
* is no next (uncommented) line.
*/
private Entry( Scanner in ) {
// Skip to the start of the next real line of input:
Entry.skipComments( in, true );
// Now, the first real line: this will be the entry's type.
this.type = myNextLine(in).trim().toLowerCase();
boolean moreLinesToRead = true;
while ((hasNextGrueDatum(in) && moreLinesToRead)) {
moreLinesToRead = this.addKeyValuePair( in );
}
validateSyntax();
}
/** Read one line of the input as a key=value pair,
* and add it as an attribute to this Entry.
* @param in the input to read the next line of
* (comments are skipped over).
* @return true if there are (potentially) more lines
* to read from the current paragraph.
*/
private boolean addKeyValuePair( Scanner in ) {
Entry.skipComments( in, false );
if (!hasNextGrueDatum(in)) return false; // End of file reached.
String currLine = myNextLine(in);
if (currLine.equals("")) return false; // End of paragraph reached.
/* Split the current line into 2 parts, separated by "=".
* The first part will be the key; the second part will be the value.
* Then add that key/value pair to our attributes.
*/
String[] keyAndVal = currLine.split(DELIMITER, 2);
if (keyAndVal.length == 2) { // Successful split!
this.attrs.put( keyAndVal[0].trim().toLowerCase(),
keyAndVal[1].trim() );
}
else { // Unsuccessful split
System.err.println( "Entry(): "
+ "malformed line in file; ignoring the line: "
+ currLine );
}
return true;
}
/** @return the type of this entry -- one of the strings
* in {@value VALID_ENTRY_TYPES}.
*/
public String getType() { return type; }
/** Given an attribute-key, look up an attribute-value.
* @see getWithDefault
* @param key The attribute-key to look up inside this Entry.
* @return The value associated with key.
* For example, if this Entry had been created from a file
* the file which included "swag = some big long string",
* then get("swag") = "some big long string".
* @throws RuntimeException if the key doesn't exist in this Entry's attributes.
*/
public String get( String key ) {
if (attrs.containsKey(key)) {
return attrs.get(key);
}
else {
throw new RuntimeException( "get: The key \"" + key + "\" is not in the Entry:\n" + this.toString() );
}
}
/** Given an attribute-key, look up an attribute-value;
* if the key isn't found, return the second argument.
* @see get
* @param key The attribute-key to look up inside this Entry.
* @param defaultReturn The value to return if key doesn't exist
* in this Entry's attributes.
* @return The value associated with key.
* For example, if this Entry had been created from a file
* the file which included "swag = some big long string",
* then get("swag", "garbage") = "some big long string",
* and get("badkey", "garbage") = "garbage"
* (Assuming there was no line 'badkey = ...' in the input, 'course).
*/
public String getWithDefault( String key, String defaultReturn ) {
if (attrs.containsKey(key)) {
return attrs.get(key);
}
else {
return defaultReturn;
}
}
/** return whether or not this entry contains an attribute with the given key.
* @param key The key to look for.
* @return whether or not this entry contains an attribute with the given key.
*/
boolean containsKey( String key ) { return attrs.containsKey( key ); }
/** Return a string representation of this Entry.
* @return a string representation of this Entry.
* It will be in a paragraph-form suitable for reading in by our constructor.
*/
public String toString() {
String attrLines = "";
for ( String k : attrs.keySet() ) {
attrLines += k + " " + DELIMITER + " " + attrs.get(k) + "\n";
}
return getType() + "\n" + attrLines;
}
/* Test driver */
public static void main( String[] args ) {
String e1 = "Room\nid=hi";
String e2 = "Roooom\nid=hi";
String e3 = "Roooom\n";
String e4 = "Room\n id = a grand day \n description =this is a descr ";
System.out.println( Entry.getEntries(new Scanner(e1)).toString());
System.out.println( Entry.getEntries(new Scanner(e2)).toString());
System.out.println( Entry.getEntries(new Scanner(e3)).toString());
System.out.println( Entry.getEntries(new Scanner(e4)).toString());
System.out.println( Entry.getEntries(new Scanner(e1+"\n\n"+e4)).toString());
}
/* ******** End of methods dealing just with reading the file ********* */
/* ******************************************************************** */
/* ******* Begin methods which depend on the details of other classes */
/* like Room, Treasure. */
/** Read {@VALUE DEFAULT_DATABASE_FILENAME}, and create all the indicated
* Room and Treasure objects, and connections between Rooms.
*
* @param filename The name of the grue-database file to use.
* @param startingPointID The ID of a room listed in the database file,
* which you
* @return the Room object whose ID is startingPointID.
*/
public static Room buildWorld( String startingPointID ) {
return buildWorld(DEFAULT_DATABASE_FILENAME, startingPointID );
}
/** Read a grue-database file, and create all the indicated
* Room and Treasure objects, and connections between Rooms.
*
* @param startingPointID The ID of a room listed in the database file/wiki.
* I suggest using "nexus".
* @param filenameOrURL The name of the grue-database file to use, or a wiki page.
* @return the Room object whose ID is startingPointID.
*/
public static Room buildWorld( String filenameOrURL, String startingPointID ) {
// Later on, we'll need to be able to look up Treasures and Rooms by their id
// attribute (as mentioned in the database file). We'll use java.util.Map for that.
// How java.util.Map works:
// (It's like an address book, where first you write down buddy-names and their
// full-email-addresses. Then, later, you can walk up and give it just the
// buddy-name and have it return to you the full-email-address.
// We say that the buddy-name is the *key*, and
// the full-email-address is the *value* associated with that key.)
//
// In this example, the key is the ID-string, and the value is the corresponding
// Treasure object. That is:
//
// First we'll add Treasures (with their ID) to the Map called 'worldTreasures',
// using the Map's method put(String,Treasure).
// Later, we can walk up to the Map call its method get(String) -- giving
// it an ID and it retrieves previously-stored Treasure.
//
Map worldTreasures = new java.util.HashMap();
Map worldRooms = new java.util.HashMap();
// allThings are the Entries listed in the database file -- paragraphs
// which describe rooms and treasures and connections.
// Our job now, is to process these Entries:
// create actual Room and Treasure objects, make actual connections between them.
//
List allThings = Entry.getEntries( filenameOrURL );
// First, create all Treasures (ignoring, for now, other Entries).
//
for (Entry e : allThings) {
if (e.getType().equals("treasure")) {
worldTreasures.put( e.get("id"), e.createTreasure() );
}
}
// Okay, now that all treasures are can be looked up in worldItems,
// we can go and process the Entries which correspond to Rooms:
//
for (Entry e : allThings) {
if (e.getType().equals("room")) {
worldRooms.put( e.get("id"), e.createRoom( worldTreasures ) );
}
}
// Now that all Rooms have been created, we can go back through the Entries
// and create connections between rooms.
//
List connectionTypes
= new java.util.ArrayList( java.util.Arrays.asList( "connection", "biconnection" ) );
for (Entry e : allThings) {
if (connectionTypes.contains( e.getType() )) {
e.createConnection(worldRooms);
}
}
// We're done. Return the Room they had initially asked for.
//
if (worldRooms.containsKey(startingPointID)) {
return worldRooms.get(startingPointID);
}
else {
throw new RuntimeException( "World created, but no room has ID " + startingPointID + "." );
}
}
/** Return a newly-created Treasure, made out of the current Entry.
* @return a newly-created Treasure, made out of the current Entry.
* Throws an error if this Entry doesn't have a name or description or weight.
*/
public Treasure createTreasure() {
boolean debug = false;
Treasure t = new Treasure( this.get("name"), this.get("description"), Double.valueOf(this.get("weight")) );
if (debug) System.out.println( "Created treasure " + t.toString() );
return t;
}
/** Return a newly-created Room, made out of the current Entry.
* @param treasureRepository A Map which can take a treasure's ID String (the key),
* and return the indicated Treasure object (the value).
* @return a newly-created Room, made out of the current Entry.
* Throws an error if this Entry doesn't have a name or description,
* or if it mentions treasure IDs which aren't in treasureRepository.
*/
public Room createRoom( Map treasureRepository ) {
boolean debug = false;
// First, create the Room, for the moment without any Treasures:
// (This relies on Room having a two-argument constructor.)
Room r = new Room( this.get("name"), this.get("description") );
if (this.containsKey("treasures")) {
// Get the "treasure" line from this entry, and split it into words
// (separated by space). Then for each such word 't', ...
for ( String t : java.util.Arrays.asList( (this.get("treasures")).split(" ")) ) {
// ...take the word 't', look up the (already-created) Treasure object with
// that id, and add that Treasure to the Room r:
r.addContent( treasureRepository.get(t) );
}
}
if (debug) System.out.println( "Created room " + r.toString() );
return r;
}
/** Call 'connect' or 'biconnect' on the room mentioned by this Entry.
* @param roomRepository A Map which can take a room's ID String (the key),
* and return the indicated Room object (the value).
*
* Throws an error if this Entry doesn't have a "from" and a "to" attribute,
* or if it mentions room IDs which aren't in roomRepository.
*/
public void createConnection( Map roomRepository ) {
boolean debug = false;
Room r1 = roomRepository.get( this.get("from") );
Room r2 = roomRepository.get( this.get("to" ) );
if (r1 == null) {
System.err.println( "No starting room found with id " + this.get("from")
+ " when trying to connect:\n" + this.toString() );
return;
}
if (r2 == null) {
System.err.println( "No destination room found with id " + this.get("to" )
+ " when trying to connect:\n" + this.toString() );
}
if (this.getType().equals("connection")) {
r1.connectTo(r2);
}
else if (this.getType().equals("biconnection")) {
r1.biconnectTo(r2);
}
else {
throw new RuntimeException( "Uh-oh, confusing problem in Entry.createConection." );
}
if (debug) System.out.println( "Connected " + r1.getName() + " and " + r2.getName() + "." );
}
/*=======================*/
/* The following methods are all about parsing
* (and in particular, parsing wiki with html-ish input).
* Really, I should have my own "class wikiScanner extends Scanner";
* that would simplify a lot.
*
* But for ease-of-distribution, I really want all
* this in one file.
*/
/** The string which, at the start of a line, begins a comment. */
private static final String COMMENT_START = ";";
/** In a data line, DELIMITER separates the key from the value. */
private static final String DELIMITER = "=";
/** A regular expression for zero or more comment lines: */
private static final String COMMENT_LINE = "(" + COMMENT_START + ".*\n)";
private static final String COMMENT_LINES = COMMENT_LINE + "*";
private static final String COMMENT_AND_BLANK_LINES = "(" + COMMENT_LINE + "|\n)*";
/** Store peekahead data. */
private static java.util.Stack buffer = new java.util.Stack();
/** Like hasNext, except that we return false for EOF *or* having
* reached END_GRUE_DATA_MARKER_PATTERN.
* @param s A scanner to read from.
* @return whether or not there is any more potential (gRUe) data in s.
*/
private static boolean hasNextGrueDatum( java.util.Scanner s ) {
if (!buffer.empty()) return true;
if (!s.hasNext()) return false;
if (seenEndOfData) return false;
if (s.findInLine(END_GRUE_DATA_MARKER_PATTERN) == null) {
return true;
}
else {
seenEndOfData = true;
return false;
}
}
/** Convert END_GRUE_DATA_MARKER into a pattern just once and remember the result. */
private static final java.util.regex.Pattern END_GRUE_DATA_MARKER_PATTERN = java.util.regex.Pattern.compile(END_GRUE_DATA_MARKER);
/** Return the next line from the scanner, removing any trailing "
" or "
".
* @Return the next line from the scanner, removing any trailing "
" or "
".
*/
private static String myNextLine( java.util.Scanner s ) {
if (!buffer.empty()) {
return buffer.pop();
}
String answer = s.nextLine();
if (answer.contains( "
" )) {
buffer.push( "" );
}
for ( String suffix : removableSuffixes ) {
if (answer.endsWith(suffix)) {
answer = answer.substring(0,answer.length()-suffix.length());
}
}
// Okay, I *could* push answer, and then make a recursive call.
// Will hand-optimize:
return answer;
}
private static final String[] removableSuffixes = { "
", "" };
private static boolean justSawParaEnd = false;
/** Skip over any comments (and possibly blank lines) in the input.
* @param blankLinesToo If true, also skip over blank lines in addition to comments.
*/
private static void skipComments( Scanner s, boolean blankLinesToo ) {
// We *would* just call s's method "skip" with a regular expression,
// except that when we are parsing a wiki file w/ html cruft, we need
// to realize that "
" is really a blank line. So we have our own
// look which calls (our own) myNextLine.
boolean skipping = true;
String n = "dummy Value to appease compiler";
while (skipping && hasNextGrueDatum(s)) {
n = myNextLine(s);
skipping = ((n.equals("")) && blankLinesToo)
|| n.startsWith( COMMENT_START );
}
if (hasNextGrueDatum(s)) {
// We've gone too far!
buffer.push(n);
}
}
}