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); } } }