JMU
Design and Implementation of an HTTP Server
A Simple Network Application in Java


Prof. David Bernstein
James Madison University

Computer Science Department
bernstdh@jmu.edu


Hypertext Transfer Protocol
The Process
  1. The client opens a connection
  2. The client sends a request (GET, HEAD, or POST)
  3. The client waits for a response
  4. The server processes the request
  5. The server sends a response
  6. The connection is closed
Handling Special Characters
Uniform Resource Identifiers
A Sequence Diagram
images/http-example-v2_sequence.gif
HTTP 1.0 GET/HEAD Requests without Headers
HTTP 1.0 GET without Headers (cont.)

Ignoring the Query String

javaexamples/http/v0/HttpRequest.java
import java.io.*;
import java.net.*;
import java.util.*;


/**
 * An encapsulation of an HTTP request
 *
 * This version:
 *
 *     Does not handle headers
 *     Does not handle query string parameters
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 0.0
 */
public class HttpRequest
{
    private String         method, requestURI, version;
    private URI            uri;

    /**
     * Default Constructor
     */
    public HttpRequest()
    {
       method          = null;       
       requestURI      = null;       
       version         = null;
       uri             = null;       
    }

    /**
     * Returns the name of the HTTP method with which this request was made, 
     * (for example, GET, POST, or PUT)
     *
     * @return  The method
     */
    public String getMethod()
    {
       return method;
    }

    /**
     * Returns the part of this request's URI from the protocol name 
     * up to the query string in the first line of the HTTP request
     *
     * @return   The URI
     */
    public String getRequestURI()
    {
        return uri.getPath();
    }

    /**
     * Returns the the URI including the protocol name, host, and path
     * but not the query string
     *
     * @return   The URL
     */
    public String getRequestURL()
    {
        return uri.getScheme()+"://"+getRequestURI();
    }

    /**
     * Read this request
     *
     * @param in  The HttpInputStream to read from
     */
    public void read(HttpInputStream in)
    {
       String             line, request, token, value;
       String[]           tokens;

       try
       {
          line = in.readHttpLine();

          tokens = line.split("\\s");

          method  = tokens[0];
          request = tokens[1];
          if (tokens.length > 2) version = tokens[2];
          else                   version = "HTTP/0.9";

          // Parse the URI
          tokens = request.split("?");
          requestURI   = tokens[0].substring(1);
          
          try
          {
             uri = new URI(requestURI);
          }
          catch (URISyntaxException urise)
          {
             uri = null;
          }

       } 
       catch (IndexOutOfBoundsException ioobe)
       {
          // There was a problem processing the request
       } 
       catch (IOException ioe)
       {
          // There was a problem reading the request or
          // processing the headers
       }
    }

    /**
     * Returns a String representation of this Object
     *
     * @return   The String representation
     */
    public String toString()
    {
       Enumeration   e;
       String        name, s, value;

       s  = "Method: \n\t" + getMethod() + "\n";
       s += "URI: \n\t"    + getRequestURI()+"\n";

       return s;
    }
}
        
HTTP 1.0 Responses without Headers

HTTP/1.0 ResponseCode ResponseText CRLF
CRLF
Content-Length:length CRLF
Content-Type:type CRLF
CRLF
Data

javaexamples/http/v0/HttpResponse.java
import java.io.*;
import java.text.*;
import java.util.*;

/**
 * An encapsulation of an HTTP response
 *
 * This version:
 *
 *    Only supports two header elements, Content-Type and Content-Length
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 0.0
 */
public class HttpResponse
{
    private byte[]         data;
    private int            contentLength, status;
    private NumberFormat   nf;
    private String         contentType;

    public static final int SC_ACCEPTED             = 202;
    public static final int SC_BAD_GATEWAY          = 502;
    public static final int SC_BAD_REQUEST          = 400;
    public static final int SC_CREATED              = 201;
    public static final int SC_FORBIDDEN            = 403;
    public static final int SC_INTERNAL_ERROR       = 500;
    public static final int SC_MOVED                = 301;
    public static final int SC_NO_RESPONSE          = 204;
    public static final int SC_NOT_FOUND            = 404;
    public static final int SC_NOT_IMPLEMENTED      = 501;
    public static final int SC_OK                   = 200;
    public static final int SC_PARTIAL_INFORMATION  = 203;
    public static final int SC_PAYMENT_REQUIRED     = 402;
    public static final int SC_SERVICE_OVERLOADED   = 503;
    public static final int SC_UNAUTHORIZED         = 401;


    /**
     * Default Constructor
     *
     */
    public HttpResponse()
    {
        nf              = NumberFormat.getIntegerInstance();
        status          = SC_OK;
        data            = null;        
    }

    /**
     * Get the default message associated with a status code
     *
     * @param sc   The status code
     * @return     The associated default message
     */
    public static String getStatusMessage(int sc)
    {
        switch (sc) {
        case SC_ACCEPTED:              return "Accepted";
        case SC_BAD_GATEWAY:           return "Bad Gateway";
        case SC_BAD_REQUEST:           return "Bad Request";
        case SC_CREATED:               return "Created";
        case SC_FORBIDDEN:             return "Forbidden";
        case SC_INTERNAL_ERROR:        return "Internal Error";
        case SC_MOVED:                 return "Moved";
        case SC_NO_RESPONSE:           return "No Response";
        case SC_NOT_FOUND:             return "Not Found";
        case SC_NOT_IMPLEMENTED:       return "Not Implemented";
        case SC_OK:                    return "OK";
        case SC_PARTIAL_INFORMATION:   return "Partial Information";
        case SC_PAYMENT_REQUIRED:      return "Payment Required";
        case SC_SERVICE_OVERLOADED:    return "Service Overloaded";
        case SC_UNAUTHORIZED:          return "Unauthorized";
        default:                       return "Unknown Status Code " + sc;
        }
    }

    /**
     * Send an error response to the client.
     *
     * After using this method, the response should be considered to
     * be committed and should not be written to.
     *
     * @param sc    The status code
     * @param out   The HttpOutputStream to write to
     */
    public void sendError(int sc, HttpOutputStream out)
    {
       String     errorHTML;
       
       errorHTML = "<HTML><BODY><P>HTTP Error "+ sc + " - " + 
                   getStatusMessage(sc)+
                   "</P></BODY></HTML>\r\n";
       
       setStatus(sc);
       setData(errorHTML.getBytes());
       
       try
       {
          write(out);
       }
       catch (IOException ioe)
       {
          // Nothing can be done
       }
    }

    /**
     * Sets the status code for this response
     *
     * @param sc   The status code
     */
    public void setStatus(int sc)
    {
        status = sc;
    }

    /**
     * Sets the length of the content the server returns to the client
     *
     * @param length   The length (in bytes)
     */
    public void setContentLength(int length)
    {
       contentLength = length;
    }

    /**
     * Sets the type of the content the server returns to the client
     *
     * @param type   The type
     */
    public void setContentType(String type)
    {
        contentType = type;
    }

    /**
     * Set the payload/data for this HttpResponse
     *
     * @param data   The payload/data
     */
    public void setData(byte[] data)
    {
       this.data = data;
    }

    /**
     * Write this HttpResponse
     *
     * @param out    The HttpOutputStream to write to
     */
    public void write(HttpOutputStream out) throws IOException
    {
       if (data != null) setContentLength(data.length);
       else              setContentLength(0);

       writeStatusLine(out);
       writeHeaders(out);

       if (data != null) out.write(data);
       out.flush();
       out.close();
    }
    


    /**
     * Write the headers to an output stream
     *
     * @param out   The HttpOutputStream to write to
     */
    private void writeHeaders(HttpOutputStream out)
    {
        boolean          hasHeaders;
        Enumeration      e;

        out.printHeaderLine("Content-Length",
                            String.valueOf(contentLength));
        out.printHeaderLine("Content-Type", contentType);
        out.printEOL();
        out.flush();
    }

    /**
     * Write the status line to an output stream
     *
     * @param out   The HttpOutputStream to write to
     */
    private void writeStatusLine(HttpOutputStream out)
    {
        out.print("HTTP/1.0");
        out.print(" ");

        nf.setMaximumIntegerDigits(3);
        nf.setMinimumIntegerDigits(3);
        out.print(nf.format(status));
        out.print(" ");

        out.print(getStatusMessage(status));
        out.printEOL();

        out.flush();
    }
}
        
Name=Value Pairs
A NameValueMap Class
A NameValueMap Class (cont.)
javaexamples/http/v1/NameValueMap.java
import java.io.*;
import java.util.*;
import java.util.concurrent.*;

/**
 * A mapping of name=value pairs.
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class NameValueMap
{
    private Map<String,String>       delegate;
    

    /**
     * Explicit Value Constructor
     *
     * @param delegate    The Map to delegate to
     */
    private NameValueMap(Map<String,String> delegate)
    {
       this.delegate = delegate;
    }

    /**
     * Construct a NameValueMap (that is not thread safe)
     */
    public static NameValueMap createNameValueMap()
    {
       return new NameValueMap(new HashMap<String,String>());
    }


    /**
     * Construct a thread-safe NameValueMap
     */
    public static NameValueMap createConcurrentNameValueMap()
    {
       return new NameValueMap(new ConcurrentHashMap<String,String>());
    }

    /**
     * Remove all name=value pairs from this map.
     */
    public void clear()
    {
       try
       {
          delegate.clear();
       }
       catch (UnsupportedOperationException uoe)
       {
          // Couldn't clear
          //
          // Note: We could iterate through each element and remove()
          // it but this could cause a concurrent modification problem.
          // So, this would only work if the iterator supported the
          // remove() operation.
       }
    }


    /**
     * Get all of the names
     *
     * @return All of the names in this Map
     */
    public Iterator<String> getNames()
    {
       return delegate.keySet().iterator();       
    }

    /**
     * Get the value for a particular name
     *
     * @param  name   The name of interest
     * @return        The corresponding value (or null)
     */
    public String getValue(String name)
    {
       return delegate.get(name);       
    }


    /**
     * Add a name=value pair to this map.
     *
     * @param name   The name
     * @param value  The corresponding value
     */
    public void put(String name, String value)
    {
       delegate.put(name, value);
    }
    


    /**
     * Add a name=value pair to this map.
     *
     * Note: Only the first occurrence of the delimiter is significant.
     * The delimiter may appear in the value.
     *
     * @param pair   The String containing the name=value pair
     * @param regex  The delimiter between the name and value
     */
    public void putPair(String pair, String regex)
    {
       String   value;       
       String[] components;
       
       components = pair.split(regex);
       if (components.length == 2) delegate.put(components[0], components[1]);
       if (components.length >  2)
       {
          value = "";          
          for (int i=1; i<components.length; i++) value += components[i];
          delegate.put(components[0], value);
       }
    }
    

    /**
     * Add a name=value pair to this map (assuming the delimiter is
     * the '=' character).
     *
     * @param pair   The String containing the name=value pair
     */
    public void putPair(String pair)
    {
       putPair(pair, "=");       
    }
    

    /**
     * Add one or more name=value pairs to this map.
     *
     * @param pairs      The BufferedReader containing the lines of pairs
     * @param regexLine  The delimiter between the different pairs
     * @param regexPair  The delimiter between the name and value in each pair
     */
    public void putPairs(String pairs, String regexLine, String regexPair)
    {
       String[] components, lines;
       
       lines = pairs.split(regexLine);
       for (int i=0; i<lines.length; i++)
       {
          putPair(lines[i], regexPair);
       }
    }
    
    

    /**
     * Add one or more name=value pairs to this map.
     *
     * This method assumes that the pairs are delimited by "&" and the
     * names and values are delimited by "=".
     *
     * @param pairs      The BufferedReader containing the lines of pairs
     */
    public void putPairs(String pairs)
    {
       putPairs(pairs, "&", "=");       
    }
    

    /**
     * Add one or more name=value pairs to this map.
     *
     * This method reads from the BufferedReader until either an
     * end-of-stream is encountered or a line contains the String "".
     *
     * In the event of an IOException, this method will return (but
     * will not remove any pairs that might have been added).
     *
     * @param in     The BufferedReader containing the lines of pairs
     * @param regex  The delimiter between name and value in each pair
     */
    public void putPairs(BufferedReader in, String regex)
    {
       String      line;
       
       try
       {
          while (((line=in.readLine()) != null) && !line.equals(""))
          {
             putPair(line, regex);
          }
       }
       catch (IOException ioe)
       {
       }
    }
    

    /**
     * Add one or more name=value pairs to this map.
     *
     * This method reads from the BufferedReader until either an
     * end-of-stream is encountered or a line contains the String "".
     *
     * In the event of an IOException, this method will return (but
     * will not remove any pairs that might have been added).
     *
     * @param in     The HttpInputStream containing the lines of pairs
     * @param regex  The delimiter between name and value in each pair
     */
    public void putPairs(HttpInputStream in, String regex)
    {
       String      line;
       
       try
       {
          while (((line=in.readHttpLine()) != null) && !line.equals(""))
          {
             putPair(line, regex);
          }
       }
       catch (IOException ioe)
       {
       }
    }
    
}
        
HTTP 1.0 GET Requests with Headers

GET URI HTTP/1.0 CRLF
Name1: Value1 CRLF
Name2: Value2 CRLF
.
.
.
NameN: ValueN CRLF
CRLF

HTTP 1.0 GET with Headers (cont.)
javaexamples/http/v1/HttpRequest.java
import java.io.*;
import java.net.*;
import java.util.*;

/**
 * An encapsulation of an HTTP request
 *
 * This version:
 *
 *     Handles headers
 *     Handles query string parameters
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 0.1
 */
public class HttpRequest
{
    private int            contentLength;    
    private NameValueMap   headers, queryParameters;
    private String         method, queryString, version;
    private URI            uri;


    /**
     * Default Constructor
     */
    public HttpRequest()
    {
       method          = null;       
       queryString     = null;       
       version         = null;
    }

    /**
     * Returns the name of the HTTP method with which this request was made, 
     * (for example, GET, POST, or PUT)
     *
     * @return  The method
     */
    public String getMethod()
    {
       return method;
    }

    /**
     * Returns the part of this request's URI from the protocol name 
     * up to the query string in the first line of the HTTP request
     *
     * @return   The URI
     */
    public String getRequestURI()
    {
       String     path;
       
       path = uri.getPath();
       if ((path == null) || path.equals("")) return "index.html";
       else                                   return path;
    }

    /**
     * Read this request
     *
     * @param in   The HttpInputStream to read from
     */
    public void read(HttpInputStream in) throws IndexOutOfBoundsException,
                                                IOException, URISyntaxException
    {
       InputStream          tis;       
       String               line, request, token, value;
       String[]             pair, tokens;


       line = in.readHttpLine();

       tokens = line.split("\\s");

       method  = tokens[0];
       request = tokens[1];
       if (tokens.length > 2) version = tokens[2];
       else                   version = "HTTP/0.9";

       // Parse the URI
       uri = new URI(request);

       // Get the decoded query string
       queryString = uri.getQuery();

       // Process the query string
       queryParameters = NameValueMap.createNameValueMap();
       if (queryString != null) queryParameters.putPairs(queryString,"&","=");
          
       // Process the headers
       headers = NameValueMap.createNameValueMap();
       headers.putPairs(in, ":");

       // Get the content length
       token = headers.getValue("Content-Length");
       try 
       {
           contentLength = Integer.parseInt(token.trim());
       } 
       catch (Exception e) 
       {
          contentLength = -1;
       }
    }

    /**
     * Returns a String representation of this Object
     *
     * @return   The String representation
     */
    public String toString()
    {
       Iterator<String>  i;
       String            name, s, value;

       s  = "Method: \n\t" + getMethod() + "\n";
       s += "URI: \n\t"    + getRequestURI()+"\n";

       s += "Parameters:\n"+ queryString + "\n";

       s += "Headers:\n";
       i = headers.getNames();
       while (i.hasNext()) 
       {
          name  = i.next();
          value = headers.getValue(name);
          s += "\t" + name + "\t" + value + "\n";
       }

       return s;
    }
}
        
HTTP 1.0 Responses with Headers

HTTP/1.0 ResponseCode ResponseText CRLF
Name1: Value1 CRLF
Name2: Value2 CRLF
.
.
.
NameN: ValueN CRLF
CRLF
Data

javaexamples/http/v1/HttpResponse.java
import java.io.*;
import java.text.*;
import java.util.*;

/**
 * An encapsulation of an HTTP response
 *
 * This version:
 *
 *    Addse support for headers
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 0.1
 */
public class HttpResponse
{
    private byte[]         data;
    private int            contentLength, status;
    private NumberFormat   nf;
    private NameValueMap   headers;
    private String         contentType;

    public static final int SC_ACCEPTED             = 202;
    public static final int SC_BAD_GATEWAY          = 502;
    public static final int SC_BAD_REQUEST          = 400;
    public static final int SC_CREATED              = 201;
    public static final int SC_FORBIDDEN            = 403;
    public static final int SC_INTERNAL_ERROR       = 500;
    public static final int SC_MOVED                = 301;
    public static final int SC_NO_RESPONSE          = 204;
    public static final int SC_NOT_FOUND            = 404;
    public static final int SC_NOT_IMPLEMENTED      = 501;
    public static final int SC_OK                   = 200;
    public static final int SC_PARTIAL_INFORMATION  = 203;
    public static final int SC_PAYMENT_REQUIRED     = 402;
    public static final int SC_SERVICE_OVERLOADED   = 503;
    public static final int SC_UNAUTHORIZED         = 401;


    /**
     * Default Constructor
     *
     */
    public HttpResponse()
    {
        headers         = NameValueMap.createNameValueMap();
        nf              = NumberFormat.getIntegerInstance();
        status          = SC_OK;
        data            = null;        
    }

    /**
     * Get the default message associated with a status code
     *
     * @param sc   The status code
     * @return     The associated default message
     */
    public static String getStatusMessage(int sc)
    {
        switch (sc) {
        case SC_ACCEPTED:              return "Accepted";
        case SC_BAD_GATEWAY:           return "Bad Gateway";
        case SC_BAD_REQUEST:           return "Bad Request";
        case SC_CREATED:               return "Created";
        case SC_FORBIDDEN:             return "Forbidden";
        case SC_INTERNAL_ERROR:        return "Internal Error";
        case SC_MOVED:                 return "Moved";
        case SC_NO_RESPONSE:           return "No Response";
        case SC_NOT_FOUND:             return "Not Found";
        case SC_NOT_IMPLEMENTED:       return "Not Implemented";
        case SC_OK:                    return "OK";
        case SC_PARTIAL_INFORMATION:   return "Partial Information";
        case SC_PAYMENT_REQUIRED:      return "Payment Required";
        case SC_SERVICE_OVERLOADED:    return "Service Overloaded";
        case SC_UNAUTHORIZED:          return "Unauthorized";
        default:                       return "Unknown Status Code " + sc;
        }
    }

    /**
     * Send an error response to the client.
     *
     * After using this method, the response should be considered to
     * be committed and should not be written to.
     *
     * @param sc    The status code
     * @param out   The HttpOutputStream to write to
     */
    public void sendError(int sc, HttpOutputStream out)
    {
       String     errorHTML;
       
       errorHTML = "<HTML><BODY><P>HTTP Error "+ sc + " - " + 
                   getStatusMessage(sc)+
                   "</P></BODY></HTML>\r\n";
       
       setStatus(sc);
       setData(errorHTML.getBytes());
       
       try
       {
          write(out);
       }
       catch (IOException ioe)
       {
          // Nothing can be done
       }
    }

    /**
     * Sets the length of the content the server returns to the client
     *
     * @param length   The length (in bytes)
     */
    public void setContentLength(int length)
    {
       contentLength = length;
       setHeader("Content-Length", Integer.toString(contentLength));       
    }

    /**
     * Sets the type of the content the server returns to the client
     *
     * @param type   The type
     */
    public void setContentType(String type)
    {
        contentType = type;
        setHeader("Content-Type", contentType);       
    }

    /**
     * Set the payload/data for this HttpResponse
     *
     * @param data   The payload/data
     */
    public void setData(byte[] data)
    {
       this.data = data;
    }

    /**
     * Adds a field (with the given name and value) to the 
     * response header
     *
     * Note: This method must be called before getOutputStream()
     *
     * @param name   The name of the field
     * @param value  The value of the field
     */
    public void setHeader(String name, String value)
    {
        headers.put(name, value);
    }

    /**
     * Sets the status code for this response
     *
     * @param sc   The status code
     */
    public void setStatus(int sc)
    {
        status = sc;
    }

    /**
     * Write this HttpResponse
     *
     * @param out    The HttpOutputStream to write to
     */
    public void write(HttpOutputStream out) throws IOException
    {
       if (data != null) setContentLength(data.length);
       else              setContentLength(0);

       writeStatusLine(out);
       writeHeaders(out);

       if (data != null) out.write(data);
       out.flush();
       out.close();
    }


    /**
     * Write the headers
     *
     * @param out   The HttpOutputStream to write to
     */
    private void writeHeaders(HttpOutputStream out)
    {
       Iterator<String> i;
       String           name, value;

       i = headers.getNames();
       while (i.hasNext())
       {
          name =  i.next();
          value = headers.getValue(name);

          if ((value != null) && (!value.equals("")))
             out.printHeaderLine(name, value);
       }
       out.printEOL();
       out.flush();
    }

    /**
     * Write the status line
     *
     * @param out   The HttpOutputStream to write to
     */
    private void writeStatusLine(HttpOutputStream out)
    {
        out.print("HTTP/1.0");
        out.print(" ");

        nf.setMaximumIntegerDigits(3);
        nf.setMinimumIntegerDigits(3);
        out.print(nf.format(status));
        out.print(" ");

        out.print(getStatusMessage(status));
        out.printEOL();

        out.flush();
    }
}
        
A Shortcoming of the Current Design
An Improved Design
javaexamples/http/v2/HttpMessage.java
import java.io.*;
import java.net.*;
import java.util.*;

/**
 * A partial encapsulation of an HTTP message (i.e., request
 * or response)
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 0.2
 */
public abstract class HttpMessage
{
    protected byte[]         content;
    protected NameValueMap   headers;


    /**
     * Default Constructor
     */
    public HttpMessage()
    {
       content         = null;        
       headers         = NameValueMap.createNameValueMap();       
    }

    /**
     * Get the content of this HttpMessage
     *
     * @return   The content
     */
    public byte[] getContent()
    {
       return content;
    }

    /**
     * Returns the length, in bytes, of the content contained in the 
     * request and sent by way of the input stream
     *
     * @return  The length or -1 if the length is not known
     */
    public int getContentLength()
    {
       int        result;
       
       try
       {
          result = Integer.parseInt(getHeader("Content-Length"));
       }
       catch (Exception e)
       {
          result = -1;
       }

       return result;
    }

    /**
     * Returns the value of the specified header
     *
     * @param name   The name of the header
     * @return       The value of the header
     */
    public String getHeader(String name)
    {
        return headers.getValue(name);
    }

    /**
     * Returns the names of all headers
     *
     * @return       The names of all headers
     */
    public Iterator<String> getHeaderNames()
    {
        return headers.getNames();
    }

    /**
     * Read this HttpMessage (up to, but not including, the
     * content).
     *
     * Note: The content is not read so that the content may
     * be read in a specialized way (e.g., as formatted binary data)
     * or in case the content is large (and should be "streamed").
     *
     * @param in   The HttpInputStream to read from
     */
    public abstract void read(HttpInputStream in) throws 
                                                IndexOutOfBoundsException,
                                                IOException, 
                                                URISyntaxException;
    
    

    /**
     * Read the content of this HttpMessage.
     * Note: This method must only be called after read()
     * in the child classes.
     *
     * @param in   The HttpInputStream to read from
     */
    public void readContent(HttpInputStream in)
    {
       int          contentLength;
       
       contentLength = getContentLength();

       if (contentLength > 0)
       {
          content = new byte[contentLength];
          try
          {
             in.readFully(content);
          }
          catch (IOException ioe)
          {
             content = null;
             setContentLength(-1);             
          }
       }
    }

    /**
     * Set the content (i.e., payload) for this HttpMessage
     *
     * @param content   The payload/content
     */
    public void setContent(byte[] content)
    {
       this.content = content;
       setContentLength(content.length);
    }

    /**
     * Sets the content length
     * 
     * @param  contentLength    The contentLength
     */
    public void setContentLength(int contentLength)
    {
        setHeader("Content-Length", Integer.toString(contentLength));
    }

    /**
     * Sets the type of the content the server returns to the client
     *
     * @param type   The type
     */
    public void setContentType(String type)
    {
       setHeader("Content-Type", type);       
    }

    /**
     * Adds a field (with the given name and value) to the 
     * response header
     *
     * Note: This method must be called before getOutputStream()
     *
     * @param name   The name of the field
     * @param value  The value of the field
     */
    public void setHeader(String name, String value)
    {
        headers.put(name, value);
    }

    /**
     * Set the headers
     * 
     * @param   headers The headers
     */
    public void setHeaders(NameValueMap headers)
    {
        this.headers         = headers;
    }
}
        
An Improved Design (cont.)
javaexamples/http/v2/HttpRequest.java
import java.io.*;
import java.net.*;
import java.util.*;


/**
 * An encapsulation of an HTTP request
 *
 * This version:
 *
 *     Extends the abstract class HttpMessage
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 0.2
 */
public class HttpRequest extends HttpMessage
{
    private NameValueMap   queryParameters;
    private String         method, queryString, version;
    private URI            uri;

    /**
     * Default Constructor
     */
    public HttpRequest()
    {
       super();       
       method          = null;       
       queryString     = null;       
       version         = null;
    }

    /**
     * Returns the name of the HTTP method with which this request was made, 
     * (for example, GET, POST, or PUT)
     *
     * @return  The method
     */
    public String getMethod()
    {
       return method;
    }


    /**
     * Returns the part of this request's URI from the protocol name 
     * up to the query string in the first line of the HTTP request
     *
     * @return   The URI
     */
    public String getRequestURI()
    {
       String     path;

       if (uri == null) path = null;
       else             path = uri.getPath();
       if ((path == null) || path.equals("")) return "index.html";
       else                                   return path;
    }


    /**
     * Read this HttpRequest (up to, but not including, the
     * content).
     *
     * Note: The content is not read so that the content may
     * be read in a specialized way (e.g., as formatted binary data)
     * or in case the content is large (and should be "streamed").
     *
     * @param in   The HttpInputStream to read from
     */
    public void read(HttpInputStream in) throws IndexOutOfBoundsException,
                                                IOException, URISyntaxException
    {
       String               line, request, token, value;
       String[]             pair, tokens;

       line    = in.readHttpLine();

       tokens  = line.split("\\s");
       method  = tokens[0];
       request = tokens[1];
       if (tokens.length > 2) version = tokens[2];
       else                   version = "HTTP/0.9";

       // Parse the URI
       uri = new URI(request);

       // Get the decoded query string
       queryString = uri.getQuery();

       // Process the query string
       queryParameters = NameValueMap.createNameValueMap();
       if (queryString != null) queryParameters.putPairs(queryString,"&","=");
          
       // Process the headers
       headers = NameValueMap.createNameValueMap();
       headers.putPairs(in, ":");

       // Get the content length
       token = headers.getValue("Content-Length");
       try 
       {
           setContentLength(Integer.parseInt(token.trim()));
       } 
       catch (Exception e) 
       {
          setContentLength(-1);
       }
    }

    /**
     * Set the method
     */
    public void setMethod(String method)
    {
       this.method = method;       
    }

    /**
     * Set the URI
     */
    public void setURI(URI uri)
    {
       this.uri = uri;       
    }

    /**
     * Set the version
     */
    public void setVersion(String version)
    {
       this.version = version;       
    }

    /**
     * Returns a String representation of this Object
     *
     * @return   The String representation
     */
    public String toString()
    {
       Iterator<String>  i;
       String            name, s, value;

       s  = "Method: \n\t" + getMethod() + "\n";
       s += "URI: \n\t"    + getRequestURI()+"\n";

       s += "Parameters:\n"+ queryString + "\n";

       s += "Headers:\n";
       i = getHeaderNames();
       while (i.hasNext()) 
       {
          name  = i.next();
          value = headers.getValue(name);
          s += "\t" + name + "\t" + value + "\n";
       }

       return s;
    }

    /**
     * Write this HttpRequest
     *
     * @param out   The HttpOutputStream to write to
     */
    public void write(HttpOutputStream out)
    {
       Iterator<String>  i;
       String            name, value;

       out.printHttpLine(method+" "+getRequestURI()+" HTTP/"+version);
       i = getHeaderNames();
       while (i.hasNext()) 
       {
          name  = i.next();
          value = headers.getValue(name);
          out.printHeaderLine(name, value);
       }
       out.printEOL();
       try
       {
          if (content != null) out.write(content);
       }
       catch (IOException ioe)
       {
          // Couldn't write the content
       }
       out.flush();
    }
    
}
        
An Improved Design (cont.)
javaexamples/http/v2/HttpResponse.java
import java.io.*;
import java.text.*;
import java.util.*;

/**
 * An encapsulation of an HTTP response
 *
 * This version adds:
 *
 *     Extends the abstract class HttpMessage
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 0.2
 */
public class HttpResponse extends HttpMessage
{
    private int            status;
    private NumberFormat   nf;

    public static final int SC_ACCEPTED             = 202;
    public static final int SC_BAD_GATEWAY          = 502;
    public static final int SC_BAD_REQUEST          = 400;
    public static final int SC_CREATED              = 201;
    public static final int SC_FORBIDDEN            = 403;
    public static final int SC_INTERNAL_ERROR       = 500;
    public static final int SC_MOVED                = 301;
    public static final int SC_NO_RESPONSE          = 204;
    public static final int SC_NOT_FOUND            = 404;
    public static final int SC_NOT_IMPLEMENTED      = 501;
    public static final int SC_OK                   = 200;
    public static final int SC_PARTIAL_INFORMATION  = 203;
    public static final int SC_PAYMENT_REQUIRED     = 402;
    public static final int SC_SERVICE_OVERLOADED   = 503;
    public static final int SC_UNAUTHORIZED         = 401;


    /**
     * Default Constructor
     *
     */
    public HttpResponse()
    {
       super();       
       nf              = NumberFormat.getIntegerInstance();
       status          = SC_OK;
    }

    /**
     * Get the status associated with this HttpResponse
     *
     * @return   The status
     */
    public int getStatus()
    {
       return status;       
    }
    

    /**
     * Get the default message associated with a status code
     *
     * @param sc   The status code
     * @return     The associated default message
     */
    public static String getStatusMessage(int sc)
    {
       switch (sc) {
          case SC_ACCEPTED:              return "Accepted";
          case SC_BAD_GATEWAY:           return "Bad Gateway";
          case SC_BAD_REQUEST:           return "Bad Request";
          case SC_CREATED:               return "Created";
          case SC_FORBIDDEN:             return "Forbidden";
          case SC_INTERNAL_ERROR:        return "Internal Error";
          case SC_MOVED:                 return "Moved";
          case SC_NO_RESPONSE:           return "No Response";
          case SC_NOT_FOUND:             return "Not Found";
          case SC_NOT_IMPLEMENTED:       return "Not Implemented";
          case SC_OK:                    return "OK";
          case SC_PARTIAL_INFORMATION:   return "Partial Information";
          case SC_PAYMENT_REQUIRED:      return "Payment Required";
          case SC_SERVICE_OVERLOADED:    return "Service Overloaded";
          case SC_UNAUTHORIZED:          return "Unauthorized";
          default:                       return "Unknown Status Code " + sc;
       }
    }

    /**
     * Read this HttpResponse (up to, but not including, the
     * content).
     *
     * Note: The content is not read so that the content may
     * be read in a specialized way (e.g., as formatted binary data)
     * or in case the content is large (and should be "streamed").
     *
     * @param in   The HttpInputStream to read from
     */
    public void read(HttpInputStream in)
    {
       String           line;
       String[]         tokens;
       
       try
       {
          line = in.readHttpLine();
          tokens = line.split("\\s");
          try
          {
             setStatus(Integer.parseInt(tokens[1]));
          }
          catch (NumberFormatException nfe)
          {
             setStatus(-1);          
          }
          headers.putPairs(in, ":");
       }
       catch (IOException ioe)
       {
          setStatus(SC_NO_RESPONSE);          
       }
    }

    /**
     * Send an error response to the client.
     *
     * After using this method, the response should be considered to
     * be committed and should not be written to.
     *
     * @param sc    The status code
     * @param out   The HttpOutputStream to write to
     */
    public void sendError(int sc, HttpOutputStream out)
    {
       String     errorHTML;
       
       errorHTML = "<HTML><BODY><P>HTTP Error "+ sc + " - " + 
          getStatusMessage(sc)+
          "</P></BODY></HTML>\r\n";
       
       setStatus(sc);
       setContent(errorHTML.getBytes());
       
       try
       {
          write(out);
       }
       catch (IOException ioe)
       {
          // Nothing can be done
       }
    }

    /**
     * Sets the status code for this response
     *
     * @param sc   The status code
     */
    public void setStatus(int sc)
    {
       status = sc;
    }


    /**
     * Returns a String representation of this Object
     *
     * @return   The String representation
     */
    public String toString()
    {
       Iterator<String>  i;
       String            name, s, value;

       s  = "Status: \n\t" + status + "\n";
       s += "Headers:\n";
       i = getHeaderNames();
       while (i.hasNext()) 
       {
          name  = i.next();
          value = headers.getValue(name);
          s += "\t" + name + "\t" + value + "\n";
       }

       return s;
    }

    /**
     * Write this HttpResponse
     *
     * @param out    The HttpOutputStream to write to
     */
    public void write(HttpOutputStream out) throws IOException
    {
       if (content != null) setContentLength(content.length);
       else                 setContentLength(0);

       writeStatusLine(out);
       writeHeaders(out);

       if (content != null) out.write(content);
       out.flush();
       out.close();
    }

    /**
     * Write the headers to an output stream
     *
     * @param out   The HttpOutputStream to write to
     */
    private void writeHeaders(HttpOutputStream out)
    {
       Iterator<String> i;
       String           name, value;

       i = headers.getNames();
       while (i.hasNext())
       {
          name =  i.next();
          value = headers.getValue(name);

          if ((value != null) && (!value.equals(""))) 
             out.printHeaderLine(name,value);
       }
       out.printEOL();
       out.flush();
    }

    /**
     * Write the status line to an output stream
     *
     * @param out   The HttpOutputStream to write to
     */
    private void writeStatusLine(HttpOutputStream out)
    {
       out.print("HTTP/1.0");
       out.print(" ");

       nf.setMaximumIntegerDigits(3);
       nf.setMinimumIntegerDigits(3);
       out.print(nf.format(status));
       out.print(" ");

       out.print(getStatusMessage(status));
       out.printEOL();

       out.flush();
    }
}
        
Content Types
A MIMETyper Class
javaexamples/http/v2/MIMETyper.java
import java.io.*;
import java.net.FileNameMap;



/**
 * A utility class for working with MIME types
 *
 * Note: This class makes use of the Singleton Pattern since there
 * is never need for more than one MIMETyper.  The MIMETyper class is
 * thread-safe,
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class MIMETyper implements FileNameMap
{
    private NameValueMap         types;


    private static MIMETyper     instance = new MIMETyper();
    private static final String  DEFAULT = "application/octet-stream";

    /**
     * Default Constructor
     */
    private MIMETyper()
    {
       types = NameValueMap.createConcurrentNameValueMap();
       initializeTypes();
    }



    /**
     * Create an instance of a MIMETyper if necessary.
     * Otherwise, return the existing instance.
     *
     * @return   The instance
     */
    public static MIMETyper createInstance()
    {
       return instance;
    }



    /**
     * Guess the MIME type from a file extension
     *
     * @param  ext   The extension (e.g., ".gif")
     * @return       The MIME type (e.g., "image/gif")
     */
    public String getContentTypeForExtension(String ext)
    {
       String    type;

       type = types.getValue(ext.toLowerCase());
       if (type == null) type = DEFAULT;

       return type;
    }




    /**
     * Guess the MIME type from a file name
     * (possibly including a path)
     *
     * @param  name  The name (e.g., "/pictures/dome.gif")
     * @return       The MIME type (e.g., "image/gif")
     */
    public String getContentTypeFor(String name)
    {
       String    ext;

       ext = FileTyper.getExtension(name);

       return getContentTypeForExtension(ext);
    }




    /**
     * Initialize the types table
     */
    private void initializeTypes()
    {
       BufferedReader       in;
       String               line;       

       try
       {
          in = new BufferedReader(new FileReader("mimetypes.dat"));
          types.putPairs(in, "\t");
       }
       catch (IOException ioe)
       {
          types.put(".htm","text/html");
          types.put(".html","text/html");
          types.put(".text","text/plain");
       }
    }
}
        
An HTTP Connection Handler
javaexamples/http/v2/HttpConnectionHandler.java
import java.io.*;
import java.net.*;
import java.util.*;


/**
 * Handle an HTTP 1.0 connection in a new thread of execution
 *
 * This version:
 *
 *     Only supports GET requests
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 0.2
 */
public class HttpConnectionHandler implements Runnable
{
    private HttpInputStream  in;
    private HttpOutputStream out;    
    private MIMETyper        mimeTyper;
    private Socket           socket;


    /**
     * Explicit Value Constructor
     * (Starts the thread of execution)
     *
     * @param s    The TCP socket for the connection
     */
    public HttpConnectionHandler(Socket s)
    {
       socket = s;
       mimeTyper = MIMETyper.createInstance();
    }



    /**
     * The entry point for the thread
     */
    public void run()
    {
       HttpRequest     request;
       HttpResponse    response;
       InputStream     is;
       OutputStream    os;       
       String          method;

       try 
       {
          // Get the I/O streams for the socket
          is = socket.getInputStream();
          os = socket.getOutputStream();

          in  = new HttpInputStream(is);
          out = new HttpOutputStream(os);

          // Create an empty request and response
          response = new HttpResponse();
          request  = new HttpRequest();

          try
          {
             // Read and parse the request information
             request.read(in);


             // Determine the method to use
             method   = request.getMethod().toUpperCase();

             // Respond to the request
             if      ((request == null) || (method == null))
                response.sendError(HttpResponse.SC_BAD_REQUEST, out);
             else if (!method.equals("GET"))
                response.sendError(HttpResponse.SC_NOT_IMPLEMENTED, out);
             else                 
                doGet(request, response);
          }
          catch (Exception e)
          {
             response.sendError(HttpResponse.SC_BAD_REQUEST, out);
          }
       } 
       catch (IOException ioe) 
       {
          // I/O problem so terminate the thread.
          // The server should close the socket.
       }
    }


    /**
     * Handle the GET request
     *
     * @param request   Contents of the request
     * @param response  Used to generate the response
     */
    private void doGet(HttpRequest request, HttpResponse response)
    {
       byte[]             content;
       FileInputStream    fis;
       int                length;
       String             uri;

       // NOTE: We should check to make sure that the
       //       URI is in a "public" portion of the file system
       uri = "../public_html"+request.getRequestURI();

       try 
       {
          // Create a stream for the file
          // and determine its length
          fis = new FileInputStream(uri);
          length = fis.available();
          response.setStatus(HttpResponse.SC_OK);

          // Set the content type
          response.setContentType(mimeTyper.getContentTypeFor(uri));

          // Read the file
          content = new byte[length];
          fis.read(content);

          // Set the payload
          response.setContent(content);          

          //Write the response
          response.write(out);

          // Close the file
          fis.close();
       } 
       catch (IOException ioe) 
       {
          response.sendError(HttpResponse.SC_NOT_FOUND, out);
       }
    }

}
        
An HTTP Server
An HTTP Server
javaexamples/http/v2/HttpServer.java
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
import java.util.logging.*;


/**
 * A simplified HTTP server
 *
 * Note: This version only supports GET requests
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 3.0
 */
public class HttpServer implements Runnable
{
    private volatile boolean            keepRunning;
    private final    ExecutorService    threadPool;    
    private final    ServerSocket       serverSocket;
    private          Thread             controlThread;

    private static   Logger             logger;    
    private static   final int          MAX_THREADS = 100; // Should be tuned

    /**
     * The entry point of the application
     *
     * @param args    The command line arguments
     */
    public static void main(String[] args)
    {
       BufferedReader        in;       
       HttpServer            server;
       Handler               logHandler;       

       // Setup the logging system
       logger     = Logger.getLogger("edu.jmu.cs");
       try
       {
          logHandler = new FileHandler("log.txt");
          logHandler.setFormatter(new SimpleFormatter());
          logger.addHandler(logHandler);          
          logger.setLevel(Level.parse(args[0]));
          logger.setUseParentHandlers(false);          
       }
       catch (Exception e)
       {
          // The FileHandler couldn't be constructed or the Level was bad
          // so use the default ConsoleHandler (at the default Level.INFO)
          logger.setUseParentHandlers(true);          
       }

       server = null;          
       try
       {
          in = new BufferedReader(new InputStreamReader(System.in));
       
          // Construct and start the server
          server = new HttpServer();
          server.start();

          System.out.println("Press [Enter] to stop the server...");

          // Block until the user presses [Enter]
          in.readLine();
       }
       catch (IOException ioe)
       {
          System.out.println("  Stopping because of an IOException");
       }

       // Stop the server
       if (server != null) server.stop();
    }



    /**
     * Default COnstructor
     */
    public HttpServer() throws IOException
    {
       serverSocket = new ServerSocket(8080);
       logger.log(Level.INFO, "Created Server Socket on 8080");

       threadPool   = Executors.newFixedThreadPool(MAX_THREADS);

       serverSocket.setSoTimeout(5000);          
    }
    

    /**
     * The code to run in the server's thread of execution
     */
    public void run()
    {
       HttpConnectionHandler  connection;
       Socket                 s;

       while (keepRunning) 
       {
          try
          {
             s = serverSocket.accept();
             logger.log(Level.INFO, "Accepted a connection");
             connection = new HttpConnectionHandler(s);

             // Add the connection to a BlockingQueue<Runnable> object
             // and, ultimately, call it's run() method in a thread
             // in the pool
             threadPool.execute(connection);                
          }
          catch (SocketTimeoutException ste)
          {
             // The accept() method timed out.  Check to see if
             // the thread should keep running or not.
          }
          catch (IOException ioe)
          {
             // Problem with accept()
          }
       }

       stopPool();
       controlThread = null;
    }

    /**
     * Stop the threads in the pool
     */
    private void stopPool()
    {
       // Prevent new Runnable objects from being submitted
       threadPool.shutdown();
       
       try
       {
          // Wait for existing connections to complete
          if (!threadPool.awaitTermination(5, TimeUnit.SECONDS))
          {
             // Stop executing threads
             threadPool.shutdownNow();
             
             // Wait again
             if (!threadPool.awaitTermination(5, TimeUnit.SECONDS))
             {
                logger.log(Level.INFO, "Could not stop thread pool.");
             }
          }
       }
       catch (InterruptedException ie)
       {
          // Stop executing threads
          threadPool.shutdownNow();

          // Propagate the interrupt status
          controlThread.interrupt();
       }
    }
    


    /**
     * Start the thread of execution
     */
    public void start()
    {
       if (controlThread == null)
       {
          controlThread = new Thread(this);
          keepRunning = true;

          controlThread.start();
       }
    }


    /**
     * Stop the thread of execution (after it finishes the
     * current connection)
     */
    public void stop()
    {
       keepRunning = false;
    }
}
        
HTTP 1.0 POST Requests

POST URI HTTP/1.0 CRLF
Content-type: Type CRLF
Content-length: Bytes CRLF
CRLF
Data