alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*
 *  Copyright 1999-2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.ajp;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.http.BaseRequest;
import org.apache.tomcat.util.http.HttpMessages;
import org.apache.tomcat.util.http.MimeHeaders;


/**
 * Handle messages related with basic request information.
 *
 * This object can handle the following incoming messages:
 * - "FORWARD_REQUEST" input message ( sent when a request is passed from the web server )
 * - "PING REQUEST" input message (sent by the web server to determine if tomcat is not frozen,
 *                                 a PONG REPLY will be sent back)
 * - "RECEIVE_BODY_CHUNK" input ( sent by container to pass more body, in response to GET_BODY_CHUNK )
 *
 * It can handle the following outgoing messages:
 * - SEND_HEADERS. Pass the status code and headers.
 * - SEND_BODY_CHUNK. Send a chunk of body
 * - GET_BODY_CHUNK. Request a chunk of body data
 * - END_RESPONSE. Notify the end of a request processing.
 *
 * @author Henri Gomez [hgomez@apache.org]
 * @author Dan Milstein [danmil@shore.net]
 * @author Keith Wannamaker [Keith@Wannamaker.org]
 * @author Costin Manolache
 */
public class RequestHandler extends AjpHandler
{
    // XXX Will move to a registry system.
    
    // Prefix codes for message types from server to container
	public static final byte JK_AJP13_FORWARD_REQUEST   = 2;
	public static final byte JK_AJP13_SHUTDOWN          = 7;
	public static final byte JK_AJP13_PING_REQUEST      = 8;    
	public static final byte JK_AJP13_CPING_REQUEST     = 10;    

    // Prefix codes for message types from container to server
    public static final byte JK_AJP13_SEND_BODY_CHUNK   = 3;
    public static final byte JK_AJP13_SEND_HEADERS      = 4;
    public static final byte JK_AJP13_END_RESPONSE      = 5;
	public static final byte JK_AJP13_GET_BODY_CHUNK    = 6;
	public static final byte JK_AJP13_CPONG_REPLY       = 9;
	
    // Integer codes for common response header strings
    public static final int SC_RESP_CONTENT_TYPE        = 0xA001;
    public static final int SC_RESP_CONTENT_LANGUAGE    = 0xA002;
    public static final int SC_RESP_CONTENT_LENGTH      = 0xA003;
    public static final int SC_RESP_DATE                = 0xA004;
    public static final int SC_RESP_LAST_MODIFIED       = 0xA005;
    public static final int SC_RESP_LOCATION            = 0xA006;
    public static final int SC_RESP_SET_COOKIE          = 0xA007;
    public static final int SC_RESP_SET_COOKIE2         = 0xA008;
    public static final int SC_RESP_SERVLET_ENGINE      = 0xA009;
    public static final int SC_RESP_STATUS              = 0xA00A;
    public static final int SC_RESP_WWW_AUTHENTICATE    = 0xA00B;
	
    // Integer codes for common (optional) request attribute names
    public static final byte SC_A_CONTEXT       = 1;  // XXX Unused
    public static final byte SC_A_SERVLET_PATH  = 2;  // XXX Unused
    public static final byte SC_A_REMOTE_USER   = 3;
    public static final byte SC_A_AUTH_TYPE     = 4;
    public static final byte SC_A_QUERY_STRING  = 5;
    public static final byte SC_A_JVM_ROUTE     = 6;
    public static final byte SC_A_SSL_CERT      = 7;
    public static final byte SC_A_SSL_CIPHER    = 8;
    public static final byte SC_A_SSL_SESSION   = 9;
    public static final byte SC_A_SSL_KEY_SIZE  = 11; // ajp14 originally, now in ajp13 with jk 1.2/2.0
    public static final byte SC_A_SECRET        = 12;

    // Used for attributes which are not in the list above
    public static final byte SC_A_REQ_ATTRIBUTE = 10; 

    // Terminates list of attributes
    public static final byte SC_A_ARE_DONE      = (byte)0xFF;
    
    // Translates integer codes to names of HTTP methods
    public static final String []methodTransArray = {
        "OPTIONS",
        "GET",
        "HEAD",
        "POST",
        "PUT",
        "DELETE",
        "TRACE",
        "PROPFIND",
        "PROPPATCH",
        "MKCOL",
        "COPY",
        "MOVE",
        "LOCK",
        "UNLOCK",
        "ACL",
        "REPORT",
        "VERSION-CONTROL",
        "CHECKIN",
        "CHECKOUT",
        "UNCHECKOUT",
        "SEARCH",
        "MKWORKSPACE",
        "UPDATE",
        "LABEL",
        "MERGE",
        "BASELINE-CONTROL",
        "MKACTIVITY"
    };
    
    // id's for common request headers
    public static final int SC_REQ_ACCEPT          = 1;
    public static final int SC_REQ_ACCEPT_CHARSET  = 2;
    public static final int SC_REQ_ACCEPT_ENCODING = 3;
    public static final int SC_REQ_ACCEPT_LANGUAGE = 4;
    public static final int SC_REQ_AUTHORIZATION   = 5;
    public static final int SC_REQ_CONNECTION      = 6;
    public static final int SC_REQ_CONTENT_TYPE    = 7;
    public static final int SC_REQ_CONTENT_LENGTH  = 8;
    public static final int SC_REQ_COOKIE          = 9;
    public static final int SC_REQ_COOKIE2         = 10;
    public static final int SC_REQ_HOST            = 11;
    public static final int SC_REQ_PRAGMA          = 12;
    public static final int SC_REQ_REFERER         = 13;
    public static final int SC_REQ_USER_AGENT      = 14;

    // Translates integer codes to request header names    
    public static final String []headerTransArray = {
        "accept",
        "accept-charset",
        "accept-encoding",
        "accept-language",
        "authorization",
        "connection",
        "content-type",
        "content-length",
        "cookie",
        "cookie2",
        "host",
        "pragma",
        "referer",
        "user-agent"
    };

    public RequestHandler() 
    {
    }

    public void init( Ajp13 ajp14 ) {
	// register incoming message handlers
	ajp14.registerMessageType( JK_AJP13_FORWARD_REQUEST,
				   "JK_AJP13_FORWARD_REQUEST",
				   this, null); // 2
	// register outgoing messages handler
	ajp14.registerMessageType( JK_AJP13_SEND_BODY_CHUNK, // 3
				   "JK_AJP13_SEND_BODY_CHUNK",
				   this,null );
	ajp14.registerMessageType( JK_AJP13_SEND_HEADERS,  // 4
				   "JK_AJP13_SEND_HEADERS",
				   this,null );
	ajp14.registerMessageType( JK_AJP13_END_RESPONSE, // 5
				   "JK_AJP13_END_RESPONSE",
				   this,null );
	ajp14.registerMessageType( JK_AJP13_GET_BODY_CHUNK, // 6
				   "JK_AJP13_GET_BODY_CHUNK",
				   this, null );
	ajp14.registerMessageType( JK_AJP13_CPING_REQUEST,
				   "JK_AJP13_PING_REQUEST",
				   this, null); // 10
	ajp14.registerMessageType( JK_AJP13_CPONG_REPLY,
				   "JK_AJP13_PONG_REPLY",
				   this, null); // 9
    }
    
    /**
     * Send a CPONG REPLY to web server to its CPING request
     * 
     * @param ch the Ajp13 channel
     * @param outBuf the Ajp13Packet output packet to use
     */
    public int sendCPong(Ajp13 ch, Ajp13Packet outBuf)
    {
		outBuf.reset();
		outBuf.appendByte(JK_AJP13_CPONG_REPLY);
    	
    	try
    	{
			ch.send(outBuf);
    	}
    	catch (IOException ioe)
    	{
    		log("can't send pong reply");
    	}
    	
    	return (999);	// success but no need to process farther
    }
    
    // -------------------- Incoming message --------------------
    public int handleAjpMessage( int type, Ajp13 channel,
				 Ajp13Packet ajp, BaseRequest req )
	throws IOException
    {
	switch( type ) {
	case RequestHandler.JK_AJP13_FORWARD_REQUEST:
	    return decodeRequest(channel, channel.hBuf, req );
		
	default:
	    return UNKNOWN;
	}
    }
    
    /**
     * Parse a FORWARD_REQUEST packet from the web server and store its
     * properties in the passed-in request object.
     *
     * @param req An empty (newly-recycled) request object.
     * @param msg Holds the packet which has just been sent by the web
     * server, with its read position just past the packet header (which in
     * this case includes the prefix code for FORWARD_REQUEST).
     *
     * @return 200 in case of a successful decoduing, 500 in case of error.  
     */
    protected int decodeRequest(Ajp13 ch, Ajp13Packet msg, BaseRequest req)
        throws IOException
    {
        
        if (debug > 0) {
            log("decodeRequest()");
        }

	// XXX Awful return values

        boolean isSSL = false;

        // Translate the HTTP method code to a String.
        byte methodCode = msg.getByte();
        req.method().setString(methodTransArray[(int)methodCode - 1]);

        msg.getMessageBytes(req.protocol()); 
        msg.getMessageBytes(req.requestURI());

        msg.getMessageBytes(req.remoteAddr());
        msg.getMessageBytes(req.remoteHost());
        msg.getMessageBytes(req.serverName());
        req.setServerPort(msg.getInt());

	isSSL = msg.getBool();

	// Decode headers
	MimeHeaders headers = req.headers();
	int hCount = msg.getInt();
        for(int i = 0 ; i < hCount ; i++) {
            String hName = null;

	    // Header names are encoded as either an integer code starting
	    // with 0xA0, or as a normal string (in which case the first
	    // two bytes are the length).
            int isc = msg.peekInt();
            int hId = isc & 0xFF;

	    MessageBytes vMB=null;
            isc &= 0xFF00;
            if(0xA000 == isc) {
                //
                // header name is encoded as an int
                //
                msg.getInt(); // To advance the read position
                hName = headerTransArray[hId - 1];
		vMB= headers.addValue(hName);
                msg.getMessageBytes(vMB);

                if (hId == SC_REQ_CONTENT_LENGTH) {
                    // just read content-length header
                    int contentLength = (vMB == null) ? -1 : vMB.getInt();
                    req.setContentLength(contentLength);
                } else if (hId == SC_REQ_CONTENT_TYPE) {
                    // just read content-type header
                    ByteChunk bchunk = vMB.getByteChunk();
                    req.contentType().setBytes(bchunk.getBytes(),
                                               bchunk.getOffset(),
                                               bchunk.getLength());
                } else if (hId == SC_REQ_AUTHORIZATION) {
                    ByteChunk bchunk = vMB.getByteChunk();
                    req.authorization().setBytes(bchunk.getBytes(),
                                               bchunk.getOffset(),
                                               bchunk.getLength());
                }
            } else {
                //
                // header name is a string
                //
		// XXX Not very elegant
		vMB = msg.addHeader(headers);
		if (vMB == null) {
                    return 500; // wrong packet
                }
                msg.getMessageBytes(vMB);
            }
        }

	byte attributeCode;
        for(attributeCode = msg.getByte() ;
            attributeCode != SC_A_ARE_DONE ;
            attributeCode = msg.getByte()) {
            switch(attributeCode) {
	    case SC_A_CONTEXT      :
                break;
		
	    case SC_A_SERVLET_PATH :
                break;
		
	    case SC_A_REMOTE_USER  :
                msg.getMessageBytes(req.remoteUser());
                break;
		
	    case SC_A_AUTH_TYPE    :
                msg.getMessageBytes(req.authType());
                break;
		
	    case SC_A_QUERY_STRING :
		msg.getMessageBytes(req.queryString());
                break;
		
	    case SC_A_JVM_ROUTE    :
                msg.getMessageBytes(req.jvmRoute());
                break;
		
	    case SC_A_SSL_CERT     :
		isSSL = true;
                // Transform the string into certificate.
                String certString = msg.getString();
                byte[] certData = certString.getBytes();
                ByteArrayInputStream bais = new ByteArrayInputStream(certData);
 
                // Fill the first element.
                X509Certificate jsseCerts[] = null;
                try {
                    CertificateFactory cf =
                        CertificateFactory.getInstance("X.509");
                    X509Certificate cert = (X509Certificate)
                        cf.generateCertificate(bais);
                    jsseCerts =  new X509Certificate[1];
                    jsseCerts[0] = cert;
                } catch(java.security.cert.CertificateException e) {
                    log("Certificate convertion failed" + e );
                }
 
                req.setAttribute("javax.servlet.request.X509Certificate",
                                 jsseCerts);
                break;
		
	    case SC_A_SSL_CIPHER   :
		isSSL = true;
		req.setAttribute("javax.servlet.request.cipher_suite",
				 msg.getString());
                break;
		
	    case SC_A_SECRET   :
                // If a request has a secret attribute, set it on
                // channel - it'll be visible to the caller ( Interceptor,
                // Connector ) and it can check it against its settings before
                // trusting us.
                String secret=msg.getString();
                if(secret!=null) {
                    ch.setSecret( secret );
                }
                break;
		
	    case SC_A_SSL_SESSION  :
		isSSL = true;
		req.setAttribute("javax.servlet.request.ssl_session",
				  msg.getString());
                break;
		
	    case SC_A_REQ_ATTRIBUTE :
		req.setAttribute(msg.getString(), 
				 msg.getString());
                break;

	    case SC_A_SSL_KEY_SIZE: // Ajp13 !
                isSSL = true;
		req.setAttribute("javax.servlet.request.key_size",
				 Integer.toString(msg.getInt()));
		break;
	    default:
                // Ignore. Assume a single-string value - we shouldn't
                // allow anything else.
                msg.getString();
                break;
	    }
        }

        if(isSSL) {
            req.setScheme(req.SCHEME_HTTPS);
            req.setSecure(true);
        }

        // set cookies on request now that we have all headers
        req.cookies().setHeaders(req.headers());

	// Check to see if there should be a body packet coming along
	// immediately after
    	if(req.getContentLength() > 0) {

	    /* Read present data */
	    int err = ch.receive(ch.inBuf);
            if(err < 0) {
            	return 500;
	    }
	    
	    ch.blen = ch.inBuf.peekInt();
	    ch.pos = 0;
	    ch.inBuf.getBytes(ch.bodyBuff);
    	}
    
        if (debug > 5) {
            log(req.toString());
        }

        return 200; // Success
    }
    

    // -------------------- Messages from container to server ------------------
    
    /**
     * Send the HTTP headers back to the web server and on to the browser.
     *
     * @param status The HTTP status code to send.
     * @param statusMessage the HTTP status message to send.
     * @param headers The set of all headers.
     */
    public void sendHeaders(Ajp13 ch, Ajp13Packet outBuf,
			    int status, String statusMessage,
                            MimeHeaders headers)
        throws IOException
    {
	// XXX if more headers that MAX_SIZE, send 2 packets!
	if( statusMessage==null ) statusMessage=HttpMessages.getMessage(status);
	outBuf.reset();
        outBuf.appendByte(JK_AJP13_SEND_HEADERS);
        outBuf.appendInt(status);
	
	outBuf.appendString(statusMessage);
        
	int numHeaders = headers.size();
        outBuf.appendInt(numHeaders);
        
	for( int i=0 ; i < numHeaders ; i++ ) {
	    String headerName = headers.getName(i).toString();
	    int sc = headerNameToSc(headerName);
            if(-1 != sc) {
                outBuf.appendInt(sc);
            } else {
                outBuf.appendString(headerName);
            }
            outBuf.appendString(headers.getValue(i).toString() );
        }

        outBuf.end();
        ch.send(outBuf);
    } 


    /**
     * Signal the web server that the servlet has finished handling this
     * request, and that the connection can be reused.
     */
    public void finish(Ajp13 ch, Ajp13Packet outBuf) throws IOException {
        if (debug > 0)  log("finish()");

	outBuf.reset();
        outBuf.appendByte(JK_AJP13_END_RESPONSE);
        outBuf.appendBool(true); // Reuse this connection
        outBuf.end();
        ch.send(outBuf);
    }

    /**
     * Send a chunk of response body data to the web server and on to the
     * browser.
     *
     * @param b A huffer of bytes to send.
     * @param off The offset into the buffer from which to start sending.
     * @param len The number of bytes to send.
     */    
    public void doWrite(Ajp13 ch, Ajp13Packet outBuf,
			byte b[], int off, int len)
	throws IOException
    {
        if (debug > 0) log("doWrite(byte[], " + off + ", " + len + ")");

	int sent = 0;
	while(sent < len) {
	    int to_send = len - sent;
	    to_send = to_send > Ajp13.MAX_SEND_SIZE ? Ajp13.MAX_SEND_SIZE : to_send;

	    outBuf.reset();
	    outBuf.appendByte(JK_AJP13_SEND_BODY_CHUNK);	        	
	    outBuf.appendBytes(b, off + sent, to_send);	        
	    ch.send(outBuf);
	    sent += to_send;
	}
    }

    // -------------------- Utils -------------------- 

    /**
     * Translate an HTTP response header name to an integer code if
     * possible.  Case is ignored.
     * 
     * @param name The name of the response header to translate.
     *
     * @return The code for that header name, or -1 if no code exists.
     */
    protected int headerNameToSc(String name)
    {       
        switch(name.charAt(0)) {
	case 'c':
	case 'C':
	    if(name.equalsIgnoreCase("Content-Type")) {
		return SC_RESP_CONTENT_TYPE;
	    } else if(name.equalsIgnoreCase("Content-Language")) {
		return SC_RESP_CONTENT_LANGUAGE;
	    } else if(name.equalsIgnoreCase("Content-Length")) {
		return SC_RESP_CONTENT_LENGTH;
	    }
            break;
            
	case 'd':
	case 'D':
	    if(name.equalsIgnoreCase("Date")) {
                return SC_RESP_DATE;
	    }
            break;
            
	case 'l':
	case 'L':
	    if(name.equalsIgnoreCase("Last-Modified")) {
		return SC_RESP_LAST_MODIFIED;
	    } else if(name.equalsIgnoreCase("Location")) {
		return SC_RESP_LOCATION;
	    }
            break;

	case 's':
	case 'S':
	    if(name.equalsIgnoreCase("Set-Cookie")) {
		return SC_RESP_SET_COOKIE;
	    } else if(name.equalsIgnoreCase("Set-Cookie2")) {
		return SC_RESP_SET_COOKIE2;
	    }
            break;
            
	case 'w':
	case 'W':
	    if(name.equalsIgnoreCase("WWW-Authenticate")) {
		return SC_RESP_WWW_AUTHENTICATE;
	    }
            break;          
        }
        
        return -1;
    }
   
    private int debug=0;
    private Logger logger = new Logger();
    
    public void setDebug(int debug) {
        this.debug = debug;
    }

    public void setLogger(Logger l) {
        this.logger = l;
    }
    
    void log(String s) {
        logger.log("[RequestHandler] " + s );
    }

    // ==================== Servlet Input Support =================
    // XXX DEPRECATED
    
    public int available(Ajp13 ch) throws IOException {
        if (debug > 0) {
            log("available()");
        }

        if (ch.pos >= ch.blen) {
            if( ! refillReadBuffer(ch)) {
		return 0;
	    }
        }
        return ch.blen - ch.pos;
    }

    /**
     * Return the next byte of request body data (to a servlet).
     *
     * @see Request#doRead
     */
    public int doRead(Ajp13 ch) throws IOException 
    {
        if (debug > 0) {
            log("doRead()");
        }

        if(ch.pos >= ch.blen) {
            if( ! refillReadBuffer(ch)) {
		return -1;
	    }
        }
        return ch.bodyBuff[ch.pos++] & 0xFF;
    }
    
    /**
     * Store a chunk of request data into the passed-in byte buffer.
     *
     * @param b A buffer to fill with data from the request.
     * @param off The offset in the buffer at which to start filling.
     * @param len The number of bytes to copy into the buffer.
     *
     * @return The number of bytes actually copied into the buffer, or -1
     * if the end of the stream has been reached.
     *
     * @see Request#doRead
     */
    public int doRead(Ajp13 ch, byte[] b, int off, int len) throws IOException 
    {
        if (debug > 0) {
            log("doRead(byte[], int, int)");
        }

	if(ch.pos >= ch.blen) {
	    if( ! refillReadBuffer(ch)) {
		return -1;
	    }
	}

	if(ch.pos + len <= ch.blen) { // Fear the off by one error
	    // Sanity check b.length > off + len?
	    System.arraycopy(ch.bodyBuff, ch.pos, b, off, len);
	    ch.pos += len;
	    return len;
	}

	// Not enough data (blen < pos + len)
	int toCopy = len;
	while(toCopy > 0) {
	    int bytesRemaining = ch.blen - ch.pos;
	    if(bytesRemaining < 0) 
		bytesRemaining = 0;
	    int c = bytesRemaining < toCopy ? bytesRemaining : toCopy;
            
	    System.arraycopy(ch.bodyBuff, ch.pos, b, off, c);

	    toCopy    -= c;

	    off       += c;
	    ch.pos       += c; // In case we exactly consume the buffer

	    if(toCopy > 0) 
		if( ! refillReadBuffer(ch)) { // Resets blen and pos
		    break;
		}
	}

	return len - toCopy;
    }
    
    /**
     * Get more request body data from the web server and store it in the 
     * internal buffer.
     *
     * @return true if there is more data, false if not.    
     */
    public boolean refillReadBuffer(Ajp13 ch) throws IOException 
    {
        if (debug > 0) {
            log("refillReadBuffer()");
        }

	// If the server returns an empty packet, assume that that end of
	// the stream has been reached (yuck -- fix protocol??).

	// Why not use outBuf??
	ch.inBuf.reset();
	ch.inBuf.appendByte(JK_AJP13_GET_BODY_CHUNK);
	ch.inBuf.appendInt(Ajp13.MAX_READ_SIZE);
	ch.send(ch.inBuf);
	
	int err = ch.receive(ch.inBuf);
        if(err < 0) {
	    throw new IOException();
	}
	
        // check for empty packet, which means end of stream
        if (ch.inBuf.getLen() == 0) {
            if (debug > 0) {
                log("refillReadBuffer():  "
                    + "received empty packet -> end of stream");
            }
            ch.blen = 0;
            ch.pos = 0;
            return false;
        }

    	ch.blen = ch.inBuf.peekInt();
    	ch.pos = 0;
    	ch.inBuf.getBytes(ch.bodyBuff);

	return (ch.blen > 0);
    }    

    // ==================== Servlet Output Support =================
    
    /**
     */
    public void beginSendHeaders(Ajp13 ch, Ajp13Packet outBuf,
				 int status,
                                 String statusMessage,
                                 int numHeaders) throws IOException {

        if (debug > 0) {
            log("sendHeaders()");
        }

	// XXX if more headers that MAX_SIZE, send 2 packets!

	outBuf.reset();
        outBuf.appendByte(JK_AJP13_SEND_HEADERS);

        if (debug > 0) {
            log("status is:  " + status +
                       "(" + statusMessage + ")");
        }

        // set status code and message
        outBuf.appendInt(status);
        outBuf.appendString(statusMessage);

        // write the number of headers...
        outBuf.appendInt(numHeaders);
    }

    public void sendHeader(Ajp13Packet outBuf,
			   String name, String value)
	throws IOException
    {
        int sc = headerNameToSc(name);
        if(-1 != sc) {
            outBuf.appendInt(sc);
        } else {
            outBuf.appendString(name);
        }
        outBuf.appendString(value);
    }

    public void endSendHeaders(Ajp13 ch, Ajp13Packet outBuf)
	throws IOException
    {
        outBuf.end();
        ch.send(outBuf);
    }

    /**
     * Send the HTTP headers back to the web server and on to the browser.
     *
     * @param status The HTTP status code to send.
     * @param headers The set of all headers.
     */
    public void sendHeaders(Ajp13 ch, Ajp13Packet outBuf,
			    int status, MimeHeaders headers)
        throws IOException
    {
        sendHeaders(ch, outBuf, status, HttpMessages.getMessage(status),
                    headers);
    }
    

 }
... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.