/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.clients.http;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.Map.Entry;

import javax.naming.SizeLimitExceededException;

import freenet.support.Fields;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.MultiValueTable;
import freenet.support.SimpleReadOnlyArrayBucket;
import freenet.support.URLEncoder;
import freenet.support.Logger.LogLevel;
import freenet.support.api.Bucket;
import freenet.support.api.BucketFactory;
import freenet.support.api.HTTPRequest;
import freenet.support.api.HTTPUploadedFile;
import freenet.support.api.RandomAccessBucket;
import freenet.support.io.BucketTools;
import freenet.support.io.Closer;
import freenet.support.io.LineReadingInputStream;

/**
 * Used for passing all HTTP request information to the FredPlugin that handles
 * the request. It parses the query string and has several methods for accessing
 * the request parameter values.
 * 
 * @author nacktschneck
 */
public class HTTPRequestImpl implements HTTPRequest {

	/**
	 * This map is used to store all parameter values.
	 *  
	 * Don't access this map directly, use {@link #getParameterValueList(String)} and
	 * {@link #isParameterSet(String)} instead
	 */
	private final Map<String, List<String>> parameterNameValuesMap = new HashMap<String, List<String>>();

	/**
	 * the original URI as given to the constructor
	 */
	private URI uri;
	
	/**
	 * The headers sent by the client
	 */
	private MultiValueTable<String, String> headers;
	
	/**
	 * The data sent in the connection
	 */
	private Bucket data;
	
	/**
	 * A hashmap of buckets that we use to store all the parts for a multipart/form-data request
	 */
	private HashMap<String, RandomAccessBucket> parts;
	
	private boolean freedParts;
	
	/** A map for uploaded files. */
	private Map<String, HTTPUploadedFileImpl> uploadedFiles = new HashMap<String, HTTPUploadedFileImpl>();
	
	private final BucketFactory bucketfactory;
	
	private final String method;

        private static volatile boolean logMINOR;
	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}

	/**
	 * Create a new HTTPRequest for the given URI and parse its request
	 * parameters.
	 * 
	 * @param uri
	 *            the URI being requested
	 */
	public HTTPRequestImpl(URI uri, String method) {
		this.uri = uri;
		this.parseRequestParameters(uri.getRawQuery(), true, false);
		this.data = null;
		this.parts = null;
		this.bucketfactory = null;
		this.method = method;
	}

	/**
	 * Creates a new HTTPRequest for the given path and url-encoded query string
	 * 
	 * @param path i.e. /test/test.html
	 * @param encodedQueryString a=some+text&b=abc%40def.de
	 * @throws URISyntaxException if the URI is invalid
	 */
	public HTTPRequestImpl(String path, String encodedQueryString, String method) throws URISyntaxException {
		this.data = null;
		this.parts = null;
		this.bucketfactory = null;
		if ((encodedQueryString!=null) && (encodedQueryString.length()>0)) {
			this.uri = new URI(path+ '?' +encodedQueryString);
		} else {
			this.uri = new URI(path);
		}
		this.method = method;
		this.parseRequestParameters(uri.getRawQuery(), true, false);
	}
	
	/**
	 * Creates a new HTTPRequest for the given URI and data.
	 * 
	 * @param uri The URI being requested
	 * @param h Client headers
	 * @param d The data
	 * @param ctx The toadlet context (for headers and bucket factory)
	 * @throws URISyntaxException if the URI is invalid
	 */
	public HTTPRequestImpl(URI uri, Bucket d, ToadletContext ctx, String method) {
		this.uri = uri;
		this.headers = ctx.getHeaders();
		this.parseRequestParameters(uri.getRawQuery(), true, false);
		this.data = d;
		this.parts = new HashMap<String, RandomAccessBucket>();
		this.bucketfactory = ctx.getBucketFactory();
		this.method = method;
		if(data != null) {
			try {
				this.parseMultiPartData();
			} catch (IOException ioe) {
				Logger.error(this, "Temporary files error ? Could not parse: "+ioe, ioe);
			}
		}
	}
	

	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#getPath()
	 */
	@Override
	public String getPath() {
		return this.uri.getPath();
	}


	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#hasParameters()
	 */
	@Override
	public boolean hasParameters() {
		return ! this.parameterNameValuesMap.isEmpty();
	}

	/**
	 * Returns the names of all parameters.
	 *
	 * @return The names of all parameters
	 */
	@Override
	public Collection<String> getParameterNames() {
		return parameterNameValuesMap.keySet();
	}

	/**
	 * Parse the query string and populate {@link #parameterNameValuesMap} with
	 * the lists of values for each parameter. If this method is not called at
	 * all, all other methods would be useless. Because they rely on the
	 * parameter map to be filled.
	 * 
	 * @param queryString
	 *            the query string in its raw form (not yet url-decoded)
	 * @param doUrlDecoding TODO
	 */
	private void parseRequestParameters(String queryString, boolean doUrlDecoding, boolean asParts) {

		if(logMINOR) Logger.minor(this, "queryString is "+queryString+", doUrlDecoding="+doUrlDecoding);

		Map<String, List<String>> parameters = parseUriParameters(queryString, doUrlDecoding);

		if (asParts) {
			for (Entry<String, List<String>> parameterValues : parameters.entrySet()) {
				List<String> values = parameterValues.getValue();
				String value = values.get(values.size() - 1);
				byte[] buf;
				try {
					buf = value.getBytes("UTF-8");
				} catch (UnsupportedEncodingException e) {
					throw new Error("Impossible: JVM doesn't support UTF-8: " + e, e);
				} // FIXME some other encoding?
				RandomAccessBucket b = new SimpleReadOnlyArrayBucket(buf);
				parts.put(parameterValues.getKey(), b);
				if(logMINOR)
					Logger.minor(this, "Added as part: name="+parameterValues.getKey()+" value="+value);
			}
		} else {
			parameterNameValuesMap.clear();
			parameterNameValuesMap.putAll(parameters);
		}
	}

	/**
	 * Get the first value of the parameter with the given name.
	 * 
	 * @param name
	 *            the name of the parameter to get
	 * @return the first value or <code>null</code> if the parameter was not
	 *         set
	 */
	private String getParameterValue(String name) {
		if (!this.isParameterSet(name)) {
			return null;
		}
		List<String> allValues = this.getParameterValueList(name);
		return allValues.get(0);
	}

	/**
	 * Get the list of all values for the parameter with the given name. When
	 * this method is called for a given parameter for the first time, a new
	 * empty list of values is created and stored in
	 * {@link #parameterNameValuesMap}. This list is returned and should be
	 * used to add parameter values. If you only want to check if a parameter is
	 * set at all, you must use {@link #isParameterSet(String)}.
	 * 
	 * @param name
	 *            the name of the parameter to get
	 * @return the list of all values for this parameter that were parsed so
	 *         far.
	 */
	private List<String> getParameterValueList(String name) {
		List<String> values = this.parameterNameValuesMap.get(name);
		if (values == null) {
			values = new LinkedList<String>();
			this.parameterNameValuesMap.put(name, values);
		}
		return values;
	}

	/**
	 * Parses the parameters from the given query string, optionally decoding
	 * the parameters using UTF-8.
	 *
	 * @param queryString
	 *            The query string to decode
	 * @param doUrlDecoding
	 *            {@code true} to decode the parameter names and values
	 * @return The decoded parameters
	 */
	public static Map<String, List<String>> parseUriParameters(String queryString, boolean doUrlDecoding) {
		if(logMINOR) Logger.minor(HTTPRequestImpl.class, "queryString is "+queryString+", doUrlDecoding="+doUrlDecoding);

		/* create result map. */
		Map<String, List<String>> parameters = new HashMap<String, List<String>>();

		// nothing to do if there was no query string in the URI
		if ((queryString == null) || (queryString.length() == 0)) {
			return parameters;
		}

		// iterate over all tokens in the query string (separated by &)
		StringTokenizer tokenizer = new StringTokenizer(queryString, "&");
		while (tokenizer.hasMoreTokens()) {
			String nameValueToken = tokenizer.nextToken();

			if(logMINOR) Logger.minor(HTTPRequestImpl.class, "Token: "+nameValueToken);

			// a token can be either a name, or a name value pair...
			String name = null;
			String value = "";
			int indexOfEqualsChar = nameValueToken.indexOf('=');
			if (indexOfEqualsChar < 0) {
				// ...it's only a name, so the value stays emptys
				name = nameValueToken;
				if(logMINOR) Logger.minor(HTTPRequestImpl.class, "Name: "+name);
			} else if (indexOfEqualsChar == nameValueToken.length() - 1) {
				// ...it's a name with an empty value, so remove the '='
				// character
				name = nameValueToken.substring(0, indexOfEqualsChar);
				if(logMINOR) Logger.minor(HTTPRequestImpl.class, "Name: "+name);
			} else {
				// ...it's a name value pair, split into name and value
				name = nameValueToken.substring(0, indexOfEqualsChar);
				value = nameValueToken.substring(indexOfEqualsChar + 1);
				if(logMINOR) Logger.minor(HTTPRequestImpl.class, "Name: "+name+" Value: "+value);
			}

			// url-decode the name and value
			if (doUrlDecoding) {
					try {
						name = URLDecoder.decode(name, "UTF-8");
						value = URLDecoder.decode(value, "UTF-8");
					} catch (UnsupportedEncodingException e) {
						throw new Error("Impossible: JVM doesn't support UTF-8: " + e, e);
					}
				if(logMINOR) {
					Logger.minor(HTTPRequestImpl.class, "Decoded name: "+name);
					Logger.minor(HTTPRequestImpl.class, "Decoded value: "+value);
				}
			}

			List<String> values = parameters.get(name);
			if (values == null) {
				values = new ArrayList<String>();
				parameters.put(name, values);
			}
			values.add(value);
		}

		return parameters;
	}

	/**
	 * Creates a query string from the given parameters.
	 *
	 * @param parameterValues
	 *            The parameters to create a query string from
	 * @param doUrlEncoding
	 *            {@code true} if encoding for HTTP headers, {@code false} to
	 *            only encode unsafe characters
	 * @return The query string
	 */
	public static String createQueryString(Map<String, List<String>> parameterValues, boolean doUrlEncoding) {
		StringBuilder queryString = new StringBuilder();
		for (Entry<String, List<String>> parameter : parameterValues.entrySet()) {
			for (String value : parameter.getValue()) {
				if (queryString.length() > 0) {
					queryString.append('&');
				}
				queryString.append(URLEncoder.encode(parameter.getKey(), doUrlEncoding));
				queryString.append('=');
				queryString.append(URLEncoder.encode(value, doUrlEncoding));
			}
		}
		return queryString.toString();
	}

	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#isParameterSet(java.lang.String)
	 */
	@Override
	public boolean isParameterSet(String name) {
		return this.parameterNameValuesMap.containsKey(name);
	}

	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#getParam(java.lang.String)
	 */
	@Override
	public String getParam(String name) {
		return this.getParam(name, "");
	}

	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#getParam(java.lang.String, java.lang.String)
	 */
	@Override
	public String getParam(String name, String defaultValue) {
		String value = this.getParameterValue(name);
		if (value == null || value.isEmpty()) {
			return defaultValue;
		}
		return value;
	}

	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#getIntParam(java.lang.String)
	 */
	@Override
	public int getIntParam(String name) {
		return this.getIntParam(name, 0);
	}

	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#getIntParam(java.lang.String, int)
	 */
	@Override
	public int getIntParam(String name, int defaultValue) {
		if (!this.isParameterSet(name)) {
			return defaultValue;
		}
		String value = this.getParameterValue(name);
		try {
			return Integer.parseInt(value);
		} catch (NumberFormatException e) {
			return defaultValue;
		}
	}

	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#getIntPart(java.lang.String, int)
	 */
	@Override
	public int getIntPart(String name, int defaultValue) {
		if (!this.isPartSet(name)) {
			return defaultValue;
		}
		String value = this.getPartAsString(name, 32);
		try {
			return Integer.parseInt(value);
		} catch (NumberFormatException e) {
			return defaultValue;
		}
	}

	// TODO: add similar methods for long, boolean etc.

	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#getMultipleParam(java.lang.String)
	 */
	@Override
	public String[] getMultipleParam(String name) {
		List<String> valueList = this.getParameterValueList(name);
		String[] values = new String[valueList.size()];
		valueList.toArray(values);
		return values;
	}

	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#getMultipleIntParam(java.lang.String)
	 */
	@Override
	public int[] getMultipleIntParam(String name) {
		List<String> valueList = this.getParameterValueList(name);

		// try parsing all values and put the valid Integers in a new list
		List<Integer> intValueList = new ArrayList<Integer>();
		for (int i = 0; i < valueList.size(); i++) {
			try {
				intValueList.add(Integer.valueOf(valueList.get(i)));
			} catch (Exception e) {
				// ignore invalid parameter values
			}
		}

		// convert the valid Integers to an array of ints
		int[] values = new int[intValueList.size()];
		for (int i = 0; i < intValueList.size(); i++) {
			values[i] = intValueList.get(i);
		}
		return values;
	}


	// TODO: add similar methods for multiple long, boolean etc.
	
	
	/**
	 * Parse submitted data from a bucket.
	 * Note that if this is application/x-www-form-urlencoded, it will come out as
	 * params, whereas if it is multipart/form-data it will be separated into buckets.
	 */
	private void parseMultiPartData() throws IOException {
		InputStream is = null;
		LineReadingInputStream lis = null;
		OutputStream bucketos = null;

		try {
			if(data == null)
				return;
			String ctype = this.headers.get("content-type");
			if(ctype == null)
				return;
			if(logMINOR)
				Logger.minor(this, "Uploaded content-type: " + ctype);
			String[] ctypeparts = ctype.split(";");
			if(ctypeparts[0].equalsIgnoreCase("application/x-www-form-urlencoded")) {
				// Completely different encoding, but easy to handle
				if(data.size() > 1024 * 1024)
					throw new IOException("Too big");
				byte[] buf = BucketTools.toByteArray(data);
				String s = new String(buf, "us-ascii");
				parseRequestParameters(s, true, true);
			}
			if(!ctypeparts[0].trim().equalsIgnoreCase("multipart/form-data") || (ctypeparts.length < 2))
				return;

			String boundary = null;
			for(String ctypepart: ctypeparts) {
				String[] subparts = ctypepart.split("=");
				if((subparts.length == 2) && subparts[0].trim().equalsIgnoreCase("boundary"))
					boundary = subparts[1];
			}

			if((boundary == null) || (boundary.length() == 0))
				return;
			if(boundary.charAt(0) == '"')
				boundary = boundary.substring(1);
			if(boundary.charAt(boundary.length() - 1) == '"')
				boundary = boundary.substring(0, boundary.length() - 1);

			boundary = "--" + boundary;

			if(logMINOR)
				Logger.minor(this, "Boundary is: " + boundary);

			is = this.data.getInputStream();
			lis = new LineReadingInputStream(is);

			String line;
			line = lis.readLine(100, 100, false); // really it's US-ASCII, but ISO-8859-1 is close enough.
			while((is.available() > 0) && !line.equals(boundary)) {
				line = lis.readLine(100, 100, false);
			}

			boundary = "\r\n" + boundary;

			RandomAccessBucket filedata = null;
			String name = null;
			String filename = null;
			String contentType = null;

			while(is.available() > 0) {
				name = null;
				filename = null;
				contentType = null;
				// chomp headers
				while((line = lis.readLine(200, 200, true)) /* should be UTF-8 as we told the browser to send UTF-8 */ != null) {
					if(line.length() == 0)
						break;

					String[] lineparts = line.split(":");
					if(lineparts == null || lineparts.length == 0)
						continue;
					String hdrname = lineparts[0].trim();

					if(hdrname.equalsIgnoreCase("Content-Disposition")) {
						if(lineparts.length < 2)
							continue;
						String[] valueparts = lineparts[1].split(";");

						for(int i = 0; i < valueparts.length; i++) {
							String[] subparts = valueparts[i].split("=");
							if(subparts.length != 2)
								continue;
							String fieldname = subparts[0].trim();
							String value = subparts[1].trim();
							if(value.startsWith("\"") && value.endsWith("\""))
								value = value.substring(1, value.length() - 1);
							if(fieldname.equalsIgnoreCase("name"))
								name = value;
							else if(fieldname.equalsIgnoreCase("filename"))
								filename = value;
						}
					}
					else if(hdrname.equalsIgnoreCase("Content-Type")) {
						contentType = lineparts[1].trim();
						if(logMINOR)
							Logger.minor(this, "Parsed type: " + contentType);
					}
					else {
					// Do nothing, irrelevant header
					}
				}

				if(name == null)
					continue;

				// we should be at the data now. Start reading it in, checking for the
			// boundary string

				// we can only give an upper bound for the size of the bucket
				filedata = this.bucketfactory.makeBucket(is.available());
				bucketos = filedata.getOutputStream();
				// buffer characters that match the boundary so far
			// FIXME use whatever charset was used
				byte[] bbound = boundary.getBytes("UTF-8"); // ISO-8859-1? boundary should be in US-ASCII
				int offset = 0;
				while((is.available() > 0) && (offset < bbound.length)) {
					byte b = (byte) is.read();

					if(b == bbound[offset])
						offset++;
					else if((b != bbound[offset]) && (offset > 0)) {
						// offset bytes matched, but no more
					// write the bytes that matched, then the non-matching byte
						bucketos.write(bbound, 0, offset);
						offset = 0;
						if(b == bbound[0])
							offset = 1;
						else
							bucketos.write(b);
					}
					else
						bucketos.write(b);
				}

				bucketos.close();
				bucketos = null;
			
				parts.put(name, filedata);
				if(logMINOR)
					Logger.minor(this, "Name = " + name + " length = " + filedata.size() + " filename = " + filename);
				if(filename != null)
					uploadedFiles.put(name, new HTTPUploadedFileImpl(filename, contentType, filedata));
			}
		}
		finally {
			Closer.close(bucketos);
			Closer.close(lis);
			Closer.close(is);
			Closer.close(is);
		}
	}
	
	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#getUploadedFile(java.lang.String)
	 */
	@Override
	public HTTPUploadedFile getUploadedFile(String name) {
		return uploadedFiles.get(name);
	}
	
	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#getPart(java.lang.String)
	 */
	@Override
	public RandomAccessBucket getPart(String name) {
		if(freedParts) throw new IllegalStateException("Already freed");
		return this.parts.get(name);
	}
	
	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#isPartSet(java.lang.String)
	 */
	@Override
	public boolean isPartSet(String name) {
		if(freedParts) throw new IllegalStateException("Already freed");
		if(parts == null)
			return false;

		return this.parts.containsKey(name);
	}

	@Override
	@Deprecated
	public String getPartAsString(String name, int maxlength) {
		try {
			return new String(getPartAsBytes(name, maxlength), "UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new Error("Impossible: JVM doesn't support UTF-8: " + e, e);
		}
	}
	
	@Override
	public String getPartAsStringThrowing(String name, int maxLength) throws NoSuchElementException, SizeLimitExceededException {
		if(freedParts) throw new IllegalStateException("Already freed");
		Bucket part = this.parts.get(name);
		
		if(part == null)
			throw new NoSuchElementException(name);
		
		if(part.size() > maxLength)
			throw new SizeLimitExceededException();
		
		return getPartAsLimitedString(part, maxLength);
	}
	
	@Override
	public String getPartAsStringFailsafe(String name, int maxLength) {
		if(freedParts) throw new IllegalStateException("Already freed");
		Bucket part = this.parts.get(name);
		return part == null ? "" : getPartAsLimitedString(part, maxLength);
	}
	
	private String getPartAsLimitedString(Bucket part, int maxLength) {
		try {
			return new String(getPartAsLimitedBytes(part, maxLength), "UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new Error("Impossible: JVM doesn't support UTF-8: " + e, e);
		}
	}

	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#getPartAsString(java.lang.String, int)
	 */
	@Override
	@Deprecated
	public byte[] getPartAsBytes(String name, int maxlength) {
		if(freedParts) throw new IllegalStateException("Already freed");
		Bucket part = this.parts.get(name);
		if(part == null) return new byte[0];
		
		if (part.size() > maxlength) return new byte[0];
		
		InputStream is = null;
		DataInputStream dis = null;
		try {
			is = part.getInputStream();
			dis = new DataInputStream(is);
			byte[] buf = new byte[(int)Math.min(part.size(), maxlength)];
			dis.readFully(buf);
			return buf;
		} catch (IOException ioe) {
	         Logger.error(this, "Caught IOE:" + ioe.getMessage());
		} finally {
			Closer.close(dis);
			if(dis == null) Closer.close(is); // DataInputStream.close() does this for us normally
		}
		
		return new byte[0];
	}
	
	@Override
	public byte[] getPartAsBytesThrowing(String name, int maxLength) throws NoSuchElementException, SizeLimitExceededException {
		if(freedParts) throw new IllegalStateException("Already freed");
		Bucket part = this.parts.get(name);
		
		if(part == null)
			throw new NoSuchElementException(name);
		
		if(part.size() > maxLength)
			throw new SizeLimitExceededException();
		
		return getPartAsLimitedBytes(part, maxLength);
	}
	
	@Override
	public byte[] getPartAsBytesFailsafe(String name, int maxLength) {
		if(freedParts) throw new IllegalStateException("Already freed");
		Bucket part = this.parts.get(name);
		return part == null ? new byte[0] : getPartAsLimitedBytes(part, maxLength);
	}
	
	private byte[] getPartAsLimitedBytes(Bucket part, int maxLength) {
		InputStream is = null;
		DataInputStream dis = null;
		try {
			is = part.getInputStream();
			dis = new DataInputStream(is);
			byte[] buf = new byte[(int)Math.min(part.size(), maxLength)];
			dis.readFully(buf, 0, buf.length);
			return buf;
		} catch (IOException ioe) {
	         Logger.error(this, "Caught IOE:" + ioe.getMessage());
	         return new byte[0];
		} finally {
			Closer.close(dis);
			if(dis == null) Closer.close(is); // DataInputStream.close() does this for us normally
		}
	}
	
	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#freeParts()
	 */
	@Override
	public void freeParts() {
		if (this.parts == null) return;
		
		for (Bucket b : this.parts.values()) {
			b.free();
		}
		parts.clear();
		freedParts = true;
		// Do not free data. Caller is responsible for that.
	}

	/* (non-Javadoc)
	 * @see freenet.clients.http.HTTPRequest#getLongParam(java.lang.String, long)
	 */
	@Override
	public long getLongParam(String name, long defaultValue) {
		if (!this.isParameterSet(name)) {
			return defaultValue;
		}
		String value = this.getParameterValue(name);
		try {
			return Fields.parseLong(value);
		} catch (NumberFormatException e) {
			return defaultValue;
		}
	}

	/**
	 * Container for uploaded files in HTTP POST requests.
	 * 
	 * @author David 'Bombe' Roden &lt;bombe@freenetproject.org&gt;
	 * @version $Id$
	 */
	public static class HTTPUploadedFileImpl implements HTTPUploadedFile {

		/** The filename. */
		private final String filename;

		/** The content type. */
		private final String contentType;

		/** The data. */
		private final Bucket data;

		/**
		 * Creates a new file with the specified filename, content type, and
		 * data.
		 * 
		 * @param filename
		 *            The name of the file
		 * @param contentType
		 *            The content type of the file
		 * @param data
		 *            The data of the file
		 */
		public HTTPUploadedFileImpl(String filename, String contentType, Bucket data) {
			this.filename = filename;
			this.contentType = contentType;
			this.data = data;
		}

		/* (non-Javadoc)
		 * @see freenet.clients.http.HTTPUploadedFile#getContentType()
		 */
		@Override
		public String getContentType() {
			return contentType;
		}

		/* (non-Javadoc)
		 * @see freenet.clients.http.HTTPUploadedFile#getData()
		 */
		@Override
		public Bucket getData() {
			return data;
		}

		/* (non-Javadoc)
		 * @see freenet.clients.http.HTTPUploadedFile#getFilename()
		 */
		@Override
		public String getFilename() {
			return filename;
		}

	}

	@Override
	public String getMethod() {
		return method;
	}

	@Override
	public Bucket getRawData() {
		return data;
	}

	@Override
	public String getHeader(String name) {
		assert(name.equals(name.toLowerCase()));
		return this.headers.get(name.toLowerCase());
	}

	@Override
	public int getContentLength() {
		String slen = headers.get("content-length");
		if (slen == null)
			return -1;
		// it is already parsed, so NumberFormatException can not happens here
		return Integer.parseInt(slen);
	}

	@Override
	public String[] getParts() {
		if(freedParts) throw new IllegalStateException("Already freed");
		return parts.keySet().toArray(new String[parts.size()]);
	}

	@Override
	public boolean isIncognito() {
		if(isParameterSet("incognito"))
			return Boolean.valueOf(getParam("incognito"));
		return false;
	}

	@Override
	public boolean isChrome() {
		String ua = getHeader("user-agent");
		if(ua != null) {
			if(ua.contains("Chrome")) return true;
		}
		return false;
	}

}
