/*
 * eXist Open Source Native XML Database Copyright (C) 2001-06 Wolfgang M.
 * Meier meier@ifs.tu-darmstadt.de http://exist.sourceforge.net
 *
 * This program 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 of the License, or (at your
 * option) any later version.
 *
 * This program 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.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: HttpServletRequestWrapper.java 4403 2006-09-27 12:27:37Z wolfgang_m $
 */

package org.exist.http.servlets;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.Principal;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Vector;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;


/** A wrapper for HttpServletRequest
 * - differentiates between POST parameters in the URL or Content Body
 * - keeps content Body of the POST request, making it available many times 
 * 		through {@link #getContentBodyInputStream()} .
 * 
 * A method of differentiating between POST parameters in the URL or Content Body of the request was needed.
 * The standard javax.servlet.http.HTTPServletRequest does not differentiate between URL or content body parameters,
 * this class does, the type is indicated in RequestParameter.type.
 * 
 * To differentiate manually we need to read the URL (getQueryString()) and the Content body (getInputStream()),
 * this is problematic with the standard javax.servlet.http.HTTPServletRequest as parameter functions (getParameterMap(), getParameterNames(), getParameter(String), getParameterValues(String)) 
 * affect the  input stream functions (getInputStream(), getReader()) and vice versa.
 * 
 * This class solves this by reading the Request Parameters initially from both the URL and the Content Body of the Request
 * and storing them in the private variable params for later use.
 * 
 * @author Adam Retter <adam.retter@devon.gov.uk>
 * @serial 2006-02-28
 * @version 1.1
 */

//TODO: check loops to make sure they only iterate as few times as needed
//TODO: do we need to do anything with encoding strings manually?


public class HttpServletRequestWrapper implements HttpServletRequest
{
	/** Simple Enumeration implementation for String's, needed for getParameterNames() */
	private class StringEnumeration implements Enumeration
	{
		private String[] strings = null;	//Strings in the Enumeration
		int aryPos = -1;					//Current Position in Enumeration
		
		/**
		 * StringEnumeration Constructor
		 * @param strings[] 	an array of strings for the Enumeration
		 */
		StringEnumeration(String[] strings)
		{
			if(strings != null)
			{
				if(strings.length > 0)
				{
					//Create a new array
					this.strings = new String[strings.length];

					//Copy the data over
					System.arraycopy(strings, 0, this.strings, 0, strings.length);

					//Set the position to the start of the array
					aryPos = 0;
				}
			}
		}
		
		/**
		 * @see java.util.Enumeration#hasMoreElements
		 */
		public boolean hasMoreElements()
		{
			if(aryPos != -1)
			{
				if(aryPos < strings.length)
				{
					return true;
				}
			}
			return false;
		}
		
		/**
		 * @see java.util.Enumeration#nextElement
		 */
		public Object nextElement()
		{
			if(aryPos != -1)
			{
				if(aryPos < strings.length)
				{
					Object s = (Object)strings[aryPos];
					aryPos++;
					return s;
				}
				else
				{
					throw new NoSuchElementException("No more String's in the Enumeration, End Reached");
				}
			}
			else
			{
				throw new NoSuchElementException("Enumeration is empty");
			}
		}
	}
	
	/** Simple class to hold the value and type of a request parameter */
	private class RequestParamater
	{
		public final static int PARAM_TYPE_URL = 1;		//parameter from the URL of the request
		public final static int PARAM_TYPE_CONTENT = 2;	//parameter from the Content of the request

		private String value = null;	//parameter value
		private int type = 0;			//parameter type, either PARAM_TYPE_URL or PARAM_TYPE_CONTENT
		
		/**
		 * RequestParameter Constructor
		 * @param value 	Value of the Request Parameter
		 * @param type		Type of the Request Parameter, URL (1) or Content (2)
		 */
		RequestParamater(String value, int type)
		{
			this.value = value;
			this.type = type;
		}
		
		/**
		 * Request parameter value accessor
		 * @return		Value of Request parameter 
		 */
		public String getValue()
		{
			return(value);
		}
		
		/**
		 * Request parameter type accessor
		 * @return		Type of Request parameter
		 */
		public int getType()
		{
			return(type);
		}
	}
	
	
	//Members
	private HttpServletRequest request = null;		//The Request
	/** The encoding for the Request */
	private String formEncoding = null;
	
	/** The Request Parameters
	 * 
	 * params LinkedHashMap
	 * ====================
	 * params keys are String
	 * params values are Vector of RequestParameter's
	 */
	private LinkedHashMap params = null;			

	/** the content Body of the POST request; 
	 * it must be stored because once the Servlet input stream has been 
	 * consumed, the content Body is no more readable. */
	private String contentBodyAsString;
	byte[] contentBody;

	
	/**
	 * HttpServletRequestWrapper Constructor
	 * @param request		The HttpServletRequest to wrap
	 * @param formEncoding		The encoding to use
	 */
	public HttpServletRequestWrapper(HttpServletRequest request, String formEncoding) throws UnsupportedEncodingException
	{
		this.request = request;
		this.formEncoding = formEncoding;
		params = new LinkedHashMap();
		
		initialiseWrapper();
	}
	
	//Initalises the wrapper, setup encoding and parameter hashtable
	private void initialiseWrapper() throws UnsupportedEncodingException
	{
		//encoding
		if(request.getCharacterEncoding() == null)
		{
			request.setCharacterEncoding(formEncoding);
		}
		else
		{
			formEncoding = getCharacterEncoding();
		}
		
		//Parse out parameters from the URL
		parseURLParameters(this.request.getQueryString());
		
		//If POST request, Parse out parameters from the Content Body
		if(request.getMethod().toUpperCase().equals("POST"))
		{
			//If there is some Content
			if(request.getContentLength() > 0)
			{
				// If a form POST , and not a document POST
				String contentType = request.getContentType().toLowerCase();
				if( contentType.equals("application/x-www-form-urlencoded")
						&& request.getHeader("ContentType") == null )
				{
					//Parse out parameters from the Content Body
					parseContentBodyParameters();
					
				} else if ( contentType.equals("text/xml") ) {
					// if an XML-RPC
					contentBodyAsString = getContentBody();
				}
			}
		}
	}
	
	//Stores parameters from the QueryString of the request
	private void parseURLParameters(String querystring)
	{
		if(querystring != null)
		{
			//Parse any parameters from the URL
			parseParameters(querystring, RequestParamater.PARAM_TYPE_URL);
		}
	}
	
	/** Stores parameters from the Content Body of the Request */
	private void parseContentBodyParameters()
	{
		String content = getContentBody();
		
		//Parse any parameters from the Content Body
		parseParameters( content, RequestParamater.PARAM_TYPE_CONTENT);
	}

	private String getContentBody() {
		
		//Create a buffer big enough to hold the Content Body		
		contentBody = new byte[ request.getContentLength() ];
		String result = "";
		
		try	{
	        String encoding = request.getCharacterEncoding();
	        if(encoding == null)
	            encoding = "UTF-8";

			//Read the Content Body into the buffer
			InputStream is = request.getInputStream();
			
			int bytes = 0;
			int offset = 0;
			int max = 4096;
		    while (( bytes = is.read( contentBody, offset, max )) != -1) {
				offset += bytes;
		    }
			result = new String(contentBody, encoding);

		}
		catch(IOException ioe) {
			//TODO: handle this properly
			System.err.println( "Error Reading the Content Body into the buffer: " + ioe );
			ioe.printStackTrace();
		}
		return result;
	}
	
	/** Parses Parameters into param objects and stores them in a vector in params */
	private void parseParameters(String parameters, int type)
	{
		//Split parameters into an array
		String[] nameValuePairs = parameters.split("&");
		
		for (int k = 0; k < nameValuePairs.length; k++)
		{
			//Split parameter into name and value
			String[] thePair = nameValuePairs[k].split("=");
			
			try
			{
				//URL Decode the parameter name and value
				thePair[0] = URLDecoder.decode(thePair[0], formEncoding);
				if(thePair.length == 2)
				{
					thePair[1] = URLDecoder.decode(thePair[1], formEncoding);
				}
			}
			catch(UnsupportedEncodingException uee)
			{
				//TODO: handle this properly
				uee.printStackTrace();
			}
			
			//Have we encountered a parameter with this name?
			if(params.containsKey(thePair[0]))
			{
				//key exists in hash map, add value and type to vector
				Vector vecValues = (Vector)params.get(thePair[0]);
				vecValues.add(new RequestParamater((thePair.length == 2 ? thePair[1] : new String()), type));
				params.put(thePair[0], vecValues);
			}
			else
			{
				//not in hash map so add a vector with the initial value
				Vector vecValues = new Vector();
				vecValues.add(new RequestParamater((thePair.length == 2 ? thePair[1] : new String()), type));
				params.put(thePair[0], vecValues);
			}
		}
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getAuthType
	 */
	public String getAuthType()
	{
		return request.getAuthType();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getCookies
	 */
	public Cookie[] getCookies()
	{
		return request.getCookies();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getDateHeader
	 */
	public long getDateHeader(String name)
	{
		return request.getDateHeader(name);
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getHeader
	 */
	public String getHeader(String name)
	{
		return request.getHeader(name);
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getHeaders
	 */
	public Enumeration getHeaders(String name)
	{
		return request.getHeaders(name);
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getHeaderNames
	 */
	public Enumeration getHeaderNames()
	{
		return request.getHeaderNames();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getIntHeader
	 */
	public int getIntHeader(String name)
	{ 
		return request.getIntHeader(name);
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getMethod
	 */
	public String getMethod()
	{
		return request.getMethod();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getPathInfo
	 */
	public String getPathInfo()
	{
		return request.getPathInfo();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getPathTranslated
	 */
	public String getPathTranslated()
	{
		return request.getPathInfo();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getContextPath
	 */
	public String getContextPath()
	{
		return request.getContextPath();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getQueryString
	 */
	public String getQueryString()
	{
		return request.getQueryString();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getRemoteUser
	 */
	public String getRemoteUser()
	{
		return request.getRemoteUser();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#isUserInRole
	 */
	public boolean isUserInRole(String name)
	{
		return request.isUserInRole(name);
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getUserPrincipal
	 */
	public Principal getUserPrincipal()
	{
		return request.getUserPrincipal();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getRequestedSessionId
	 */
	public String getRequestedSessionId()
	{
		return request.getRequestedSessionId();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getRequestURI
	 */
	public String getRequestURI()
	{
		return request.getRequestURI();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getRequestURL
	 */
	public StringBuffer getRequestURL()
	{	
		return request.getRequestURL();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getServletPath
	 */
	public String getServletPath()
	{
		return request.getServletPath();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getSession
	 */
	public HttpSession getSession(boolean create)
	{
		return request.getSession(create);
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getSession
	 */
	public HttpSession getSession()
	{
		return request.getSession();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdValid
	 */
	public boolean isRequestedSessionIdValid()
	{
		return request.isRequestedSessionIdValid();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromCookie
	 */
	public boolean isRequestedSessionIdFromCookie()
	{
		return request.isRequestedSessionIdFromCookie();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromURL
	 */
	public boolean isRequestedSessionIdFromURL()
	{
		return request.isRequestedSessionIdFromURL();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromUrl
	 */
	public boolean isRequestedSessionIdFromUrl()
	{	
		return request.isRequestedSessionIdFromUrl();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getAttribute
	 */
	public Object getAttribute(String name)
	{
		return request.getAttribute(name);
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getAttributeNames
	 */
	public Enumeration getAttributeNames()
	{
		return request.getAttributeNames();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getCharacterEncoding
	 */
	public String getCharacterEncoding()
	{
		return request.getCharacterEncoding();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#setCharacterEncoding
	 */
	public void setCharacterEncoding(String env) throws UnsupportedEncodingException
	{
		request.setCharacterEncoding(env);
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getContentLength
	 */
	public int getContentLength()
	{
		return request.getContentLength();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getContentType
	 */
	public String getContentType()
	{	
		return request.getContentType();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getInputStream
	 */
	public ServletInputStream getInputStream() throws IOException
	{
		return request.getInputStream();
	}

//	public InputStream getStringBufferInputStream() throws IOException {
//		return new StringBufferInputStream( contentBodyAsString );
//	}

	/** making the content Body of the POST request available many times, for processing by , e.g. Rpc processor . */
	public InputStream getContentBodyInputStream() throws IOException {
		return new ByteArrayInputStream( contentBody );
	}
	
	/**
	 * get the value of a Request parameter by its name from the local parameter store
	 * @param name		The name of the Request parameter to get the value for
	 * @return		The value of the Request parameter with the specified name
	 */
	public String getParameter(String name)
	{
		//Does the parameter exist?
		if(params.containsKey(name))
		{
			//Get the parameters vector of values
			Vector vecParameterValues = (Vector)params.get(name);

			//return the first value in the vector
			return ((RequestParamater)vecParameterValues.get(0)).getValue();
		}
		else
		{
			return null;
		}
	}

	/**
	 * get the names of the Request parameters from the local parameter store
	 * @return		An enumeration of string values representing the Request parameters names
	 */
	public Enumeration getParameterNames()
	{	
		//get the key set as an array
		Object[] keySet = params.keySet().toArray();
		
		//create a new string array, the same size as the ket set array 
		String[] strKeySet = new String[keySet.length];
		
		//copy the data from the key set to the string key set
		System.arraycopy(keySet, 0, strKeySet, 0, keySet.length);
		
		//return an enumeration of strings of the keys
		return new StringEnumeration(strKeySet);
	}

	/**
	 * get the values of the Request parameter indicated by name from the local parameter store
	 * @param name		The name of the Request parameter to get the values for
	 * @return		The String array of the Request parameter's values
	 */
	public String[] getParameterValues(String name)
	{
		//Does the parameter exist?
		if(params.containsKey(name))
		{
			//Get the parameters vector of values
			Vector vecParameterValues = (Vector)params.get(name);
			
			//Create a string array to hold the values
			String[] values = new String[vecParameterValues.size()];
			
			//Copy each value into the string array
			for(int i = 0; i < vecParameterValues.size(); i++)
			{
				values[i] = ((RequestParamater)vecParameterValues.get(i)).getValue();
			}
			
			//return the string array of values
			return(values);
		}
		else
		{
			return null;
		}
	}

	/**
	 * get a Map of Request parameters (keys and values) from the local parameter store
	 * @return		Map of Request Parameters. Key is of type String and Value is of type String[].
	 */
	public Map getParameterMap()
	{
		//Map to hold the parameters
		LinkedHashMap mapParameters = new LinkedHashMap();
		
		Set setParams = params.entrySet();
		
		//iterate through the Request Parameters
		for(Iterator itParams = setParams.iterator(); itParams.hasNext(); )
		{
			//Get the parameter
			Map.Entry me = (Map.Entry)itParams.next();
			
			//Get the parameters values
			Vector vecParamValues = (Vector)me.getValue();
			
			//Create a string array to hold the parameter values
			String[] values = new String[vecParamValues.size()];
			
			//Copy the parameter values into a string array
			int i = 0;
			for(Iterator itParamValues = vecParamValues.iterator(); itParamValues.hasNext(); i++)
			{
				values[i] = ((RequestParamater)itParamValues.next()).getValue();
			}
			mapParameters.put(me.getKey(), values); //Store the parameter in a map
		}
		return mapParameters; //return the Map of parameters
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getProtocol
	 */
	public String getProtocol()
	{
		return request.getProtocol();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getScheme
	 */
	public String getScheme()
	{
		return request.getScheme();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getServerName
	 */
	public String getServerName()
	{
		return request.getServerName();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getServerPort
	 */
	public int getServerPort()
	{
		return request.getServerPort();
	}

	public BufferedReader getReader() throws IOException
	{
		return request.getReader();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getRemoteAddr
	 */
	public String getRemoteAddr()
	{
		return request.getRemoteAddr();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getRemoteHost
	 */
	public String getRemoteHost()
	{
		return request.getRemoteHost();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#setAttribute
	 */
	public void setAttribute(String name, Object o)
	{
		request.setAttribute(name, o);
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#removeAttribute
	 */
	public void removeAttribute(String name)
	{
		request.removeAttribute(name);
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getLocale
	 */
	public Locale getLocale()
	{
		return request.getLocale();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getLocales
	 */
	public Enumeration getLocales()
	{
		return request.getLocales();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#isSecure
	 */
	public boolean isSecure()
	{
		return request.isSecure();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getRequestDispatcher
	 */
	public RequestDispatcher getRequestDispatcher(String name)
	{
		return request.getRequestDispatcher(name);
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getRealPath
	 */
	public String getRealPath(String path)
	{
		return request.getRealPath(path);
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getRemotePort
	 */
	public int getRemotePort()
	{
		return request.getRemotePort();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getLocalName
	 */
	public String getLocalName()
	{	
		return request.getLocalName();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getLocalAddr
	 */
	public String getLocalAddr()
	{
		return request.getLocalAddr();
	}

	/**
	 * @see javax.servlet.http.HttpServletRequest#getLocalPort
	 */
	public int getLocalPort()
	{	
		return request.getLocalPort();
	}
	
	/**
	 * Similar to javax.servlet.http.HttpServletRequest.toString() ,
	 * except it includes output of the Request parameters from the Request's Content Body
	 * @return		String representation of HttpServletRequestWrapper
	 */
	public String toString() {
		// If POST request AND there is some content AND its not a file upload
		// 	AND content Body has not been recorded
		if (	    request.getMethod().toUpperCase().equals("POST")
				&&  request.getContentLength() > 0
				&& !request.getContentType().toUpperCase().startsWith(
						"MULTIPART/")
				&& ! contentBodyRecorded() ) {
			
			// Also return the content parameters, these are not part 
			// of the standard HttpServletRequest.toString() output
			StringBuffer buf = new StringBuffer( request.toString());

			Set setParams = params.entrySet();

			for (Iterator itParams = setParams.iterator(); itParams.hasNext();) {
				Map.Entry me = (Map.Entry) itParams.next();
				Vector vecParamValues = (Vector) me.getValue();

				for (Iterator itParamValues = vecParamValues.iterator(); itParamValues
						.hasNext();) {
					RequestParamater p = (RequestParamater) itParamValues
							.next();

					if (p.type == RequestParamater.PARAM_TYPE_CONTENT) {
						if (buf.charAt(buf.length() - 1) != '\n')
							buf.append("&");
						buf.append((String) me.getKey());
						buf.append("=");
						buf.append(p.getValue());
					}
				}
			}

			buf.append(	System.getProperty("line.separator") +
						System.getProperty("line.separator") );

			return buf.toString();

		} else if ( contentBodyRecorded() ) {
			
			// XML-RPC request or plain XML REST POST
			StringBuffer buf = new StringBuffer( request.toString() );
			buf.append(contentBodyAsString);
			
			buf.append(	System.getProperty("line.separator") +
						System.getProperty("line.separator") );
			return buf.toString();

		} else {
			//Return standard HttpServletRequest.toString() output
			return request.toString();
		}
	}

	private boolean contentBodyRecorded() {
		return contentBody != null 
			&& contentBody.length > 0;
	}

}
