I/O Package

From the programmer's standpoint, the WFC ms.com.wfc.io package consists of two families of classes: the IByteStream family and the IReader/IWriter family. The IByteStream family, which includes the file I/O class File, provides the basic read and write operations and is the more fundamental of the two. The IReader/IWriter family provides the extra features necessary for the manipulation of ANSI text.

Any Java program you want to perform WFC file I/O should import com.ms.wfc.io rather than the conventional java.io package. You should not try to import both the wfc.io and java.io packages in the same program since several classes have the same name in both packages—for example, the class File.

IByteStream Family

The most fundamental class in the IByteStream family is the File class, which is demonstrated in the following WFCEcho1 app.

NOTE
All the sample apps in this chapter are located in the Windows apps subfolder on the companion CD.

WFCEcho1

The following program is the WFC equivalent of the Echo1 program presented in Chapter 1. This program relies on the class File to handle all of its I/O needs.

/**
 * WFCEcho1 - copies the file args[0] to the file args[1]
 * using the WFC I/O package.
 */
import com.ms.wfc.io.*;
public class WFCEcho1
{
 // standard input and output static File in = File.openStandardInput();
 static File out = File.openStandardOutput();
 /**
 * 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)
 {
 print("Enter: wfcEcho1 source dest\n");
 print("to copy <source> to <dest>\n");
 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
 {
 // if the output file exists, make sure it's okay
 // to overwrite it
 okayToCopy(args[1]);
 // it's okay, so copy away
 File.copyOver(args[0], args[1]);
 }
 // catch any exception thrown
 catch(Exception e)
 {
 print("Error:" + e.getMessage() + "\n");
 }
 }
 /**
 * Check the argument to see whether it exists, and then
 * 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 Exception
 {
 // first check whether the file exists
 if (File.exists(outputFileName))
 {
 // it does, so check whether it's okay to overwrite it
 print("File exists. Overwrite? [Y or N]");
 String answer = read();
 if (!answer.equalsIgnoreCase("Y"))
 {
 throw new Exception("Copy failed");
 }
 }
 }
 /**
 * Print a message to standard output.
 * * @param outString - string to output
 */
 private static void print(String s)
 {
 out.writeStringCharsAnsi(s);
 }
 /**
 * Read a single character from standard input.
 */
 private static String read()
 {
 // keep reading until we see a character
 char[] cArray = new char[1];
 do
 {
 cArray[0] = (char)in.readByte(); } while(!Character.isLetter(cArray[0]));
 // convert the character into a string
 return new String(cArray);
 }
}


If you ignore for a minute the code to write messages to standard output and to read from standard input, the code to perform the actual echo function is simple. First, the File class provides a File.copyOver(src, dest) method that copies the src file to the dest file.

The only logic we have to provide in WFCEcho1 is the okayToCopy() method. Just like its predecessor in the Echo1 program in Chapter 1, okayToCopy() checks whether the destination file exists, and if it does, checks whether it's okay to write over the destination file. The File class makes even this job a little easier. The File.exists(name) method returns true if a file of name exists. Since the exists() method is static, it's not necessary to create a File object before calling its exists() method.

What is File, anyway?

When I'm trying to understand a class, I like to see where it fits in the package hierarchy. Figure 2-1 shows the chain of com.ms.wfc.io classes of which File is a member.

Java Click to view at full size.

Screenshot-1. The WFC I/O File class hierarchy.

You can see that File extends a class called DataStream. Much like the Java standard InputDataStream and OutputDataStream classes, the WFC DataStream class provides formatting capability. This class provides methods to output chars, ints, floats, doubles, and Strings as well as bytes. You will also notice that File has two brethren, the BufferedStream and MemoryStream classes. BufferedStream provides a buffering capability, and MemoryStream allows file I/O to a byte array in memory.

NOTE
The DataStream class, with its formatting capabilities, is much like the C++ ios class upon which most of the C++ iostream classes are built.

Interface-based packaging

The WFC package is interface-based, as opposed to other Java packages, which tend to be class-based. For example, notice that the File class extends the DataStream class; DataStream implements the IDataStream interface; and IDataStream in turn extends the interface IbyteStream. The methods of other WFC classes will (almost) never require a File object, or even a DataStream object. Instead, the methods of other classes require an object that implements the IDataStream interface or, more often, the IByteStream interface. Of course, you are free to pass a File object to such a method since the File object implements these interfaces. However, you might want to write your own class to implement one of these interfaces without basing your class on DataStream. This enables you to avoid the excess baggage of inheritance by implementing just those features required by the interface.

NOTE
A class implements an interface using the implements keyword. An interface looks like a class prototype declaration except that it does not have any data members. Implementing an interface represents a promise to provide a function in your class for each of the prototype declarations in the interface. See Appendix A for more details.
NOTE
Interface names under WFC always begin with a capital I. WFC avoids giving a class a name beginning with I. This makes interface names easy to pick out of a classy crowd.

Standard I/O with the File class

Notice how the WFCEcho1 program uses the class File to access standard I/O.

NOTE
We'll see later in this chapter that the IReader/IWriter group of classes is better for accessing standard I/O.

The public static methods File.openStandardInput() and File.openStandardOutput() return a File object that is tied to the program's standard input and output. The okayToCopy() method in WFCEcho1 defines two local functions that access these standard File objects: print(), which performs output, and read(), which performs input.

NOTE
Like a file class in other languages such as C++, the single WFC class File is capable of both input and output.

The print() function is straightforward enough. The method File.writeStringCharsAnsi() outputs the String class object in ANSI format as required by the system console. The similarly named method File.writeString() outputs the string in Java's Unicode 16-bit format.

The read() function is slightly more complicated. This function uses the File.readByte() method to read individual keystrokes until it sees a letter. Like File.writeString(), the File.readChar() method reads Java 16-bit Unicode characters that require two 8-bit keystrokes. Therefore, read() calls readByte() instead of readChar().

The read() function calls readByte() in a loop to read 8-bit characters (bytes) until it sees a character that is a letter. This loop is sort of a kludge: the isLetter() test makes sure that the program isn't confused by some stray carriage return or line feed left in the input buffer from a previous invocation of the program.

IReader/IWriter Group

The File class looks downright clumsy when it accesses the keyboard for input. This is because File is not designed to handle ANSI input and output. A group of classes that implement the IReader and IWriter interfaces is much more adept at performing text I/O.

WFCEcho2

In the following WFCEcho2 app, the okayToCopy() method is rewritten to use the proper IReader-based and IWriter-based classes. Only the okayToCopy() method is shown here because most of the remainder of the program has not changed. The entire WFCEcho2 app is on the companion CD-ROM.

 /**
 * Check the argument to see whether it exists, and then
 * 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 Exception
 {
 // first check whether the file exists
 if (File.exists(outputFileName))
 {
 // it does, so check whether it's okay to overwrite it
 Text.out.writeLine("File exists. Overwrite? [Y or N]");
 String answer = Text.in.readLine();
 if (!answer.equalsIgnoreCase("Y"))
 {
 throw new Exception("Copy failed");
 }
 }
 }


The first thing you will notice is the absence of the print() method. It has been replaced by a direct call to Text.out.writeLine(). The class Text is a placeholder for the public static members in, out, and err. Text.out is an object of class TextWriter that is tied to standard output. The TextWriter class is ready made to handle ANSI character output to the screen and to the printer through the writeLine() and write() methods. The writeLine() method tacks a newline character onto its output, whereas write() does not.

The TextWriter class, with its superior string-handling methods, is nice. However, the File class did okay in outputting ANSI characters; it was in performing ANSI character input that the File class fell down. The TextReader class is a big improvement over File for reading ANSI text strings. In WFCEcho2, I can use the single call to Text.in.readLine() to return a String object containing the entire line, without resorting to the tricks I had to use in WFCEcho1. The basic logic of the okayToCopy() method is identical to its predecessor.

Where do TextReader and TextWriter fit?

Screenshot-2 shows the hierarchy of the IReader and IWriter groups. The IReader interface defines the methods necessary to perform basic ANSI text output. The abstract class Reader implements these methods without knowing what type of object it is reading from. The concrete class TextReader extends the Reader class to read from a file. An underlying File class data member performs the output. Similarly, the class StringReader reads ANSI text from a Java String class.

Java Click to view at full size.

Screenshot-2. The hierarchy of the IReader and IWriter groups.

NOTE
The relationship between TextReader and Reader isn't unlike that of ifstream and ios in C++, with File playing the role of the C++ filebuffer. TextReader uses Reader to perform formatting chores and File to perform the actual input.

In a similar vein, the abstract class Writer implements the IWriter interface. The concrete TextWriter class uses the class File to extend Writer's formatting capabilities to files. StringWriter performs the same trickery on Java strings.

Combining the Two Groups

TextReader and TextWriter each provide a simple constructor that takes as input the name of the file to be read or written. These default constructors take care of the chore of creating a File object and storing it away locally. When using these constructors, however, you must be willing to live with these default creation and storage options.

Fortunately, you don't have to live with these default constructors. You can construct the File object yourself and pass the result to the TextReader or TextWriter constructor. This combination gives you the control provided by the File class with the advanced text handling capabilities of the TextReader and TextWriter classes.

WFCEcho3

The WFCEcho3 program demonstrates how to combine the File class with the TextReader and TextWriter classes. Just to prove that we haven't lost any capability when compared with the standard Java io classes, WFCEcho3 performs Echo2's trick of capitalizing each word in the provided text.

/**
 * WFCEcho3 - copies the file args[0] to the file args[1]
 * using the WFC I/O package. This time capitalize * the output (the way we did in Echo2) by extending
 * the TextWriter class and overriding write(String)
 * and writeLine(String).
 */
import com.ms.wfc.io.*;
public class WFCEcho3
{
 // class variables
 TextReader inText; // used to read the input file
 TextWriter outText; // used to write to output file
 /**
 * 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)
 {
 Text.out.writeLine("Enter: wfcecho3 source dest");
 Text.out.writeLine("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
 {
 // copy the file
 WFCEcho3 wfc = new WFCEcho3(args[0], args[1]);
 wfc.copyOver();
 }
 // catch any exception thrown
 catch(Exception e)
 {
 Text.out.writeLine("Error:" + e.getMessage());
 }
 }
 /**
 * Open the specified input and output files as Text files.
 * * @param in - name of input file
 * @param out - name of output file
 */
 public WFCEcho3(String in, String out)
 throws Exception {
 // first open the input file and convert
 // it into a TextReader
 File inFile = new File(in, FileMode.OPEN, FileAccess.READ);
 inText = new TextReader(inFile);
 // now the output file - prompt before overwriting
 File outFile = null;
 try
 {
 outFile = new File(out,FileMode.CREATE_NEW, FileAccess.WRITE);
 }
 catch(IOException e)
 {
 okayToOverwrite(out);
 outFile = new File(out, FileMode.CREATE, FileAccess.WRITE);
 }
 outText = new CapTextWriter(outFile);
 }
 /**
 * Copy the file one line at a time.
 */
 public void copyOver()
 throws Exception
 {
 // read a line at a time and then
 // write it back out; once we read a
 // null, we've hit end-of-file
 String s;
 while((s = inText.readLine()) != null)
 {
 outText.writeLine(s);
 }
 outText.flush();
 }
 /**
 * Make sure it's okay to overwrite the existing file. *
 * @param outputFileName - the file name to overwrite
 */
 private static void okayToOverwrite(String outputFileName)
 throws Exception
 {
 // prompt the user Text.out.writeLine("File exists. Overwrite? [Y or N]");
 String answer = Text.in.readLine();
 if (!answer.equalsIgnoreCase("Y"))
 {
 throw new Exception("Copy failed");
 }
 }
}


Just as in Echo2, this program creates a class object and then uses that object to perform the copy operation in the following two statements:

// copy the file WFCEcho3 wfc = new WFCEcho3(args[0], args[1]); wfc.copyOver();


constructor

The WFCEcho3 class constructor uses the following statement to create a TextReader object from a manually created File object:

File inFile = new File(in, FileMode.OPEN, FileAccess.READ);
inText = new TextReader(inFile);


The first argument, in, contains the name of the file to open. The second argument, FileMode.OPEN, says that we want to open an existing file. If the file doesn't exist, it won't be created and an IOException will be thrown. The class FileMode is a placeholder for all of the different mode constants. The third argument, FileAccess.READ, says that we are opening the file in READ mode (as opposed to in WRITE or READWRITE mode). Opening a file in READ mode allows other apps to read the file at the same time. An optional fourth argument uses the FileShare placeholder to specify whether it's okay to share the file with other computers on the LAN. The next line passes the constructed file object to the TextReader(File) constructor.

The following lines from WFCEcho3 create a TextWriter object from a manually generated File object:

File outFile = null;
try
{
 outFile = new File(out,FileMode.CREATE_NEW, FileAccess.WRITE);
}
catch(IOException e)
{
 okayToOverwrite(out);
 outFile = new File(out, FileMode.CREATE, FileAccess.WRITE);
}
outText = new CapTextWriter(outFile);


This code segment first tries to create a new file in write mode using the CREATE_NEW mode. In CREATE_NEW mode, the constructor throws an exception if the file can't be created, perhaps because it already exists. The WFCEcho3() constructor catches that exception and retries the request using the CREATE mode, if okayToOverwrite() approves. Unlike CREATE_NEW, CREATE will delete the output file if it exists.

The okayToOverwrite() method is identical to its predecessors.

Copying

WFCEcho3 implements its own copyOver() method, which copies the contents of the source file to the destination file. It does this by reading the TextReader object one line at a time using the readLine() function and writing each line out to the TextWriter object using writeLine(). The fact that readLine() strips the newline character at the end of each line is of no consequence because writeLine() compensates by adding a newline character.

The copyOver() method continues to read and write until the String returned by readLine() is a null, indicating that the function has hit the end of the file.

Once control has exited the loop, copyOver() still has one important task to perform. Before returning, copyOver() calls TextWriter.flush(). Both TextReader and TextWriter are buffered, meaning that text is accumulated into conveniently sized blocks to improve program performance. This is particularly important when reading something as small as individual lines from a text file. Calling flush() forces any buffered output to disk. If you forget to call flush(), you are likely to lose the last few bytes of your output.

Alternatively, you can call TextWriter.setAutoFlush(true). Setting autoflush to true forces TextWriter to flush its buffer after every call to writeLine(). While this will relieve you of the need to call flush(), the increased disk activity will most likely greatly reduce your app's performance.

NOTE
Remember to set autoflush to true for small output files or for output files that are not accessed often, or to call flush() at the end to force all output buffers to disk.

Text.out flushes the buffer by default. Thus, if you are performing large amounts of output to standard output you probably want to set autoflush to false.

NOTE
TextWriter doesn't support the C++ iostream equivalent tie(), by which the output file is automatically flushed when the program reads from the "tied" input file.

Capitalization

If you look more closely at the code snippet from the WFCEcho3 constructor, the output File object is used to create a CapTextWriter class rather than a TextWriter class.

The class CapTextWriter extends TextWriter by capitalizing any text passed to its write() or writeLine() method. This class appears in the following code:

/**
 * CapTextWrite - a TextWriter that capitalizes
 * output through write(String) or
 * writeLine(String).
 */
import com.ms.wfc.io.*;
public class CapTextWriter extends TextWriter
{
 String specialChars = " -.;\t\n";
 /**
 * Constructor.
 * * @param outFile - File object to use for output
 */
 public CapTextWriter(File outFile)
 {
 super(outFile);
 }
 /**
 * Write a line of text after capitalizing each word.
 * * @param s - the string to output
 */
 public void writeLine(String s)
 {
 super.writeLine(toUpper(s));
 }
 /**
 * Write a line of text after capitalizing each word.
 * * @param s - the string to output
 */
 public void write(String s)
 {
 super.write(toUpper(s));
 }
 /**
 * Capitalize the buffer provided.
 *
 * @param s - string to capitalize
 */
 String toUpper(String s)
 {
 // first convert the string into a string buffer
 StringBuffer sb = new StringBuffer(s);
 // loop through the new buffer, capitalizing any
 // alphanumeric character 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);
 }
 }
 // now convert that back into a string for output
 return sb.toString();
 }
}


Notice that this class is declared public. Being declared public means this class can be used by any other class that needs capitalization of output. It also means, however, that CapTextWriter must reside in its own CapTextWriter.java file.

Adding a file to your project

To create a new Java file and make it part of an existing project, activate Project Explorer and right-click the project name. From the context menu, select Add, and from the submenu, choose Add Class. Visual J Plus Plus will prompt you for the class name. In this case, providing the name CapTextWriter provides you with an empty Java source file and attaches that file to the project.

NOTE
Adding a file to the project ensures that the file is recompiled whenever the project is rebuilt and the file has changed.

If the Java file you want to add to the project already exists, select Add from the context menu. Choose Add Class, select the Existing tab, and select the Java file to add.

Converting text to upper case in CapTextWriter

The constructor for the CapTextWriter class does nothing more than pass its argument to the base class constructor.

NOTE
When used as a function, super() invokes the base class constructor. When used in this way, super() can be called only from the constructor and it must be the first line of the constructor.

CapTextWriter defines the method toUpper(String) to convert the String passed to it into an equivalent String but with each word capitalized. The toUpper() method uses the same logic as the toUpper() method in the Echo2 program. It first converts the input String into a StringBuffer object. StringBuffer is more efficient to use when you are modifying the text string often. In addition, the StringBuffer class provides the convenient charAt() and setCharAt() methods.

Just as in Echo2, toUpper() loops through each character in the StringBuffer object. If the cap flag is true and the character is a letter, toUpper() converts the character to upper case and writes it back into the StringBuffer object before setting cap to false.

If cap is false, toUpper() uses the String.indexOf() method to compare the retrieved character to a string of characters it knows to be word separators. If the index returned by indexOf() is -1, the character was not one of the separator characters and cap is set to false again. If the index is anything other than -1, the character was a word separator and cap is set to true.

The CapTextWriter class calls toUpper() on the String input. It then overrides the write() and writeLine() methods by calling super.write() and super.writeLine() to perform the actual output.

NOTE
When used in this way, super is a this pointer that has been converted to the current class's base class. Thus, if this is of class CapTextWriter, super points to the current object but is of class TextWriter. The absence of a super pointer in C++ forces the C++ programmer to refer to the base class by name from within the code. This is a source of errors when the base class changes. The Java programmer never has to refer to the base class except in the extends statement.

Outputting Different Object Types

The Writer class supports the output of various object types. This is demonstrated in the following code.

/**
 * WriteTest1 - outputs several types of objects
 * using the Writer class.
 */
import com.ms.wfc.io.*;
public class WriteTest1
{
 /**
 * The main entry point for the app. */
 public static void main (String[] args)
 {
 TextWriter out = Text.out;
 out.setAutoFlush(true);
 out.writeLine("This is a String");
 out.write("This is an int = ");
 out.writeLine(10);
 out.write("This is a double = ");
 out.writeLine(10.10);
 out.write("This is a Student = ");
 out.writeLine(new Student("Jenny", "Davis", 12, 3.5));
 try
 {
 Thread.currentThread().sleep(2000);
 }
 catch(Exception e)
 {
 }
 }
}
class Student
{
 String firstName;
 String lastName;
 int semesterHours;
 double gradePointAverage;
 Student(String firstName,
 String lastName,
 int semesterHours,
 double gradePointAverage)
 {
 this.firstName = firstName;
 this.lastName = lastName;
 this.semesterHours = semesterHours;
 this.gradePointAverage = gradePointAverage;
 }
}


Both write() and writeLine() are overloaded for each of the intrinsic variable types plus String and Object. Providing a writeLine(Object) allows the Student object created in the above example to be passed to writeLine(). However, as you can see in Figure 2-3, the output is not what you might expect.

Java Click to view at full size.

Screenshot-3. The output from writeLine(Object) is disappointing.

NOTE
The call to sleep() delays the program long enough to allow the user to see the output before the program terminates and the MS-DOS window is closed. This delay isn't necessary when you execute the program from the command line.

Extending writeLine()

Fortunately, writeLine(Object) calls Object.toString() to create a string it then passes to writeLine(String). This means that by overriding the toString() method you can extend writeLine() to output any new class you might define.

The following code snippet from the WriteTest2 example shows Student extended by the addition of a toString() method. Figure 2-4 shows the output from WriteTest2.

class Student
{
 String firstName;
 String lastName;
 int semesterHours;
 double gradePointAverage;
 Student(String firstName,
 String lastName,
 int semesterHours,
 double gradePointAverage)
 {
 this.firstName = firstName;
 this.lastName = lastName;
 this.semesterHours = semesterHours;
 this.gradePointAverage = gradePointAverage;
 }
 public String toString()
 {
 StringWriter sw = new StringWriter();
 sw.write(lastName + ", " + firstName);
 sw.write(" - " + gradePointAverage + "/" + semesterHours
 + " [GPA/semesterHours]");
 return new String(sw.getStringBuffer());
 }
}


Java Click to view at full size.

Screenshot-4. The output from writeLine(Object) with Student.toString() added. Comments