/******************************************************************
*
*	CyberHTTP for Java
*
*	Copyright (C) Satoshi Konno 2002-2004
*
*	File: HTTPConnection.java
*
*	Revision;
*
*	11/18/02
*		- first revision.
*	09/02/03
*		- Giordano Sassaroli <sassarol@cefriel.it>
*		- Problem : The API is unable to receive responses from the Microsoft UPnP stack
*		- Error : the Microsoft UPnP stack is based on ISAPI on IIS, and whenever IIS
*                 receives a post request, it answers with two responses: the first one has no 
*		          body and it is a code 100 (continue) response, which has to be ignored. The
*		          second response is the actual one and should be parsed as the response.
*	02/09/04
*		- Ralf G. R. Bergs" <Ralf@Ber.gs>
*		- Why do you strip leading and trailing white space from the response body?
*		- Disabled to trim the content string.
*	03/11/04
*		- Added some methods about InputStream content.
*		  setContentInputStream(), getContentInputStream() and hasContentInputStream().
*	03/16/04
*		- Thanks for Darrell Young
*		- Added setVersion() and getVersion();
*	03/17/04
*		- Added hasFirstLine();
*	05/26/04
*		- Jan Newmarch <jan.newmarch@infotech.monash.edu.au> (05/26/04)
*		- Changed setCacheControl() and getChcheControl();
*	08/25/04
*		- Added the following methods.
*		  hasContentRange(), setContentRange(), getContentRange(), 
*		  getContentRangeFirstPosition(), getContentRangeLastPosition() and getContentRangeInstanceLength()
*	08/26/04
*		- Added the following methods.
*		  hasConnection(), setConnection(), getConnection(), 
*		  isCloseConnection() and isKeepAliveConnection()
*	08/27/04
*		- Added a updateWithContentLength paramger to setContent().
*		- Changed to HTTPPacket::set() not to change the header of Content-Length.
*	08/28/04
*		- Added init() and read().
*	09/19/04
*		- Added a onlyHeaders parameter to set().
*	10/20/04 
*		- Brent Hills <bhills@openshores.com>
*		- Changed hasContentRange() to check Content-Range and Range header.
*		- Added support for Range header to getContentRange().
*	02/02/05
*		- Mark Retallack <mretallack@users.sourceforge.net>
*		- Fixed set() not to read over the content length when the stream is keep alive.
*	02/28/05
*		- Added the following methods for chunked stream support.
*		  hasTransferEncoding(), setTransferEncoding(), getTransferEncoding(), isChunked().
*	03/02/05
*		- Changed post() to suppot chunked stream.
*	06/11/05
*		- Added setHost().
*	07/07/05
*		- Lee Peik Feng <pflee@users.sourceforge.net>
*		- Andrey Ovchar <AOvchar@consultitnow.com>
*		- Fixed set() to parse the chunk size as a hex string.
*	11/02/05
*		- Changed set() to use BufferedInputStream instead of BufferedReader to
*		  get the content as a byte stream.
*	11/06/05
*		- Added getCharSet().
*		- Changed getContentString() to return the content string using the charset.
*
*******************************************************************/

package org.cybergarage.http;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.Calendar;
import java.util.StringTokenizer;
import java.util.Vector;

import org.cybergarage.net.HostInterface;
import org.cybergarage.util.Debug;
import org.cybergarage.util.StringUtil;

public class HTTPPacket 
{
	////////////////////////////////////////////////
	//	Constructor
	////////////////////////////////////////////////
	
	public HTTPPacket()
	{
		setVersion(HTTP.VERSION);
		setContentInputStream(null);
	}

	public HTTPPacket(HTTPPacket httpPacket)
	{
		setVersion(HTTP.VERSION);
		set(httpPacket);
		setContentInputStream(null);
	}

	public HTTPPacket(InputStream in)
	{
		setVersion(HTTP.VERSION);
		set(in);
		setContentInputStream(null);
	}

	////////////////////////////////////////////////
	//	init
	////////////////////////////////////////////////
	
	public void init()
	{
		setFirstLine("");
		clearHeaders();
		setContent(new byte[0], false);
		setContentInputStream(null);
	}

	////////////////////////////////////////////////
	//	Version
	////////////////////////////////////////////////
	
	private String version;
	
	public void setVersion(String ver)
	{
		version = ver;
	}
	
	public String getVersion()
	{
		return version;
	}
		
	////////////////////////////////////////////////
	//	set
	////////////////////////////////////////////////
	
	private String readLine(BufferedInputStream in)
	{
		ByteArrayOutputStream lineBuf = new ByteArrayOutputStream();
		byte readBuf[] = new byte[1];
		
 		try {
 			int	readLen = in.read(readBuf);
 			while (0 < readLen) {
 				if (readBuf[0] == HTTP.LF)
 					break;
 				if (readBuf[0] != HTTP.CR) 
 					lineBuf.write(readBuf[0]);
 	 			readLen = in.read(readBuf);
			}
 		}
 		catch (InterruptedIOException e) {
 			//Ignoring warning because it's a way to break the HTTP connecttion
 			//TODO Create a new level of Logging and log the event
		}
		catch (IOException e) {
			Debug.warning(e);
		}

		return lineBuf.toString();
	}
	
	protected boolean set(InputStream in, boolean onlyHeaders)
	{
 		try {
 			BufferedInputStream reader = new BufferedInputStream(in);
			
			String firstLine = readLine(reader);
			if (firstLine == null || firstLine.length() <= 0)
				return false;
			setFirstLine(firstLine);
			
			// Thanks for Giordano Sassaroli <sassarol@cefriel.it> (09/03/03)
			HTTPStatus httpStatus = new HTTPStatus(firstLine);
			int statCode = httpStatus.getStatusCode();
			if (statCode == HTTPStatus.CONTINUE){
				//ad hoc code for managing iis non-standard behaviour
				//iis sends 100 code response and a 200 code response in the same
				//stream, so the code should check the presence of the actual
				//response in the stream.
				//skip all header lines
				String headerLine = readLine(reader);
				while ((headerLine != null) && (0 < headerLine.length()) ) {
					HTTPHeader header = new HTTPHeader(headerLine);
					if (header.hasName() == true)
						setHeader(header);
					headerLine = readLine(reader);
				}
				//look forward another first line
				String actualFirstLine = readLine(reader);
				if ((actualFirstLine != null) && (0 < actualFirstLine.length()) ) {
					//this is the actual first line
					setFirstLine(actualFirstLine);
				}else{
					return true;
				}
			}
				
			String headerLine = readLine(reader);
			while ((headerLine != null) && (0 < headerLine.length()) ) {
				HTTPHeader header = new HTTPHeader(headerLine);
				if (header.hasName() == true)
					setHeader(header);
				headerLine = readLine(reader);
			}
				
			if (onlyHeaders == true) {
				setContent("", false);
				return true;
			}
				
			boolean isChunkedRequest = isChunked();
				
			long contentLen = 0;
			if (isChunkedRequest == true) {
				try {
					String chunkSizeLine = readLine(reader);
					// Thanks for Lee Peik Feng <pflee@users.sourceforge.net> (07/07/05)
					//contentLen = Long.parseLong(new String(chunkSizeLine.getBytes(), 0, chunkSizeLine.length()-2), 16);
					contentLen = (chunkSizeLine != null) ? Long.parseLong(chunkSizeLine.trim(), 16) : 0;
				}
				catch (Exception e) {};
			}
			else
				contentLen = getContentLength();
						
			ByteArrayOutputStream contentBuf = new ByteArrayOutputStream();
			
			while (0 < contentLen) {
				int chunkSize = HTTP.getChunkSize();
				
				/* Thanks for Stephan Mehlhase (2010-10-26) */
				byte readBuf[] = new byte[(int) (contentLen > chunkSize ? chunkSize : contentLen)];
				
				long readCnt = 0;
				while (readCnt < contentLen) {
					try {
						// Thanks for Mark Retallack (02/02/05)
						long bufReadLen = contentLen - readCnt;
						if (chunkSize < bufReadLen)
							bufReadLen = chunkSize;
						int readLen = reader.read(readBuf, 0, (int)bufReadLen);
						if (readLen < 0)
							break;
						contentBuf.write(readBuf, 0, readLen);
						readCnt += readLen;
					}
					catch (Exception e)
					{
						Debug.warning(e);
						break;
					}
				}
				if (isChunkedRequest == true) {
					// skip CRLF
					long skipLen = 0;
					do {
						long skipCnt = reader.skip(HTTP.CRLF.length() - skipLen);
						if (skipCnt < 0)
							break;
						skipLen += skipCnt;
					} while (skipLen < HTTP.CRLF.length());
					// read next chunk size
					try {
						String chunkSizeLine = readLine(reader);
						// Thanks for Lee Peik Feng <pflee@users.sourceforge.net> (07/07/05)
						contentLen = Long.parseLong(new String(chunkSizeLine.getBytes(), 0, chunkSizeLine.length()-2), 16);
					}
					catch (Exception e) {
						contentLen = 0;
					};
				}
				else
					contentLen = 0;
			}

			setContent(contentBuf.toByteArray(), false);
 		}
		catch (Exception e) {
			Debug.warning(e);
			return false;
		}
		
		return true;
	}

	protected boolean set(InputStream in)
	{
		return set(in, false);
	}
	
	protected boolean set(HTTPSocket httpSock)
	{
		return set(httpSock.getInputStream());
	}

	protected void set(HTTPPacket httpPacket)
	{
		setFirstLine(httpPacket.getFirstLine());
		
		clearHeaders();
		int nHeaders = httpPacket.getNHeaders();
		for (int n=0; n<nHeaders; n++) {
			HTTPHeader header = httpPacket.getHeader(n);
			addHeader(header);
		}
		setContent(httpPacket.getContent());
	}

	////////////////////////////////////////////////
	//	read
	////////////////////////////////////////////////
	
	public boolean read(HTTPSocket httpSock)
	{
		init();
		return set(httpSock);
	}
	
	////////////////////////////////////////////////
	//	String
	////////////////////////////////////////////////

	private String firstLine = "";
	
	private void setFirstLine(String value)
	{
			firstLine = value;
	}
	
	protected String getFirstLine()
	{
		return firstLine;
	}

	protected String getFirstLineToken(int num)
	{
		StringTokenizer st = new StringTokenizer(firstLine, HTTP.REQEST_LINE_DELIM);
		String lastToken = "";
		for (int n=0; n<=num; n++) {
			if (st.hasMoreTokens() == false)
				return "";
			lastToken = st.nextToken();
		}
		return lastToken;
     }
	
	public boolean hasFirstLine()
	{
		return (0 < firstLine.length()) ? true : false;
	}
	
	////////////////////////////////////////////////
	//	Header
	////////////////////////////////////////////////

	private Vector<HTTPHeader> httpHeaderList = new Vector<HTTPHeader>();
	
	public int getNHeaders()
	{
		return httpHeaderList.size();
	}

	public void addHeader(HTTPHeader header)
	{
		httpHeaderList.add(header);
	}

	public void addHeader(String name, String value)
	{
		HTTPHeader header = new HTTPHeader(name, value);
		httpHeaderList.add(header);
	}

	public HTTPHeader getHeader(int n)
	{
		return httpHeaderList.get(n);
	}
	
	public HTTPHeader getHeader(String name)
	{
		int nHeaders = getNHeaders();
		for (int n=0; n<nHeaders; n++) {
			HTTPHeader header = getHeader(n);
			String headerName = header.getName();
			if (headerName.equalsIgnoreCase(name) == true)
				return header;			
		}
		return null;
	}

	public void clearHeaders()
	{
		httpHeaderList.clear();
		httpHeaderList = new Vector<HTTPHeader>();
	}
	
	public boolean hasHeader(String name)
	{
		return (getHeader(name) != null) ? true : false;
	}

	public void setHeader(String name, String value)
	{
		HTTPHeader header = getHeader(name);
		if (header != null) {
			header.setValue(value);
			return;
		}
		addHeader(name, value);
	}

	public void setHeader(String name, int value)
	{
		setHeader(name, Integer.toString(value));
	}

	public void setHeader(String name, long value)
	{
		setHeader(name, Long.toString(value));
	}
	
	public void setHeader(HTTPHeader header)
	{
		setHeader(header.getName(), header.getValue());
	}

	public String getHeaderValue(String name)
	{
		HTTPHeader header = getHeader(name);
		if (header == null)
			return "";
		return header.getValue();
	}

	////////////////////////////////////////////////
	// set*Value
	////////////////////////////////////////////////

	public void setStringHeader(String name, String value, String startWidth, String endWidth)
	{
		String headerValue = value;
		if (headerValue.startsWith(startWidth) == false)
			headerValue = startWidth + headerValue;
		if (headerValue.endsWith(endWidth) == false)
			headerValue = headerValue + endWidth;
		setHeader(name, headerValue);
	}

	public void setStringHeader(String name, String value)
	{
		setStringHeader(name, value, "\"", "\"");
	}
	
	public String getStringHeaderValue(String name, String startWidth, String endWidth)
	{
		String headerValue = getHeaderValue(name);
		if (headerValue.startsWith(startWidth) == true)
			headerValue = headerValue.substring(1, headerValue.length());
		if (headerValue.endsWith(endWidth) == true)
			headerValue = headerValue.substring(0, headerValue.length()-1);
		return headerValue;
	}
	
	public String getStringHeaderValue(String name)
	{
		return getStringHeaderValue(name, "\"", "\"");
	}

	public void setIntegerHeader(String name, int value)
	{
		setHeader(name, Integer.toString(value));
	}
	
	public void setLongHeader(String name, long value)
	{
		setHeader(name, Long.toString(value));
	}
	
	public int getIntegerHeaderValue(String name)
	{
		HTTPHeader header = getHeader(name);
		if (header == null)
			return 0;
		return StringUtil.toInteger(header.getValue());
	}

	public long getLongHeaderValue(String name)
	{
		HTTPHeader header = getHeader(name);
		if (header == null)
			return 0;
		return StringUtil.toLong(header.getValue());
	}

	////////////////////////////////////////////////
	//	getHeader
	////////////////////////////////////////////////
	
	public String getHeaderString()
	{
		StringBuffer str = new StringBuffer();
	
		int nHeaders = getNHeaders();
		for (int n=0; n<nHeaders; n++) {
			HTTPHeader header = getHeader(n);
			str.append(header.getName() + ": " + header.getValue() + HTTP.CRLF);
		}
		
		return str.toString();
	}

	////////////////////////////////////////////////
	//	Contents
	////////////////////////////////////////////////

	private byte content[] = new byte[0];
	
	public void setContent(byte data[], boolean updateWithContentLength)
	{
		content = data;
		if (updateWithContentLength == true)
			setContentLength(data.length);
	}

	public void setContent(byte data[])
	{
		setContent(data, true);
	}
	
	public void setContent(String data, boolean updateWithContentLength)
	{
		setContent(data.getBytes(), updateWithContentLength);
	}

	public void setContent(String data)
	{
		setContent(data, true);
	}
	
	public  byte []getContent()
	{
		return content;
	}

	public  String getContentString()
	{
		String charSet = getCharSet();
		if (charSet == null || charSet.length() <= 0)
			return new String(content);
		try {
			return new String(content, charSet);
		}
		catch (Exception e) {
			Debug.warning(e);
		}
		return new String(content);
	}
	
	public boolean hasContent()
	{
		return (content.length > 0) ? true : false;
	}

	////////////////////////////////////////////////
	//	Contents (InputStream)
	////////////////////////////////////////////////

	private InputStream contentInput = null;
	
	public void setContentInputStream(InputStream in)
	{
		contentInput = in;
	}

	public InputStream getContentInputStream()
	{
		return contentInput;
	}

	public boolean hasContentInputStream()
	{
		return (contentInput != null) ? true : false;
	}

	////////////////////////////////////////////////
	//	ContentType
	////////////////////////////////////////////////

	public void setContentType(String type)
	{
		setHeader(HTTP.CONTENT_TYPE, type);
	}

	public String getContentType()
	{
		return getHeaderValue(HTTP.CONTENT_TYPE);
	}

	////////////////////////////////////////////////
	//	ContentLanguage
	////////////////////////////////////////////////

	public void setContentLanguage(String code)
	{
		setHeader(HTTP.CONTENT_LANGUAGE, code);
	}

	public String getContentLanguage()
	{
		return getHeaderValue(HTTP.CONTENT_LANGUAGE);
	}
	
	////////////////////////////////////////////////
	//	Charset
	////////////////////////////////////////////////

	public String getCharSet()
	{
		String contentType = getContentType();
		if (contentType == null)
			return "";
		contentType = contentType.toLowerCase();
		int charSetIdx = contentType.indexOf(HTTP.CHARSET);
		if (charSetIdx < 0)
			return "";
		int charSetEndIdx = charSetIdx + HTTP.CHARSET.length() + 1; 
		String charSet = new String(contentType.getBytes(), charSetEndIdx, (contentType.length() - charSetEndIdx));
		if (charSet.length() < 0)
			return "";
		if (charSet.charAt(0) == '\"')
			charSet = charSet.substring(1, (charSet.length() - 1));
		if (charSet.length() < 0)
			return "";
		if (charSet.charAt((charSet.length()-1)) == '\"')
			charSet = charSet.substring(0, (charSet.length() - 1));
		return charSet;
	}

	////////////////////////////////////////////////
	//	ContentLength
	////////////////////////////////////////////////

	public void setContentLength(long len)
	{
		setLongHeader(HTTP.CONTENT_LENGTH, len);
	}

	public long getContentLength()
	{
		return getLongHeaderValue(HTTP.CONTENT_LENGTH);
	}

	////////////////////////////////////////////////
	//	Connection
	////////////////////////////////////////////////

	public boolean hasConnection()
	{
		return hasHeader(HTTP.CONNECTION);
	}

	public void setConnection(String value)
	{
		setHeader(HTTP.CONNECTION, value);
	}

	public String getConnection()
	{
		return getHeaderValue(HTTP.CONNECTION);
	}

	public boolean isCloseConnection()
	{	
		if (hasConnection() == false)
			return false;
		String connection = getConnection();
		if (connection == null)
			return false;
		return connection.equalsIgnoreCase(HTTP.CLOSE);
	}

	public boolean isKeepAliveConnection()
	{	
		if (hasConnection() == false)
			return false;
		String connection = getConnection();
		if (connection == null)
			return false;
		return connection.equalsIgnoreCase(HTTP.KEEP_ALIVE);
	}
	
	////////////////////////////////////////////////
	//	ContentRange
	////////////////////////////////////////////////

	public boolean hasContentRange()
	{
		return (hasHeader(HTTP.CONTENT_RANGE) || hasHeader(HTTP.RANGE));
	}
	
	public void setContentRange(long firstPos, long lastPos, long length)
	{
		String rangeStr = "";
		rangeStr += HTTP.CONTENT_RANGE_BYTES + " ";
		rangeStr += Long.toString(firstPos) + "-";
		rangeStr += Long.toString(lastPos) + "/";
		rangeStr += ((0 < length) ? Long.toString(length) : "*");
		setHeader(HTTP.CONTENT_RANGE, rangeStr);
	}

	public long[] getContentRange()
	{
		long range[] = new long[3];
		range[0] = range[1] = range[2] = 0;
		if (hasContentRange() == false)
			return range;
		String rangeLine = getHeaderValue(HTTP.CONTENT_RANGE);
		// Thanks for Brent Hills (10/20/04)
		if (rangeLine.length() <= 0)
			rangeLine = getHeaderValue(HTTP.RANGE);
		if (rangeLine.length() <= 0)
			return range;
		// Thanks for Brent Hills (10/20/04)
		StringTokenizer strToken = new StringTokenizer(rangeLine, " =");
		// Skip bytes
		if (strToken.hasMoreTokens() == false)
			return range;
		String bytesStr = strToken.nextToken(" ");
		// Get first-byte-pos
		if (strToken.hasMoreTokens() == false)
			return range;
		String firstPosStr = strToken.nextToken(" -");
		try {
			range[0] = Long.parseLong(firstPosStr);
		}
		catch (NumberFormatException e) {};
		if (strToken.hasMoreTokens() == false)
			return range;
		String lastPosStr = strToken.nextToken("-/");
		try {
			range[1] = Long.parseLong(lastPosStr);
		}
		catch (NumberFormatException e) {};
		if (strToken.hasMoreTokens() == false)
			return range;
		String lengthStr = strToken.nextToken("/");
		try {
			range[2] = Long.parseLong(lengthStr);
		}
		catch (NumberFormatException e) {};
		return range;
	}
	
	public long getContentRangeFirstPosition()
	{
		long range[] = getContentRange();
		return range[0];
	}

	public long getContentRangeLastPosition()
	{
		long range[] = getContentRange();
		return range[1];
	}

	public long getContentRangeInstanceLength()
	{
		long range[] = getContentRange();
		return range[2];
	}
	
	////////////////////////////////////////////////
	//	CacheControl
	////////////////////////////////////////////////

	public void setCacheControl(String directive)
	{
		setHeader(HTTP.CACHE_CONTROL, directive);
	}
	
	public void setCacheControl(String directive, int value)
	{
		String strVal = directive + "=" + Integer.toString(value);
		setHeader(HTTP.CACHE_CONTROL, strVal);
	}
	
	public void setCacheControl(int value)
	{
		setCacheControl(HTTP.MAX_AGE, value);
	}

	public String getCacheControl()
	{
		return getHeaderValue(HTTP.CACHE_CONTROL);
	}

	////////////////////////////////////////////////
	//	Server
	////////////////////////////////////////////////

	public void setServer(String name)
	{
		setHeader(HTTP.SERVER, name);
	}

	public String getServer()
	{
		return getHeaderValue(HTTP.SERVER);
	}

	////////////////////////////////////////////////
	//	Host
	////////////////////////////////////////////////

	public void setHost(String host, int port)
	{
		String hostAddr = host;
		if (HostInterface.isIPv6Address(host) == true)
			hostAddr = "[" + host + "]";
		setHeader(HTTP.HOST, hostAddr + ":" + Integer.toString(port));
	}

/*  I2P No - we always want port also. libupnp-based devices will reject 403 without the port
	public void setHost(String host)
	{
		String hostAddr = host;
		if (HostInterface.isIPv6Address(host) == true)
			hostAddr = "[" + host + "]";
		setHeader(HTTP.HOST, hostAddr);
	}
*/
	
	public String getHost()
	{
		return getHeaderValue(HTTP.HOST);
	}


	////////////////////////////////////////////////
	//	Date
	////////////////////////////////////////////////

	public void setDate(Calendar cal)
	{
		Date date = new Date(cal);
		setHeader(HTTP.DATE, date.getDateString());
	}

	public String getDate()
	{
		return getHeaderValue(HTTP.DATE);
	}

	////////////////////////////////////////////////
	//	Connection
	////////////////////////////////////////////////

	public boolean hasTransferEncoding()
	{
		return hasHeader(HTTP.TRANSFER_ENCODING);
	}

	public void setTransferEncoding(String value)
	{
		setHeader(HTTP.TRANSFER_ENCODING, value);
	}

	public String getTransferEncoding()
	{
		return getHeaderValue(HTTP.TRANSFER_ENCODING);
	}

	public boolean isChunked()
	{	
		if (hasTransferEncoding() == false)
			return false;
		String transEnc = getTransferEncoding();
		if (transEnc == null)
			return false;
		return transEnc.equalsIgnoreCase(HTTP.CHUNKED);
	}
	
	////////////////////////////////////////////////
	//	set
	////////////////////////////////////////////////

/*
	public final static boolean parse(HTTPPacket httpPacket, InputStream in)
	{
 		try {
			BufferedReader reader = new BufferedReader(new InputStreamReader(in));
			return parse(httpPacket, reader);
		}
		catch (Exception e) {
			Debug.warning(e);
		}
		return false;
	}
*/
}

