Chapter 12

Handling Dynamic Content


CONTENTS


This chapter introduces the HotJava browser and explains why it is so groundbreaking, then introduces the HotJava source release, used to modify HotJava itself. The HTTP server from previous chapters gets a needed face-lift before being used to serve dynamic content to HotJava. At the end of the chapter, you will actually write and test two HotJava content handlers.

Introducing the HotJava Browser

The developers at Sun had a problem; they had a new language, Java, envisioned as dominating the World Wide Web (WWW), but they didn't have a way to reach the public. People need to see demonstrations before they can fully appreciate a new technology, and they need to work with things before they will adopt them. How could Sun convince people to use their distributed language without a demonstration vehicle? The answer is: They couldn't. A Web browser that could execute Java was needed, so the HotJava browser was developed.

HotJava is completely implemented in the Java language itself! It was started to demonstrate interactive content (Java applets) and to prove that Java was a viable language for advanced applications (the browser).

Dynamic Content

Traditional Web browsers are a monolithic collection of protocol handlers and display routines. Typically, the manufacturer equips them to respond to a certain subset of the huge number of protocols and data formats that make up the Internet. HotJava essentially understands none of the protocols or data formats on the Internet. This distinction is important. HotJava was not conceived to speak any specific protocol; it was developed to speak them all.

HotJava implements a concept called dynamic content. When you point HotJava at a URL, the browser searches for the code to converse in whatever protocol is being used. If you entered

http://www.javasoft.com/

HotJava would load its HTTP protocol handler. If you entered

ftp://ftp.some.host/

HotJava would load its FTP protocol handler. What if it didn't have an FTP handler? Well, it would ask the host whether it had an FTP handler. If the host had one, HotJava would download it, then use the new handler to fetch the original URL. HotJava upgrades itself on the fly!

Protocol Handlers

HotJava actually does come with a great many protocol handlers already installed-that is, already present on the local computer. When a new protocol is needed, HotJava searches locally first; only then does it resort to asking the remote host.

The distinction is that it makes no difference to HotJava whether the handler is locally available or gotten from halfway around the world. This dynamic behavior extends to display content as well.

Content Handlers

Traditional Web browsers understand a small subset of display formats. Most common are GIF, JPEG, and X11 bitmap. The content type comes encoded in a Multipurpose Internet Mail Extension (MIME) header. You experienced MIME content types when you worked with the HTTP server. Remember how the server sent everything as Content-type: text/html? This was a MIME content type. Servers are configured to send each file format with a specific MIME type. Typically, this is done by tying the suffix of a file to a specific content type. Table 12.1 lists some common file extensions and their MIME content types.

When HotJava sees a MIME content type, it tries to load the appropriate handler. Again, if it can't find a local handler, it will ask the remote server for one.

Table 12.1. Common file extensions and their MIME content types.

File ExtensionMIME Content Type
html text/html
htm text/html
txt text/plain
rtf text/rtf
ps text/postscript
doc text/msword
gif image/gif
jpg image/jpeg
mpg video/mpeg
wav audio/wav
au audio/ulaw
tar file/tar
arc file/arc
lha file/lharc
zip file/zip

Netscape attempts to mimic this behavior with "plug-ins." You can configure Netscape Navigator to run plug-in programs when it encounters certain MIME content types. The one thing that Netscape can't do is use the server to load one on the fly. Even if it could, there are so many different platforms available, who knows if it would load the correct executable for your specific architecture. Java is portable, so you can compile it once and execute it everywhere there's an interpreter present. If you're using the HotJava browser, then you have a running Java interpreter already in action.

Security Model

HotJava has a much more flexible security model than Netscape Navigator. Instead of banning all file access and limiting socket access, HotJava allows the severity of restrictions to be configured.

Network security has four modes:

In addition, you can also apply these modes to applet loading:

Accessing local file storage is also a configurable option. HotJava uses two environment variables to control file access by applets:

Both these variables consist of a semicolon-separated list of directories. Applets can access any files in these directories or their subdirectories.

Firewall Security Model

The firewall security model is a powerful option. The corporate world is only now becoming aware of the vast power inherent in HTML. Corporate intranets are sprouting up everywhere; these corporate nets offer full access for machines behind their firewall, but limited or no access to the Internet at large. Allowing a browser to have full access rights within a firewall offers the necessary flexibility that corporations demand. The current Netscape security model is too restrictive for business use. Real-world applications need local storage and flexible connectivity options to be of any use. Browsers will probably adopt this firewall model as business demands for Java grow.

Alpha3 Distribution Differences

Unfortunately, HotJava is mired in the world of Java Alpha3. It can't run applets written for version 1.0, but it still has instructive use. The Java language is quite stable, so you'll find no differences in the language syntax or semantics, but there are some major differences in the class packages and libraries.

To begin with, there is no classes.zip file. All the Java classes are contained in a directory tree that reflects the package name of each class. Actually, if you peered inside the zipped class file from version 1.0, you would find that this same tree has been maintained in the compressed image. Figure 12.1 shows the major packages within the directory hierarchy.

Figure 12.1 : Directory structory and major packages.

This layout should look familiar to you, with the exception of the browser package. This package contains classes and interfaces to HotJava's general functionality. In particular, the Applet class is located within the browser package.

The protocol and content handler classes are under the following directories:

classes/net/www/protocol
classes/net/www/content

When HotJava makes a request to a server for a specific handler, it uses the same path as the local directories. For example, if you encounter MIME type text/plain and there's no plain.class within the local text subdirectory, HotJava issues the following request:

GET /classes/net/www/content/text/plain.class HTTP/1.0

Unfortunately, this request issued to the previous chapter's HTTP server would go unanswered because HotJava is waiting for a socket close notification that will never arrive due to a bug in the JDK. It seems as though socket.close() doesn't work under either Windows 95 or Windows NT. You need to change HotJava's source code to use the Content-length MIME parameter that the HTTP server dutifully places into the outbound stream.

Altering the HotJava Source

To alter the HotJava source, you must first have a thorough understanding of the inner workings of the HotJava protocol handler; its principle function is to provide the input stream:

public InputStream openStream(URL u);

The handler for HTTP constructs an instance of the HttpClient class to perform its low-level work; this class extends NetworkClient. Then the socket connection is opened and the request is sent. The input stream is acquired and wrapped in a BufferedInputStream. The HttpClient parses the response header and stores the results.

At this point, the constructor for HttpClient returns. The HTTP protocol handler now processes the response. If the MIME header contains a valid Content-length: field, the input stream is further encapsulated in a MeteredStream. The Stream class MeteredStream is the perfect place to make the alterations.

Buffered Streams Primer

The standard input stream hierarchy looks like this:

InputStream
    FilterInputStream
        BufferedInputStream

The following list shows the public methods of the FilterInputStream class:

FilterInputStream Members
available()
close()
mark(int)
markSupported()
read()
read(byte[])
read(byte[], int, int)
reset()
skip(int)

This class is extended by BufferedInputStream. Buffered streams offer a look-ahead buffer that enables a stream to be read in large chunks, but processed a character at a time. They also provide mark/reset capability. Frequently, applications will need to parse a stream, looking for a specific string. If this string is not found, then the application should reset the stream so other processes can try to parse the same data. Marking a stream tells the BufferedInputStream object to remember the current location within the buffer. If a subsequent reset is called, the object will march backward in its buffer to the previously stored location. New read requests will be satisfied from the old location. The following list shows the public methods of the BufferedInputStream class; all these methods override the equivalent named method in the FilterInputStream class:

Public Methods of BufferedInputStream
available()
mark(int)
markSupported()
read()
read(byte[], int, int)
reset()
skip(int)

Start with the following InputStream:

"BufferedInputStreams are handy!"

Next, execute the following code snippet on the stream:

byte data[] = new byte[13];
is.read(data);       // read 13 characters [BufferedInput]
mark(200);            // save this position for up to 200 characters
is.read(data, 0, 7);  // read 7 more characters [Streams]
reset();              // go back
byte rest[] = new byte[18];
is.read(rest);       // read 18 more characters [Streams are handy!]

A MeteredStream extends FilterInputStream; you can find its source code in the HotJava directory:

classsrc\net\www\html\MeteredStream.java

Metered Stream and HTTP

HotJava has a progress display, performed by the MeteredStream class, that can show how a transfer is progressing. Whenever the HTTP protocol handler detects a length parameter in the response header, it will encapsulate the current input stream within a MeteredStream:

String ct = http.getHeaderField("content-length");
int len = 0;
if (ct != null && (len = Integer.parseInt(ct)) != 0) {
    is = new MeteredStream(is, len, url);

Since all subsequent reads will be done through the MeteredStream class, this is the ideal location for the changes.

Making the Changes

Now you simply need to make stream reads return end-of-stream once the length parameter has been read. The following list shows the overriding methods in the MeteredStream class:

MeteredStream Public Methods
read()
read(byte[], int, int)
skip(int)
close()

Both read routines must be changed so that there are no further read attempts once the entire length of bytes has been returned:

public int read() {
    if ( count >= expected ) return -1;    // add this line
    int c = super.read();
    if (c != -1) {
        justRead(1);
    }
    return c;
}
public int read(byte b[], int off, int len) {
    if ( count >= expected ) return -1;
    if ( count + len > expected ) len = expected - count;    // add both of these
Âlines
    int n = super.read(b, off, len);
    if (n != -1) {
        justRead(n);
    }
    return n;
}

In addition, mark and reset operations must be intercepted so the counts can be updated. As you can see from the previous list of public methods, these routines are absent, so add the following two routines:

public synchronized void mark(int readlimit) {
        meterMark = count;
        super.mark(readlimit);
    }

    public synchronized void reset() {
        if ( meterMark != -1 )
            count = meterMark;
        super.reset();
    }

The class variable meterMark also needs to be added to the class:

public
class MeteredStream extends FilterInputStream
{
    // Class variables.
       ...
    // Instance variables.
       ...
    int meterMark = -1;    // add this line
       ...
}

That's it! Recompile MeteredStream and move the class file into the classes/net/www/html subdirectory.

Compiling Under HotJava

Compiling under HotJava is the same as compiling under the JDK; the only difference is which version of javac you use. I prefer not to alter my path and environment for HotJava. Simply issue the command specifying the full path to HotJava's version of javac:

c:\hotjava\bin\javac MeteredStream.java

Assuming you made the alterations correctly, this will create a MeteredStream.class file in the current directory. Rename the original MeteredStream.class to MeteredStream.orig, then copy your altered MeteredStream.class into its place. You have just changed the source for HotJava! Now give it a try-run the server from Chapters 9, "Java Socket Programming," 10, "Native Methods and Java," or 11, "Building a Live Data Applet," and point your new HotJava at the server. The files transfer perfectly.

Before addressing content handlers, you need to add some enhancements to the server.

Toward a More Perfect Server

The server used in Part IV, "Managing Live Data," was useful, but it lacked many features of an actual HTTP server. Specifically, there is no configuration file to map file extensions to MIME content strings. In addition, some log information would be helpful.

The first change is to the class name. Up to this point, the main class has been called BasicWebServer. Change this to HTTPServer, then perform a global search and replace to make the change. Now the configuration and logging methods can be added.

Adding a Configuration File

Since the server is an application, it is free to read and even write the disk. The format of the configuration file is very straightforward:

New methods are added to handle reading the configuration data. Listing 12.1 shows the new routines.


Listing 12.1. New routines for reading configuration files.
    /**
     * Read the config file.
     */
    private void initialize()
    {
        String line = null;
        String configFile = "server.cfg";

        // setup defaults
        HTTP_PORT = 80;
        suffix = new Hashtable();

        try
        {
            FileInputStream inFile = new FileInputStream(configFile);
            DataInputStream is = new DataInputStream(inFile);

            while ((line = is.readLine()) != null)
            {
                StringTokenizer icmd = new StringTokenizer(line, " \t=");
                addConfigEntry(icmd);
            }
        }
        catch (FileNotFoundException fnf)
        {
            suffix.put("html", "text/html");
            suffix.put("htm", "text/html");
            suffix.put("class", "text/plain");
            suffix.put("gif", "image/gif");
        }
        catch (IOException ioe)
        {
            System.out.println("Error reading config file: " + ioe);
        }
    }

    /**
     * Add a config entry.
     * @param icmd contains the tokenized line.
     */
    private void addConfigEntry(StringTokenizer icmd)
    {
        if ( icmd.hasMoreTokens() )
        {
            String param = null;
            String command = icmd.nextToken();
            if ( icmd.hasMoreTokens() )
            {
                param = icmd.nextToken();
                if ( command.equalsIgnoreCase("PORT") )
                {
                    HTTP_PORT = Integer.valueOf(param).intValue();
                    if (debug)
                        System.out.println("Monitoring port " + HTTP_PORT);
                }
                else if ( command.equalsIgnoreCase("SUFFIX") )
                {
                    if ( icmd.hasMoreTokens() )
                    {
                        String param2 = icmd.nextToken();
                        suffix.put(param, param2);
                        if (debug)
                        {
                            System.out.print(
                                "Adding suffix: '" + param + "'");
                            System.out.println("  '" + param2 + "'");
                         }
                    }
                }
                else if ( command.equalsIgnoreCase("LOG") )
                {
                    openLog(param);
                }
                else
                {
                    System.out.print("Error: Unknown cfg entry: ");
                    System.out.println(command);
                }
            }
        }
    }

    /**
     * Open a log file
     * @param logfile contains the filename to open
     */
    public void openLog(String logfile)
    {
        try
        {
            logFile = new PrintStream( new FileOutputStream(logfile) );
            logging = true;
        }
        catch (IOException ioe)
        {
            System.out.println("Error opening log file: " + ioe);
        }
    }

The StringTokenizer is instructed to parse equal sign characters out of the stream. This allows the configuration file to omit the equal sign and still function as expected. Only one config entry per line is allowed because readLine() is used to retrieve the entries.

File extension to MIME type mapping is contained in a hash table called suffix. The file extension is used as the key. If a configuration file contains multiple entries for an extension, only the last one will be stored. Hash table put operations overwrite duplicate entries.

Adding Standard Logging

There are many third-party tools available to analyze server log files, provided they are in a "common log format":

hostname identd authuser [date] "request" status length

The first entry, hostname, is the name of the host this connection was received from. If the name cannot be established, this field will display the numeric IP address of the caller. The next two fields contain information for "login" and authorized user names. The HTTP server does not support remote identification dialogs, so these fields will appear as a single dash (-). The date field has this format:

DD/MMM/YYY:HH:MM:SS GGGG

All these fields are evident except GGGG, which represents the number of time zones away from GMT.

The request field is the first line from the request header. status is the HTTP response code that was sent, typically 200, and length is the total number of file bytes sent in the response. Here is a sample log entry:

some.name.com - - [25/Mar/1996:08:48:23 -0400] "GET / HTTP/1.0" 200 342

The 200 indicates that a response containing 342 bytes was sent.

Building Log Information

HTTP requests are already stored in the HTTPrequest class, so it's reasonable to also store the log information in this class. If the server is ever made to work concurrently, the log information will have to travel with each request, so just add it there now:

/**
* This class maintains all of the information from a HTTP request
*/
public class HTTPrequest
{
        ...
    public StringBuffer log;

    /**
     * Create an instance of this class
     */
    public HTTPrequest()
    {
        ...
        log = null;
    }
        ...
}

A StringBuffer is used because the string will grow and be altered. Java String objects are immutable-they can never be altered after they are created. If you use the plus operator to grow a String, you are actually creating a new String:

String first = new String("This is the first string.");
first += " This is the second string";

The first variable now contains a completely different string, constructed from the first string and the addition of the second string. The original contents of the variable are lost.

The log string will need some tweaking to bring it in line with the common log format. That is why a StringBuffer class is used.

After a request is parsed, a new routine is added to HTTPrequest to format the initial portions of the log entry:

/**
* Log a complete request.
*/
public void logRequest()
{
    Date current = new Date();

    log = new StringBuffer("");
    if ( clientSocket == null )
        log.append("-");
    else
        log.append(clientSocket.getInetAddress().getHostName());
    log.append(" - - ");
    log.append(formatDate(current));
    log.append(" \"" + firstLine + "\" ");
}

The Date Class

The Date class is part of the java.util package. It provides full support for both date and time. Six different constructors for the class, shown in the following list, offer a great deal of flexibility in how a date is specified.

Date Class Constructors
Date()
Date(long totalSecs)
Date(int yr, int mth, int day)
Date(int yr, int mth, int day, int hrs, int min)
Date(int yr, int mth, int day, int hrs, int min, int sec)
Date(String s)

The first constructor, the most commonly used, constructs a Date class reflecting the current date and time. The other constructors are used mainly to construct Date objects that are either forward or backward in time. This could be done for comparison purposes:

Date c = new Date();
Date p = new Date( c.getYear() - 1, c.getMonth(), c.getDate() );
Date n = new Date( c.getYear() + 1, c.getMonth(), c.getDate() );
if ( p.before( c ) )
    System.out.println("Date " + p + " is before Date " + c);
if ( n.after( c ) )
    System.out.println("Date " + n + " is after Date " + c);

The Date class provides three useful comparison functions:

Several public methods get and manipulate Date class variables. Table 12.2 lists the remaining public methods of the Date class.

Table 12.2. Public methods of the Date class.

MethodSynopsis
int getYear() Year since 1900
void setYear(int y) Year since 1900
int getMonth() Month (0-11)
void setMonth(int m) Month (0-11)
int getDate() Day of month (1-31)
void setDate(int d) Day of month (1-31)
int getDay() Day of week (0-6)[0=Sunday]
int getHours() (0-23)
void setHours(int h) (0-23)
int getMinutes() (0-59)
void setMinutes(int m) (0-59)
int getSeconds() (0-59)
void setSeconds(int s) (0-59)
long getTime() Milliseconds since 1970
void setTime(long totalSecs) Milliseconds since 1970
int hashCode() Return the object's hash code
String toString() Uses UNIX ctime conventions
String toLocaleString() Uses locale conventions
String toGMTString() Uses Internet GMT conventions
int getTimezoneOffset() Minutes from GMT

Creating Common Log Date Format

None of the toString() variants in the Date class match common log format. The following routines use a Date class to create the correct common log date format:

    /**
     * Return the passed date as a common log format String.
     * @param d contains the date to convert
     */
    public String formatDate(Date d)
    {
        return "[" + formatFor2(d.getDate()) + "/" + getMonthName(d) +
                     "/" + (d.getYear() + 1900) +
                     ":" + formatFor2(d.getHours()) +
                     ":" + formatFor2(d.getMinutes()) +
                     ":" + formatFor2(d.getSeconds()) +
                     " " + getTimezone(d) + "]";
    }

    /**
     * return a String of the passed int.  The String will
     * be formatted to take up at least two places.
     */
    public String formatFor2(int n)
    {
        String ret;

        if ( n < 10 )
            ret = new String("0" + n);
        else
            ret = new String("" + n);
        return ret;
    }

    /**
     * Return the timezone formatted for common log format.
     * @param d contains the date to convert
     */
    public String getTimezone(Date d)
    {
        String ret;

        int tz = d.getTimezoneOffset();
        int lf = tz % 60;                // check for remainders
        tz /= 60;                        // change to hours

        // The polarity of getTimezoneOffset is backwards.
        // Positive values mean you are behind GMT.
        // Negative values mean GMT is behind you.

        if ( tz > 0 )
            ret = "-";
        else if ( tz < 0 )
            ret = "+";
        else
            ret = "";
        ret += formatFor2(tz) + formatFor2(lf);
        return ret;
    }

    /**
     * Return a String representing the month name in
     * common log format.
     * @param d contains the date to convert
     */
    public String getMonthName(Date d)
    {
        switch ( d.getMonth() )
        {
        case 0: return "Jan";
        case 1: return "Feb";
        case 2: return "Mar";
        case 3: return "Apr";
        case 4: return "May";
        case 5: return "Jun";
        case 6: return "Jul";
        case 7: return "Aug";
        case 8: return "Sep";
        case 9: return "Oct";
        case 10: return "Nov";
        case 11: return "Dec";
        }
        return "-";
    }
}

Integers will print in their current precision. Common log format calls for two-digit numbers. The method formatFor2() will add a zero at the beginning if the passed integer is less than 10.

Notice the polarity switch of getTimezoneOffset(). Common log format as well as most other time-zone representations use hours away from GMT (locale - GMT).

Note
The getTimezoneOffset() method returns the number of minutes GMT is away from your locale (GMT - locale). Normal convention is to use the number of minutes your locale is away from GMT (locale - GMT). Be careful when displaying time-zone information.

Now the HTTPrequest contains the request and the initial parts of the log string. The remaining two pieces of log information, status and length, are filled in by the send routines.

Altering the Send Routines

A nice feature of many servers is that they send a standard HTML file for error states. For instance, file error404.html is returned when a request is not found, which enables you to configure what is sent for various error conditions. Previously, the status of a transmission was always 200 OK. Now, negative responses also need to use the services of sendFile(). Status and the description string need to be passed in by the caller. This has another advantage because log information is written in the sendFile() routine, which means that negative responses are also recorded.

    /**
     * Send a negative (404 NOT FOUND) response
     * @param request the HTTP request to respond to.
     */
    private void sendNegativeResponse(HTTPrequest request)
    {
        try
        {
            String fileToGet = "error404.html";
            FileInputStream inFile = new FileInputStream(fileToGet);
            if (debug & level < 4)
            {
                System.out.print("DEBUG: Sending -rsp ");
                System.out.print(fileToGet + " " + inFile.available());
                System.out.println(" Bytes");
            }
            sendFile(request, inFile, 404, "Not Found", fileToGet);
            inFile.close();
        }
        catch (FileNotFoundException fnf)
        {
            System.out.println("Error: No error404.html file for -rsp");
        }
        catch (ProtocolException pe)
        {
        }
        catch (IOException ioe)
        {
            System.out.println("Unknown file length: " + ioe);
        }
    }

    /**
     * Send the passed file
     * @param request the HTTP request instance
     * @param inFile the opened input file stream to send
     */
    private void sendFile(HTTPrequest request,
                          FileInputStream inFile,
                          int status, String describe,
                          String fileToGet)
    {
        DataOutputStream outbound = null;
        String type = null;

        try
        {
            // Acquire an output stream
            outbound = new DataOutputStream(
                request.clientSocket.getOutputStream());

            // Send the response header
            int period = fileToGet.lastIndexOf('.');
            if ( period != -1 && period != fileToGet.length() )
                type = (String)suffix.get(fileToGet.substring(period + 1));
            if (type == null)
                type = "text/plain";

            if (debug & level < 4)
            {
                System.out.println("DEBUG: Type " + type);
            }
            outbound.writeBytes("HTTP/1.0 " + status + " " + describe + "\r\n");
            outbound.writeBytes("Content-type: " + type + "\r\n");
            outbound.writeBytes("Content-Length: " + inFile.available() + "\r\n");
            outbound.writeBytes("\r\n");

            request.log.append(status + " " + inFile.available());
            System.out.println(request.log.toString());
            if ( logging )
                logFile.print(request.log.toString() + "\r\n");

            // Added to allow Netscape to process header properly
            // This is needed because the close is not recognized
            sleep(5000);

            // If not a HEAD request, send the file body.
            // HEAD requests only solicit a header response.
            if (!request.method.equals("HEAD"))
            {
                byte dataBody[] = new byte[1024];
                int cnt;
                while ((cnt = inFile.read(dataBody)) != -1)
                    outbound.write(dataBody, 0, cnt);
            }

            // Cleanup
            outbound.flush();
            outbound.close();
            request.inbound.close();
        }
        catch (IOException ioe)
        {
            System.out.println("IOException while sending file: " + ioe);
        }
    }

Notice how the MIME content type is derived from the file's extension. Using a hash table for this information allows a rapid lookup. If the extension is not found, or if the file has no extension, the file is sent as type text/plain.

Creating New Content Types

Now that the server can send various MIME content types, it's time to investigate how to use HotJava content handlers. This exercise is small in scope, but large in know-how.

HotJava comes equipped with four content handlers, in addition to its standard HTML handler:

This exercise adds two new types, text/fee and text/foo. Since these types aren't within the local system, HotJava will try to load them from the server:

GET /classes/net/www/content/text/fee.class HTTP/1.0
GET /classes/net/www/content/text/foo.class HTTP/1.0

Writing Content Handlers

A HotJava content handler consists of a single class that extends the ContentHandler class. Its class name must be the same as the MIME sub-type that it handles. In addition, it must have the same path it would have if it were local to HotJava.

The code below implements the text/fee content handler:

package net.www.content.text;

import net.www.html.ContentHandler;
import net.www.html.URL;
import java.io.InputStream;

/**
* A content handler for text/fee objects
*/
public class fee extends ContentHandler
{
    /**
     * Read in an ASCII text file and append an
     * ID string to the end.
     * @param is holds the input stream for the content
     * @param u holds the URL for the object
     */
    public Object getContent(InputStream is, URL u)
    {
        StringBuffer sb = new StringBuffer();
        int c;

        while ((c = is.read()) >= 0)
        {
            sb.appendChar((char)c);
        }
        sb.append("\n\n\nThis is a fee ASCII text file.\n");
        sb.append("Rendered by the fee content handler!\n");
        is.close();
        return sb.toString();      // return a string object
    }
}

This handler expects an ASCII text file to be its input. It will continue reading until the end of the stream. When the stream is finished, this handler adds an identification string to the end of the text.

All content handlers return some type of object to the browser. The browser uses instanceof calls to figure out how to display the object. Obviously, HotJava can handle string data. More complex types, such as images, would return an instance of the Observable class. Inspecting the GIF content handler shows that it returns a GIFImage, which descends from DIBitmap, which itself descends from class Observable.

Compile the code by invoking HotJava's version of javac:

c:\hotjava\bin\javac fee.java

Now add the new content type to the server config file: server.cfg.

suffix= fee text/fee

Finally, run the server and then HotJava. Remember to use the server's new name: HTTPServer. Point the browser at the server test file: test.fee.

http://your_host/test.fee

You should see the display shown in Figure 12.2.

Figure 12.2 : Rendition of test fee using the fee content handler.

HotJava downloaded the new handler, installed it, and used it to render the new type! You can check this by looking at the output of the server:

merlin - - [25/Mar/1996:16:07:56 -0400] "GET /test.fee HTTP/1.0" 200 120
merlin - - [25/Mar/1996:16:08:09 -0400]
          "GET /classes/net/www/content/text/fee.class HTTP/1.0" 200 834

Did you notice the strange boxes at the end of each text line in the HotJava display? Those are carriage returns. HotJava doesn't know how to display these, so it uses a box.

Copy the fee.java file to foo.java. Edit the new foo handler so that it removes carriage returns whenever it sees them.

package net.www.content.text;

import net.www.html.ContentHandler;
import net.www.html.URL;
import java.io.InputStream;

/**
* A content handler for text/foo objects
*/
public class foo extends ContentHandler
{
    /**
     * Read in an ASCII text file and append an
     * ID string to the end.
     * @param is holds the input stream for the content
     * @param u holds the URL for the object
     */
    public Object getContent(InputStream is, URL u)
    {
        StringBuffer sb = new StringBuffer();
        int c;

        while ((c = is.read()) >= 0)
        {
            if ( c == '\r' ) continue;    // remove carriage returns
            sb.appendChar((char)c);
        }
        sb.append("\n\n\nThis is a foo ASCII text file.\n");
        sb.append("Rendered by the foo content handler!\n");
        is.close();
        return sb.toString();      // return a string object
    }
}

First, compile the new foo handler:

c:\hotjava\bin\javac foo.java

Add the new "foo" suffix to the server configuration file:

suffix= foo text/foo

Next, restart the server and HotJava. Point the browser to the following statement:

http://your_host/test.foo

The boxes have disappeared! You should see the display shown in Figure 12.3.

Figure 12.3 : Rendition of test foo using the foo content handler.

The server log shows the following:

merlin - - [25/Mar/1996:16:10:17 -0400] "GET /test.foo HTTP/1.0" 200 120
merlin - - [25/Mar/1996:16:10:24 -0400]
          "GET /classes/net/www/content/text/foo.class HTTP/1.0" 200 845

Again, the handler was dynamically loaded by HotJava.

Summary

This chapter covers the dynamic Web browser HotJava. After some initial background, the HotJava class hierarchy is reviewed and the HotJava concept of dynamic content handlers is introduced. To demonstrate dynamic content, HotJava's source is patched to enable it to operate with the HTTP server from previous chapters.

The server itself receives a face-lift that brings it more in line with a standard HTTP server. In light of the changes, its name is changed to HTTPServer. You add a configuration file and logging.

You learn about the MIME content types as well as how servers use file extensions to decide what content type to send for a specific file.

The Date class is reviewed, and the common log format is introduced. Finally, you learn the basics of writing a HotJava content handler.