Tomcat-5 A simple Connector

There are two modules in Catalina: the connector and the container. In this topic you we will enhance the applications in the topic "A Simple Servlet Container" by writing a connector that create better request and response object. A connector compliant with Servlet 2.3 and 2.4 specfication must create instances of javax.servlet.http.HttpServletRequest and javax.servlet.http.HttpServletResponse to be passed the invoked servlet's service method.

Illustration shows the UML diagram of the classes in this topic.

 A-Simple-Connector

Compare the diagram with the one in the topic "A Simple Container". The HttpServer class in "A Simple Container" has been broken into two classes: HttpConnector and HttpProcessor. Request has been replaced by HttpRequest, and Reponse by HttpResponse. Also more classes are used in this topic's application.

The HttpServer class in "A Simple Container" is responsible for warting for HTTP requests and creating request objects and response objects. In this topic's application, the task of waiting for HTTP requests is given to HttpConnector instance, and the task of creating request objects and response objects is assigned to the HttpProcessor instance.

In this topic, HTTP request object is reprerented by the HttpRequest class, which implements javax.servlet.http.HttpServletRequest. An Http Request object will be cast to a HttpServletRequest instance and passed to the invoked servlet's service method. Therefore, every HttpRequest must have its fields properly populated so that the servlet can use them. Values that need to be assigned to HttpRequest object include the URL, query string, parameters, cookies and other headers, etc. Becuase 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 HTTP request. However, parsing an HTTP request involves expensive string and other operations, and the connector can save a lot of cycles life if it passes only values that will be needed by servlet.

The HttpConnector class represents a connector responsible for creating a server socket that waits for incoming HTTP requests. This class implements the Runnable class so that it can be dedicated a thread of its own. The run method in this class is similar to the await method of HttpServer1 class, except that after a socket is obtained from the accept method of SocketServer, an HttpProcessor instance is created and its process method is called, passing the socket object.

The main method in the Bootstrap class instantiates the HttpConnector class and calls its start method. When you start the application, an instance of HttpConnector is created and its run method executed. The run method contains s while loop that does the following:

  • wait for HTTP request.
  • Create and HttpProcessor instance for each request.
  • calls the process method of HttpProcessor.

The HttpProcess object create an instance of HttpRequest and therefore must populated fields in them. The HttpProcessor class, using its parse method, parses both request line and header 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 themselves. Only if the servlet needs a parameter will the query string or request body be parsed.

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

  • Create an HttpRequest object.
  • Create an HttpResponse object.
  • Parse the HTTP request’s first line and headers and populate the HttpRequest object.
  • Pass the HttpRequest object and HttpResponse object to either a ServletProcessor or a StaticResourceProcessor. The ServletPorcessor invokes the service method of the requested servlet and the StaticResourceProcessor sends the content of the static resource.

Method process of code snippet of HttpProcessor:

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 stocket. Note, however, in this method we use the SocketInputStream class that implements the java.io.InputStream. Then it create the HttpRequest object and HttpResponse object and assigned the HttpRequest instance to HttpReponse instance. Next, the process method calls two private methods in the HttpProcessor class for passing the request. Then, it hands off HttpRequest and HttpResponse for either a ServletProcessor or StaticResourceProcessor, depending the URI pattern of the request. Finally, it closes the socket.

HttpRequest:

SocketInputStream extends the InputStream class and instance of SocketInputStream wraps the java.io.InputStream instance returned by socket's getInputStream method. The SocketInputStream provides two important methods readRequestLine and readHeader.readRequestLine returns the first line in an HTTP request, i.e. the line containing URL, method and HTTP version. Because processing byte stream from socket's input stream means reading from the first byte to the last byte(and never moves backwards), readRequestLine must be called only one 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.

URL diagram:

A-Simple-Connector-HttpRequest

Needless to say, the main challenge here is to parse HTTP request and populate the HttpRequest object. For Header and cookies, the HttpRequest class provides the addHeader and addCookie methods that are called from the parseHeader method of HttpProcessor. Parameters are parsed when they are needed, using the HeepRequest class’s parseParameter method.

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

  • Reading the socket’s input stream.
  • Parsing the request line.
  • parsing header.
  • parsing cookie.
  • Obtaining parameters.

Reading the socket’s input stream

SocketInputStream class provides methods for obtaining not only the request line, but also the request headers.

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

Parsing the request line:

The process method of HttpProceeeor calls the private method parseRequest 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 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 one zero or more parameters. 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 a query string. for example if the browser’s support for cookies is being turned off.

When the parseMethod method is called from HttpProcess’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 the HttpRequest object. Now let’s take a close look at the parseRequest method:

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. Invoking its readRequestLine tells the SocketInputStream to populate the HttpRequestLine instance. Next, the parseRequest method obtain the method, URI and protocol of the request line. 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) {
    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 check this:

// 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);
        }
    }
}

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 session identifier and assigns the value to the HttpRequest instance by calling its setRequestSessionId 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, is not in the cookie. Therefore, pass true to the request’s setRequestSessionURL method. Otherwise, pass false to the setRequestSessionURL method and null to setRequestSessionId method. At this point, the value of uri has been stripped off jsessionid.

Then, the parseRequest method passes uri to normalize method to correct an “abnormal” URI. 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 can not be corrected, it will be considered invalid and normalize returns null. On such an occasion(normalize returns null), the parseRequest method will throw an exception at the end of the method.

Finally, the parseRequest method sets some properties of the HttpProcessor object. And if the return value from the normalized method is null, the method throw an exception.

Parsing headers:

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

parserHeaders method of HttpProcessor class:

private void parseHeaders(SocketInputStream input) throws IOException, ServletException {
        while (true) {
            HttpHeader header = new HttpHeader();
            // Read the next header
            input.readHeader(header);
            if (header.nameEnd == 0) {
                if (header.valueEnd == 0) {
                    return;
                } else {
                    throw new ServletException(sm.getString("httpProcessor.parseHeaders.colon"));
                }
            }
            String name = new String(header.name, 0, header.nameEnd);
            String value = new String(header.value, 0, header.valueEnd);
            request.addHeader(name, value);
            // do something for some headers, ignore others.
            if (name.equals("cookie")) {
                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]);
                }
            } 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);
            }
        } // end while
    }

Parsing Cookies:

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

Cookie:useName=bid;password=pwd;

Cookie parsing is done using the parseCookieHeader method of the org.apache.catalina.util.RequestUtil class. This method accepts the cookie head 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 org.apache.catalina.util.RequestUtil'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:

// do something for some headers, ignore others.
if (name.equals("cookie")) {
    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 not parse query string or HTTP request body until the servlet needs to read one or all of them by calling the getParameter* methods of javax.servlet.http.HttpServletRequest. Therefore, the implementations of these four methods in the HttpServerRequest 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 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 a query string or in the request body. If the user requested the servlet using GET method, all parameters are in the query string. If POST method is used, you may found some in the request body too. All the name/value pairs are stored in a HashMap object. Servlet programmers are not allowed to change parameter values. Therefore, a special HashMap is used: org.apache.catalina.util.ParameterMap.

The ParameterMap extends java.util.HashMap and employs a boolean called locked. The name/value pair can only be added, update,removed if locked is false. Otherwise, an IllegaStateException is thrown. Reading the value, however, can be done any time.  This class overrides the methods for adding, updating, removing values. Those methods can only be called when locked is false.

import java.util.HashMap;
import java.util.Map;
import org.apache.tomcat.util.res.StringManager;
/**
* Extended implementation of HashMap that includes a locked property.  This class can be used to safely expose Catalina
* internal parameter map objects to user classes without having to clone them in order to avoid modifications.  When first
* created, a ParmaeterMap instance is not locked.
*/
public final class ParameterMap<K,V> extends HashMap<K,V> { 
/**
* Constructors 
* Construct a new, empty map with the default initial capacity and load factor. 
*/
    public ParameterMap() { super(); } 

/** 
* Construct a new, empty map with the specified initial capacity and default load factor. 
* @param initialCapacity The initial capacity of this map 
*/
    public ParameterMap(int initialCapacity) { super(initialCapacity); }

    /**
     * Construct a new, empty map with the specified initial capacity and load factor.
     *
     * @param initialCapacity The initial capacity of this map
     * @param loadFactor The load factor of this map
     */
    public ParameterMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
    }

    /**
     * Construct a new map with the same mappings as the given map.
     * @param map Map whose contents are duplicated in the new map
     */
    public ParameterMap(Map<K,V> map) {
        super(map);
    }

    //  Properties 
    // The current lock state of this parameter map.  
    private boolean locked = false; 

    // Return the locked state of this parameter map. 
    public boolean isLocked() { return (this.locked);  }

    /**
     * Set the locked state of this parameter map. 
     * @param locked The new locked state
     */
    public void setLocked(boolean locked) {        this.locked = locked;     } 

    // The string manager for this package. 
    private static final StringManager sm = StringManager.getManager("org.apache.catalina.util");


    // Public Methods
    /**
     * Remove all mappings from this map. 
     * @exception IllegalStateException if this map is currently locked
     */
    @Override
    public void clear() {
        if (locked)
            throw new IllegalStateException(sm.getString("parameterMap.locked"));
        super.clear();
    }

    /**
     * Associate the specified value with the specified key in this map. 
     * If the map previously contained a mapping for this key, the old value is replaced. 
     * @param key Key with which the specified value is to be associated
     * @param value Value to be associated with the specified key 
     * @return The previous value associated with the specified key, or null if there was no mapping for key 
     * @exception IllegalStateException if this map is currently locked
     */
    @Override
    public V put(K key, V value) {
        if (locked)
            throw new IllegalStateException (sm.getString("parameterMap.locked"));
        return (super.put(key, value));
    }

    /**
     * Copy all of the mappings from the specified map to this one. These mappings replace any mappings
     * that this map had for any of the keys currently in the specified Map. 
     * @param map Mappings to be stored into this map 
     * @exception IllegalStateException if this map is currently locked
     */
    @Override
    public void putAll(Map<? extends K,? extends V> map) {
        if (locked)
            throw new IllegalStateException (sm.getString("parameterMap.locked"));
        super.putAll(map);
    }

    /**
     * Remove the mapping for this key from the map if present. 
     * @param key Key whose mapping is to be removed from the map 
     * @return The previous value associated with the specified key, or null if there was no mapping for that key 
     * @exception IllegalStateException if this map is currently locked
     */
    @Override
    public V 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 the parameters can exit in the query string or the HTTP request body, the parseParameters method checks both query string and request body. Once parsed, parameters can be found in the object variable parameters, so the method starts by checking the parsed, which is true if parsing has been done before.

Please see parseParameters method of HttpRequest class

/**
* Parse the parameters of this request, if it has not already occurred. If parameters are present in both the query string and the request content,  they are merged.
*/
protected void parseParameters() {
   
// Starts by checking variable parsed, which is true if parsing has been done before.
    if (parsed)
        return;

// Then creates a ParameterMap object and points it to parameters.
    ParameterMap results = parameters;

// If parameters is null it creates a new ParameterMap instance.
    if (results == null)
        results = new ParameterMap();

// Then opens the ParameterMap's lock to enable writing to it.
    results.setLocked(false);

// Checks encoding and assigns a default encoding if the encoding is null.
    String encoding = getCharacterEncoding();
    if (encoding == null)
        encoding = "ISO-8859-1";

// Tries query string. Parsing any parameters specified in the query string is done using parseParameters method of org.apache.catalina.util.RequestUtil class.
    String queryString = getQueryString();
    try {
        RequestUtil.parseParameters(results, queryString, encoding);
    } catch (UnsupportedEncodingException e) {
        ;
    }

// Next, tries to see if the HTTP request contains parameters. This happens if the user sends the request suing POST method, the content length is greater than zero, and the content type is application/x-www-form-urlencoded.
    // 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");
        }
    }
// Store the final results
// Finally, locks the ParameterMap back, sets pared to true, assigns results to parameters.

    results.setLocked(true);
    parsed = true;
    parameters = results;
}

From inside a servlet, you sue a PrintWriter to write characters. You may use any encoding you desire, however the characters will be sent to browser as byte streams. Therefore, it is not surprising that in topic "Tomcat-4 A Simple Container",  the ex02.pyrmont.HttpResponse class 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 stream that will be sent through the underlying OutputStream.

In this section, you use an instance of the ex03.pyrmont.connector.ResponseStream class as the OutputStream for PrintWriter. Note that the ResponseStream class is indirectly derived from the Java.io.OutputStream. you also have the ex03.pyrmont.connector.ResponseWriter class that extend 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 Response object.

We could instantiate the ResponseWriter class by pass an ResponseStream instance. However, we use a java.io.OutputStreamWriter object to serve as a bridge between the ResponseWriter object and ResponseStream object. With an OutputStreamWriter, characters written to it are encoded into bytes using a specified charest. The character it used may be specified by name or may be given explicitly, or the platform’s default character may be accepted. Each invocation of a write method causes the encoding converter to be invoked on the given characters. The resulting bytes are accumulated in a buffer before writing 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 method are not buffered.

Snippet of ResponseWriter class:

//ResponseWriter class
public class ResponseWriter extends PrintWriter { 
  public ResponseWriter(OutputStreamWriter writer) {
    super(writer);
  }


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;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章