Common Console app Features

Although it's difficult to make sweeping statements about all console apps, I can say that many console apps perform both file I/O and the parsing of arguments to the program. In this section, we'll look at example programs that demonstrate both of these features.

A Simple Replacement for MS-DOS copy: Echo1

The following console program, Echo1, performs a very simple operation. Echo1 accepts two arguments that it assumes are file names. Echo1 opens the file represented by the first argument and copies it to the file represented by the second argument.

/**
 * Echo1 - Copies the file args[0] to the file args[1].
 */
import java.io.*;
public class Echo1
{
 /**
 * The main entry point for the app. *
 * @param arg[0] - the input file
 * @param arg[1] - the output file
 */
 public static void main (String[] args)
 {
 // make sure that the user understands what
 // to do
 if (args.length != 2)
 {
 System.out.println("Enter: echo1 source dest");
 System.out.println("to copy <source> to <dest>");
 System.exit(-1);
 }
 // catch any problems at the bottom of the
 // program - no matter what the problem, we'll
 // output an error message and terminate
 try
 {
 // open the two files:
 // first the input file; an error here is fatal
 FileInputStream fin = new FileInputStream(args[0]);
 BufferedInputStream in = new BufferedInputStream(fin);
 // now the output file - first let's see whether the // file exists
 okayToCopy(args[1]);
 // must be okay - open the target file, if it
 // exists, and truncate it
 FileOutputStream out= new FileOutputStream(args[1], false);
 // now perform the copy
 byte[] buffer = new byte[1024];
 while(in.available() > 0)
 {
 int bytesRead = in.read(buffer);
 out.write(buffer, 0, bytesRead);
 }
 }
 // catch any exception thrown
 catch(IOException e)
 {
 System.out.println("Error:" + e.getMessage());
 }
 }
 /**
 * Check the argument to see whether it exists. If so, is it
 * okay to overwrite it? If not, it throws an exception. *
 * @param outputFileName - the file name to overwrite
 */
 private static void okayToCopy(String outputFileName)
 throws IOException
 {
 // first check whether the file exists
 File file = new File(outputFileName);
 if (file.exists())
 {
 // it does, so check whether it's okay to overwrite it System.out.println("File exists. Overwrite? [Y or N]");
 byte[] answer = new byte[1]; // read in a single byte
 System.in.read(answer);
 if (answer[0] != 'Y' && answer[0] != 'y')
 {
 throw new IOException("Copy failed");
 }
 }
 }
}


Changing the main class name

Notice that in the previous example I have stopped calling the main class Class1. Instead, I renamed the main class to match the project name. To rename the main class, you start in the usual way by using the Console app Wizard to create the Echo1 project, as we did with the HelloWorld1 project at the beginning of this chapter. As usual, the wizard creates the one and only Java source file and names it Class1. First you rename the file in the Project to Echo1. This necessitates that you change the class name to Echo1 as well.

NOTE
There can be only one public class per Java source file and its name must exactly match that of the Java source file in which it resides. As we will see in Part II, this is necessary so that browsers can find applet classes quickly and easily.

Unfortunately, changing the names isn't enough. If you compile the Echo1 program after changing the project name and class name, the compiler claims that it can no longer find Class1. This is because the Console app Wizard directs the Project file to start debugging with Class1. (Visual J-- 1.x used to ask the user for the class name the first time the program was run. This turned out to be a nuisance, so now the Wizard sets the class name.)

To change the class name in the Echo1 project, you right-click the Echo1 project in Project Explorer and choose Echo1 Properties. From there, you select the Launch tab. In the When Project Runs, Load: list box, select the only class available, Echo1, and then choose OK. The resulting Properties page is shown in Figure 1-14.

Java Click to view at full size.

Screenshot-14. The project Properties window after the launch class has been changed from Class1 to Echo1.

NOTE
Even though Echo1 was the only option available, you must still manually select it and apply it. This option won't be available until you have edited the name to Echo1 in the Java source file.

Echo1 source code

Echo1 begins by importing the java.io package. This is where the file I/O classes reside.

NOTE
Importing a package is the Java equivalent to including a C++ #include file at compile time and adding the corresponding library at link time. The package informs Visual J Plus Plus of our intent to access classes from the java.io package.

Once Echo1 begins execution, it immediately checks to see whether the user has provided two arguments. It does that by checking the length of the args argument.

NOTE
The args argument to main() is an array of String objects, each one corresponding to an argument to the program.

If the user doesn't know what a console app does, it is common to simply run it without any arguments. In that case, it is important that the program provide the user some help by explaining what arguments it expects and what it does with them.

Before the program begins the actual copy operation, it opens up a try block. Most of Java's file operations throw an IOException when something goes wrong, so we better be prepared to catch these exceptions.

The first order of business before we can actually copy anything is to open the input file. The program does this by creating a FileInputStream object with the input file name. The FileInputStream constructor opens the file provided. If the file doesn't exist, it throws an IOException explaining as much.

Notice how the program follows this up by wrapping the FileInputStream class in a BufferedInputStream. Doing this is almost always a good idea. The FileInputStream class isn't buffered, which means that every time a read is performed the class must go back to the disk to get the data. The BufferedInputStream class adds a buffer to reduce the number of disk reads. Thus, if the program reads a single byte, the BufferedInputStream class reads an entire block. (The actual size of the block is dependent on the operating system and the underlying disk, but it's some natural size, usually a multiple of 1024 bytes.) The BufferedInputStream class returns a single byte to the caller and retains the remaining data. When the program asks for another byte, the class returns the next byte from the buffer without going back to the disk. Since memory operations are much faster than disk accesses, BufferedInputStream can speed up I/O access considerably.

The false value passed to the FileOutputStream() constructor indicates that the program doesn't want to append a file if the output file already exists. Instead, it wants to truncate any existing output file. Before the program opens the output file, however, it invokes the static method okayToCopy() to see whether it's okay to overwrite an existing file. The method okayToCopy() accepts the name of the output file. It throws an IOException if it isn't okay to overwrite an existing file.

NOTE
Unlike C++, a method must use the throws keyword to declare any exceptions that it, or any method it calls, might throw.

The okayToCopy() method first checks for the presence of the target file by creating a File object. You might think that a File object is used to perform I/O, but its real use is for testing the properties of a file on the disk. The method File.exists() returns true if the File object is pointing to an existing file.

If the target file does exist, the program asks the user whether it should be overwritten or not. If the user enters anything other than y or Y, the program throws an IOException and thereby saves the target file from extinction. (This exception is handled back at the end of main().) Otherwise, the okayToCopy() method returns to the caller.

Once the source and target files have been opened, there's nothing left but the while loop that performs the read and write operations. The method InputStream.available() returns a count of the number of bytes available to be read in the input file. The InputStream.read() method reads up to the number of buffer bytes. The return value is the number of bytes actually read. The call to OutputStream.write() writes the number of bytes from buffer that are indicated by bytesRead, starting at offset 0.

More Sophisticated Echo2

The Echo2 program presents several enhancements to the Echo1 program. First, the second argument—the destination file—is optional. If it isn't present, output goes to standard output. Further, Echo2 accepts the /C switch, which if present indicates that the input is a text string that is to be capitalized before being output to the destination stream.

While this may not sound like much, I have also added several techniques to Echo2 that you can use in other apps.

Echo2 class

The following code represents the Echo2 class:

/**
 * Echo2 - Copies the first argument to the second.
 * If output file is not given, output goes to
 * standard output. If /C switch is present,
 * attach a filter to convert each word to
 * uppercase.
 */
import java.io.*;
public class Echo2
{
 // global data
 // input file object
 InputStream in = null;
 // output file object
 OutputStream out = null;
 /**
 * The main entry point for the app. *
 * @param arg[0] - the input file (optional)
 * @param arg[1] - the output file
 */
 public static void main (String[] args)
 {
 // start parsing arguments
 try
 {
 boolean caps = false;
 String input = null;
 String output= null;
 int nextArg = 0;
 // first look for switches
 if (args[nextArg].equalsIgnoreCase("/C"))
 {
 caps = true;
 nextArg++;
 }
 // save input argument
 input = args[nextArg++];
 // the output file name is optional -
 // an exception here is okay
 try
 {
 output = args[nextArg++];
 }
 catch(Exception e)
 {
 }
 // create an object to do all the work
 new Echo2(caps, input, output).copy();
 }
 catch(IOException e)
 {
 print("Error:" + e.getMessage());
 }
 catch(Exception e)
 {
 print("Enter: echo2 [/C] source [dest]");
 print("to copy <source> to <dest>");
 print("If dest is absent, output is to standard output");
 print("/C -> capitalize each word");
 } }
 /**
 * The Echo2 constructor sets up the files.
 * By providing an object, the program now has a place
 * to store things.
 * @param capitalize - TRUE->capitalize each word on output
 * @param input - the name of the input file (null->none)
 * @param output - the name of the output file
 */
 Echo2(boolean capitalize, String input, String output)
 throws IOException
 {
 // open the two files:
 // first the input file; an error here is fatal
 FileInputStream fin = new FileInputStream(input);
 in = new BufferedInputStream(fin);
 // must be okay - use either standard output or the
 // specified output file as output
 out = System.out; // the default is standard output
 if (output != null)
 {
 okayToCopy(output);
 out = new FileOutputStream(output, false);
 }
 // if we are to convert to upper case, then…
 if (capitalize)
 {
 out = new CapFilterOutputStream(out);
 }
 }
 /**
 * Perform the copy operation. *
 */
 void copy()
 throws IOException
 {
 byte[] buffer = new byte[1024];
 while(in.available() > 0)
 {
 int bytesRead = in.read(buffer);
 out.write(buffer, 0, bytesRead);
 }
 }
 /**
 * Write input to standard output. *
 * @param outputFileName - the file name to overwrite
 */
 static void print(String outString)
 {
 System.out.println(outString);
 }
 /**
 * Check the argument to see whether it exists and whether it's
 * okay to overwrite it. If not, it throws an exception. *
 * @param outputFileName - the file name to overwrite
 */
 private static void okayToCopy(String outputFileName)
 throws IOException
 {
 // first check whether the file exists
 File file = new File(outputFileName);
 if (file.exists())
 {
 // it does, so check whether it's okay to overwrite it System.out.println("File exists. Overwrite? [Y or N]");
 byte[] answer = new byte[1]; // read in a single byte
 System.in.read(answer);
 if (answer[0] != 'Y' && answer[0] != 'y')
 {
 throw new IOException("Copy failed");
 }
 }
 }
}


The main() function in Echo2 looks quite a bit different from its Echo1 ancestor. This is primarily because Echo2 must be flexible enough to handle a varied number of arguments.

Echo2 first initializes each of the three variables it intends to read from the argument list. The program then defines the index nextArg to be used in scanning through the argument list. Echo2 compares the first argument to the "/C" string using String.equalsIgnoreCase(). If this function call returns true, the program knows that the /C switch is present. In this case, the program sets the caps flag to true and then increments nextArg to point to the next argument.

NOTE
The two argument variables, input and output, are initialized to null. null isn't numerically equivalent to 0, as it is in C++, but is a unique value.

The program assumes the next argument to be the name of the input file. If either of these first two arguments are not present, args[] will be referenced beyond the end of the array, which will cause Java to throw an ArrayIndexOutOfBoundsException. This exception will be caught at the bottom of main() by the catch(Exception) catch phrase.

Next the program attempts to reference the second file name, except this time an ArrayIndexOutOfBoundsException does not represent an error because the second file name is optional. Therefore, the program catches the possible exception on the spot. In that event, output retains its null value.

Once the arguments have been parsed, main() creates an object of the class Echo2 and then immediately passes that object to Echo2.copy().

NOTE
Under Java, it is perfectly legal to allocate an object off the heap and then immediately use it as the object of a method call.

Why create an Echo2 object?

The main() method is static and therefore has no object associated with it. This isn't a problem with programs that consist of only a single function, like Echo1. However, in larger object-oriented programs it is more convenient to store information in the data members of a class object. Creating an object in the static member main() provides just such a place to store information.

The constructor for the Echo2 class performs and looks a lot like the file creation logic in Echo1. The primary difference is that Echo2 first assigns System.out to the output object out. If an output file name is present, the output object is replaced by a FileOutputStream opened on that output file. This works because PrintStream—the class of System.out—and FileOutputStream both extend a common base class, OutputStream.

Finally, Echo2 creates a CapFilterOutputStream object to perform the capitalization function.

NOTE
CapFilterOutputStream extends the base class FilterOutputStream. You can use subclasses of FilterOutputStream to perform conversion operations on data being sent out of an output stream.

The copy() and okayToCopy() methods are basically identical to their Echo1 equivalents.

CapFilterOutputStream class

Now all that's left is to present the CapFilterOutputStream class (located in the Echo2 program) that performs the capitalization operation. The source code is as follows:

/**
 * CapFilterOutputStream - A filter output stream
 * that converts its output to
 * uppercase.
 */
class CapFilterOutputStream extends FilterOutputStream
{
 // the list of characters which cause capitalization
 static String specialChars = " .-\t";
 // the output stream to use for actual output
 OutputStream os = null;
 CapFilterOutputStream(OutputStream os)
 {
 super(os);
 this.os = os;
 }
 /**
 * Write the buffer in capitalized form. *
 * @param buffer - buffer to write
 * @param offset - where to start
 * @param length - number of bytes to write
 */
 public void write(byte[] buffer, int offset, int length)
 throws IOException
 {
 // make a string buffer out of the byte array -
 // here we are assuming a text byte array
 // (we use StringBuffer because you can write
 // directly to it)
 String s = new String(buffer, offset, length);
 StringBuffer sb = new StringBuffer(s);
 // convert the StringBuffer into uppercase according
 // to the rules of this class
 toUpper(sb);
 // now convert the string back into an array of bytes,
 // and write it out using the base class' method
 buffer = sb.toString().getBytes();
 os.write(buffer, 0, buffer.length);
 }
 /**
 * Write the buffer in capitalized form. *
 * @param buffer - buffer to write
 */
 public void write(byte[] buffer)
 throws IOException
 {
 write(buffer, 0, buffer.length);
 }
 /**
 * Capitalize the buffer provided.
 *
 * @param buffer - buffer to write
 * @param offset - where to start
 * @param length - number of bytes to write
 */
 static void toUpper(StringBuffer sb)
 {
 // loop through the new buffer, capitalizing any
 // alphanumeric that appears after a special
 // character
 boolean cap = true;
 int length = sb.length();
 for (int i = 0; i < length; i++)
 {
 char c = sb.charAt(i);
 // if we're supposed to cap this letter…
 if (cap)
 {
 // and if this is a letter…
 if (Character.isLetter(c));
 {
 // then capitalize and restore it
 c = Character.toUpperCase(c);
 sb.setCharAt(i, c);
 // okay, it's done
 cap = false;
 }
 }
 else
 {
 // (not in cap mode)
 // look for one of the special characters
 cap = (specialChars.indexOf((int)c) != -1);
 }
 }
 }
}


The CapFilterOutputStream class extends the FilterOutputStream class by overriding the write() method.

TIP
It is something of a Java standard that when you extend a class, the name of the new class should contain the name of the base class in addition to some prefix to indicate what the new class does. In this case, CapFilterOutputStream extends FilterOutputStream to provide capitalization. That way, a programmer who uses your class knows immediately what your class does and what class it's based on.
NOTE
The term extend, when used for class inheritance, is the Java equivalent of the colon in C++.

The CapFilterOutputStream class starts by defining a string of special characters. The program assumes that these characters divide words. As a result, the program capitalizes the first letter appearing after any one of these characters.

The CapFilterOutputStream class overrides the write(byte[], int, int) method to implement the capitalization. The write() method begins by converting the byte buffer passed to it into a StringBuffer. (There are several methods for character manipulation provided by the StringBuffer class.) Then it calls toUpper() to capitalize the StringBuffer and uses the write() method of its base class to perform the actual I/O.

The toUpper() method loops through the StringBuffer sb one character at a time. If the capitalization flag cap is set to true and the character is a letter, the character is converted to uppercase and restored to the StringBuffer. If the flag isn't set, the character is looked up in the set of specialChars. If the indexOf() method returns a -1, the c character isn't one of the special characters. If indexOf() returns anything other than -1, the c character is one of the special characters.

The output in Figure 1-15 shows the results of running Echo2.

Java Click to view at full size.

Screenshot-15. Output from the Echo2 program showing different options.

Reading Formatted Data

The standard Java FileInputStream and FileOutStream classes we have used up until now are ideal for reading in large blocks of data. Containing nothing more than the simple read() and write() methods, these classes are extremely limited in their formatting capability. The DataInputStream and DataOutputStream classes are provided for the purpose of extending formatting options. For example, the following code snippet can be used to read a floating point grade, an integer number of class hours, and a class name from a file named courses.txt.

FileInputStream fin = new FileInputStream("courses.txt");
DataInputStream din = new DataInputStream(fin);
float grade = din.readFloat();
int hours = din.readInt();
String title = din.readUTF();


NOTE

Since Java assigns 16 bits to each character in a string, Java methods do not normally have to worry about 1-, 2-, and 3-byte Unicode characters. The readUTF() method reads Unicode Text Format-8 (UTF-8) files that might have been written in another language such as C++ and converts the data into a Java string. Unfortunately, readUTF() reads to the end of the file. There is no DataInputStream equivalent to the C++ getLine() function, which reads until the end of the line.

Comments