HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

In the previous two parts of this three-part article (JDJ Vol. 3, Issue 12 and JDJ Vol. 4, Issue 1), we looked at the fundamentals of programming with Java I/O streams and the various APIs they can be used with. This month we'll conclude this article by discussing the concept of writing custom (or, specialized) stream classes that can process (or, filter) data in a special fashion.

Overview
The JDK provides several specialized stream classes, the majority of which exist in the java.io package. These classes provide a variety of functionality and usually come in pairs - that is - one for reading data and the other for writing data. For example, the java.io.FileReader and java.io.FileWriter provide the capability for file reading and writing, while the java.io.StringReader and java.io.StringWriter provide the capability to use java.lang.String as a source for reading data from and writing data to.

JDK Stream Classes
Before you decide to write your own specialized stream classes, it would be worth your while to inspect the stream classes provided in the JDK. There are two reasons for this. First, there may already be a stream class available for your purpose. Second, you can use their source and documentation as examples for writing your own.

To give you a general idea of the various streaming classes available in the JDK, Table 1 shows a complete list (excluding deprecated classes) of Stream classes available in the java.io package in JDK 1.2. Table 2 shows some additional stream classes available in the java.util.zip and java.util.jar packages.

For more information refer to the JDK 1.2 documentation at http://java.sun.com.

Why Write Your Own Stream Classes?
With such a variety of I/O stream classes available in the JDK, you might be wondering why anyone would ever need to write their own stream classes. Well, there are several reasons why, but it's mainly because your application might require the processing of a data stream in a special way for which the JDK has no sufficient classes.

The following are a few possible candidates for specialized stream classes.

  • Accessing Special Storage Devices - With so many storage devices available on the market (such as tape and zip), there may be a need to access them from a Java application.
  • Spell Checker - Java apps could use specialized stream classes that spell-check data in a stream and give back a list of misspelled words.
  • Language Converter - A stream subclass could be written to convert words in an input or output stream from one language to another.
  • Text Search Or Screening - Specialized stream classes could be used to search for specific words and obtain their locations in a data stream. Example uses of such classes could include a find feature in a text editor or the ability to block out certain content (e.g. adult content) in a proxy server.
So, how does one go about writing a specialized stream class?

How To Write Specialized Stream Classes
Writing your own stream classes is relatively simple: for reading character streams, you basically extend the java.io.Reader class (or java.io.FilterReader); for writing character streams, you extend the java.io.Writer class (or java.io.FilterWriter). For byte streams, you extend the java.io.InputStream (or java.io.FilterInputStream) for reading streams; and java.io.OutputStream (or java.io.FilterOutputStream) for writing streams.

Note: To get a better understanding of the differences between byte and character streams, please refer to the first installment of this three-part article (JDJ Vol. 3, Issue 12).

Filter vs Top Level Stream Classes
When developing specialized stream classes you can choose to either directly subclass the top level classes (InputStream, OutputStream, Reader, Writer) or you may subclass the Filter classes (InputFilterStream, OutputFilterStream, FilterReader, FilterWriter).

The filter stream classes seem a bit superfluous at times. However, they were originally designed for convenience because they provide default implementations for all the methods found in the top level classes; this way, you override only the methods you need to. For example, if you extend InputStream directly, you'd need to override the flush() and close() methods, even though they're not abstract methods. This is necessary because these methods have empty bodies in the InputStream class. On the other hand, when you extend the FilterInputStream class, you generally end up providing implementations for two read() methods (versus one if you extended InputStream directly).

Developing Specialized Character Stream Classes
To develop specialized character stream classes, you either subclass the Reader class for reading data or the Writer class for writing data. To develop a Reader subclass, you must override and provide implementations for the following methods:

  • int read(char[] cbuf, int off, int len)
  • void close()
To develop a Writer subclass you must override and provide implementations for the following methods:
  • void write(char[] cbuf, int off, int len)
  • void close()
  • void flush()

Developing Specialized Byte Stream Classes
To develop specialized character stream classes, you either subclass the InputStream class for reading data or the OutputStream class for writing data. To develop a InputStream subclass you must override and provide implementations for the following method:

int read()

To develop a OutputStream subclass, you must override and provide implementations for the following method:

Void write(int b)

Examples Of Specialized Stream Classes Outside the JDK
Now that you have a general idea of the variety of reasons for which stream classes can be written, it would serve you well to look at some concrete examples of specialized (non-JDK) stream classes.

Simple Examples
The following two examples, TeeOutputStream and CountReader, provide simple examples of writing specialized stream classes. Later in this section, you'll see a more robust, real-world example using Divya's BackOnline commercial application.

TeeOutputStream
The class in Listing 1, TeeOutputStream, provides an example of an OutputStream subclass. The purpose of this class is not only to write the bytes to the chained output stream, but also to write them to another output stream such as the System.out device, or to a trace/debug file for debugging purposes. The functionality provided by this class is similar to the Unix tee command.

The code should be fairly easy to follow. Basically, this class overrides the write(int c), flush() and close() methods of its parent class, OutputStream. Additionally, this class provides a main() method for "self-testing" this class. The write() method performs the task of writing the data to two streams.

CountReader
The class in Listing 2, CountReader, provides an example of a Reader subclass. This class counts the number of characters and lines in an input stream of characters. At any point in reading the stream the get*Count() methods can be used to obtain the character, word and/or line count for the data read up to that point. The functionality provided by this class is similar to the Unix wc command.

As with the TeeOutputStream class, this class should also be easy to follow. The key method in this example is read(), which handles the task of counting characters, words and lines.

A Real World Example: Divya's 100% Pure Java Backup Application
Divya's 100% pure Java applet/application - BackOnline - is an Internet-based backup, briefcase and archival application that backs up a user's data files from the client machine to a server, as can be seen in Figure 1.

Figure 1
Figure 1:

BackOnline makes extensive use of streams - everything from File Input/Output streams to Socket streams to it's own specialized Protocol and Encryption/ Decryption streams. When a user backs up a file (or files) in BackOnline, the file is processed via a compression stream, then an encryption stream, then sent over the wire on a socket stream. The BackOnline server receives this data using socket streams and stores it in a file using file streams. For restoring files, this process simply works in reverse. The streams in use during the backup and restore processing are shown in the Figure 2.

Figure 2
Figure 2:

Though most of the stream classes used in BackOnline are available in the JDK (such as socket, file and GZIP compression), there are also two pairs of specialized stream classes written by Divya; one for the protocol used by the BackOnline client and server to communicate with each other, the other for encrypting and decrypting the stream using 56-bit DES encryption.

Protocol Stream Classes
BackOnline uses a protocol almost identical to HTTP version 1.1. Each client request (such as file backup) contains a header followed by a blank line followed by the data for that request. The stream classes automatically handle the separation of the header and data portions in the stream and provide the appropriate methods to obtain the header values and read() methods to read the actual data.

The Java source for the ProtocolInputStream class (used for reading in the protocol stream) is shown in Listing 3.

Encryption Stream Classes
When a user logs in to BackOnline, he is provided with a userid/password for authentication, as well as an optional encryption key (sort of like a second password), which is used to encrypt and decrypt the stream. In other words, BackOnline uses a key/password-based encryption/decryption scheme. Since this key is always entered by the user and is never physically stored anywhere, it provides an extremely secure mechanism for transmitting and storing the user's data.

The Java source code for the CryptOutputStream class (used for writing an encrypted stream) is provided in Listing 4. While perusing this code, pay special attention to the write, flush and close methods as they override their parent stream class' methods.

After looking at the source code for the TeeOutputStream, CountReader, ProtocolInputStream and CryptOutputStream classes, you should have a very good idea of how to write your own stream classes.

Summary
Although by now you might have an in-depth understanding of how the Java I/O stream classes work, it'll still be worth your while to peruse the JDK 1.2 source code and documentation for the various stream classes, especially the classes in the java.io package (both can be downloaded from http://java.sun.com). Remember, while I/O streaming isn't the most glamorous subject when compared to other Java technologies, it is, in a sense, the backbone of Java and Java applications since they're used in so many APIs. Hence, a good understanding of them is essential.

About the Author
Anil is a senior consultant at Divya Incorporated, a consulting firm specializing in Java/Internet software solutions. Anil provides Java/Internet-based architecture, design and development solutions to Fortune 500 companies, and occasionally writes articles and speaks at conferences. He can be reached at [email protected]

	

Listing 1: TeeOutputStream.java

import java.io.* ; 


public class  TeeOutputStream
    extends OutputStream
{
  OutputStream tee = null, out = null;


  public TeeOutputStream(OutputStream chainedStream, OutputStream teeStream)
  {
    out = chainedStream;

    if (teeStream == null)
      tee = System.out;
    else
      tee = teeStream;
  }
 

  /**
   * Implementation for parent's abstract write method. This writes out the
   * passed in character to the both, the chained stream and "tee" stream.
   */
  public void write(int c) throws IOException
  {
    out.write(c);

    tee.write(c);
    tee.flush();
  }


  /**
   * Closes both, chained and tee, streams.
   */
  public void close() throws IOException
  {
    flush();

    out.close();
    tee.close();
  }


  /**
   * Flushes chained stream; the tee stream is flushed each time a character
   * is written to it.
   */
  public void flush() throws IOException
  {
    out.flush();
  }



  /** Test driver */
  public static void main(String args[]) throws Exception
  {
    FileOutputStream fos = new FileOutputStream("test.out");
    TeeOutputStream tos = new TeeOutputStream(fos, System.out);
    PrintWriter   pw = new PrintWriter(new OutputStreamWriter(tos));

    pw.println("Testing line 1");
    pw.println("Testing line 2");

    pw.close();
  }
}

Listing 2: CountReader.java

import java.io.* ;


public class  CountReader
    extends Reader
{
  Reader in = null;
  int read = 0, charCount=0, wordCount = 0, lineCount = 0;
  boolean whiteSpace = true;


  public CountReader (Reader r)
  {
    in = r;
  }
  
  
  /**
   * Implementation for parent's read method. Counts chars, words, and
   * lines.
   */
  public int read(char[] array, int off, int len) throws IOException
  {
    if (array == null)
      throw new IOException("Null array");

    // Do actual read
    read    = in.read(array, off, len);

    // Now count
    charCount += read; // Increment character count
    char c;

    for (int i=0; i < read; i++)
    {
      c = array[i];

      // Line count
      if (c == '\n')
        lineCount++;

      // Word count
      if (Character.isWhitespace(c))
       whiteSpace = true;
      else
      if (Character.isLetterOrDigit(c) && whiteSpace)
      {
       wordCount++;
       whiteSpace = false;
      }
    }

    return read;
  }


  public void close() throws IOException
  {
    in.close();
  }


  public int getCharCount() { return charCount; }
  public int getWordCount() { return wordCount; }
  public int getLineCount() { return lineCount; }



  /** Test driver */
  public static void main(String args[]) throws Exception
  {
    CountReader cr = new CountReader(new FileReader("CountReader.java"));
    char c[]    = new char[4096];
    int read    = 0;
 
    while ((read = cr.read(c, 0, c.length)) != -1)
      System.out.print(new String(c, 0, read));

    cr.close();

    System.out.println("\n\nRead chars: " + cr.getCharCount() +
               "\n   words: " + cr.getWordCount() +
               "\n   lines: " + cr.getLineCount());
  }

}




  
      
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.