/* 
 * E-XML Library:  For XML, XML-RPC, HTTP, and related.
 * Copyright (C) 2002-2008  Elias Ross
 * 
 * genman@noderunner.net
 * http://noderunner.net/~genman
 * 
 * 1025 NE 73RD ST
 * SEATTLE WA 98115
 * USA
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * $Id$
 */

package net.noderunner.http;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;

/**
 * An easy-to-use HTTP client that can perform any standard HTTP operation.
 * Opens a connection that can be used over and over again, unlike the
 * Java HTTP client.  Also allows for streamed data input and output,
 * and allows for data operations to be performed without the use of
 * call-backs.
 *
 * <p>
 * The underlying connection is kept active until {@link #close} is called.
 * After every operation, the data sent by the HTTP server must be fully read,
 * otherwise, any following operation on the same connection may not execute
 * successfully.  The character readers returned can be read or discarded
 * easily with the {@link HttpUtil#read} and {@link HttpUtil#discard} methods.
 * If the HTTP connection is to be used again, <i>do not</i> call
 * <code>close</code> on any returned character readers.
 * </p>
 *
 * <p>
 * Example GET usage:  (Retrieves the root document)
 * <pre>
 * URL url = new URL("http://example.com");
 * EasyHttpClient client = new EasyHttpClient(url);
 * String document = HttpUtil.read(client.doGet());
 * client.setFile("/somedir/somefile.html");
 * String document2 = HttpUtil.read(client.doGet());
 * client.close();
 * </pre>
 * </p>
 *
 * <p>
 * Example POST usage:  (Posts URL encoded data to the same CGI script)
 * <pre>
 * URL url = new URL("http://example.com/post.cgi");
 * EasyHttpClientFactory factory = new EasyHttpClientFactory();
 * EasyHttpClient client = factory.makePostClient(url);
 * BufferedReader br;
 * java.util.Map map = new HashMap();
 * map.put("name", "Joe");
 * map.put("business", "Bar");
 * br = client.doPostUrlEncoded(HttpUtil.urlEncode(map));
 * EasyHttpClient.discard(br);
 *
 * map.put("name", "Alice");
 * map.put("business", "Foo");
 * br = client.doPostUrlEncoded(HttpUtil.urlEncode(map));
 * EasyHttpClient.discard(br);
 *
 * client.close();
 * </pre>
 * </p>
 *
 * <p>
 * Example DELETE method usage:  (Deletes two remote files)
 * <pre>
 * URL url = new URL("http://example.com/somefile");
 * EasyHttpClientFactory factory = new EasyHttpClientFactory();
 * EasyHttpClient client = factory.makeClient(url, RequestLine.METHOD_DELETE);
 * client.doOperation();
 * client.setFile("/somefile2");
 * client.doOperation();
 * </pre>
 * </p>
 *
 * <p>
 * Example binary POST method usage:  (Posts an image to a CGI script)
 * <pre>
 * URL url = new URL("http://example.com/post.cgi");
 * EasyHttpClientFactory factory = new EasyHttpClientFactory();
 * EasyHttpClient client = factory.makePostClient();
 * InputStream fileIS = new FileInputStream("somefile.jpeg");
 * InputStream resultIS = client.doOperation(fileIS, -1, "image/jpeg");
 * <pre>
 * </p>
 *
 * <p>
 * <i>Design notes:  This class is designed as a wrapper,
 * allowing the underlying {@link HttpClient} behavior to be delegated
 * in another class.  The goal is to provide a lot of functionality
 * that users will not have to re-implement deal with many of the common
 * HTTP use-cases.  If something significant must be altered in what
 * HTTP-level functionality is needed, it should be implementable
 * by extending an existing {@link HttpClient}.  The operations
 * on any {@link HttpClient} can then be simply extended with this wrapper.
 * </i>
 * </p>
 * @see EasyHttpClientFactory
 * @see HttpClient
 */
public class EasyHttpClient
{

	private HttpClient client;
	private RequestLine requestLine;
	private MessageHeaders headers;
	private ClientResponse lastResponse;
	private boolean checkStatus;
	private GeneralDataPoster dataPoster; // Constructed if needed

	/**
	 * Constructs a new HTTP client with a specific wrapped
	 * client, request line, and headers.
	 */
	public EasyHttpClient(HttpClient client, RequestLine requestLine, MessageHeaders headers) {
		if (client == null)
			throw new IllegalArgumentException("Null client");
		if (requestLine == null)
			throw new IllegalArgumentException("Null requestLine");
		if (headers == null)
			throw new IllegalArgumentException("Null headers");
		this.client = client;
		this.requestLine = requestLine;
		this.headers = headers;
		this.checkStatus = true;
		this.lastResponse = null;
	}

	/**
	 * Constructs a new HTTP client.
	 */
	public EasyHttpClient(URL url, Method method) {
		this(makeHttpClient(url), RequestLine.create(url, method), MessageHeaders.defaultHeaders(url));
	}

	/**
	 * Constructs a new HTTP client.
	 */
	public EasyHttpClient(HttpClient c, URL url, Method method) {
		this(c, RequestLine.create(url, method), MessageHeaders.defaultHeaders(url));
	}

	/**
	 * Constructs a new HTTP client.
	 */
	public EasyHttpClient(URL url) {
		this(url, Method.GET);
	}

	/**
	 * Creates and returns a new {@link HttpClient}.
	 * By default, returns a new instance of {@link RetryHttpClient}.
	 */
	private static HttpClient makeHttpClient(URL url) {
		return new RetryHttpClient(url);
	}

	/**
	 * Allows a subsequent operation to be repeated with a different file on
	 * the same connection.  Any previously set headers remain identical.
	 */
	public void setFile(String fileName)
	{
		requestLine = new RequestLine(requestLine, fileName);
	}

	/**
	 * Allows a subsequent operation to be repeated with a different method on
	 * the same connection.  Any previously set headers remain identical.
	 */
	public void setMethod(Method method)
	{
		if (requestLine.getMethod().equals(method))
			return;
		requestLine = new RequestLine(method,
			requestLine.getRequestURI(), requestLine.getHttpVersion());
	}
	
	/**
	 * Returns the message headers in use.
	 * These may be modified as required.
	 */
	public MessageHeaders getHeaders() {
		return headers;
	}

	/**
	 * Reads the HTTP response, checks the status, and
	 * returns a wrapped input stream based on the headers given.
	 */
	private InputStream readResponse2()
		throws IOException
	{
		lastResponse = client.readResponse();
		if (checkStatus) {
			int code = lastResponse.getStatusLine().getStatusCode();
			if ((code / 100) != 2) {
				// Throw away contents
				lastResponse.readFully();
				throw new HttpException("Bad HTTP Status, expected 200 level: " + lastResponse);
			}
		}
		MessageHeaders hl = lastResponse.getHeaders();
		return HttpUtil.wrapInputStream(lastResponse.getInputStream(), hl);
	}

	private BufferedReader readResponse()
		throws IOException
	{
		InputStream s = readResponse2();
		if (s == null)
			return null;
		// TODO determine content encoding, and properly wrap that
		return new BufferedReader(new InputStreamReader(s));
	}

	/**
	 * Sets if the status will automatically be checked for a 200-level
	 * response or whether or not the status will be ignored.  By default, status
	 * is checked.
	 */
	public void setCheckStatus(boolean checkStatus) {
		this.checkStatus = checkStatus;
	}

	/**
	 * Returns the last HTTP response, including headers, resulting
	 * from the last <code>doPost</code>, <code>doGet</code>, or
	 * <code>doOperation</code> call.
	 * Returns null if no response information exists.
	 */
	public Response getLastResponse() {
		return lastResponse;
	}

	/**
	 * Performs a <code>GET</code> operation,
	 * returning a <code>BufferedReader</code>, which can be used to read the
	 * response body.
	 *
	 * @throws HttpException if the input stream could
	 * not be created, or the status was not allowed
	 * @return null if no body was obtained
	 * @see #getLastResponse
	 * @see #setCheckStatus
	 * @see HttpUtil#read
	 * @see HttpUtil#discard
	 */
	public BufferedReader doGet()
		throws IOException
	{
		setMethod(Method.GET);
		client.writeRequest(new ClientRequest(requestLine, headers));
		client.getOutputStream(); // does nothing for now
		return readResponse();
	}

	private void setLenHeader(int len) {
		String slen = String.valueOf(len);
		MessageHeader mh = new MessageHeader(MessageHeader.FN_CONTENT_LENGTH, slen);
		headers.add(mh);
	}

	private void setContentType(String contentType) {
		if (contentType != null) {
			MessageHeader mh = new MessageHeader(MessageHeader.FN_CONTENT_TYPE, contentType);
			headers.add(mh);
		}
	}

	private void setChunkedHeader() {
		headers.add(MessageHeader.MH_TRANSFER_ENCODING_CHUNKED);
	}

	/**
	 * Performs a <code>POST</code> operation, returning a
	 * <code>BufferedReader</code> for reading the response body.  The data
	 * type to be transferred may be indicated.
	 *
	 * @param data source data array
	 * @param off zero-based offset in source
	 * @param len length of array to send
	 * @param contentType content type to indicate, optionally
	 * <code>null</code> to indicate no content type
	 *
	 * @return null if no body was obtained
	 * @see #getLastResponse
	 * @see #setCheckStatus
	 * @see HttpUtil#read
	 * @see HttpUtil#discard
	 */
	public BufferedReader doPost(byte[] data, int off, int len, String contentType)
		throws IOException
	{
		setMethod(Method.POST);
		setLenHeader(len);
		setContentType(contentType);

		DataPoster p = new ByteArrayDataPoster(data, off, len);
		client.writeRequest(new ClientRequest(requestLine, headers, p));

		return readResponse();
	}

	/**
	 * Performs a POST operation, returning a <code>BufferedReader</code>
	 * for reading the response body.  The content type header is set
	 * to indicate the <code>x-www-form-urlencoded</code> type.
	 *
	 * @param urlEncodedData data to send (ASCII format) 
	 * @see HttpUtil#urlEncode
	 */
	public BufferedReader doPostUrlEncoded(byte[] urlEncodedData)
		throws IOException
	{
		if (urlEncodedData == null)
			throw new IllegalArgumentException("null urlEncodedData");
		headers.add(MessageHeader.MH_URL_ENCODED);
		return doPost(urlEncodedData, 0, urlEncodedData.length, null);
	}
	
	/**
	 * Performs whatever operation was specified in the request line, as
	 * passed into the constructor.  Handles no input or output.
	 * This method assumes no data will be returned by the server.
	 * If response data is returned, it is thrown away.
	 * Call the other {@link #doOperation(InputStream, int, String)
	 * doOperation} method to have the data returned.
	 *
	 * @throws HttpException if (unexpectedly) HTTP was obtained
	 */
	public void doOperation()
		throws IOException
	{
		doOperation(null, 0, null);
		lastResponse.readFully();
	}

	/**
	 * Performs whatever operation was specified in the request, as
	 * passed into the constructor.  This method can be used to perform
	 * tasks not handled by the basic {@link #doPost doPost} and {@link
	 * #doGet doGet} methods.  Utilizes the class {@link GeneralDataPoster}
	 * to do the data posting. 
	 *
	 * @param is
	 *   data stream to be copied and output over HTTP;
	 *   if null, no data is written;  if the input stream
	 *   supports marking, the post operation may be retried,
	 *   if not any retry will throw an <code>HttpException</code>
	 * @param len 
	 *   if len &gt;= 0, sets the content-length header to this length;
	 *   if len &lt; 0, sets the chunked-encoding header
	 * @param contentType if not null, specifies the data content type
	 *   in the request
	 *
	 * @return wrapped input stream for reading HTTP server data from
	 *
	 * @throws HttpException if the supplied input stream does not contain
	 * enough data to be sent
	 * @throws IllegalArgumentException if the supplied input stream is null
	 * and a non-zero length was indicated
	 *
	 * @see HttpUtil#readFully
	 */
	public InputStream doOperation(InputStream is, int len, String contentType)
		throws IOException
	{
		setContentType(contentType);

		if (len < 0)
			setChunkedHeader();
		else if (is != null)
			setLenHeader(len);

		if (dataPoster == null)
			dataPoster = new GeneralDataPoster();
		dataPoster.init(is, len);
		client.writeRequest(new ClientRequest(requestLine, headers, dataPoster));
		return readResponse2();
	}

	/**
	 * Closes the wrapped <code>HttpClient</code>.
	 */
	public void close()
		throws IOException
	{
		client.close();
	}

	/**
	 * Returns debug information.
	 */
	public String toString() {
		return "EasyHttpClient client=[" + client + "]";
	}

	/**
	 * Performs a command-line test.
	 */
	public static void main(String args[])
		throws Exception
	{
		if (args.length == 0) {
			System.err.println("Usage:  EasyHttpClient URL ['post'] [post params]");
			System.err.println("  Performs an HTTP GET on the given URL");
			System.err.println("  If 'post' is indicated, does an HTTP POST with a string");
			System.err.println("  Else, if params are given, does an HTTP POST");
			System.err.println("  Post param format: a=b,c=d,e=f");
			return;
		}
		EasyHttpClient c = null;
		try {
			BufferedReader br;
			if (args.length == 2) {
				c = new EasyHttpClient(new URL(args[0]), Method.POST);
				byte pp[];
				StringTokenizer st = new StringTokenizer(args[1], "=, ");
				Map<String, String> m = new HashMap<String, String>();
				while (st.hasMoreTokens()) {
					m.put(st.nextToken(), st.nextToken());
				}
				pp = HttpUtil.urlEncode(m); 
				System.err.println("Post body");
				System.err.println(new String(pp));
				br = c.doPostUrlEncoded(pp);
			} else if (args.length == 3) {
				c = new EasyHttpClient(new URL(args[0]), Method.POST);
				byte pp[];
				
				// PLAIN TEXT
				pp = args[2].getBytes();
				System.err.println("Post body");
				System.err.println(new String(pp));
				
				// BAD CHUNKED
				// c.getHeaders().add("Transfer-Encoding", "Chunked");
				br = c.doPost(pp, 0, pp.length, "text/xml");
				
				/* CORRECTLY CHUNKED
				ByteArrayInputStream is = new ByteArrayInputStream("foobar asdjfklajdsf".getBytes());
            	InputStream is1 = c.doOperation(is, -1, "text/plain");
        		br = new BufferedReader(new InputStreamReader(is1));
        		*/
			} else {
				c = new EasyHttpClient(new URL(args[0]), Method.GET);
				br = c.doGet();
			}
			String s = HttpUtil.read(br);
			System.out.println(s);
			c.getLastResponse();
			// System.out.println(HttpUtil.read(br));
		} catch (Exception e) {
			System.err.println("Client failed: " + c);
			e.printStackTrace();
		}
	}

}
