This lab introduces how to handle files as both input and output. We’re coming back to Tracery (which you implemented in Lab 1) with this assignment but instead of always reading the grammar from a file and writing the expanded grammar to the command line, we will also be able to read a grammar from the command line and write the expanded grammar to a file. To do this, you’ll be learning about some of the IO (input/output) classes in Java, and how the Java concept of a Stream provides an abstraction layer above files and console I/O.
You will be doing the following:
- Import the correct IO packages from the Standard Library
- Process command line arguments that determine whether the program is reading/writing from the console (command line) or from a file
- Load a grammar either from the console or from a file, based on the arguments the program was given
- Write the expanded grammar to either the command line or a file, based on the arguments the program was given
Open the file TraceryRecursion.java that came with the lab. This is where you’ll make all your changes. Every place in the file where you’re supposed to make a change is marked with a comment labeled To Do.
Importing packages in Java
Try compiling the code we’ve given you. If you haven’t made any changes it’ll tell you it can’t find the following symbols:
- class Hashtable
- class PrintStream
- class InputStream
But in Lab 1 we were able to use a Hashtable, so what’s different? The problem is that we’re trying to use something from the Java Standard Library without telling Java that we are. To tell Java that we’re using something from the standard library, you use the import statement. In Lab 1 we had already included the correct imports for you. For lab 2 the compiler is telling you that you need to import these three classes in order to use them.
As an example of importing a class, to import Hashtable from the standard library so you can use it, you would put this statement at the beginning of your program:
The java.util part is the package (a collection of classes) that Hashtable belongs to. To figure out the package of a class you want to import from the standard library, you look at its documentation page. The documentation for Java’s Hashtable is here. In addition to all the info about where it is in the type hierarchy and its accessible fields and methods, the documentation page says what package the class is part of at the top of the page right above the class name.
To import PrintStream and InputStream you can look up the package they belong to at their respective documentation pages (here and here). Another trick to know about, though you don’t really need to use it in this lab, is how to import a bunch of classes from the same package at once. To import all of the classes from a package, you can use statement:
import <package name>.*
Command line arguments
This program can take the following command line arguments:
-in <input file> -out <output file>
The ”-in” and ”-out” part of the arguments allow us to specify the input and output file arguments in either order. Below are described the different valid combinations of arguments.
$ java TraceryRecursion
This reads the grammar from the console and writes the expanded grammar to the console. Reading from the console is called standard in and writing to the console is called standard out. Standard in is represented by the InputStream object stored in System.in, while standard out is represented by the PrintStream object stored in
System.out. We will be reading from the console using pipes and the command cat (in unix systems like Mac OS and Linux and gitbash for Windows). There is more about this below.
|$ java TraceryRecursion -in grammar-story.txt|
|This reads a grammar from file||grammar-story.txt||and writes to standard out. This|
was the behavior you implemented in Lab 1.
$ java TraceryRecursion -out grammar-out.txt
This reads grammar from standard in and writes the expanded grammar to the text file grammar-out.txt.
|$ java TraceryRecursion -out grammar-out.txt -in grammar-story.txt|
|This reads a grammar from from file||grammar-story.txt||and writes the expanded|
grammar to file grammar-out.txt. Note that since our command line arguments specify the input and output file with -in and -out you can put the arguments in either order (that is -out first like the example above, or -in first).Your code doesn’t need to handle bad command line arguments. Since -in and -out are what tell the program to use files, junk arguments will default the program to reading from standard in and writing to standard out.
Processing command line arguments
We use the command line arguments to figure out where we’re reading the grammar from and where we’re writing the grammar to. This is done with two helper functions that you need to write:
public static InputStream getInputStream(String args); public static PrintStream getOutputStream(String args);
There are comments marked with To Do in the file to remind you to define these methods. You’ll be calling these methods in main, passing them the command line arguments, in order to determine the InputStream and PrintStream to use.
The nice thing about these methods is that they abstract away whether you’re reading/writing from standard IO or a file. The rest of the code can just read from the returned InputStream and write to the returned OutputStream without having to know or care whether its a file or the console.
To understand this, here’s a bit more information about how Java abstracts input and output. You’ll notice that getInputStream() returns an InputStream which is an abstract class (an abstract class, like an interface, can’t be directly instantiated as an object). This means you will actually be returning a subclass of InputStream, where which subclass depends on whether it’s the console or a file. If reading from a file, you’ll be returning something called a FileInputStream. The diagram below shows where it sits in the class hierarchy for IO.
A FileInputStream is the subclass of InputStream for reading from a File. A File is Java’s representation of a file in the computer’s file system. The field System.in stores an InputStream for standard input.
To return the correct input location you’ll:
- Look for a ”-in” in args
- If there is an ”-in” then you’ll make a new FileInputStream that will take a new File named whatever is in args in the index after the location you found ”-in”. Return the FileInputStream.
- Otherwise return in which is also an InputStream
You can assume that there will be 0, 2 or 4 command line arguments.
Now since we’re dealing with user input and we don’t know what exactly is in the folder with our program (from the program’s point of view), we want to handle the possibility that the input file we were told to open doesn’t exist. We’ll want to use a try-catch block to catch a FileNotFoundException within getInputStream(). If you catch the exception, print the error message ”Input grammar file does not exist.” and return System.in (as if no -in argument was specified).
Our function getOutputStream() is very similar to getInputStream() but instead of returning an InputStream we’re returning a PrintStream (System.out is a PrintStream).
To return the correct output location you’ll:
- Look for a ”-out” in args
- If there is a ”-out” then you’ll make a new PrintStream that will take a
new File named whatever is in args in the index after the location you found ”-out”. Return the PrintStream
- Otherwise return out which is also a PrintStream
As above, you’ll want to catch a FileNotFoundException, print the error message
“Output file can not be created.”, and return System.out (as if no -out argument was specified).
Piping on the command line
Another part of this lab is giving you practice using pipes on the command line to use the standard output of one program as the standard input of another. For our purposes we’ll be using pipes to pipe the contents of a file into TraceryRecursion, but in general it’s possible to hook lots of programs together on the command line using pipes. The general form of using pipes looks like
<program 1> | <program 2>
where the | character is the pipe. The line above would takes the standard output of <program 1> (which would normally just print to the console) and feeds it into the standard input of <program 2> (which would normally read interactively from the console).
To feed a file into the standard input of TraceryRecursion, we’ll use the the unix command cat (short for concatenate). This command simply reads a file and writes the content to standard out. For example, if we type:
this will print out the file contents to the console:
Name:emily,luis,otavio,anna,charlie character:#name# the #adjective# #animal# place:school,the beach,the zoo,Burning Man adjective:#color#,#emotion#, origin:once #character# and #character# went to #place#
To take this output and feed it into TraceryRecursion we can pipe the output of cat into TraceryRecursion:
cat grammar-story.txt | java TraceryRecursion
More generally this looks like:
cat <file name> | java TraceryRecursion <arguments>
Try running TraceryRecursion without a -out argument using pipes to feed grammar files into TraceryRecursion.
Loading the grammar
For this lab, the loadGrammar() function will be very similar to what it was in Lab 1 with one major difference: instead of giving it a string as an argument we’re passing it the InputStream we returned from getInputStream().
We’re going to use an instance of BufferedReader to read from this InputStream . The main reason for using this class instead of the raw InputStream is for the function readLine() which allows us to read strings that correspond to a line of the file, automatically handling the fact that different operating systems using different characters to indicate the end of a line. You can read more about BufferedReaders here. To make a new BufferedReader you need to give it a new InputStreamReader which requires an InputStream as part of its constructor. You can see the details of an InputStreamReader here. So you’ll be creating a new BufferedReader from a new InputStreamReader which is created from the InputStream.
Note: The constructor for BufferedReader wants an instance of the abstract class Reader as the argument. An InputStreamReader is a subclass of Reader, which is why this works.
We’ll use the readLine() method on BufferedReader to read each line of the grammar and add it to the Hashtable. The loop where you’re calling readLine() will have to know when to stop. Fortunately, it returns null when it gets to the end of the stream, so we can use this to know when the loop should stop.
Specifying that loadGrammar() throws an exception
You’ve probably seen at least a few RuntimeExceptions being thrown in your experience as a Java programmer. Among the most common are the
ArrayOutOfBoundsException (when you’re trying to access an index that isn’t in an array) and a NullPointerException (when you try to access a field or method on a reference variable that hasn’t been set equal to anything). These are unchecked exceptions, and programmers don’t usually try to catch them.
The BufferedReader methods in loadGrammar() throw the checked exception IOException. Since it’s checked, the compiler flags it as an error, requiring the programmer to either catch the exception or declare that the method can throw the exception. For example, to declare that main() can throw an IOException, you’d change the signature to:
public static void main(String args) throws IOException
You’ll need to do this for loadGrammar(). When you declare that a method throws a checked exception, the compiler is happy, because now it can check that anyone who calls the method that can throw an exception either catches the exception or declares that they throw the exception in turn.
Changes in m ain()
There are two things you’ll need to change in main():
- Calling the methods getInputStream() and getPrintStream() to get the InputStream and PrintStream based on the arguments
- Changing the println() in main() to be called on the PrintStream that getOutputStream() returned
Outputting the grammar
Unlike in Lab 1, the method outputGrammar() has two arguments. Like the previous lab, the first one is the grammar we’re expanding. The second is the PrintStream we’re going to print the grammar to. All we have to do here is to call println() on the PrintStream argument rather than on standard output.
Example arguments and output
$ java TraceryRecursion -in grammar-story.txt -out output.txt
Set seed 1
The contents of output.txt are:
adjective: ”#color#”,”#emotion#”, place: ”school”,”the beach”,”the zoo”,”Burning Man”, emotion: ”happy”,”sad”,”elated”,”curious”,”sleepy”, origin: ”once #character# and #character# went to
#place#”, color: ”red”,”green”,”blue”, name: ”emily”,”luis”,”otavio”,”anna”,”charlie”, character: ”#name# the #adjective# #animal#”,
animal: ”cat”,”emu”,”okapi”, once otavio the blue emu and luis the curious emu went to school once otavio the elated cat and charlie the blue emu went to school once anna the elated okapi and charlie the sad okapi went to Burning Man once emily the happy okapi and otavio the red cat went to school once otavio the blue cat and emily the sleepy okapi went to the beach once luis the green cat and charlie the blue emu went to school once otavio the red emu and otavio the red okapi went to the zoo once charlie the green cat and luis the green okapi went to the zoo once luis the red emu and emily the red cat went to school once otavio the sad okapi and luis the happy emu went to school
- Overview of using package members:
- Overview of catch blocks in Java:
https://docs.oracle.com/javase/tutorial/essential/exceptions/catch.html ● Throwables in Java:
- Relationship of the various Input and Output Streams: https://stackoverflow.com/questions/22563986/understanding-getinputstre am-and-getoutputstream