view src/org/eclipse/jetty/http/HttpFields.java @ 1009:c3a04bded909

remove AbstractBuffers
author Franklin Schmidt <fschmidt@gmail.com>
date Sun, 23 Oct 2016 21:28:56 -0600
parents fa6158f29c45
children f126d30e04a4
line wrap: on
line source

//
//  ========================================================================
//  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.http;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.BufferCache;
import org.eclipse.jetty.io.BufferCache.CachedBuffer;
import org.eclipse.jetty.io.BufferUtil;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.util.LazyList;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/* ------------------------------------------------------------ */
/**
 * HTTP Fields. A collection of HTTP header and or Trailer fields. 
 * 
 * <p>This class is not synchronized as it is expected that modifications will only be performed by a
 * single thread.
 * 
 * 
 */
public class HttpFields
{
	private static final Logger LOG = LoggerFactory.getLogger(HttpFields.class);
	
	/* ------------------------------------------------------------ */
	public static final String __COOKIE_DELIM="\"\\\n\r\t\f\b%+ ;=";
	public static final TimeZone __GMT = TimeZone.getTimeZone("GMT");
	
	/* ------------------------------------------------------------ */
	private static final String[] DAYS =
	{ "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
	private static final String[] MONTHS =
	{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan"};

	
	/* ------------------------------------------------------------ */
	private static class DateGenerator
	{
		private final StringBuilder buf = new StringBuilder(32);
		private final GregorianCalendar gc = new GregorianCalendar(__GMT);
		
		/**
		 * Format HTTP date "EEE, dd MMM yyyy HH:mm:ss 'GMT'" 
		 */
		public String formatDate(long date)
		{
			buf.setLength(0);
			gc.setTimeInMillis(date);
			
			int day_of_week = gc.get(Calendar.DAY_OF_WEEK);
			int day_of_month = gc.get(Calendar.DAY_OF_MONTH);
			int month = gc.get(Calendar.MONTH);
			int year = gc.get(Calendar.YEAR);
			int century = year / 100;
			year = year % 100;
			
			int hours = gc.get(Calendar.HOUR_OF_DAY);
			int minutes = gc.get(Calendar.MINUTE);
			int seconds = gc.get(Calendar.SECOND);

			buf.append(DAYS[day_of_week]);
			buf.append(',');
			buf.append(' ');
			StringUtil.append2digits(buf, day_of_month);

			buf.append(' ');
			buf.append(MONTHS[month]);
			buf.append(' ');
			StringUtil.append2digits(buf, century);
			StringUtil.append2digits(buf, year);
			
			buf.append(' ');
			StringUtil.append2digits(buf, hours);
			buf.append(':');
			StringUtil.append2digits(buf, minutes);
			buf.append(':');
			StringUtil.append2digits(buf, seconds);
			buf.append(" GMT");
			return buf.toString();
		}

		/* ------------------------------------------------------------ */
		/**
		 * Format "EEE, dd-MMM-yy HH:mm:ss 'GMT'" for cookies
		 */
		public void formatCookieDate(StringBuilder buf, long date)
		{
			gc.setTimeInMillis(date);
			
			int day_of_week = gc.get(Calendar.DAY_OF_WEEK);
			int day_of_month = gc.get(Calendar.DAY_OF_MONTH);
			int month = gc.get(Calendar.MONTH);
			int year = gc.get(Calendar.YEAR);
			year = year % 10000;

			int epoch = (int) ((date / 1000) % (60 * 60 * 24));
			int seconds = epoch % 60;
			epoch = epoch / 60;
			int minutes = epoch % 60;
			int hours = epoch / 60;

			buf.append(DAYS[day_of_week]);
			buf.append(',');
			buf.append(' ');
			StringUtil.append2digits(buf, day_of_month);

			buf.append('-');
			buf.append(MONTHS[month]);
			buf.append('-');
			StringUtil.append2digits(buf, year/100);
			StringUtil.append2digits(buf, year%100);
			
			buf.append(' ');
			StringUtil.append2digits(buf, hours);
			buf.append(':');
			StringUtil.append2digits(buf, minutes);
			buf.append(':');
			StringUtil.append2digits(buf, seconds);
			buf.append(" GMT");
		}
	}

	/* ------------------------------------------------------------ */
	private static final ThreadLocal<DateGenerator> __dateGenerator =new ThreadLocal<DateGenerator>()
	{
		@Override
		protected DateGenerator initialValue()
		{
			return new DateGenerator();
		}
	};
	
	/* ------------------------------------------------------------ */
	/**
	 * Format HTTP date "EEE, dd MMM yyyy HH:mm:ss 'GMT'" 
	 */
	public static String formatDate(long date)
	{
		return __dateGenerator.get().formatDate(date);
	}

	/* ------------------------------------------------------------ */
	/**
	 * Format "EEE, dd-MMM-yyyy HH:mm:ss 'GMT'" for cookies
	 */
	public static void formatCookieDate(StringBuilder buf, long date)
	{
		__dateGenerator.get().formatCookieDate(buf,date);
	}
	
	/* ------------------------------------------------------------ */
	/**
	 * Format "EEE, dd-MMM-yyyy HH:mm:ss 'GMT'" for cookies
	 */
	public static String formatCookieDate(long date)
	{
		StringBuilder buf = new StringBuilder(28);
		formatCookieDate(buf, date);
		return buf.toString();
	}

	/* ------------------------------------------------------------ */
	private final static String __dateReceiveFmt[] =
	{   
		"EEE, dd MMM yyyy HH:mm:ss zzz", 
		"EEE, dd-MMM-yy HH:mm:ss",
		"EEE MMM dd HH:mm:ss yyyy",

		"EEE, dd MMM yyyy HH:mm:ss", "EEE dd MMM yyyy HH:mm:ss zzz", 
		"EEE dd MMM yyyy HH:mm:ss", "EEE MMM dd yyyy HH:mm:ss zzz", "EEE MMM dd yyyy HH:mm:ss", 
		"EEE MMM-dd-yyyy HH:mm:ss zzz", "EEE MMM-dd-yyyy HH:mm:ss", "dd MMM yyyy HH:mm:ss zzz", 
		"dd MMM yyyy HH:mm:ss", "dd-MMM-yy HH:mm:ss zzz", "dd-MMM-yy HH:mm:ss", "MMM dd HH:mm:ss yyyy zzz", 
		"MMM dd HH:mm:ss yyyy", "EEE MMM dd HH:mm:ss yyyy zzz",  
		"EEE, MMM dd HH:mm:ss yyyy zzz", "EEE, MMM dd HH:mm:ss yyyy", "EEE, dd-MMM-yy HH:mm:ss zzz", 
		"EEE dd-MMM-yy HH:mm:ss zzz", "EEE dd-MMM-yy HH:mm:ss",
	};

	/* ------------------------------------------------------------ */
	private static class DateParser
	{
		final SimpleDateFormat _dateReceive[]= new SimpleDateFormat[__dateReceiveFmt.length];
 
		long parse(final String dateVal)
		{
			for (int i = 0; i < _dateReceive.length; i++)
			{
				if (_dateReceive[i] == null)
				{
					_dateReceive[i] = new SimpleDateFormat(__dateReceiveFmt[i], Locale.US);
					_dateReceive[i].setTimeZone(__GMT);
				}

				try
				{
					Date date = (Date) _dateReceive[i].parseObject(dateVal);
					return date.getTime();
				}
				catch (java.lang.Exception e)
				{
					// LOG.ignore(e);
				}
			}
			
			if (dateVal.endsWith(" GMT"))
			{
				final String val = dateVal.substring(0, dateVal.length() - 4);

				for (int i = 0; i < _dateReceive.length; i++)
				{
					try
					{
						Date date = (Date) _dateReceive[i].parseObject(val);
						return date.getTime();
					}
					catch (java.lang.Exception e)
					{
						// LOG.ignore(e);
					}
				}
			}    
			return -1;
		}
	}

	/* ------------------------------------------------------------ */
	public static long parseDate(String date)
	{
		return __dateParser.get().parse(date);
	}

	/* ------------------------------------------------------------ */
	private static final ThreadLocal<DateParser> __dateParser =new ThreadLocal<DateParser>()
	{
		@Override
		protected DateParser initialValue()
		{
			return new DateParser();
		}
	};

	/* -------------------------------------------------------------- */
	public final static String __01Jan1970=formatDate(0);
	public final static Buffer __01Jan1970_BUFFER=new ByteArrayBuffer(__01Jan1970);
	public final static String __01Jan1970_COOKIE = formatCookieDate(0).trim();

	/* -------------------------------------------------------------- */
	private final ArrayList<Field> _fields = new ArrayList<Field>(20);
	private final HashMap<Buffer,Field> _names = new HashMap<Buffer,Field>(32);
	
	/* ------------------------------------------------------------ */
	/**
	 * Constructor.
	 */
	public HttpFields()
	{
	}

	// TODO externalize this cache so it can be configurable
	private static ConcurrentMap<String, Buffer> __cache = new ConcurrentHashMap<String, Buffer>();
	private static int __cacheSize = Integer.getInteger("org.eclipse.jetty.http.HttpFields.CACHE",2000);
	
	/* -------------------------------------------------------------- */
	private Buffer convertValue(String value)
	{
		Buffer buffer = __cache.get(value);
		if (buffer!=null)
			return buffer;
		
		try
		{   
			buffer = new ByteArrayBuffer(value,StringUtil.__ISO_8859_1);
			
			if (__cacheSize>0)
			{
				if (__cache.size()>__cacheSize)
					__cache.clear();
				Buffer b=__cache.putIfAbsent(value,buffer);
				if (b!=null)
					buffer=b;
			}
			
			return buffer;
		}
		catch (UnsupportedEncodingException e)
		{
			throw new RuntimeException(e);
		}
	}
	
	/* -------------------------------------------------------------- */
	/**
	 * Get Collection of header names. 
	 */
	public Collection<String> getFieldNamesCollection()
	{
		final List<String> list = new ArrayList<String>(_fields.size());

	for (Field f : _fields)
	{
		if (f!=null)
			list.add(BufferUtil.to8859_1_String(f._name));
	}
	return list;
	}
	
	/* -------------------------------------------------------------- */
	/**
	 * Get enumeration of header _names. Returns an enumeration of strings representing the header
	 * _names for this request.
	 */
	public Enumeration<String> getFieldNames()
	{
		final Enumeration<?> buffers = Collections.enumeration(_names.keySet());
		return new Enumeration<String>()
		{
			public String nextElement()
			{
				return buffers.nextElement().toString();
			}
			
			public boolean hasMoreElements()
			{
				return buffers.hasMoreElements();
			}
		}; 
	}
	
	/* ------------------------------------------------------------ */
	public int size()
	{
		return _fields.size();
	}
	
	/* ------------------------------------------------------------ */
	/**
	 * Get a Field by index.
	 * @return A Field value or null if the Field value has not been set
	 * 
	 */
	public Field getField(int i)
	{
		return _fields.get(i);
	}

	/* ------------------------------------------------------------ */
	private Field getField(String name)
	{
		return _names.get(HttpHeaders.CACHE.lookup(name));
	}

	/* ------------------------------------------------------------ */
	private Field getField(Buffer name)
	{
		return _names.get(HttpHeaders.CACHE.lookup(name));
	}

	/* ------------------------------------------------------------ */
	public boolean containsKey(Buffer name)
	{
		return _names.containsKey(HttpHeaders.CACHE.lookup(name));
	}

	/* ------------------------------------------------------------ */
	public boolean containsKey(String name)
	{
		return _names.containsKey(HttpHeaders.CACHE.lookup(name));
	}

	/* -------------------------------------------------------------- */
	/**
	 * @return the value of a field, or null if not found. For multiple fields of the same name,
	 *         only the first is returned.
	 * @param name the case-insensitive field name
	 */
	public String getStringField(String name)
	{
		Field field = getField(name);
		return field==null?null:field.getValue();
	}

	/* -------------------------------------------------------------- */
	/**
	 * @return the value of a field, or null if not found. For multiple fields of the same name,
	 *         only the first is returned.
	 * @param name the case-insensitive field name
	 */
	public String getStringField(Buffer name)
	{
		Field field = getField(name);
		return field==null?null:field.getValue();
	}

	/* -------------------------------------------------------------- */
	/**
	 * @return the value of a field, or null if not found. For multiple fields of the same name,
	 *         only the first is returned.
	 * @param name the case-insensitive field name
	 */
	public Buffer get(Buffer name)
	{
		Field field = getField(name);
		return field==null?null:field._value;
	}


	/* -------------------------------------------------------------- */
	/**
	 * Get multi headers
	 * 
	 * @return Enumeration of the values, or null if no such header.
	 * @param name the case-insensitive field name
	 */
	public Collection<String> getValuesCollection(String name)
	{
		Field field = getField(name);
	if (field==null)
		return null;

		final List<String> list = new ArrayList<String>();

	while(field!=null)
	{
		list.add(field.getValue());
		field=field._next;
	}
	return list;
	}

	/* -------------------------------------------------------------- */
	/**
	 * Get multi headers
	 * 
	 * @return Enumeration of the values
	 * @param name the case-insensitive field name
	 */
	public Enumeration<String> getValues(String name)
	{
		final Field field = getField(name);
		if (field == null) 
		{
			List<String> empty=Collections.emptyList();
			return Collections.enumeration(empty);
		}

		return new Enumeration<String>()
		{
			Field f = field;

			public boolean hasMoreElements()
			{
				return f != null;
			}

			public String nextElement() throws NoSuchElementException
			{
				if (f == null) throw new NoSuchElementException();
				Field n = f;
				f = f._next;
				return n.getValue();
			}
		};
	}

	/* -------------------------------------------------------------- */
	/**
	 * Get multi headers
	 * 
	 * @return Enumeration of the value Strings
	 * @param name the case-insensitive field name
	 */
	public Enumeration<String> getValues(Buffer name)
	{
		final Field field = getField(name);
		if (field == null) 
		{
			List<String> empty=Collections.emptyList();
			return Collections.enumeration(empty);
		}

		return new Enumeration<String>()
		{
			Field f = field;

			public boolean hasMoreElements()
			{
				return f != null;
			}

			public String nextElement() throws NoSuchElementException
			{
				if (f == null) throw new NoSuchElementException();
				Field n = f;
				f = f._next;
				return n.getValue();
			}
		};
	}

	/* -------------------------------------------------------------- */
	/**
	 * Get multi field values with separator. The multiple values can be represented as separate
	 * headers of the same name, or by a single header using the separator(s), or a combination of
	 * both. Separators may be quoted.
	 * 
	 * @param name the case-insensitive field name
	 * @param separators String of separators.
	 * @return Enumeration of the values, or null if no such header.
	 */
	public Enumeration<String> getValues(String name, final String separators)
	{
		final Enumeration<String> e = getValues(name);
		if (e == null) 
			return null;
		return new Enumeration<String>()
		{
			QuotedStringTokenizer tok = null;

			public boolean hasMoreElements()
			{
				if (tok != null && tok.hasMoreElements()) return true;
				while (e.hasMoreElements())
				{
					String value = e.nextElement();
					tok = new QuotedStringTokenizer(value, separators, false, false);
					if (tok.hasMoreElements()) return true;
				}
				tok = null;
				return false;
			}

			public String nextElement() throws NoSuchElementException
			{
				if (!hasMoreElements()) throw new NoSuchElementException();
				String next = (String) tok.nextElement();
				if (next != null) next = next.trim();
				return next;
			}
		};
	}

	
	/* -------------------------------------------------------------- */
	/**
	 * Set a field.
	 * 
	 * @param name the name of the field
	 * @param value the value of the field. If null the field is cleared.
	 */
	public void put(String name, String value)
	{
		if (value==null)
			remove(name);
		else
		{
			Buffer n = HttpHeaders.CACHE.lookup(name);
			Buffer v = convertValue(value);
			put(n, v);
		}
	}

	/* -------------------------------------------------------------- */
	/**
	 * Set a field.
	 * 
	 * @param name the name of the field
	 * @param value the value of the field. If null the field is cleared.
	 */
	public void put(Buffer name, String value)
	{
		Buffer n = HttpHeaders.CACHE.lookup(name);
		Buffer v = convertValue(value);
		put(n, v);
	}

	/* -------------------------------------------------------------- */
	/**
	 * Set a field.
	 * 
	 * @param name the name of the field
	 * @param value the value of the field. If null the field is cleared.
	 */
	public void put(Buffer name, Buffer value)
	{
		remove(name);
		if (value == null)
			return;

		if (!(name instanceof BufferCache.CachedBuffer)) 
			name = HttpHeaders.CACHE.lookup(name);
		if (!(value instanceof CachedBuffer))
			value= HttpHeaderValues.CACHE.lookup(value).asImmutableBuffer();
		
		// new value;
		Field field = new Field(name, value);
		_fields.add(field);
		_names.put(name, field);
	}

	/* -------------------------------------------------------------- */
	/**
	 * Set a field.
	 * 
	 * @param name the name of the field
	 * @param list the List value of the field. If null the field is cleared.
	 */
	public void put(String name, List<?> list)
	{
		if (list == null || list.size() == 0)
		{
			remove(name);
			return;
		}
		Buffer n = HttpHeaders.CACHE.lookup(name);

		Object v = list.get(0);
		if (v != null)
			put(n, HttpHeaderValues.CACHE.lookup(v.toString()));
		else
			remove(n);

		if (list.size() > 1)
		{
			java.util.Iterator<?> iter = list.iterator();
			iter.next();
			while (iter.hasNext())
			{
				v = iter.next();
				if (v != null) put(n, HttpHeaderValues.CACHE.lookup(v.toString()));
			}
		}
	}

	/* -------------------------------------------------------------- */
	/**
	 * Add to or set a field. If the field is allowed to have multiple values, add will add multiple
	 * headers of the same name.
	 * 
	 * @param name the name of the field
	 * @param value the value of the field.
	 * @exception IllegalArgumentException If the name is a single valued field and already has a
	 *                value.
	 */
	public void add(String name, String value) throws IllegalArgumentException
	{
		if (value==null)
			return;
		Buffer n = HttpHeaders.CACHE.lookup(name);
		Buffer v = convertValue(value);
		add(n, v);
	}

	/* -------------------------------------------------------------- */
	/**
	 * Add to or set a field. If the field is allowed to have multiple values, add will add multiple
	 * headers of the same name.
	 * 
	 * @param name the name of the field
	 * @param value the value of the field.
	 * @exception IllegalArgumentException If the name is a single valued field and already has a
	 *                value.
	 */
	public void add(Buffer name, Buffer value) throws IllegalArgumentException
	{   
		if (value == null) throw new IllegalArgumentException("null value");

		if (!(name instanceof CachedBuffer))
			name = HttpHeaders.CACHE.lookup(name);
		name=name.asImmutableBuffer();
		
		if (!(value instanceof CachedBuffer) && HttpHeaderValues.hasKnownValues(HttpHeaders.CACHE.getOrdinal(name)))
			value= HttpHeaderValues.CACHE.lookup(value);
		value=value.asImmutableBuffer();
		
		Field field = _names.get(name);
		Field last = null;
		while (field != null)
		{
			last = field;
			field = field._next;
		}

		// create the field
		field = new Field(name, value);
		_fields.add(field);

		// look for chain to add too
		if (last != null)
			last._next = field;
		else
			_names.put(name, field);
	}

	/* ------------------------------------------------------------ */
	/**
	 * Remove a field.
	 * 
	 * @param name
	 */
	public void remove(String name)
	{
		remove(HttpHeaders.CACHE.lookup(name));
	}

	/* ------------------------------------------------------------ */
	/**
	 * Remove a field.
	 * 
	 * @param name
	 */
	public void remove(Buffer name)
	{
		if (!(name instanceof BufferCache.CachedBuffer)) 
			name = HttpHeaders.CACHE.lookup(name);
		Field field = _names.remove(name);
		while (field != null)
		{
			_fields.remove(field);
			field = field._next;
		}
	}

	/* -------------------------------------------------------------- */
	/**
	 * Get a header as an long value. Returns the value of an integer field or -1 if not found. The
	 * case of the field name is ignored.
	 * 
	 * @param name the case-insensitive field name
	 * @exception NumberFormatException If bad long found
	 */
	public long getLongField(String name) throws NumberFormatException
	{
		Field field = getField(name);
		return field==null?-1L:field.getLongValue();
	}

	/* -------------------------------------------------------------- */
	/**
	 * Get a header as an long value. Returns the value of an integer field or -1 if not found. The
	 * case of the field name is ignored.
	 * 
	 * @param name the case-insensitive field name
	 * @exception NumberFormatException If bad long found
	 */
	public long getLongField(Buffer name) throws NumberFormatException
	{
		Field field = getField(name);
		return field==null?-1L:field.getLongValue();
	}

	/* -------------------------------------------------------------- */
	/**
	 * Get a header as a date value. Returns the value of a date field, or -1 if not found. The case
	 * of the field name is ignored.
	 * 
	 * @param name the case-insensitive field name
	 */
	public long getDateField(String name)
	{
		Field field = getField(name);
		if (field == null) 
			return -1;

		String val = valueParameters(BufferUtil.to8859_1_String(field._value), null);
		if (val == null) 
			return -1;

		final long date = __dateParser.get().parse(val);
		if (date==-1)
			throw new IllegalArgumentException("Cannot convert date: " + val);
		return date;
	}

	/* -------------------------------------------------------------- */
	/**
	 * Sets the value of an long field.
	 * 
	 * @param name the field name
	 * @param value the field long value
	 */
	public void putLongField(Buffer name, long value)
	{
		Buffer v = BufferUtil.toBuffer(value);
		put(name, v);
	}

	/* -------------------------------------------------------------- */
	/**
	 * Sets the value of an long field.
	 * 
	 * @param name the field name
	 * @param value the field long value
	 */
	public void putLongField(String name, long value)
	{
		Buffer n = HttpHeaders.CACHE.lookup(name);
		Buffer v = BufferUtil.toBuffer(value);
		put(n, v);
	}

	/* -------------------------------------------------------------- */
	/**
	 * Sets the value of an long field.
	 * 
	 * @param name the field name
	 * @param value the field long value
	 */
	public void addLongField(String name, long value)
	{
		Buffer n = HttpHeaders.CACHE.lookup(name);
		Buffer v = BufferUtil.toBuffer(value);
		add(n, v);
	}

	/* -------------------------------------------------------------- */
	/**
	 * Sets the value of an long field.
	 * 
	 * @param name the field name
	 * @param value the field long value
	 */
	public void addLongField(Buffer name, long value)
	{
		Buffer v = BufferUtil.toBuffer(value);
		add(name, v);
	}

	/* -------------------------------------------------------------- */
	/**
	 * Sets the value of a date field.
	 * 
	 * @param name the field name
	 * @param date the field date value
	 */
	public void putDateField(Buffer name, long date)
	{
		String d=formatDate(date);
		Buffer v = new ByteArrayBuffer(d);
		put(name, v);
	}

	/* -------------------------------------------------------------- */
	/**
	 * Sets the value of a date field.
	 * 
	 * @param name the field name
	 * @param date the field date value
	 */
	public void putDateField(String name, long date)
	{
		Buffer n = HttpHeaders.CACHE.lookup(name);
		putDateField(n,date);
	}

	/* -------------------------------------------------------------- */
	/**
	 * Sets the value of a date field.
	 * 
	 * @param name the field name
	 * @param date the field date value
	 */
	public void addDateField(String name, long date)
	{
		String d=formatDate(date);
		Buffer n = HttpHeaders.CACHE.lookup(name);
		Buffer v = new ByteArrayBuffer(d);
		add(n, v);
	}

	/* ------------------------------------------------------------ */
	/**
	 * Format a set cookie value
	 * 
	 * @param cookie The cookie.
	 */
	public void addSetCookie(HttpCookie cookie)
	{
		addSetCookie(
				cookie.getName(),
				cookie.getValue(),
				cookie.getDomain(),
				cookie.getPath(),
				cookie.getMaxAge(),
				cookie.getComment(),
				cookie.isSecure(),
				cookie.isHttpOnly(),
				cookie.getVersion());
	}

	/**
	 * Format a set cookie value
	 * 
	 * @param name the name
	 * @param value the value
	 * @param domain the domain
	 * @param path the path
	 * @param maxAge the maximum age
	 * @param comment the comment (only present on versions > 0)
	 * @param isSecure true if secure cookie
	 * @param isHttpOnly true if for http only
	 * @param version version of cookie logic to use (0 == default behavior)
	 */
	public void addSetCookie(
			final String name, 
			final String value, 
			final String domain,
			final String path, 
			final long maxAge,
			final String comment, 
			final boolean isSecure,
			final boolean isHttpOnly, 
			int version)
	{
		String delim=__COOKIE_DELIM;
		
		// Check arguments
		if (name == null || name.length() == 0) 
			throw new IllegalArgumentException("Bad cookie name");

		// Format value and params
		StringBuilder buf = new StringBuilder(128);
		String name_value_params;
		QuotedStringTokenizer.quoteIfNeeded(buf, name, delim);
		buf.append('=');
		String start=buf.toString();
		boolean hasDomain = false;
		boolean hasPath = false;
		
		if (value != null && value.length() > 0)
			QuotedStringTokenizer.quoteIfNeeded(buf, value, delim);        

		if (comment != null && comment.length() > 0)
		{
			buf.append(";Comment=");
			QuotedStringTokenizer.quoteIfNeeded(buf, comment, delim);
		}

		if (path != null && path.length() > 0)
		{
			hasPath = true;
			buf.append(";Path=");
			if (path.trim().startsWith("\""))
				buf.append(path);
			else
				QuotedStringTokenizer.quoteIfNeeded(buf,path,delim);
		}
		if (domain != null && domain.length() > 0)
		{
			hasDomain = true;
			buf.append(";Domain=");
			QuotedStringTokenizer.quoteIfNeeded(buf,domain.toLowerCase(Locale.ENGLISH),delim);
		}

		if (maxAge >= 0)
		{
			// Always add the expires param as some browsers still don't handle max-age
			buf.append(";Expires=");
			if (maxAge == 0)
				buf.append(__01Jan1970_COOKIE);
			else
				formatCookieDate(buf, System.currentTimeMillis() + 1000L * maxAge);

			if (version >0)
			{
				buf.append(";Max-Age=");
				buf.append(maxAge);
			}
		}

		if (isSecure)
			buf.append(";Secure");
		if (isHttpOnly) 
			buf.append(";HttpOnly");

		name_value_params = buf.toString();
		
		// remove existing set-cookie of same name
		Field field = getField(HttpHeaders.SET_COOKIE);
		Field last=null;
		while (field!=null)
		{
			String val = (field._value == null ? null : field._value.toString());
			if (val!=null && val.startsWith(start))
			{
				//existing cookie has same name, does it also match domain and path?
				if (((!hasDomain && !val.contains("Domain")) || (hasDomain && val.contains("Domain="+domain))) &&
					((!hasPath && !val.contains("Path")) || (hasPath && val.contains("Path="+path))))
				{
					_fields.remove(field);
					if (last==null)
						_names.put(HttpHeaders.SET_COOKIE_BUFFER,field._next);
					else
						last._next=field._next;
					break;
				}
			}
			last=field;
			field=field._next;
		}

		add(HttpHeaders.SET_COOKIE_BUFFER, new ByteArrayBuffer(name_value_params));
		
		// Expire responses with set-cookie headers so they do not get cached.
		put(HttpHeaders.EXPIRES_BUFFER, __01Jan1970_BUFFER);
	}

	/* -------------------------------------------------------------- */
	public void putTo(Buffer buffer) throws IOException
	{
		for (int i = 0; i < _fields.size(); i++)
		{
			Field field = _fields.get(i);
			if (field != null) 
				field.putTo(buffer);
		}
		BufferUtil.putCRLF(buffer);
	}

	/* -------------------------------------------------------------- */
	public String toString()
	{
		try
		{
			StringBuffer buffer = new StringBuffer();
			for (int i = 0; i < _fields.size(); i++)
			{
				Field field = (Field) _fields.get(i);
				if (field != null)
				{
					String tmp = field.getName();
					if (tmp != null) buffer.append(tmp);
					buffer.append(": ");
					tmp = field.getValue();
					if (tmp != null) buffer.append(tmp);
					buffer.append("\r\n");
				}
			}
			buffer.append("\r\n");
			return buffer.toString();
		}
		catch (Exception e)
		{
			LOG.warn("",e);
			return e.toString();
		}
	}

	/* ------------------------------------------------------------ */
	/**
	 * Clear the header.
	 */
	public void clear()
	{
		_fields.clear();
		_names.clear();
	}

	/* ------------------------------------------------------------ */
	/**
	 * Add fields from another HttpFields instance. Single valued fields are replaced, while all
	 * others are added.
	 * 
	 * @param fields
	 */
	public void add(HttpFields fields)
	{
		if (fields == null) return;

		Enumeration e = fields.getFieldNames();
		while (e.hasMoreElements())
		{
			String name = (String) e.nextElement();
			Enumeration values = fields.getValues(name);
			while (values.hasMoreElements())
				add(name, (String) values.nextElement());
		}
	}

	/* ------------------------------------------------------------ */
	/**
	 * Get field value parameters. Some field values can have parameters. This method separates the
	 * value from the parameters and optionally populates a map with the parameters. For example:
	 * 
	 * <PRE>
	 * 
	 * FieldName : Value ; param1=val1 ; param2=val2
	 * 
	 * </PRE>
	 * 
	 * @param value The Field value, possibly with parameteres.
	 * @param parameters A map to populate with the parameters, or null
	 * @return The value.
	 */
	public static String valueParameters(String value, Map<String,String> parameters)
	{
		if (value == null) return null;

		int i = value.indexOf(';');
		if (i < 0) return value;
		if (parameters == null) return value.substring(0, i).trim();

		StringTokenizer tok1 = new QuotedStringTokenizer(value.substring(i), ";", false, true);
		while (tok1.hasMoreTokens())
		{
			String token = tok1.nextToken();
			StringTokenizer tok2 = new QuotedStringTokenizer(token, "= ");
			if (tok2.hasMoreTokens())
			{
				String paramName = tok2.nextToken();
				String paramVal = null;
				if (tok2.hasMoreTokens()) paramVal = tok2.nextToken();
				parameters.put(paramName, paramVal);
			}
		}

		return value.substring(0, i).trim();
	}

	/* ------------------------------------------------------------ */
	private static final Float __zero = Float.valueOf("0.0");

	/* ------------------------------------------------------------ */
	public static float getQuality(String value)
	{
		if (value == null) return 0f;

        if( value.indexOf(";") == -1 )
			return 1f;

		Map params = new HashMap();
		valueParameters(value, params);
		String qs = (String) params.get("q");
		try {
			return Float.parseFloat(qs);
		} catch (NumberFormatException e) {
			return 1f;
		}
	}

	/* ------------------------------------------------------------ */
	/**
	 * List values in quality order.
	 * 
	 * @param e Enumeration of values with quality parameters
	 * @return values in quality order.
	 */
	public static List qualityList(Enumeration e)
	{
		if (e == null || !e.hasMoreElements()) return Collections.EMPTY_LIST;

		Object list = null;
		Object qual = null;

		// Assume list will be well ordered and just add nonzero
		while (e.hasMoreElements())
		{
			String v = e.nextElement().toString();
			Float q = getQuality(v);

			if (q.floatValue() >= 0.001)
			{
				list = LazyList.add(list, v);
				qual = LazyList.add(qual, q);
			}
		}

		List vl = LazyList.getList(list, false);
		if (vl.size() < 2) return vl;

		List ql = LazyList.getList(qual, false);

		// sort list with swaps
		Float last = __zero;
		for (int i = vl.size(); i-- > 0;)
		{
			Float q = (Float) ql.get(i);
			if (last.compareTo(q) > 0)
			{
				Object tmp = vl.get(i);
				vl.set(i, vl.get(i + 1));
				vl.set(i + 1, tmp);
				ql.set(i, ql.get(i + 1));
				ql.set(i + 1, q);
				last = __zero;
				i = vl.size();
				continue;
			}
			last = q;
		}
		ql.clear();
		return vl;
	}

	/* ------------------------------------------------------------ */
	/* ------------------------------------------------------------ */
	/* ------------------------------------------------------------ */
	public static final class Field
	{
		private Buffer _name;
		private Buffer _value;
		private Field _next;

		/* ------------------------------------------------------------ */
		private Field(Buffer name, Buffer value)
		{
			_name = name;
			_value = value;
			_next = null;
		}
		
		/* ------------------------------------------------------------ */
		public void putTo(Buffer buffer) throws IOException
		{
			int o=(_name instanceof CachedBuffer)?((CachedBuffer)_name).getOrdinal():-1;
			if (o>=0)
				buffer.put(_name);
			else
			{
				int s=_name.getIndex();
				int e=_name.putIndex();
				while (s<e)
				{
					byte b=_name.peek(s++);
					switch(b)
					{
						case '\r':
						case '\n':
						case ':' :
							continue;
						default:
							buffer.put(b);
					}
				}
			}
			
			buffer.put((byte) ':');
			buffer.put((byte) ' ');
			
			o=(_value instanceof CachedBuffer)?((CachedBuffer)_value).getOrdinal():-1;
			if (o>=0)
				buffer.put(_value);
			else
			{
				int s=_value.getIndex();
				int e=_value.putIndex();
				while (s<e)
				{
					byte b=_value.peek(s++);
					switch(b)
					{
						case '\r':
						case '\n':
							continue;
						default:
							buffer.put(b);
					}
				}
			}

			BufferUtil.putCRLF(buffer);
		}

		/* ------------------------------------------------------------ */
		public String getName()
		{
			return BufferUtil.to8859_1_String(_name);
		}

		/* ------------------------------------------------------------ */
		Buffer getNameBuffer()
		{
			return _name;
		}

		/* ------------------------------------------------------------ */
		public int getNameOrdinal()
		{
			return HttpHeaders.CACHE.getOrdinal(_name);
		}

		/* ------------------------------------------------------------ */
		public String getValue()
		{
			return BufferUtil.to8859_1_String(_value);
		}

		/* ------------------------------------------------------------ */
		public Buffer getValueBuffer()
		{
			return _value;
		}

		/* ------------------------------------------------------------ */
		public int getValueOrdinal()
		{
			return HttpHeaderValues.CACHE.getOrdinal(_value);
		}

		/* ------------------------------------------------------------ */
		public int getIntValue()
		{
			return (int) getLongValue();
		}

		/* ------------------------------------------------------------ */
		public long getLongValue()
		{
			return BufferUtil.toLong(_value);
		}

		/* ------------------------------------------------------------ */
		public String toString()
		{
			return ("[" + getName() + "=" + _value + (_next == null ? "" : "->") + "]");
		}
	}
}