How Tomcat Works

Overview

As mentioned in Introduction, there are two main modules in Catalina: the connector and the container. In this chapter you will enhance the applications in Chapter 2 by writing a connector that creates better request and response objects. A connector compliant with Servlet 2.3 and 2.4 specifications must create instances of javax.servlet.http.HttpServletRequest and javax.servlet.http.HttpServletResponse to be passed to the invoked servlet's service method. In Chapter 2 the servlet containers could only run servlets that implement javax.servlet.Servlet and passed instances of javax.servlet.ServletRequest and javax.servlet.ServletResponse to the service method. Because the connector does not know the type of the servlet (i.e. whether it implements javax.servlet.Servlet, extends javax.servlet.GenericServlet, or extends javax.servlet.http.HttpServlet), the connector must always provide instances of HttpServletRequest and HttpServletResponse.

In this chapter's application, the connector parses HTTP request headers and enables a servlet to obtain headers, cookies, parameter names/values, etc. You will also perfect the getWriter method in the Response class in Chapter 2 so that it will behave correctly. Thanks to these enhancements, you will get a complete response from PrimitiveServlet and be able to run the more complex ModernServlet.

The connector you build in this chapter is a simplified version of the default connector that comes with Tomcat 4, which is discussed in detail in Chapter 4. Tomcat's default connector is deprecated as of version 4 of Tomcat, however it still serves as a great learning tool. For the rest of the chapter, "connector" refers to the module built in our application.

Note: Unlike the applications in the previous chapters, in this chapter's application the connector is separate from the container.

The application for this chapter can be found in the ex03.pyrmont package and its sub-packages. The classes that make up the connector are part of the ex03.pyrmont.connector and ex03.pyrmont.connector.http packages. Starting from this chapter, every accompanying application has a bootstrap class used to start the application. However, at this stage, there is not yet a mechanism to stop the application. Once run, you must stop the application abruptly by closing the console (in Windows) or by killing the process (in UNIX/Linux).

Before we explain the application, let me start with the StringManager class in the org.apache.catalina.util package. This class handles the internationalization of error  messages in different modules in this application and in Catalina itself. The discussion of the accompanying application is presented afterwards.

The StringManager Class

A large application such as Tomcat needs to handle error messages carefully. In Tomcat error messages are useful for both system administrators and servlet programmers. For example, Tomcat logs error messages in order for system administrator to easily pinpoint any abnormality that happened. For servlet programmers, Tomcat sends a particular error message inside every javax.servlet.ServletException thrown so that the programmer knows what has gone wrong with his/her servlet.

The approach used in Tomcat is to store error messages in a properties file, so that editing them is easy. However, there are hundreds of classes in Tomcat. Storing all error messages used by all classes in one big properties file will easily create a maintenance nightmare. To avoid this, Tomcat allocates a properties file for each package. For example, the properties file in the org.apache.catalina.connector package contains all error messages that can be thrown from any class in that package. Each properties file is handled by an instance of the org.apache.catalina.util.StringManager class. When Tomcat is run, there will be many instances of StringManager, each of which reads a properties file specific to a package. Also, due to Tomcat's popularity, it makes sense to provide error messages in multi languages. Currently, three languages are supported. The properties file for English error messages is named LocalStrings.properties. The other two are for the Spanish and Japanese languages, in the LocalStrings_es.properties and LocalStrings_ja.properties files respectively.

When a class in a package needs to look up an error message in that package's properties file, it will first obtain an instance of StringManager. However, many classes in the same package may need a StringManager and it is a waste of resources to create a StringManager instance for every object that needs error messages. The StringManager class therefore has been designed so that an instance of StringManager is shared by all objects inside a package. If you are familiar with design patterns, you'll guess correctly that StringManager is a singleton class. The only constructor it has is private so that you cannot use the new keyword to instantiate it from outside the class. You get an instance by calling its public static method getManager, passing a package name. Each instance is stored in a Hashtable with package names as its keys.

private static Hashtable managers = new Hashtable();
public synchronized static StringManager
getManager(String packageName) {
    StringManager mgr = (StringManager)managers.get(packageName);
    if (mgr == null) {
        mgr = new StringManager(packageName);
        managers.put(packageName, mgr);
    }
    return mgr;
}

Note: An article on the Singleton pattern entitled "The Singleton Pattern" can be found in the accompanying ZIP file.

For example, to use StringManager from a class in the ex03.pyrmont.connector.http package, pass the package name to the StringManager class's getManager method:

StringManager sm = StringManager.getManager("ex03.pyrmont.connector.http");

In the ex03.pyrmont.connector.http package, you can find three properties files: LocalStrings.properties, LocalStrings_es.properties and LocalStrings_ja.properties. Which of these files will be used by the StringManager instance depends on the locale of the server running the application. If you open the LocalStrings.properties file, the first non-comment line reads:

httpConnector.alreadyInitialized=HTTP connector has already been initialized

To get an error message, use the StringManager class's getString, passing an error code. Here is the signature of one of its overloads:

public String getString(String key)

Calling getString by passing httpConnector.alreadyInitialized as the argument returns HTTP connector has already been initialized.

The Application

Starting from this chapter, the accompanying application for each chapter is divided into modules. This chapter's application consists of three modules: connector, startup, and core.

The startup module consists only of one class, Bootstrap, which starts the application. The connector module has classes that can be grouped into five categories:

  • The connector and its supporting class (HttpConnector and HttpProcessor).
  • The class representing HTTP requests (HttpRequest) and its supporting classes.
  • The class representing HTTP responses (HttpResponse) and its supporting classes.
  • Façade classes (HttpRequestFacade and HttpResponseFacade).
  • The Constant class.

The core module consists of two classes: ServletProcessor and StaticResourceProcessor.

Figure 3.1 shows the UML diagram of the classes in this application. To make the diagram more readable, the classes related to HttpRequest and HttpResponse have been omitted. You can find UML diagrams for both when we discuss Request and Response objects respectively.

Figure 3.1: The UML diagram of the application

Compare the diagram with the one in Figure 2.1. The HttpServer class in Chapter 2 has been broken into two classes: HttpConnector and HttpProcessor, Request has been replaced by HttpRequest, and Response by HttpResponse. Also, more classes are used in this chapter's application.

The HttpServer class in Chapter 2 is responsible for waiting for HTTP requests and creating request and response objects. In this chapter's application, the task of waiting for HTTP requests is given to the HttpConnector instance, and the task of creating request and response objects is assigned to the HttpProcessor instance.

In this chapter, HTTP request objects are represented by the HttpRequest class, which implements javax.servlet.http.HttpServletRequest. An HttpRequest object will be cast to a HttpServletRequest instance and passed to the invoked servlet's service method. Therefore, every HttpRequest instance must have its fields properly populated so that the servlet can use them. Values that need to be assigned to the HttpRequest object include the URI, query string, parameters, cookies and other headers, etc. Because the connector does not know which values will be needed by the invoked servlet, the connector must parse all values that can be obtained from the HTTP request. However, parsing an HTTP request involves expensive string and other operations, and the connector can save lots of CPU cycles if it parses only values that will be needed by the servlet. For example, if the servlet does not need any request parameter (i.e. it does not call the getParameter, getParameterMap, getParameterNames, or getParameterValues methods of javax.servlet.http.HttpServletRequest), the connector does not need to parse these parameters from the query string and or from the HTTP request body. Tomcat's default connector (and the connector in this chapter's application) tries to be more efficient by leaving the parameter parsing until it is really needed by the servlet.

Tomcat's default connector and our connector use the SocketInputStream class for reading byte streams from the socket's InputStream. An instance of SocketInputStream wraps the java.io.InputStream instance returned by the socket's getInputStream method. The SocketInputStream class provides two important methods: readRequestLine and readHeader.readRequestLine returns the first line in an HTTP request, i.e. the line containing the URI, method and HTTP version. Because processing byte stream from the socket's input stream means reading from the first byte to the last (and never moves backwards), readRequestLine must be called only once and must be called before readHeader is called. readHeader is called to obtain a header name/value pair each time it is called and should be called repeatedly until all headers are read. The return value of readRequestLine is an instance of HttpRequestLine and the return value of readHeader is an HttpHeader object. We will discuss the HttpRequestLine and HttpHeader classes in the sections to come.

The HttpProcessor object creates instances of HttpRequest and therefore must populate fields in them. The HttpProcessor class, using its parse method, parses both the request line and headers in an HTTP request. The values resulting from the parsing are then assigned to the fields in the HttpProcessor objects. However, the parse method does not parse the parameters in the request body or query string. This task is left to the HttpRequest objects themselves. Only if the servlet needs a parameter will the query string or request body be parsed.

Another enhancement over the previous applications is the presence of the bootstrap class ex03.pyrmont.startup.Bootstrap to start the application.

We will explain the application in detail in these sub-sections:

  • Starting the Application
  • The Connector
  • Creating an HttpRequest Object
  • Creating an HttpResponse Object
  • Static resource processor and servlet processor
  • Running the Application

Starting the Application

You start the application from the ex03.pyrmont.startup.Bootstrap class. This class is given in Listing 3.1.

Listing 3.1: The Bootstrap class

package ex03.pyrmont.startup;

import ex03.pyrmont.connector.http.HttpConnector;

public final class Bootstrap {
public static void main(String[] args) {
HttpConnector connector = new HttpConnector();
connector.start();
}}

The main method in the Bootstrap class instantiates the HttpConnector class and calls its start method. The HttpConnector class is given in Listing 3.2.

Listing 3.2: The HttpConnector class's start method


package ex03.pyrmont.connector.http;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class HttpConnector implements Runnable {
    boolean stopped;
    private String scheme = "http";
    public String getScheme() {
    return scheme;
    }
    public void run() {
    ServerSocket serverSocket = null;
    int port = 8080;
    try {
        serverSocket = new
    ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
    }catch (IOException e) {
    e.printStackTrace(); System.exit(1);
    }
    while (!stopped) {
    // Accept the next incoming connection from the server         socket Socket socket = null;
    try {
        socket = serverSocket.accept();
    }catch (Exception e) {
        continue;
    }
    // Hand this socket off to an HttpProcessor
    HttpProcessor processor = new HttpProcessor(this);
    processor.process(socket);
    }}
    public void start() {
        Thread thread = new Thread(this);
        thread.start ();
    }
}

The Connector

The ex03.pyrmont.connector.http.HttpConnector class represents a connector responsible for creating a server socket that waits for incoming HTTP requests. This class is presented in Listing 3.2.

The HttpConnector class implements java.lang.Runnable so that it can be dedicated a thread of its own. When you start the application, an instance of HttpConnector is created and its run method executed.

Note: You can read the article "Working with Threads" to refresh your memory about how to create Java threads.

The run method contains a while loop that does the following:

  • Waits for HTTP requests
  • Creates an instance of HttpProcessor for each request.
  • Calls the process method of the HttpProcessor.

Note The run method is similar to the await method of the HttpServer1 class in Chapter 2.

You can see right away that the HttpConnector class is very similar to the ex02.pyrmont.HttpServer1 class, except that after a socket is obtained from the accept method of java.net.ServerSocket, an HttpProcessor instance is created and its process method is called, passing the socket.

Note: The HttpConnector class has another method calls getScheme, which returns the scheme (HTTP).

The HttpProcessor class's process method receives the socket from an incoming HTTP request. For each incoming HTTP request, it does the following:

  1. CreateanHttpRequestobject.
  2. CreateanHttpResponseobject.
  3. Parse the HTTP request's first line and header and populate the HttpRequest object.
  4. Pass the HttpRequest and HttpResponse objects to either a ServletProcessoror a StaticResourceProcessor. Like in Chapter 2, the ServletProcessor invokes the service method of the requested servlet and the StaticResourceProcessor sends the content of a static resource.

The process method is given in Listing 3.3.

Listing 3.3: The HttpProcessor class's process method.

public void process(Socket socket) {
    SocketInputStream input = null;
    OutputStream output = null;
    try {
    input = new SocketInputStream(socket.getInputStream(), 2048);
    output = socket.getOutputStream();
    // create HttpRequest object and parse
    request = new HttpRequest(input);
    // create HttpResponse object
    response = new HttpResponse(output);
    response.setRequest(request);
    response.setHeader("Server", "Pyrmont Servlet Container");
    parseRequest(input, output); parseHeaders(input);
    //check if this is a request for a servlet or a static resource
    //a request for a servlet begins with "/servlet/"
    if (request.getRequestURI().startsWith("/servlet/")) {
        ServletProcessor processor = new ServletProcessor();
        processor.process(request, response);
    } else {
        StaticResourceProcessor processor = new
StaticResourceProcessor();
        processor.process(request, response);
    }
    // Close the socket socket.close();
    // no shutdown for this application
    }catch (Exception e) {
        e.printStackTrace ();
    }
}

The process method starts by obtaining the input stream and output stream of the socket. Note, however, in this method we use the SocketInputStream class that extends java.io.InputStream.


SocketInputStream input = null;
OutputStream output = null;
try {
    input = new SocketInputStream(socket.getInputStream(), 2048);
    output = socket.getOutputStream();

Then, it creates an HttpRequest instance and an HttpResponse instance and assigns the HttpRequest to the HttpResponse.

// create HttpRequest object and parse
request = new HttpRequest(input);
// create HttpResponse object
response = new HttpResponse(output);
response.setRequest(request);

The HttpResponse class in this chapter's application is more sophisticated than the Response class in Chapter 2. For one, you can send headers to the client by calling its setHeader method.

response.setHeader("Server", "Pyrmont Servlet Container");

Next, the process method calls two private methods in the HttpProcessor class for parsing the request.

parseRequest(input, output);
parseHeaders (input);

Then, it hands off the HttpRequest and HttpResponse objects for processing to either a ServletProcessor or a StaticResourceProcessor, depending the URI pattern of the request.

if (request.getRequestURI().startsWith("/servlet/")) {
    ServletProcessor processor = new ServletProcessor();
    processor.process(request, response);
    } else {
    StaticResourceProcessor processor = new StaticResourceProcessor();
    processor.process(request, response);
}

Finally, it closes the socket.

socket.close();

Note also that the HttpProcessor class uses the org.apache.catalina.util.StringManager class for sending error messages:

protected StringManager sm = StringManager.getManager("ex03.pyrmont.connector.http");

The private methods in the HttpProcessor class--parseRequest, parseHeaders, and normalize-- are called to help populate the HttpRequest. These methods will be discussed in the next section, "Creating an HttpRequest Object".

Creating an HttpRequest Object

The HttpRequest class implements javax.servlet.http.HttpServletRequest. Accompanying it is a façade class called HttpRequestFacade. Figure 3.2 shows the UML diagram of the HttpRequest class and its related classes.

Figure 3.2: The HttpRequest class and related classes

Many of the methods in the HttpRequest class are left blank (you have to wait until Chapter 4 for a full implementation), but servlet programmers can already retrieve the headers, cookies and parameters of the incoming HTTP request. These three types of values are stored in the following reference variables:

protected HashMap headers = new HashMap();
protected ArrayList cookies = new ArrayList();
protected ParameterMap parameters = null;

Note ParameterMap class will be explained in the section "Obtaining Parameters".

Therefore, a servlet programmer can get the correct return values from the following methods in javax.servlet.http.HttpServletRequest:getCookies,getDateHeader, getHeader, getHeaderNames, getHeaders, getParameter, getPrameterMap, getParameterNames, and getParameterValues. Once you get headers, cookies, and parameters populated with the correct values, the implementation of the related methods are easy, as you can see in the HttpRequest class.

Needless to say, the main challenge here is to parse the HTTP request and populate the HttpRequest object. For headers and cookies, the HttpRequest class provides the addHeader and addCookie methods that are called from the parseHeaders method of HttpProcessor. Parameters are parsed when they are needed, using the HttpRequest class's parseParameters method. All methods are discussed in this section.

Since HTTP request parsing is a rather complex task, this section is divided into the following subsections:

  • Reading the socket's input stream
  • Parsing the request line
  • Parsing headers
  • Parsing cookies
  • Obtaining parameters

Reading the Socket's Input Stream

In Chapters 1 and 2 you did a bit of request parsing in the ex01.pyrmont.HttpRequest and ex02.pyrmont.HttpRequest classes. You obtained the request line containing the method, the URI, and the HTTP version by invoking the read method of the java.io.InputStream class:

byte[] buffer = new byte [2048];
try {
// input is the InputStream from the socket.
i = input.read(buffer);
}

You did not attempt to parse the request further for the two applications. In the application for this chapter, however, you have the ex03.pyrmont.connector.http.SocketInputStream class, a copy of org.apache.catalina.connector.http.SocketInputStream. This class provides methods for obtaining not only the request line, but also the request headers.

You construct a SocketInputStream instance by passing an InputStream and an integer indicating the buffer size used in the instance. In this application, you create a SocketInputStream object in the process method of ex03.pyrmont.connector.http.HttpProcessor, as in the following code fragment:

SocketInputStream input = null;
OutputStream output = null;
try {
    input = new SocketInputStream(socket.getInputStream(), 2048);
    ...

As mentioned previously, the reason for having a SocketInputStream is for its two important methods: readRequestLine and readHeader. Read on.

Parsing the Request Line

The process method of HttpProcessor calls the private parseRequest method to parse the request line, i.e. the first line of an HTTP request. Here is an example of a request line:

GET /myApp/ModernServlet?userName=tarzan&password=pwd HTTP/1.1

The second part of the request line is the URI plus an optional query string. In the example above, here is the URI:

/myApp/ModernServlet

And, anything after the question mark is the query string. Therefore the query string is the following:

userName=tarzan&password=pwd

The query string can contain zero or more parameters. In the example above, there are two parameter name/value pairs: userName/tarzan and password/pwd. In servlet/JSP programming, the parameter name jsessionid is used to carry a session identifier. Session identifiers are usually embedded as cookies, but the programmer can opt to embed the session identifiers in query strings, for example if the browser's support for cookies is being turned off.

When the parseRequest method is called from the HttpProcessor class's process method, the request variable points to an instance of HttpRequest. The parseRequest method parses the request line to obtain several values and assigns these values to the HttpRequest object. Now, let's take a close look at the parseRequest method in Listing 3.4.

Listing 3.4: The parseRequest method in the HttpProcessor class

private void parseRequest(SocketInputStream input, OutputStream output) throws IOException, ServletException {
    // Parse the incoming request line
    input.readRequestLine(requestLine);
    String method =
new String(requestLine.method, 0, requestLine.methodEnd);
    String uri = null;
    String protocol = new String(requestLine.protocol, 0,requestLine.protocolEnd);

    // Validate the incoming request line
    if (method, length () < 1) {
        throw new ServletException("Missing HTTP request method");
    } else if (requestLine.uriEnd < 1) {
        throw new ServletException("Missing HTTP request URI");
    }
    // Parse any query parameters out of the request URI      int question = requestLine.indexOf("?");
    if (question >= 0) {
        request.setQueryString(new String(requestLine.uri, question + 1, requestLine.uriEnd - question - 1));
        uri = new String(requestLine.uri, 0, question);
    } else {
        request.setQueryString(null);
        uri = new String(requestLine.uri, 0, requestLine.uriEnd);
    }
    // Checking for an absolute URI (with the HTTP protocol)
    if (!uri.startsWith("/")) {
        int pos = uri.indexOf("://");
        // Parsing out protocol and host name
        if (pos != -1) {
            pos = uri.indexOf('/', pos + 3);
            if (pos == -1) {
                uri = "";
            } else {
                uri = uri.substring(pos);
            }
        }
    }
    // Parse any requested session ID out of the request URI
    String match = ";jsessionid=";
    int semicolon = uri.indexOf(match);
    if (semicolon >= 0) {
        String rest = uri.substring(semicolon + match,length());
        int semicolon2 = rest.indexOf(';');
    if (semicolon2 >= 0) {
        request.setRequestedSessionId(rest.substring(0, semicolon2));
        rest = rest.substring(semicolon2);
    } else {
        request.setRequestedSessionId(rest);
        rest = "";
        }
        request.setRequestedSessionURL(true);
        uri = uri.substring(0, semicolon) + rest;
    } else {
        request.setRequestedSessionId(null);
        request.setRequestedSessionURL(false);
    }
    // Normalize URI (using String operations at the moment)
    String normalizedUri = normalize(uri);
    // Set the corresponding request properties
    ((HttpRequest) request).setMethod(method);
    request.setProtocol(protocol);
    if (normalizedUri != null) {
        ((HttpRequest) request).setRequestURI(normalizedUri);
    } else {
        ((HttpRequest) request).setRequestURI(uri);
        }
        if (normalizedUri == null) {
            throw new ServletException("Invalid URI: " + uri + "'");
} }

The parseRequest method starts by calling the SocketInputStream class's readRequestLine method:

input.readRequestLine(requestLine);

where requestLine is an instance of HttpRequestLine inside HttpProcessor:

private HttpRequestLine requestLine = new HttpRequestLine();

Invoking its readRequestLine method tells the SocketInputStream to populate the HttpRequestLine instance.

Next, the parseRequest method obtains the method, URI, and protocol of the request line:

String method = new String(requestLine.method, 0, requestLine.methodEnd);
String uri = null;
String protocol = new String(requestLine.protocol, 0, requestLine.protocolEnd);

However, there may be a query string after the URI. If present, the query string is separated by a question mark. Therefore, the parseRequest method attempts to first obtain the query string and populates the HttpRequest object by calling its setQueryString method:

// Parse any query parameters out of the request URI
int question = requestLine.indexOf("?");
if (question >= 0) {
    // there is a query string.
    request.setQueryString(new String(requestLine.uri, question + 1, requestLine.uriEnd - question - 1));
    uri = new String(requestLine.uri, 0, question); }
else {
    request.setQueryString (null);
    uri = new String(requestLine.uri, 0, requestLine.uriEnd);
}

However, while most often a URI points to a relative resource, a URI can also be an absolute value, such as the following:

http://www.brainysoftware.com/index.html?name=Tarzan

The parseRequest method also checks this:

// Checking for an absolute URI (with the HTTP protocol)
if (!uri.startsWith("/")) {
    // not starting with /, this is an absolute URI
    int pos = uri.indexOf("://");
    // Parsing out protocol and host name
    if (pos != -1) {
        pos = uri.indexOf('/', pos + 3);
        if (pos == -1) {
        uri = "";
        } else {
        uri = uri.substring(pos);
    } }
}

Then, the query string may also contain a session identifier, indicated by the jsessionid parameter name. Therefore, the parseRequest method checks for a session identifier too. If jsessionid is found in the query string, the method obtains the session identifier and assigns the value to the HttpRequest instance by calling its setRequestedSessionId method:

// Parse any requested session ID out of the request URI
String match = ";
jsessionid=";
int semicolon = uri.indexOf(match);
if (semicolon >= 0) {
    String rest = uri.substring(semicolon + match.length());
    int semicolon2 = rest.indexOf(';');
    if (semicolon2 >= 0) {
        request.setRequestedSessionId(rest.substring(0, semicolon2));
        rest = rest.substring(semicolon2);
    } else {
        request.setRequestedSessionId(rest);
        rest = "";
    }
    request.setRequestedSessionURL (true);
    uri = uri.substring(0, semicolon) + rest;
} else {
    request.setRequestedSessionId(null);
    request.setRequestedSessionURL(false);
}

If jsessionid is found, this also means that the session identifier is carried in the query string, and not in a cookie. Therefore, pass true to the request's setRequestSessionURL method. Otherwise, pass false to the setRequestSessionURL method and null to the setRequestedSessionURL method.

At this point, the value of uri has been stripped off the jsessionid.

Then, the parseRequest method passes uri to the normalize method to correct an "abnormal" URI. For example, any occurrence of \ will be replaced by /. If uri is in good format or if the abnormality can be corrected, normalize returns the same URI or the corrected one. If the URI cannot be corrected, it will be considered invalid and normalize returns null. On such an occasion (normalize returning null), the parseRequest method will throw an exception at the end of the method.

Finally, the parseRequest method sets some properties of the HttpRequest object:

((HttpRequest) request).setMethod(method);
request.setProtocol(protocol);
if (normalizedUri != null) {
    ((HttpRequest) request).setRequestURI(normalizedUri);
} else {
    ((HttpRequest) request).setRequestURI(uri);
}

And, if the return value from the normalize method is null, the method throws an exception:

if (normalizedUri == null) {
    throw new ServletException("Invalid URI: " + uri + "'");
}

Parsing Headers

An HTTP header is represented by the HttpHeader class. This class will be explained in detail in Chapter 4, for now it is sufficient to know the following:

  • You can construct an HttpHeader instance by using its class's no-argument constructor.
  • Once you have an HttpHeader instance, you can pass it to the readHeader method of SocketInputStream. If there is a header to read, the readHeader method will populate the HttpHeader object accordingly. If there is no more header to read, both nameEnd and valueEnd fields of the HttpHeader instance will be zero.
  • To obtain the header name and value, use the following:
  • String name = new String(header.name, 0, header.nameEnd);
  • String value = new String(header.value, 0, header.valueEnd);

The parseHeaders method contains a while loop that keeps reading headers from the SocketInputStream until there is no more header. The loop starts by constructing an HttpHeader instance and passing it to the SocketInputStream class's readHeader:

HttpHeader header = new HttpHeader();
// Read the next
header input.readHeader(header);

Then, you can test whether or not there is a next header to be read from the input stream by testing the nameEnd and valueEnd fields of the HttpHeader instance:

if (header.nameEnd == 0) {
    if (header.valueEnd == 0) {
    return;
    } else {
        throw new ServletException
        (sm.getString("httpProcessor.parseHeaders.colon"));
    }
}

If there is a next header, the header name and value can then be retrieved:

String name = new String(header.name, 0, header.nameEnd);
String value = new String(header.value, 0, header.valueEnd);

Once you get the header name and value, you add it to the headers HashMap in the HttpRequest object by calling its addHeader method:

request.addHeader(name, value);

Some headers also require the setting of some properties. For instance, the value of the content-length header is to be returned when the servlet calls the getContentLength method of javax.servlet.ServletRequest, and the cookie header contains cookies to be added to the cookie collection. Thus, here is some processing:

if (name.equals("cookie")) {
    ... // process cookies here
} else if (name.equals("content-length")) {
    int n = -1;
try {
    n = Integer.parseInt (value);
    } catch (Exception e) {
        throw new ServletException(sm.getString(
"httpProcessor.parseHeaders.contentLength"));
    }
    request.setContentLength(n);
    } else if (name.equals("content-type")) {
        request.setContentType(value);
}

Cookie parsing is discussed in the next section, Parsing Cookies.

Parsing Cookies

Cookies are sent by a browser as an HTTP request header. Such a header has the name "cookie" and the value is the cookie name/value pair(s). Here is an example of a cookie header containing two cookies: userName and password.

Cookie: userName=budi; password=pwd;

Cookie parsing is done using the parseCookieHeader method of the org.apache.catalina.util.RequestUtil class. This method accepts the cookie header and returns an array of javax.servlet.http.Cookie. The number of elements in the array is the same as the number of cookie name/value pairs in the header. The parseCookieHeader method is given in Listing 3.5.

Listing 3.5: The org.apache.catalina.util.RequestUtil class's parseCookieHeader method

public static Cookie[] parseCookieHeader(String header) {
    if ((header == null) || (header.length 0 < 1) )
        return (new Cookie[0]);
        ArrayList cookies = new ArrayList();
        while (header.length() > 0) {
            int semicolon = header.indexOf(';');
            if (semicolon < 0)
                semicolon = header.length();
                if (semicolon == 0)
                    break;
                    String token = header.substring(0, semicolon);
                    if (semicolon < header.length())
                        header = header.substring(semicolon + 1);
                        else
                        header = "";
                        try {
                            int equals = token.indexOf('=');
                            if (equals > 0) {
                            String name = token.substring(0, equals).trim();
                            String value = token.substring(equals+1).trim();
                            cookies.add(new Cookie(name, value));
        } } catch (Throwable e) {
            ;
        }
    }
    return ((Cookie[]) cookies.toArray (new Cookie [cookies.size ()]));
}

And, here is the part of the HttpProcessor class's parseHeader method that processes the cookies:

else if (header.equals(DefaultHeaders.COOKIE_NAME)) {
    Cookie cookies[] = RequestUtil.ParseCookieHeader (value);
    for (int i = 0; i < cookies.length; i++) {
        if (cookies[i].getName().equals("jsessionid")){
            // Override anything requested in the URL
            if (!request.isRequestedSessionIdFromCookie()) {
            // Accept only the first session id cookie                request.setRequestedSessionId(cookies[i].getValue());
            request.setRequestedSessionCookie(true);
            request.setRequestedSessionURL(false);
    } }
    request.addCookie(cookies[i]);
}
}

Obtaining Parameters

You don't parse the query string or HTTP request body to get parameters until the servlet needs to read one or all of them by calling the getParameter, getParameterMap, getParameterNames, or getParameterValues methods of javax.servlet.http.HttpServletRequest. Therefore, the implementations of these four methods in HttpRequest always start with a call to the parseParameter method.

The parameters only needs to be parsed once and may only be parsed once because if the parameters are to be found in the request body, parameter parsing causes the SocketInputStream to reach the end of its byte stream. The HttpRequest class employs a boolean called parsed to indicate whether or not parsing has been done.

Parameters can be found in the query string or in the request body. If the user requested the servlet using the GET method, all parameters are on the query string. If the POST method is used, you may find some in the request body too. All the name/value pairs are stored in a HashMap. Servlet programmers can obtain the parameters as a Map (by calling getParameterMap of HttpServletRequest) and the parameter name/value. There is a catch, though. Servlet programmers are not allowed to change parameter values. Therefore, a special HashMap is used: org.apache.catalina.util.ParameterMap.

The ParameterMap class extends java.util.HashMap and employs a boolean called locked. The name/value pairs can only be added, updated or removed if locked is false. Otherwise, an IllegalStateException is thrown. Reading the values, however, can be done any time. The ParameterMap class is given in Listing 3.6. It overrides the methods for adding, updating and removing values. Those methods can only be called when locked is false.

Listing 3.6: The org.apache.Catalina.util.ParameterMap class.

package org.apache.catalina.util;

import java.util.HashMap; import java.util.Map;

public final class ParameterMap extends HashMap {
    public ParameterMap() {
        super ();
    }
    public ParameterMap(int initialCapacity) {
        super(initialCapacity);
    }
    public ParameterMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
    }
    public ParameterMap(Map map) {
        super(map);
    }
    private boolean locked = false;
    public boolean isLocked() {
        return (this.locked);
    }
    public void setLocked(boolean locked) {
        this.locked = locked;
    }
    private static final StringManager sm = StringManager.getManager("org.apache.catalina.util");
    public void clear() {
        if (locked)
        throw new IllegalStateException
        (sm.getString("parameterMap.locked"));
        super.clear();
    }
    public Object put(Object key, Object value) {
        if (locked)
            throw new IllegalStateException(sm.getString("parameterMap.locked"));
            return (super.put(key, value));
    }
    public void putAll(Map map) {
        if (locked)
            throw new IllegalStateException(sm.getString("parameterMap.locked"));
            super.putAll(map);
    }
    public Object remove(Object key) {
        if (locked)
            throw new IllegalStateException(sm.getString("parameterMap.locked"));
            return (super.remove(key));
    }
}

Now, let's see how the parseParameters method works.

Because parameters can exist in the query string and or the HTTP request body, the parseParameters method checks both the query string and the request body. Once parsed, parameters can be found in the object variable parameters, so the method starts by checking the parsedboolean, which is true if parsing has been done before.

if (parsed) return;

Then, the parseParameters method creates a ParameterMap called results and points it to parameters. It creates a new ParameterMap if parameters is null.

ParameterMap results = parameters;
if (results == null)
results = new ParameterMap();

Then, the parseParameters method opens the parameterMap's lock to enable writing to it.

results.setLocked(false);

Next, the parseParameters method checks the encoding and assigns a default encoding if the encoding is null.

String encoding = getCharacterEncoding();
if (encoding == null)
encoding = "ISO-8859-1";

Then, the parseParameters method tries the query string. Parsing parameters is done using the parseParameters method of org.apache.Catalina.util.RequestUtil.

// Parse any parameters specified in the query string
String queryString = getQueryString();
try {
    RequestUtil.parseParameters(results, queryString, encoding); }
    catch (UnsupportedEncodingException e) {
    ;
}

Next, the method tries to see if the HTTP request body contains parameters. This happens if the user sends the request using the POST method, the content length is greater than zero, and the content type is application/x-www-form-urlencoded. So, here is the code that parses the request body.

// Parse any parameters specified in the input stream
String contentType = getContentType();
if (contentType == null)
    contentType = "";
    int semicolon = contentType.indexOf(';');
    if (semicolon >= 0) {
        contentType = contentType.substring (0, semicolon).trim();
        } else {
        contentType = contentType.trim();
        }
        if ("POST".equals(getMethod()) && (getContentLength() > 0)
&& "application/x-www-form-urlencoded".equals(contentType)) {
        try {
        int max = getContentLength();
        int len = 0;
        byte buf[] = new byte[getContentLength()];
        ServletInputStream is = getInputStream();
        while (len < max) {
            int next = is.read(buf, len, max - len);
            if (next < 0 ) {
                break;
            }
            len += next; }
            is.close();
            if (len < max) {
                throw new RuntimeException("Content length mismatch");
            }
                RequestUtil.parseParameters(results, buf, encoding);
                } catch (UnsupportedEncodingException ue){ ;
                } catch (IOException e) {
                throw new RuntimeException("Content read fail");
            }
}

Finally, the parseParameters method locks the ParameterMap back, sets parsed to true and assigns results to parameters.

// Store the final results results.setLocked(true);
parsed = true;
parameters = results;

Creating a HttpResponse Object

The HttpResponse class implements javax.servlet.http.HttpServletResponse. Accompanying it is a façade class named HttpResponseFacade. Figure 3.3 shows the UML diagram of HttpResponse and its related classes.

Figure 3.3: The HttpResponse class and related classes

In Chapter 2, you worked with an HttpResponse class that was only partially functional. For example, its getWriter method returned a java.io.PrintWriter object that does not flush automatically when one of its print methods is called. The application in this chapter fixes this problem. To understand how it is fixed, you need to know what a Writer is.

From inside a servlet, you use a PrintWriter to write characters. You may use any encoding you desire, however the characters will be sent to the browser as byte streams. Therefore, it's not surprising that in Chapter 2, the ex02.pyrmont.HttpResponse class has the following getWriter method:

public PrintWriter getWriter() {
// if autoflush is true, println() will flush,
// but print() will not.
// the output argument is an
OutputStream writer = new PrintWriter(output, true);
return writer;
}

See, how we construct a PrintWriter object? By passing an instance of java.io.OutputStream. Anything you pass to the print or println methods of PrintWriter will be translated into byte streams that will be sent through the underlying OutputStream.

In this chapter you use an instance of the ex03.pyrmont.connector.ResponseStream class as the OutputStream for the PrintWriter. Note that the ResponseStream class is indirectly derived from the java.io.OutputStream class.

You also have the ex03.pyrmont.connector.ResponseWriter class that extends the PrintWriter class. The ResponseWriter class overrides all the print and println methods and makes any call to these methods automatically flush the output to the underlying OutputStream. Therefore, we use a ResponseWriter instance with an underlying ResponseStream object.

We could instantiate the ResponseWriter class by passing an instance of ResponseStream object. However, we use a java.io.OutputStreamWriter object to serve as a bridge between the ResponseWriter object and the ResponseStream object.

With an OutputStreamWriter, characters written to it are encoded into bytes using a specified charset. The charset that it uses may be specified by name or may be given explicitly, or the platform's default charset may be accepted. Each invocation of a write method causes the encoding converter to be invoked on the given character(s). The resulting bytes are accumulated in a buffer before being written to the underlying output stream. The size of this buffer may be specified, but by default it is large enough for most purposes. Note that the characters passed to the write methods are not buffered.

Therefore, here is the getWriter method:

public PrintWriter getWriter() throws IOException {
    ResponseStream newStream = new ResponseStream(this);
    newStream.setCommit(false);
    OutputStreamWriter osr = new OutputStreamWriter(newStream, getCharacterEncoding());
    writer = new ResponseWriter(osr);
    return writer;
}

Static Resource Processor and Servlet Processor

The ServletProcessor class is similar to the ex02.pyrmont.ServletProcessor class in Chapter 2. They both have only one method: process. However, the process method in ex03.pyrmont.connector.ServletProcessor accepts an HttpRequest and an HttpResponse, instead of instances of Request and Response. Here is the signature of the process method in this chapter's application:

public void process(HttpRequest request, HttpResponse response) {

In addition, the process method uses HttpRequestFacade and HttpResponseFacade as facade classes for the request and the response. Also, it calls the HttpResponse class's finishResponse method after calling the servlet's service method.

servlet = (Servlet) myClass.newInstance();
HttpRequestFacade requestPacade = new HttpRequestFacade(request);
HttpResponseFacade responseFacade = new HttpResponseFacade(response);
servlet.service(requestFacade, responseFacade);
((HttpResponse) response).finishResponse();

The StaticResourceProcessor class is almost identical to the ex02.pyrmont.StaticResourceProcessor class.

Running the Application

To run the application in Windows, from the working directory, type the following:

java -classpath ./lib/servlet.jar;./ ex03.pyrmont.startup.Bootstrap

In Linux, you use a colon to separate two libraries.

java -classpath ./lib/servlet.jar:./ ex03.pyrmont.startup.Bootstrap

To display index.html, use the following URL:

http://localhost:808O/index.html

To invoke PrimitiveServlet, direct your browser to the following URL:

http://localhost:8080/servlet/PrimitiveServlet

You'll see the following on your browser:

Hello. Roses are red.
Violets are blue.

Note: Running PrimitiveServlet in Chapter 2 did not give you the second line.

You can also call ModernServet, which would not run in the servlet containers in Chapter 2. Here is the URL:

http://localhost:8080/servlet/ModernServlet

Note The source code for ModernServlet can be found in the webroot directory under the working directory.

You can append a query string to the URL to test the servlet. Figure 3.4 shows the result if you run ModernServlet with the following URL.

http://localhost:8080/servlet/ModernServlet?userName=tarzan&password=pwd

Figure 3.4: Running ModernServlet

Summary

In this chapter you have learned how connectors work. The connector built is a simplified version of the default connector in Tomcat 4. As you know, the default connector has been deprecated because it is not efficient. For example, all HTTP request headers are parsed, even though they might not be used in the servlet. As a result, the default connector is slow and has been replaced by Coyote, a faster connector, whose source code can be downloaded from the Apache Software Foundation's web site. The default connector, nevertheless, serves as a good learning tool and will be discussed in detail in Chapter 4.