// RoadNetwork.java /** Road network simulator (or it will become one once it's done) * @author Douglas W. Jones * @version Mar. 10, 2021 * THIS VERSION demonstrates using lambda expressions to pass * error messages to the MyScanner.getNext... methods. * * Some calls use outer classes * Some calls use inner classes * Some calls use anonymous inner classes * Some calls use lambda expressions * * They all work exactly the same way. Lambda expressions are * just syntactic sugar for anonymous inner classes which are * just syntactic sugar for inner classes which are just * syntactic sugar for classes. */ import java.io.File; import java.io.FileNotFoundException; import java.util.Collection; import java.util.LinkedList; import java.util.Random; import java.util.Scanner; import java.util.regex.Pattern; /** Centralized error reporting tools * error reports are output to System.err (standard error, aka stderr) * WARNING: Never create any instances or subclasses of Error. */ abstract class Error { private static int errorCount = 0; /** Fatal error * @param msg -- the warning message * this never returns, the application is terminated abnormally */ public static void fatal( String msg ) { System.err.println( "Error: " + msg ); System.exit( 1 ); // report failure to the OS (or to the shell) } /** Non-fatal warning * @param msg -- the warning message */ public static void warn( String msg ) { System.err.println( "Warning: " + msg ); errorCount = errorCount + 1; } /** If there have been any warnings prior to this call, exit */ public static void exitIfWarnings() { if (errorCount > 0) { System.exit( 1 ); // report failure to the OS (or to the shell) } } } /** Support for scanning input files with error reporting * @see Error * @see java.util.Scanner * Ideally, this would be extend class Scanner, but class Scanner is final * Therefore, this is a wrapper class around class Scanner */ class MyScanner { Scanner sc; // the scanner we are wrapping public MyScanner( File f ) throws FileNotFoundException { sc = new Scanner( f ); } // methods that we wish we could inhereit from Scanner public boolean hasNext() { return sc.hasNext(); } public boolean hasNextFloat() { return sc.hasNextFloat(); } public String next() { return sc.next(); } // patterns that matter here, all will match the empty string private static final Pattern delimPat = Pattern.compile( "[ \t\n\r]*" ); private static final Pattern notNamePat = Pattern.compile( "[^A-Za-z]*|" ); private static final Pattern namePat = Pattern.compile( "([A-Za-z][0-9A-Za-z]*)|" ); private static final Pattern notFloatPat = Pattern.compile( "[^0-9]*|" ); private static final Pattern floatPat = Pattern.compile( "([0-9][0-9]*\\.[0-9]*)|([0-9]*\\.[0-9][0-9]*)|([0-9]*)" ); /** tool to defer computation of messages output by methods of MyScanner * To pass a specific message, create a subclass of Message to do it */ public interface Message { String myString(); } // new methods added to class Scanner /** get the next name from the scanner or complain if missing * Names start with letter then letters or digits up to anything else. * @param defaut -- return value if there is no next item * @param errorMesage -- the message to complain with * @return the next item or the default */ public String getNextName( String defalt, Message errorMessage ) { sc.skip( delimPat ); String errorText = sc.skip( notNamePat ).match().group(); String name = sc.skip( namePat ).match().group(); if (!errorText.isEmpty()) { Error.warn( errorMessage.myString() + " skipped " + errorText ); } if (name.isEmpty()) { Error.warn( errorMessage.myString() ); return defalt; } else { return name; } } /** get the next float from the scanner or complain if missing * only support 0.0 0. .0 . (no exponent field allowed) * @param defalt -- return value if there is no next item * @param errorMesage -- the message to complain with * @return the next item or the defalt */ public float getNextFloat( float defalt, Message errorMessage ) { sc.skip( delimPat ); String errorText = sc.skip( notFloatPat ).match().group(); String text = sc.skip( floatPat ).match().group(); if (!errorText.isEmpty()) { Error.warn( errorMessage.myString() + " skipped " + errorText ); } if ( text.isEmpty() ) { Error.warn( errorMessage.myString() ); return defalt; } else { return Float.parseFloat( text ); // this will not throw uncheckedException because text is valid } } /** try to scan over the given literal and complain if missing * @param literal -- the string to scan over * @param errorMesage -- the message to complain with */ public void getNextLiteral( String literal, Message errorMessage ) { if ( sc.hasNext( literal ) ) { sc.next( literal ); } else { Error.warn( errorMessage.myString() ); } } } /** Wrapper extending class Random, turning it into a singleton class * @see Random * Ideally, no user should ever create an instance of Random, all use this! * Users can call MyRandom.stream.anyMethodOfRandom() (or of MyRandom) * or MyRandom.stream().anyMethodOfRandom() * Users can allocate MyRandom myStream = MyRandom.stream; * or MyRandom myStream = MyRandom.stream(); * No matter how they do it, they get the same stream */ class MyRandom extends Random { public static final MyRandom stream = new MyRandom(); // the only stream; // nobody can construct a MyRandom except the above line of code private MyRandom() { super(); } /* alternative access to stream * Returns the only stream */ public static MyRandom stream() { return stream; } // add distributions that weren't built in /** exponential distribution * @param mean -- the mean value of the distribution * @return a positive exponentially distributed random value */ public double nextExponential( double mean ) { return mean * -Math.log( this.nextDouble() ); } } /** Demonstrate how to contruct a simple subclass of MyScanner.Message * In practice, this is a bad place for this declaration, since it is * used in only one place, inside the constructor for class Road. */ class SourceMissing implements MyScanner.Message { public String myString() { return "road: source missing"; } } /** A more complex subclass of MyScanner.Message * In practice, this is a bad place for this declaration too. * Also note the need for a constructor to deliver the value needed * when the myString method actually does its concatenations. */ class DestinationMissing implements MyScanner.Message { private final String srcname; public DestinationMissing( String s ) { srcname = s; } public String myString() { return "road " + srcname + ": destination missing"; } } /** Roads connect intersections */ class Road { // instance variables private final Intersection source; // road from here private final Intersection destination; // road to there private final double travelTime; // time in seconds // static information about all roads // BUG: only public so test code can iterate over it -- alternatives? public final static LinkedList all = new LinkedList<>(); /** Read a road description and construct it * @param in -- the Scanner from which the road description is read */ public Road( MyScanner in ) { final String srcname; // name of source intersection final String dstname; // name of destination intersection final float travtim; // temporary travel time srcname = in.getNextName( "???", new SourceMissing() ); dstname = in.getNextName( "???", new DestinationMissing( srcname ) ); class TravTimMissing implements MyScanner.Message { // an inner class // private final fields for copies of srcname and dstname implied! // those fields still exist, as does the implicit constructor to // initialize them. public String myString() { return "road " + srcname + " " + dstname + ": travel time missing"; } } travtim = in.getNextFloat( 99999F, new TravTimMissing() ); in.getNextLiteral( ";", ()-> "road " + srcname + " " + dstname + " " + travtim + ": semicolon missing" ); // BUG: What about time units? if (travtim <= 0.0F) { Error.warn( "road " + srcname + " " + dstname + " " + travtim + ": negative travel time?" ); travelTime = 99999F; } else { travelTime = travtim; } // look up the source and destination intersections source = Intersection.lookup( srcname ); if (source == null) { Error.warn( "road " + srcname + " " + dstname + " " + travtim + ": undefined source intersection?" ); } destination = Intersection.lookup( dstname ); if (destination == null) { Error.warn( "road " + srcname + " " + dstname + " " + travtim + ": undefined destination intersection?" ); } all.add( this ); } /** Reconstruct a string approximation of the text used to create this road * WARNING: Never call this until after a call to Error.exitIfWarnings() */ public String toString() { return "road " + source.name + " " + destination.name + " " + travelTime + " ;"; } // BUG: need to add behavior for roads } /** Intersections are connected by roads */ class Intersection { // instance variables of each intersection public final String name; // the name of the intersection private double traversalTime; // time in seconds // BUG: is collection the right class? Should I pick a subclass? private Collection incoming; // the roads leading here private Collection outgoing; // the roads that come from here // static data about all the intersections // BUG: only public so test code can iterate over it -- alternatives? public final static LinkedList all = new LinkedList<>(); /** Read an intersection description and construct it * @param in -- the Scanner from which the intersection description is read */ public Intersection( MyScanner in ) { name = in.getNextName( "???", ()-> "intersection: name missing" ); traversalTime = in.getNextFloat( 99999F, ()-> "intersection " + name + ": traversal time missing" ); in.getNextLiteral( ";", ()-> "intersection " + name + " " + traversalTime + ": semicolon missing" ); // BUG: What about time units? if (lookup( name ) != null) { Error.warn( "intersection " + name + " " + traversalTime + ": name redefined?" ); // BUG: anything else to do in this case? } all.add( this ); } public String toString() { return "intersection " + name + " " + traversalTime + " ;"; } // BUG: need to add behavior for intersections /** look up an interseciton by name * @param name -- the textual name of the intersetion * @return the Intersection object or null if there is none */ public static Intersection lookup( String name ) { for (Intersection i: all) if (i.name.equals( name )) return i; return null; } } public class RoadNetwork { /** build a road network model by scanning an input file * @param in -- the scanner used to read the file * Input file contains keywords road and intersection, * details beyond that are handled by the appropriate classes */ private static void buildModel( MyScanner in ) { while (in.hasNext()) { String keyword = in.getNextName( "???", ()-> "intersection: name missing" ); if ("intersection".equals( keyword )) { new Intersection( in ); } else if ("road".equals( keyword )) { new Road( in ); } else { Error.warn( "not a keyword in the input: " + keyword ); } } Error.exitIfWarnings(); // if there were warnings, don't try to go on } /** Print out the road network. * This is intended to help with debugging buildmodel */ private static void printModel() { for (Intersection i: Intersection.all) { System.out.println( i.toString() ); } for (Road r: Road.all) { System.out.println( "" + r ); } } /** main method mostly concerned with command line arguments * @param args -- the command line arguments * Want just one argument, the file name for model description */ public static void main( String[] args ) { // arg[0] should be the name of a file describing a road network if (args.length < 1) { Error.fatal( "missing command line argument, a file name" ); } if (args.length > 1) { Error.warn( "extra command line argument: " + args[1] ); } try { buildModel( new MyScanner( new File( args[0] ) ) ); printModel(); // BUG: this is only for debugging // BUG: simulate the network that was built } catch ( FileNotFoundException e ) { Error.fatal( "could not open file: " + args[0] ); } } }