Chapter 8

Utility Classes


CONTENTS


utility \yü-'ti-l  -te-\ n: something useful or designed for use

Introduction

During the process of creating any type of software, one usually ends up with many useful utilities. These utilities can range from simple functions to complex objects or data structures. Centralizing them where they are accessible to the entire application and can easily be moved to other projects is essential. Even Java has a utilities package called java.util; no doubt you are familiar with it.

Java intranet applications are no different. You'll develop many utilities during your development cycle. The utilities you create will be Java classes, however. This chapter will serve as a starting point for your utility class creation. In it you will find several classes that are quite useful for constructing intranet applications. You'll then learn how to keep all your classes together to easily use them again.

The types of utility classes that we'll explore in this chapter follow:

Timers

A timer is a software mechanism designed to alert the program and programmer when a specified interval of time has elapsed. This mechanism is commonly used in all sorts of applications. A specific example of timer usage is in intranet database applications. You can use a timer to notify you whether certain events have or have not occurred.

For example, consider connecting to a database server. When your application attempts to connect with the server, your program and user must wait until the connection has been established. But how long do you wait? What if the server is down? What if the connection between your computer and the server is down? Many problems can occur, but if your program can be notified when a certain amount of time has elapsed, it can take action.

Another example of timer usage is for application monitoring. Perhaps you'd like to close any open server connections after a period of inactivity. When you set and reset a timer after certain events occur, your program can be notified of any inactivity.

Timer Operations

Timers work just like an alarm clock, which goes off when the alarm time has been reached. If you don't change the alarm time, the alarm will go off again in 24 hours.

Alarm clocks are far less versatile than timers, however. You cannot set the interval at which the alarm will sound; you're stuck with a 24-hour interval. If you want to set another time, you must constantly reset the alarm time. On the other hand, timers allow you to specify the interval of time between alarms, allowing you to generate constant alerts or signals to your program. You can then use these alerts however you'd like.

And how does your program receive these timer alerts? Your program can receive timer alerts in one of two ways: from a callback or from a Java event.

Callbacks

Java does not support the notion of pointers. With pointers, a conventional 3GL feature, you can pass the address of a method around in your program or from process to process so that other parts of your program, or other processes even, can call your method. This practice is commonly called a callback.

A callback is a way in which a program can register for notification when certain events occur. A real-world example of this is when you go to your book store and order a book that is not in stock. The clerk takes your name and phone number. Two weeks later, the store calls you on the telephone to notify you that your book has arrived. You've been called back.

A similar process takes place in programming. When certain programmatic or system events occur, you want to be notified of them. In other development environments, the callback is achieved by using pointers to methods. When you register for your notification, you inform the registrar of the method to call. The system then actually calls your method when the event occurs.

Look Mom, No Pointers!

As you've learned, Java has no pointers and, therefore, no method pointers. So how can you possibly create a callback mechanism? Easily-by using an interface. An interface in Java is like a template or hollow shell for classes. When a class implements an interface, it must "fill in" the methods defined in the interface.

When you define an interface for callbacks, any class that wants to receive them can implement this interface. The method that the receiving class implements is actually called by the timer object. Listing 8.1 shows an interface called TimeOut.


Listing 8.1. The TimeOut interface.
//****************************************************************************
//* TimeOut                                         &nb sp;                        *
//****************************************************************************

public interface
TimeOut
{

//****************************************************************************
//* timeOut                                         &nb sp;                        *
//****************************************************************************

    public void
    timeOut( CallbackTimer timer );

}

The TimeOut interface defines a single method: timeOut(). A timer class can use this method to notify the user that a timer event has occurred. The next section explores a class called CallbackTimer, which uses this interface to implement a callback timer.

CallbackTimer

The CallbackTimer class uses the TimeOut interface to facilitate notification of timer events. When you create a CallbackTimer, you simply pass in the object that is the recipient of the notification as an argument. This passed-in object must be an implementor of the TimeOut interface. In addition, you must pass in the amount of time between notifications. The time interval is in milliseconds.

Tip
Milliseconds are 1/1000ths of a second; there are 1,000 milliseconds per second. Trying to keep seconds and milliseconds straight can be annoying, especially if your program uses both types at once. To help you remember, name your argument something that reminds you of the type. In our class, the argument is called msInterval.

Listing 8.2 shows the constructor of the CallbackTimer. The instance variables myTarget and myInterval hold the values for the rest of the object to use.


Listing 8.2. The CallbackTimer constructor.
//****************************************************************************
//* CallbackTimer                                        &nb sp;                   *
//****************************************************************************

    public
    CallbackTimer( TimeOut target, int msInterval )
    {
        //    Save my values...
        myTarget = target;
        myInterval = msInterval;

        //    Indicate that this is not a user thread...
        setDaemon( true );
    }

The CallbackTimer class extends Java's Thread class. The reason for this is twofold. First and foremost, it is simple to implement (the best reason!). The Thread class provides much of the functionality needed for a notification process.

Second, with the CallbackTimer, you can create several timers and turn them off and on at will. After you create them, you can (and must) start() your timer, and whenever you want, you can stop() it. These methods are inherited from the Thread base class.

Note the last line of the constructor:

setDaemon( true );

This Thread method tells the Java system that this thread is not a user thread. That means that the Java engine will not stop running until all user threads have died. If you have timers on in the background, your program will not end until you've stopped them all. The setDaemon() method informs the Java system that this thread is not a user thread. Therefore, if only nonuser threads are left running (that is, only timers), the system will stop your program immediately.

Implementing the callback mechanism is simple. Listing 8.3 shows the run() method from our CallbackTimer class.


Listing 8.3. The run() method from the CallbackTimer.
    public void
    run()
    {
        //    Do this forever!
        while ( true )
        {
            try
            {
                //    Wait until next interval...
                sleep( myInterval );
            }
            catch ( InterruptedException ie ) {}

            //    Notify the target...
            myTarget.timeOut( this );
        }

Because the target of the callback implements the TimeOut interface, you are guaranteed that it will have a method called timeOut(). This way, you can call it when the timer goes off.

Let's create a Java program to illustrate the callback timer concept. The following fragments are part of the TimerTester application. The full source code, which can be found on the CD-ROM, creates 16 timers and displays the effect of them going off in 16 different windows. Let's walk through each part individually.

First, the declaration:

//****************************************************************************
//* TimerTester                                          ;                     *
//****************************************************************************

public class
TimerTester
extends Frame
implements TimeOut

As you can see, you've implemented the TimeOut interface so that you can receive timeOut() callbacks.

You need a few instance variables for your program. The declarations follow:

//****************************************************************************
//* Members                                         &nb sp;                        *
//****************************************************************************

    CallbackTimer[]        timers = new CallbackTimer[ 16 ];
    TextArea[]                showers = new TextArea[ 16 ];

The first variable, timers, is an array of 16 CallbackTimers. The second variable is showers (not "showers"; "show-ers"). This array of 16 TextArea components will show information about the 16 timers you will create.

Your next constructor follows:

//****************************************************************************
//* Constructor                                          ;                     *
//****************************************************************************

    public
    TimerTester()
    {
        super( "Timer Tester!" );

        //    Create a panel for our grid display
        Panel p = new Panel();
        p.setLayout( new GridLayout( 4, 4 ) );

        //    Create some showers & timers...
        for ( int i = 0; i < 16; i++ )
        {
            showers[ i ] = new TextArea( 5, 10 );
            timers[ i ] = new CallbackTimer( this, 1000 * ( i + 1 ) );
            p.add( showers[ i ] );
            timers[ i ].start();
        }

        //    Add the timer panel to the frame...
        add( "Center", p );

        //    Pack and show the panels...
        pack();
        show();
    }

You've done several things in this constructor. Let's take a look at them step by step:

  1. Call your base class constructor. First you must call your base class's constructor to initialize your base class's instance variable.
  2. Create a Panel with a GridLayout. The grid is set at 4 rows high by 4 columns wide, which will lay out your 16 TextAreas in a nice grid format.
  3. Next, create the array elements for your instance variables. The best way to do this is with a loop. You use a for loop to create each component. The interval for the timer is set to the iteration count times 1000. This gives you 16 timers ranging in intervals from 1 to 16.
  4. Also within the loop, you add the newly created TextArea to your GridLayout and start up each timer.
  5. Then add the panel to the center of the Frame.
  6. Finally, pack() the layout and show() the window.

The final important part of the TimerTester example is the timeOut method that implements the TimeOut interface, as shown in the following code.

//****************************************************************************
//* timeOut                                         &nb sp;                        *
//****************************************************************************

    public void
    timeOut( CallbackTimer whichTimer )
    {
        for ( int i = 0; i < TIMER_COUNT; i++ )
        {
            if ( timers[ i ] == whichTimer )
            {
                showers[ i ].appendText( "I ticked!\n" );
                return;
            }
        }
    }

}

The timeOut method is called when a timer goes off. In the example, however, you have 16 different timers. How do you know which one you're dealing with?

You use a for loop to compare each element in your array with the argument passed. The CallbackTimer class sends itself as an argument to the timeOut method. This way you can find out which timer it is.

Note
The alternative and much more object-oriented method of displaying the timer results in a window creates a new class that extends the TextArea component. This class could have a CallbackTimer as an instance variable set at construction. That way you are dealing with only a single timer and its display is itself.

Figure 8.1 shows the TimerTester program in action.

Figure 8.1 : The TimerTester program output.

Event Timers

Another method of receiving timer notifications is by using the Java event system, provided to you by the EventTimer class.

The EventTimer Class

The EventTimer class is similar to the CallbackTimer class in that it is also a timer. The notification method is completely different, however. The EventTimer class sends an ACTION_EVENT event to its owner when the timer goes off. What follows is the same example given earlier, except it uses the EventTimer.

The EventTimer declaration is similar to the CallbackTimer TimerTester example program except that classes using the EventTimer class do not need to implement the TimeOut interface. This is because the class that owns the EventTimer object will receive notifications through the Java event system. The following code is the declaration for the EventTimerTester program.

//****************************************************************************
//* EventTimerTester                                                          *
//****************************************************************************

public class
EventTimerTester
extends Frame

Notice that the preceding code does not implement any interfaces.

The constructor method of the EventTimerTester program is identical to that of TimerTester. In fact, the entire program is pretty much identical to the TimerTester program except that instead of receiving notifications via the timeOut() method, the EventTimerTester program receives its notifications via the action() event handler. The source code follows:

//****************************************************************************
//* action                                         &nbs p;                         *
//****************************************************************************

    public boolean
    action( Event event, Object arg )
    {
        //    Locate the timer that has ticked...
        for ( int i = 0; i < TIMER_COUNT; i++ )
        {
            //    Is this the one?
            if ( timers[ i ] == arg )
            {
                //    Show it in the window...
                showers[ i ].appendText( "I ticked!\n" );
                return( true );
            }
        }

        return( false );
    }

Why Have Two Timers?

You might wonder why you're using two timer classes if one is enough. One is enough, but sometimes you might want the flexibility of two.

For example, the CallbackTimer is excellent for use within other classes. If you have a class that requires some sort of timer functionality, a CallbackTimer as an instance variable is a cool thing. The timer is fully integrated into your object and the consumer or user of your object does not need to worry about using it.

The EventTimer is excellent for on-the-fly timers. It's also good for adding timers to existing code when you don't want to modify the implementations of some classes. It can be used as a plug-and-play object.

And, because the bytecode for the two timers together is about 2KB, I'm not too worried about insufficient disk space.

Java Extensions

The second half of this chapter is devoted to Java extension classes. These classes extend, or augment, the functionality that the core Java classes provide. Although this base functionality is useful, you might like these objects to perform other tricks for your intranet applications. Two classes that you extend here are the Date and Properties classes.

Extending Java's Date Class

The first class you'll extend is Java's Date class. Although this class has many features, let's add a few more that will help in building intranet applications. The big change here is a string formatting method. You can configure this method in terms of how it is constructed and which delimiters to use. You'll call your new class FileDate because the augmentations will provide nice features when you create text files on disk.

To make this class robust and able to handle a variety of date formats, you need to create input and output methods. These methods will be the central point for date parsing and date string construction, giving you a single point to make changes if you ever need to implement new date formats or remove an existing format.

Parsing the Date Within a String

Java's Date class comes complete with a method called parse() to strip apart a string and convert it into a date. Although this method can handle several formats, it is very specific about which formats will work. If you look at the actual source code to the Date.parse() method in the JDK, you'll see that it is very difficult to follow. You'll take a much simpler approach. For your applications, you want to parse dates that are in the format of MM<d>DD<d>YY[YY], where the <d> is a delimiter of the user's or programmer's choosing.

To quickly and easily parse the date from a string, you can use Java's StringTokenizer class. This breaks up the date string into tokens and you can then easily digest and regurgitate these tokens into anything you'd like. The following method, valueOf(), takes a string representing a date and parses it into its three components: month, day, and year. It looks like this:

//****************************************************************************
//* valueOf                                         &nb sp;                        *
//****************************************************************************

    public static FileDate
    valueOf( String s )
    throws IllegalArgumentException
    {
        StringTokenizer     st = new StringTokenizer( s, "/.-," );
        String                 ms, ds, ys;

        try
        {
            ms = st.nextToken();
            ds = st.nextToken();
            ys = st.nextToken();
        }
        catch ( java.util.NoSuchElementException e )
        {
            throw new IllegalArgumentException();
        }

        int m = Integer.parseInt( ms );
        int d = Integer.parseInt( ds );
        int y = Integer.parseInt( ys );

        //    Convert four digit year to Java year...
        if ( y > 1900 )
            y -= 1900;

        return( new FileDate( y, m - 1, d ) );
    }

The method is declared static so that you can use it as a conversion method. With conversion methods, you can use features of a class without actually instantiating it. A good example of this is Java's Math class. All of its methods are static and all are called by prepending the class name (Math) to the method. Another example of a static method is used in the preceding parsing routine. When you use the parseInt() method of the Integer class, you are calling a static method.

Because this method creates and returns a new object, you'll need to throw the IllegalArgumentException if you are given bad data. These are commonly called factory methods because they build objects.

What Does static Really Mean?
When you declare a method or instance variable static in a Java class, you tell the compiler that that method or variable is the only copy for all instantiations of that class. This allows some special privileges and revokes some as well. For example, while gaining the ability to perform operations without actual instantiation, you can't reference any non-static member of your class. This can be annoying at times.
The static class methods are well suited for conversion or factory functionality and static instance variables are excellent for counters. You could create an instance counter that increments each time your class is created and decrements when the class is destroyed. If you declare your counter static, only a single copy will exist for all instances of your class.

The next step is to create a StringTokenizer for the string that was passed into your method. This tokenizer should recognize the following delimiters for dates: "/", "-", ".", or ",". You specify this as the second argument in the StringTokenizer's constructor. You then attempt to pull out three tokens from the tokenizer. If this fails, you know you don't have a good date, and you throw your exception.

After you've received three good tokens from your main string, you simply convert them to integers. These integers are used to create a new instance of FileDate, which is returned.

Constructing a String with a Date

The second important method in this class is the output method, which will be responsible for creating a String from the date and returning it to the caller. This method must be able to handle a variety of formats easily and without too much effort. The more code you write, the more likely you are to make errors.

First, you need to establish the date formats that you'll support. Use three major formats: MDY, DMY, and YMD. MDY is the standard U.S. date format. DMY is common in Europe and favored by some in the U.S. The last format, YMD, can be used to format dates in a manner that will sort alphabetically. In addition to these formats, the option of returning the year in four digits must be available.

You also want your date formatter to return the month name instead of the month number. This can be used when talking to Oracle databases, for instance. The default date format accepted by Oracle is DD-MMM-YY[YY].

You'll handle these formatting options with constants. Constants in Java are declared final and static. Your declarations follow:

    public final static int        MDY = 0;
    public final static int        DMY = 1;
    public final static int        YMD = 2;

    public final static int        MMMDY = 3;
    public final static int        DMMMY = 4;
    public final static int        YMMMD = 5;

    public final static int        MDYYYY = 6;
    public final static int        DMYYYY = 7;
    public final static int        YYYYMD = 8;

    public final static int        MMMDYYYY = 9;
    public final static int        DMMMYYYY = 10;
    public final static int        YYYYMMMD = 11;

These constants mimic the order of the dates they represent. This is easy to remember from a coding standpoint and easy to debug if there is a problem. The constants with YYYY in them represent four-digit years. The constants with MMM in them represent the non-numeric months.

In one instance variable, you need to format the month into a string. This is an array to hold the names of the months, which is declared as follows:

static String                monthNamess[] = { "JAN", "FEB", "MAR", "APR",
    "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };

This array is easily accessed with a month variable. It is declared static because, as you know, month names never change.

Implementing this formatter is simple. You'll take the year, month, and day and create strings of them, padding them as necessary. When you have the three elements as strings, the rest is a cake walk. Depending on the format, you concatenate the three parts together, separated by the delimiter selected. The final result is returned to the caller.

The source code for your formatter method follows:

    public static String
    formatDateToString( int y, int m, int d, String delimiter, int fmtOpt )
    {
        String    ms, ys, ds;

        //    Build the month and day strings...
        ms = ( m < 10 ) ? "0" + Integer.toString( m ) : Integer.toString( m );
        ds = ( d < 10 ) ? "0" + Integer.toString( d ) : Integer.toString( d );

        //    Convert the year to four digit if requested...
        if ( fmtOpt == MDYYYY || fmtOpt == DMYYYY || fmtOpt == YYYYMD ||
            fmtOpt == MMMDYYYY || fmtOpt == DMMMYYYY || fmtOpt == YYYYMMMD )
        {
            if ( y < 1900 )
                y += 1900;
        }
        else
            //    Force it to two digits...
            y %= 100;

        //    Finally build the year string...
        ys = ( y < 10 ) ? "0" + Integer.toString( y ) : Integer.toString( y );

        //    Build the return string...
        switch ( fmtOpt )
        {
            case MDY:
            case MDYYYY:
                return( ms + delimiter + ds + delimiter + ys );

            case MMMDY:
            case MMMDYYYY:
                return( monthNames[ m - 1 ] + delimiter + ds +
                    delimiter + ys );

            case DMY:
            case DMYYYY:
                return( ds + delimiter + ms + delimiter + ys );

            case DMMMY:
            case DMMMYYYY:
                return( ds + delimiter + monthNames[ m - 1 ] +
                    delimiter + ys );

            case YMD:
            case YYYYMD:
                return( ys + delimiter + ms + delimiter + ds );

            case YMMMD:
            case YYYYMMMD:
                return( ys + delimiter + monthNames[ m - 1 ] +
                    delimiter + ds );
        }

        //    Unknown format option? Return MDY...
        return( ms + delimiter + ds + delimiter + ys );
    }

This method has been declared static so that it can be used as a conversion function or by an instantiation of the class.

One feature available in other languages but not in Java is a string formatting function. For you C and C++ programmers out there, I'm talking about printf functionality. With these functions, you usually can create a string of placeholders for various elements. The placeholders define the size of the substring in addition to padding. A set of arguments to put into those placeholders follows all of this. Unfortunately, Java does not have the capability to accept an unknown number of arguments to a function, which is required to provide general string formatting.

Java's String class is an awesome beast, however. It is well implemented and supports the + operator for concatenation. Although it's not the perfect solution, it will fit your needs just fine. By checking to see whether the month or day is less than 10, you know to add a leading 0.

Tip
Notice that when the requested year is not in four digits, you modulus the value with 100. This is a neat and necessary little trick to always keep your year in the range of 0 to 99. When you reach the year 2000, the Java Date will return 100 or more because the Java Date class represents the year as an offset from 1900. Any year over 1999 will result in a number greater than 100.
By using the modulus operator, you can ensure that your software will work well into the 21st century.

You also need to provide some alternative access methods to the formatDateToString() method. I have created some, but you might want to create more. Those that I have created follow.

This default date formatter will create a date of MM/DD/YY from the current value of the FileDate object:

    public String
    formatDateToString()
    {
        return( formatDateToString( "/", MDY ) );
    }

You might select the delimiter and formatting option, however, which is similar to the default:

    public String
    formatDateToString( String delimiter, int fmtOpt )
    {
        return( formatDateToString( getYear(), getMonth() + 1, getDate(),
            delimiter, fmtOpt ) );
    }

This default date formatter will create a date of MM/DD/YY from the value parsed from string s:

    public static String
    formatDateToString( String s )
    {
        return( formatDateToString( s, "/", MDY ) );
    }

This method parses string s and selects a delimiter. It defaults to MDY order:

    public static String
    formatDateToString( String s, String delimiter )
    {
        return( formatDateToString( s, delimiter, MDY ) );
    }

This method parses string s and selects a delimiter and an order:

    public static String
    formatDateToString( String s, String delimiter, int fmtOpt )
    {
        FileDate date;

        try
        {
            date = FileDate.valueOf( s );
        }
        catch ( IllegalArgumentException e )
        {
            return( "" );
        }

        //    Send a blank delimiter...
        return( formatDateToString( date.getYear(), date.getMonth() + 1,
            date.getDate(), delimiter, fmtOpt ) );
    }

This default date formatter will create a date of MM/DD/YY from the values passed in:

    public static String
    formatDateToString( int y, int m, int d )
    {
        return( formatDateToString( y, m, d, "/", MDY ) );
    }

This method uses the passed in values and allows the selection of a delimiter. It defaults to MDY order:

public static String
    formatDateToString( int y, int m, int d, String delimiter )
    {
        return( formatDateToString( y, m, d, delimiter, MDY ) );
    }

Application Configuration Parameters

The second Java class that you will create for your purposes is the ConfigProperties class. It extends the Java Properties class to add some unique functionality.

The intranet applications that you build can become quite complex, which only results in more testing when you implement changes. One excellent way to add functionality to complex programs without programming is by using configuration files. The more you are able to configure your program, the more useful it is.

Configuration files are not a new idea; they have been around for years. Many programmers avoid using them because of the parsing involved-parsing each line and figuring out what the configuration line means. Java makes this really easy for you to do, so you should take advantage of it.

Application configuration parameters, or properties, can come from two places: the command line and a configuration file. You want your class to provide a unified, or merged, set of properties. To do this, your class should accept an array of properties stored in strings as input. This array will merge with the array read from the configuration file stored on disk. The resulting combination represents all the properties.

The ConfigProperties Class

The declaration for your extension is as follows:

//****************************************************************************
//* ConfigProperties                                                          *
//****************************************************************************

public class
ConfigProperties
extends Properties

ConfigProperties simply extends the Java Properties object. The real functionality is in the constructor, which for this object accepts two parameters: an array of property strings and a filename.

Property strings are strings in the format:

key = value

Here, key is the name associated with the value. key and value are standard Java property strings that can be passed in on the command line to any Java application or applet with the -D option. For example, to set the property head.size to large in the application InflateHead, the command line would be as follows:

java -Dhead.size=large InflateHead

This and any other arguments are passed into your application's main method as an argument. If you pass this array of strings onto the ConfigProperties object, it will be merged in with the configuration parameters in the configuration file.

The property string array passed in is parsed into a temporary Properties object, as follows:

//****************************************************************************
//* parseArguments                                        &n bsp;                  *
//****************************************************************************

    public int
    parseArguments( String args[] )
    {
        int        i = 0;

        if ( args != null )
        {
            for ( i = 0; i < args.length; i++ )
            {
                StringTokenizer st = new StringTokenizer( args[ i ], "=" );

                if ( st.countTokens() == 2 )
                    argProperties.put( st.nextToken(), st.nextToken() );
            }
        }

        return( i );
    }

You use the StringTokenizer to break up the strings into their key and value components. The components are then added to the argProperties object, which is an instance variable of the class. After the argument properties are parsed and stored, the configuration file is read in and the argument properties are merged in.

An important design decision was made for this class. The configuration file properties are replaced by any matching argument properties. This is standard operating procedure. For the most part, command line parameters should always override any stored functionality, including configuration file parameters. This class implements that philosophy by adding the argument properties to the configuration properties after they have been read from disk.

The Configuration File

The configuration file is nothing more than a file containing lines of the key and value pairs. Listing 8.4 shows a sample configuration file.


Listing 8.4. A sample configuration file.
#
# Employee.cfg
# Employee maintenance configuration file
#
Title=Employee Files
user=dia_user
password=dia
server=dia_database

Use this file to store user names, titles, servers, or just about any parameter that will help the user to better control your programs. You'll use these files in your sample intranet applications later on in the book.

In Chapter 12, "Putting Them All Together," you'll see how you can use this class to provide a unique Java applet function to your intranet applications.

Summary

You've come to the end of your first foray into extending Java for intranets. Hopefully your interest is piqued. This chapter introduced you to some utility classes that will be useful when you develop intranet applications. These classes included timers, date formatters, and a class to help configure your applications.

You now should have a decent understanding of these classes and how you can use them. I showed example programs along with their output, and you can peruse and run the source code on the CD-ROM.

In Chapter 9, "Logging Classes," you will learn all about writing text to a window and to disk files. That chapter is all about logging-and not the kind that lumberjacks do!