/*
* $Id$
*
* Copyright 2006, The jCoderZ.org Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials
* provided with the distribution.
* * Neither the name of the jCoderZ.org Project nor the names of
* its contributors may be used to endorse or promote products
* derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jcoderz.commons.connector.http.transport;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Logger;
import org.jcoderz.commons.util.Constants;
/**
* Handler for http client requests.
*/
public class ClientHandler
extends Thread
{
/** Class name used for logging. */
private static final String CLASSNAME
= ClientHandler.class.getName();
/** Logger in use. */
private static final Logger logger
= Logger.getLogger(CLASSNAME);
private static final int MAX_REQUESTS = 1000;
private static final int SLEEP_TIME_BETWEEN_REQUESTS = 50;
private static final int BUFFER_SIZE = 4096;
private static final String NEW_LINE = "\n";
private static final String UTF8 = "UTF-8";
private final Socket mIncoming;
private final int mCounter;
private InputStream mInStream = null;
private OutputStream mOutStream = null;
private boolean mHaveToStop = false;
// request
private String mRequestLine;
private final Map mRequestParameter = new HashMap();
private String mRequestBody;
private String mRequestContentType;
private boolean mConnectionHaveToBeClosed = false;
private boolean mDoNotRespond = false;
private boolean mDoImmediateClose = false;
private boolean mUseEmptyResponse = false;
// response
private String mResponseLine;
private String mResponseBody;
private final Map mResponseParameter = new HashMap();
/**
* Constructor.
*
* @param incoming
* the incoming socket
* @param counter
* the number of handler
*/
public ClientHandler (Socket incoming, int counter)
{
mIncoming = incoming;
mCounter = counter;
logger.info("+++ Creating Thread "
+ mCounter + "x"
+ " for Socket(" + incoming.getPort() + ")\n");
}
/**
* Starting handler thread.
*/
public void run ()
{
try
{
mInStream = mIncoming.getInputStream();
mOutStream = mIncoming.getOutputStream();
for (int i = 1; i < MAX_REQUESTS; i++)
{
if (!gotIncomingRequest())
{
break;
}
logger.info(
"+++ ITERATION " + i + "x Thread " + mCounter + "\n");
try
{
// read response
readRequest();
// if the client wants an immediate close without response
if (mDoImmediateClose)
{
break;
}
// if the client do not want to receive a response
if (!mDoNotRespond)
{
writeResponse();
}
}
catch (Exception ie)
{
logger.warning(
"Exception (" + ie.getMessage() + ") while read/write");
ie.printStackTrace();
break;
}
if (mConnectionHaveToBeClosed)
{
break;
}
} // end for
mInStream.close();
mOutStream.close();
mIncoming.close();
}
catch (Exception ex)
{
ex.printStackTrace();
}
logger.info("+++ Terminating Thread " + mCounter + "x\n");
}
private boolean gotIncomingRequest ()
{
int bytesAvailable = 0;
boolean handleRequest = true;
for (;;)
{
try
{
bytesAvailable = mInStream.available();
}
catch (IOException ioe)
{
logger.warning(
"IOException (" + ioe.getMessage()
+ ") on inputstream");
handleRequest = false;
break;
}
// take a break..
try
{
Thread.sleep(SLEEP_TIME_BETWEEN_REQUESTS);
}
catch (InterruptedException ie)
{
// ignore
}
if (bytesAvailable > 0)
{
break;
}
if (mHaveToStop)
{
//hangUp = true;
handleRequest = false;
break;
}
} // end for
return handleRequest;
}
/**
* Reads the request.
* @throws Exception
* in case of an error
*/
public void readRequest ()
throws Exception
{
// read the response message
try
{
// try to read <POST / HTTP/1.0>
readRequestLine();
// try to read
// <Connection: Keep-Alive> etc.
readHeaderAttributes();
// try to read
// request body
readRequestBody();
}
catch (Exception ex)
{
throw ex;
}
// dump request
final StringBuffer buffer = new StringBuffer();
buffer.append(
"\n- REQUEST ------------------------------------------\n");
buffer.append(requestToString());
buffer.append(
"\n-----------------------------------------------------\n");
logger.info(buffer.toString());
}
/**
* Reads the request header line.
* @throws Exception
* in case of an error
*/
protected void readRequestLine ()
throws Exception
{
String errorText = null; // used by throwing exceptions
String path = null;
String httpVersion = null;
try
{ // try to find a HTTP status line in the read input data
mRequestLine = readLine();
while (mRequestLine != null && !mRequestLine.startsWith("POST"))
{
mRequestLine = readLine();
}
}
catch (IOException ioe)
{
errorText = "IOException with message: " + ioe.getMessage();
throw new Exception(errorText);
}
if (mRequestLine == null)
{
// A null requestLine means the connection was lost
// before we got a response. Try again.
errorText = "Error in parsing the request line : unable to find line"
+ " starting with \"POST\"";
throw new Exception(errorText);
}
// <POST> found...
final int pathIndex = mRequestLine.indexOf("/");
int afterPathIndex = 0;
if (pathIndex > 0)
{
afterPathIndex = mRequestLine.indexOf(" ", pathIndex);
if (afterPathIndex > pathIndex)
{
path = mRequestLine.substring(pathIndex, afterPathIndex - 1);
}
}
if (path == null)
{
errorText = "Error in parsing the request line : unable to find path";
throw new Exception(errorText);
}
// <POST /> found..
httpVersion = mRequestLine.substring(afterPathIndex).trim();
if (httpVersion == null)
{
errorText
= "Error in parsing the request line : unable to find HTTP version";
throw new Exception(errorText);
}
// <POST / HTTP/1.x> found..
}
/**
* Reads the header attributes.
* @throws Exception
* in case of an error
*/
private void readHeaderAttributes ()
throws Exception
{
String errorText = null; // used by throwing exceptions
String headerLine = null;
for (;;)
{
try
{
headerLine = readLine();
}
catch (IOException ioe)
{
errorText = "IOException with message: " + ioe.getMessage();
throw new Exception(errorText);
}
// if all header attributes read..
if ((headerLine == null) || (headerLine.length() < 1))
{
break;
}
final int colon = parseHeaderLine(headerLine);
final String name = headerLine.substring(0, colon).trim();
final String value = headerLine.substring(colon + 1).trim();
addRequestHeaderParameter(name, value);
} // end for
assertRequestParameterGiven();
// remember the Content-Type header
mRequestContentType
= (String) mRequestParameter.get("content-type");
checkSpecialParameter();
setEchoParameterToResponse();
}
private void checkSpecialParameter ()
{
// check for Connection: Close
final String connectionValue
= (String) mRequestParameter.get("connection");
if (connectionValue != null
&& connectionValue.toLowerCase(
Constants.SYSTEM_LOCALE).equals("close"))
{
mConnectionHaveToBeClosed = true;
}
// check for parameter "DoNotRespond"
final String doNotRespondValue
= (String) mRequestParameter.get("donotrespond");
if (doNotRespondValue != null
&& doNotRespondValue.toLowerCase(
Constants.SYSTEM_LOCALE).equals("true"))
{
mDoNotRespond = true;
}
// check for parameter "DoImmediateClose"
final String doImmediateCloseValue
= (String) mRequestParameter.get("doimmediateclose");
if (doImmediateCloseValue != null
&& doImmediateCloseValue.toLowerCase(
Constants.SYSTEM_LOCALE).equals("true"))
{
mDoImmediateClose = true;
}
// check for parameter "UseEmptyResponse"
final String useEmptyResponse
= (String) mRequestParameter.get("useemptyresponse");
if (useEmptyResponse != null
&& useEmptyResponse.toLowerCase(
Constants.SYSTEM_LOCALE).equals("true"))
{
mUseEmptyResponse = true;
}
}
/**
* Detects a request parameter with prefix "ECHO_" in request
* and adds these parameter without prefix to response.
*/
private void setEchoParameterToResponse ()
{
for (final Iterator it = mRequestParameter.keySet().iterator();
it.hasNext();)
{
final String key = (String) it.next();
final String value = (String) mRequestParameter.get(key);
final String prefix = "echo_";
final int prefixLength = prefix.length();
if (key.startsWith(prefix))
{
final String responseKey = key.substring(prefixLength);
mResponseParameter.put(responseKey, value);
}
}
}
private int parseHeaderLine (String headerLine)
throws Exception
{
// Parse the header name and value
final int colon = headerLine.indexOf(":");
if (colon < 0)
{
final String errorText
= "Unable to parse header - no colon found: " + headerLine;
throw new Exception(errorText);
}
return colon;
}
private void addRequestHeaderParameter (String name, String value)
{
if (name != null && value != null)
{
mRequestParameter.put(name.toLowerCase(
Constants.SYSTEM_LOCALE), value);
logger.info("Header Parameter: " + name + "=" + value);
}
}
private void assertRequestParameterGiven ()
throws Exception
{
if (mRequestParameter.isEmpty())
{
final String errorText = "no header parameter found";
throw new Exception(errorText);
}
}
/**
* Reads the request body.
* @throws Exception
* in case of an error
*/
private void readRequestBody ()
throws Exception
{
String errorText = null; // used by throwing exceptions
byte[] responseBody = null;
// check Content-Length
final String stringValue
= (String) mRequestParameter.get("content-length");
if (stringValue == null)
{
errorText = "no Content-Length attribute found";
logger.warning("Throwing Exception: " + errorText);
throw new Exception(errorText);
}
final int expectedLength = Integer.parseInt(stringValue);
// check Content-Type
final String encoding = getEncoding();
if (encoding == null)
{
errorText = "no Content-Type attribute found";
logger.warning("Throwing Exception: " + errorText);
throw new Exception(errorText);
}
try
{
responseBody = read(expectedLength);
}
catch (IOException ioe)
{
errorText = "IOException with message: " + ioe.getMessage();
throw new Exception(errorText);
}
try
{
mRequestBody
= new String(responseBody, 0, responseBody.length, encoding);
}
catch (UnsupportedEncodingException ee)
{
errorText = "error while reading body: "
+ " encoding (" + encoding + ") is not supported";
logger.warning("Throwing Exception: " + errorText);
throw new Exception(errorText);
}
}
/**
* Reads a line from InputStream.
* @return String
* the read line
* @throws IOException
* in case of an error
*/
public String readLine ()
throws IOException
{
String result = null;
StringBuffer buffer = new StringBuffer();
for (;;)
{
final int ch = mInStream.read();
if (ch < 0)
{
if (buffer.length() == 0)
{
buffer = null;
break;
}
else
{
break;
}
}
else if (ch == '\r')
{
continue;
}
else if (ch == '\n')
{
break;
}
buffer.append((char) ch);
}
if (buffer != null)
{
result = buffer.toString();
}
return result;
}
/**
* Reads the response body.
*
* @param expectedLength
* the expected length of body
* @return byte[]
* the request body
* @throws IOException
* in case of an error
*/
public byte[] read (int expectedLength)
throws IOException
{
final byte[] buffer = new byte[BUFFER_SIZE]; // todo ..configurable!
final ByteArrayOutputStream tempOut = new ByteArrayOutputStream();
int bytesRead = 0;
int foundLength = 0;
// if expectedLength == -1 ..infinite read til timeout
// at the moment a header with "Content-Length" -1 is invalid
// and this infinite read scenario is not possible..
while (expectedLength == -1 || foundLength < expectedLength)
{
bytesRead = mInStream.read(buffer);
// no bytes read
if (bytesRead == -1)
{
break;
}
tempOut.write(buffer, 0, bytesRead);
foundLength += bytesRead;
if (expectedLength > -1)
{
// everything read as expected
if (foundLength == expectedLength)
{
break;
}
else if (foundLength > expectedLength)
{
final StringBuffer strbuf = new StringBuffer();
strbuf.append("++ WARNING WHILE READING RESPONSE BODY ++\n");
strbuf.append("++ expected length (" + expectedLength
+ ") exceeded ++\n");
strbuf.append("++ found " + foundLength + " bytes ++");
logger.warning(strbuf.toString());
break;
}
}
} // end while
try
{
tempOut.close();
}
catch (IOException ioe)
{
final String errorText
= "unable to close buffer with data read from socket"
+ " as response body";
throw new IOException(errorText);
}
return tempOut.toByteArray();
}
/**
* Gets the encoding.
* @return String
* the used encoding
*/
public String getEncoding ()
{
String result = null;
final String contentType = (String) mRequestParameter.get("content-type");
if (contentType != null)
{
if (contentType.toUpperCase(
Constants.SYSTEM_LOCALE).equals("TEXT/PLAIN"))
{
result = UTF8;
}
else
{
result = getEncodingFromCharsetValue (contentType);
}
}
return result;
}
/**
* Gets the encoding extracted from parameter value CHARSET.
*
* @param contentType
* the parameter value set for Content-Type
* @return String
* the encoding extracted from CHARSET if found in
* Content-Type, null else
*/
private String getEncodingFromCharsetValue (String contentType)
{
String result = UTF8; // as default
final String charsetName = "CHARSET="; // must be upper case
final int charsetIndex
= contentType.toUpperCase(Constants.SYSTEM_LOCALE).
indexOf(charsetName);
if (charsetIndex > 0)
{
final int quote1 = contentType.
indexOf("\"", charsetIndex + charsetName.length());
final int quote2 = contentType.
indexOf("\"", quote1 + 1);
if (quote1 > 0)
{
result = contentType.
substring(quote1 + 1, quote2).
toUpperCase(Constants.SYSTEM_LOCALE);
}
else
{
result = contentType.
substring(charsetIndex + charsetName.length()).
toUpperCase(Constants.SYSTEM_LOCALE);
}
}
else
{
final String messageText = "no charset defined in Content-Type ("
+ contentType + ")";
logger.info(messageText);
}
return result;
}
/**
* Writes the response message.
* @throws Exception
* in case of an error
*/
public void writeResponse ()
throws Exception
{
StringBuffer buffer = new StringBuffer();
// create response
// header line
mResponseLine = "HTTP/1.0 200 OK";
// body
if (!mUseEmptyResponse)
{
buffer.append("Echo:");
buffer.append(mRequestBody);
}
mResponseBody = buffer.toString();
// the message to be send
final byte[] messageAsByte = mResponseBody.getBytes(UTF8);
//
// header parameter
if (mConnectionHaveToBeClosed)
{
mResponseParameter.put("Connection", "Close");
}
else
{
mResponseParameter.put("Connection", "Keep-Alive");
}
final int bodyLength = messageAsByte.length;
mResponseParameter.put(
"Content-Length", Integer.toString(bodyLength));
mResponseParameter.put("Content-Type", mRequestContentType);
// complete response
buffer = new StringBuffer();
buffer.append(mResponseLine);
buffer.append(NEW_LINE);
buffer.append(getResponseParameterAsString());
buffer.append(NEW_LINE);
buffer.append(mResponseBody);
// ..for dumping
final StringBuffer strbuf = new StringBuffer();
strbuf.append("\n-- RESPONSE --\n");
strbuf.append(buffer.toString());
strbuf.append("\n--------------\n");
logger.info(strbuf.toString());
// complete message as byte array
final byte[] messageAsBytes = buffer.toString().getBytes(UTF8);
mOutStream.write(messageAsBytes);
}
/**
* Gets response header parameter.
* @return String
* parameter ready for sending
*/
private String getResponseParameterAsString ()
{
final StringBuffer buffer = new StringBuffer();
final Iterator keyIterator = mResponseParameter.keySet().iterator();
while (keyIterator.hasNext())
{
final String key = (String) keyIterator.next();
final String value = (String) mResponseParameter.get(key);
buffer.append(key);
buffer.append(": ");
buffer.append(value);
buffer.append(NEW_LINE);
}
return buffer.toString();
}
/**
* Gets the request as string.
* @return String
* the incoming request
*/
private String requestToString ()
{
final StringBuffer buffer = new StringBuffer();
buffer.append(mRequestLine);
buffer.append(NEW_LINE);
final Iterator keyIterator = mRequestParameter.keySet().iterator();
while (keyIterator.hasNext())
{
final String key = (String) keyIterator.next();
final String value = (String) mRequestParameter.get(key);
buffer.append(key);
buffer.append(": ");
buffer.append(value);
buffer.append(NEW_LINE);
}
buffer.append(NEW_LINE);
buffer.append(mRequestBody);
return buffer.toString();
}
/**
* Checks flag indicating to stop handler thread.
*/
public void haveToStop ()
{
mHaveToStop = true;
}
}