View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.http;
20  
21  import static org.eclipse.jetty.util.QuotedStringTokenizer.isQuoted;
22  import static org.eclipse.jetty.util.QuotedStringTokenizer.quoteOnly;
23  
24  import java.io.IOException;
25  import java.nio.ByteBuffer;
26  import java.text.SimpleDateFormat;
27  import java.util.ArrayList;
28  import java.util.Calendar;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.Date;
32  import java.util.Enumeration;
33  import java.util.GregorianCalendar;
34  import java.util.HashMap;
35  import java.util.HashSet;
36  import java.util.Iterator;
37  import java.util.List;
38  import java.util.Locale;
39  import java.util.Map;
40  import java.util.NoSuchElementException;
41  import java.util.Set;
42  import java.util.StringTokenizer;
43  import java.util.TimeZone;
44  
45  import org.eclipse.jetty.util.ArrayTernaryTrie;
46  import org.eclipse.jetty.util.BufferUtil;
47  import org.eclipse.jetty.util.DateCache;
48  import org.eclipse.jetty.util.LazyList;
49  import org.eclipse.jetty.util.QuotedStringTokenizer;
50  import org.eclipse.jetty.util.StringMap;
51  import org.eclipse.jetty.util.StringUtil;
52  import org.eclipse.jetty.util.Trie;
53  import org.eclipse.jetty.util.log.Log;
54  import org.eclipse.jetty.util.log.Logger;
55  
56  
57  /**
58   * HTTP Fields. A collection of HTTP header and or Trailer fields.
59   *
60   * <p>This class is not synchronised as it is expected that modifications will only be performed by a
61   * single thread.
62   * 
63   * <p>The cookie handling provided by this class is guided by the Servlet specification and RFC6265.
64   *
65   */
66  public class HttpFields implements Iterable<HttpField>
67  {
68      private static final Logger LOG = Log.getLogger(HttpFields.class);
69      public static final TimeZone __GMT = TimeZone.getTimeZone("GMT");
70      public static final DateCache __dateCache = new DateCache("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
71  
72      public static final String __COOKIE_DELIM="\",;\\ \t";
73      
74      static
75      {
76          __GMT.setID("GMT");
77          __dateCache.setTimeZone(__GMT);
78      }
79      
80      public final static String __separators = ", \t";
81  
82      private static final String[] DAYS =
83          { "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
84      private static final String[] MONTHS =
85          { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan"};
86  
87      public static class DateGenerator
88      {
89          private final StringBuilder buf = new StringBuilder(32);
90          private final GregorianCalendar gc = new GregorianCalendar(__GMT);
91  
92          /**
93           * Format HTTP date "EEE, dd MMM yyyy HH:mm:ss 'GMT'"
94           */
95          public String formatDate(long date)
96          {
97              buf.setLength(0);
98              gc.setTimeInMillis(date);
99  
100             int day_of_week = gc.get(Calendar.DAY_OF_WEEK);
101             int day_of_month = gc.get(Calendar.DAY_OF_MONTH);
102             int month = gc.get(Calendar.MONTH);
103             int year = gc.get(Calendar.YEAR);
104             int century = year / 100;
105             year = year % 100;
106 
107             int hours = gc.get(Calendar.HOUR_OF_DAY);
108             int minutes = gc.get(Calendar.MINUTE);
109             int seconds = gc.get(Calendar.SECOND);
110 
111             buf.append(DAYS[day_of_week]);
112             buf.append(',');
113             buf.append(' ');
114             StringUtil.append2digits(buf, day_of_month);
115 
116             buf.append(' ');
117             buf.append(MONTHS[month]);
118             buf.append(' ');
119             StringUtil.append2digits(buf, century);
120             StringUtil.append2digits(buf, year);
121 
122             buf.append(' ');
123             StringUtil.append2digits(buf, hours);
124             buf.append(':');
125             StringUtil.append2digits(buf, minutes);
126             buf.append(':');
127             StringUtil.append2digits(buf, seconds);
128             buf.append(" GMT");
129             return buf.toString();
130         }
131 
132         /**
133          * Format "EEE, dd-MMM-yy HH:mm:ss 'GMT'" for cookies
134          */
135         public void formatCookieDate(StringBuilder buf, long date)
136         {
137             gc.setTimeInMillis(date);
138 
139             int day_of_week = gc.get(Calendar.DAY_OF_WEEK);
140             int day_of_month = gc.get(Calendar.DAY_OF_MONTH);
141             int month = gc.get(Calendar.MONTH);
142             int year = gc.get(Calendar.YEAR);
143             year = year % 10000;
144 
145             int epoch = (int) ((date / 1000) % (60 * 60 * 24));
146             int seconds = epoch % 60;
147             epoch = epoch / 60;
148             int minutes = epoch % 60;
149             int hours = epoch / 60;
150 
151             buf.append(DAYS[day_of_week]);
152             buf.append(',');
153             buf.append(' ');
154             StringUtil.append2digits(buf, day_of_month);
155 
156             buf.append('-');
157             buf.append(MONTHS[month]);
158             buf.append('-');
159             StringUtil.append2digits(buf, year/100);
160             StringUtil.append2digits(buf, year%100);
161 
162             buf.append(' ');
163             StringUtil.append2digits(buf, hours);
164             buf.append(':');
165             StringUtil.append2digits(buf, minutes);
166             buf.append(':');
167             StringUtil.append2digits(buf, seconds);
168             buf.append(" GMT");
169         }
170     }
171 
172     private static final ThreadLocal<DateGenerator> __dateGenerator =new ThreadLocal<DateGenerator>()
173     {
174         @Override
175         protected DateGenerator initialValue()
176         {
177             return new DateGenerator();
178         }
179     };
180 
181     /**
182      * Format HTTP date "EEE, dd MMM yyyy HH:mm:ss 'GMT'"
183      */
184     public static String formatDate(long date)
185     {
186         return __dateGenerator.get().formatDate(date);
187     }
188 
189     /**
190      * Format "EEE, dd-MMM-yyyy HH:mm:ss 'GMT'" for cookies
191      */
192     public static void formatCookieDate(StringBuilder buf, long date)
193     {
194         __dateGenerator.get().formatCookieDate(buf,date);
195     }
196 
197     /**
198      * Format "EEE, dd-MMM-yyyy HH:mm:ss 'GMT'" for cookies
199      */
200     public static String formatCookieDate(long date)
201     {
202         StringBuilder buf = new StringBuilder(28);
203         formatCookieDate(buf, date);
204         return buf.toString();
205     }
206 
207     private final static String __dateReceiveFmt[] =
208         {
209         "EEE, dd MMM yyyy HH:mm:ss zzz",
210         "EEE, dd-MMM-yy HH:mm:ss",
211         "EEE MMM dd HH:mm:ss yyyy",
212 
213         "EEE, dd MMM yyyy HH:mm:ss", "EEE dd MMM yyyy HH:mm:ss zzz",
214         "EEE dd MMM yyyy HH:mm:ss", "EEE MMM dd yyyy HH:mm:ss zzz", "EEE MMM dd yyyy HH:mm:ss",
215         "EEE MMM-dd-yyyy HH:mm:ss zzz", "EEE MMM-dd-yyyy HH:mm:ss", "dd MMM yyyy HH:mm:ss zzz",
216         "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",
217         "MMM dd HH:mm:ss yyyy", "EEE MMM dd HH:mm:ss yyyy zzz",
218         "EEE, MMM dd HH:mm:ss yyyy zzz", "EEE, MMM dd HH:mm:ss yyyy", "EEE, dd-MMM-yy HH:mm:ss zzz",
219         "EEE dd-MMM-yy HH:mm:ss zzz", "EEE dd-MMM-yy HH:mm:ss",
220         };
221 
222     private static class DateParser
223     {
224         final SimpleDateFormat _dateReceive[]= new SimpleDateFormat[__dateReceiveFmt.length];
225 
226         long parse(final String dateVal)
227         {
228             for (int i = 0; i < _dateReceive.length; i++)
229             {
230                 if (_dateReceive[i] == null)
231                 {
232                     _dateReceive[i] = new SimpleDateFormat(__dateReceiveFmt[i], Locale.US);
233                     _dateReceive[i].setTimeZone(__GMT);
234                 }
235 
236                 try
237                 {
238                     Date date = (Date) _dateReceive[i].parseObject(dateVal);
239                     return date.getTime();
240                 }
241                 catch (java.lang.Exception e)
242                 {
243                     // LOG.ignore(e);
244                 }
245             }
246 
247             if (dateVal.endsWith(" GMT"))
248             {
249                 final String val = dateVal.substring(0, dateVal.length() - 4);
250 
251                 for (SimpleDateFormat element : _dateReceive)
252                 {
253                     try
254                     {
255                         Date date = (Date) element.parseObject(val);
256                         return date.getTime();
257                     }
258                     catch (java.lang.Exception e)
259                     {
260                         // LOG.ignore(e);
261                     }
262                 }
263             }
264             return -1;
265         }
266     }
267 
268     public static long parseDate(String date)
269     {
270         return __dateParser.get().parse(date);
271     }
272 
273     private static final ThreadLocal<DateParser> __dateParser =new ThreadLocal<DateParser>()
274     {
275         @Override
276         protected DateParser initialValue()
277         {
278             return new DateParser();
279         }
280     };
281 
282     public final static String __01Jan1970=formatDate(0);
283     public final static ByteBuffer __01Jan1970_BUFFER=BufferUtil.toBuffer(__01Jan1970);
284     public final static String __01Jan1970_COOKIE = formatCookieDate(0).trim();
285     private final ArrayList<HttpField> _fields = new ArrayList<>(20);
286 
287     /**
288      * Constructor.
289      */
290     public HttpFields()
291     {
292     }
293 
294 
295     /**
296      * Get Collection of header names.
297      */
298     public Collection<String> getFieldNamesCollection()
299     {
300         final Set<String> list = new HashSet<>(_fields.size());
301         for (HttpField f : _fields)
302         {
303             if (f!=null)
304                 list.add(f.getName());
305         }
306         return list;
307     }
308 
309     /**
310      * Get enumeration of header _names. Returns an enumeration of strings representing the header
311      * _names for this request.
312      */
313     public Enumeration<String> getFieldNames()
314     {
315         return Collections.enumeration(getFieldNamesCollection());
316     }
317 
318     public int size()
319     {
320         return _fields.size();
321     }
322 
323     /**
324      * Get a Field by index.
325      * @return A Field value or null if the Field value has not been set
326      *
327      */
328     public HttpField getField(int i)
329     {
330         return _fields.get(i);
331     }
332 
333     @Override
334     public Iterator<HttpField> iterator()
335     {
336         return _fields.iterator();
337     }
338 
339     public HttpField getField(HttpHeader header)
340     {
341         for (int i=0;i<_fields.size();i++)
342         {
343             HttpField f=_fields.get(i);
344             if (f.getHeader()==header)
345                 return f;
346         }
347         return null;
348     }
349 
350     public HttpField getField(String name)
351     {
352         for (int i=0;i<_fields.size();i++)
353         {
354             HttpField f=_fields.get(i);
355             if (f.getName().equalsIgnoreCase(name))
356                 return f;
357         }
358         return null;
359     }
360 
361     public boolean contains(HttpHeader header, String value)
362     {
363         for (int i=0;i<_fields.size();i++)
364         {
365             HttpField f=_fields.get(i);
366             if (f.getHeader()==header && f.contains(value))
367                 return true;
368         }
369         return false;
370     }
371     
372     public boolean contains(String name, String value)
373     {
374         for (int i=0;i<_fields.size();i++)
375         {
376             HttpField f=_fields.get(i);
377             if (f.getName().equalsIgnoreCase(name) && f.contains(value))
378                 return true;
379         }
380         return false;
381     }
382     
383     public boolean containsKey(String name)
384     {
385         for (int i=0;i<_fields.size();i++)
386         {
387             HttpField f=_fields.get(i);
388             if (f.getName().equalsIgnoreCase(name))
389                 return true;
390         }
391         return false;
392     }
393 
394     public String getStringField(HttpHeader header)
395     {
396         return getStringField(header.asString());
397     }
398 
399     public String get(HttpHeader header)
400     {
401         return getStringField(header.asString());
402     }
403 
404     public String get(String header)
405     {
406         return getStringField(header);
407     }
408 
409     /**
410      * @return the value of a field, or null if not found. For multiple fields of the same name,
411      *         only the first is returned.
412      * @param name the case-insensitive field name
413      */
414     public String getStringField(String name)
415     {
416         HttpField field = getField(name);
417         return field==null?null:field.getValue();
418     }
419 
420 
421     /**
422      * Get multi headers
423      *
424      * @return Enumeration of the values, or null if no such header.
425      * @param name the case-insensitive field name
426      */
427     public Collection<String> getValuesCollection(String name)
428     {
429         final List<String> list = new ArrayList<>();
430         for (HttpField f : _fields)
431             if (f.getName().equalsIgnoreCase(name))
432                 list.add(f.getValue());
433         return list;
434     }
435 
436     /**
437      * Get multi headers
438      *
439      * @return Enumeration of the values
440      * @param name the case-insensitive field name
441      */
442     public Enumeration<String> getValues(final String name)
443     {
444         for (int i=0;i<_fields.size();i++)
445         {
446             final HttpField f = _fields.get(i);
447             
448             if (f.getName().equalsIgnoreCase(name) && f.getValue()!=null)
449             {
450                 final int first=i;
451                 return new Enumeration<String>()
452                 {
453                     HttpField field=f;
454                     int i = first+1;
455 
456                     @Override
457                     public boolean hasMoreElements()
458                     {
459                         if (field==null)
460                         {
461                             while (i<_fields.size()) 
462                             {
463                                 field=_fields.get(i++);
464                                 if (field.getName().equalsIgnoreCase(name) && field.getValue()!=null)
465                                     return true;
466                             }
467                             field=null;
468                             return false;
469                         }
470                         return true;
471                     }
472 
473                     @Override
474                     public String nextElement() throws NoSuchElementException
475                     {
476                         if (hasMoreElements())
477                         {
478                             String value=field.getValue();
479                             field=null;
480                             return value;
481                         }
482                         throw new NoSuchElementException();
483                     }
484 
485                 };
486             }
487         }
488 
489         List<String> empty=Collections.emptyList();
490         return Collections.enumeration(empty);
491 
492     }
493 
494     /**
495      * Get multi field values with separator. The multiple values can be represented as separate
496      * headers of the same name, or by a single header using the separator(s), or a combination of
497      * both. Separators may be quoted.
498      *
499      * @param name the case-insensitive field name
500      * @param separators String of separators.
501      * @return Enumeration of the values, or null if no such header.
502      */
503     public Enumeration<String> getValues(String name, final String separators)
504     {
505         final Enumeration<String> e = getValues(name);
506         if (e == null)
507             return null;
508         return new Enumeration<String>()
509         {
510             QuotedStringTokenizer tok = null;
511 
512             @Override
513             public boolean hasMoreElements()
514             {
515                 if (tok != null && tok.hasMoreElements()) return true;
516                 while (e.hasMoreElements())
517                 {
518                     String value = e.nextElement();
519                     if (value!=null)
520                     {
521                         tok = new QuotedStringTokenizer(value, separators, false, false);
522                         if (tok.hasMoreElements()) return true;
523                     }
524                 }
525                 tok = null;
526                 return false;
527             }
528 
529             @Override
530             public String nextElement() throws NoSuchElementException
531             {
532                 if (!hasMoreElements()) throw new NoSuchElementException();
533                 String next = (String) tok.nextElement();
534                 if (next != null) next = next.trim();
535                 return next;
536             }
537         };
538     }
539 
540     public void put(HttpField field)
541     {
542         boolean put=false;
543         for (int i=_fields.size();i-->0;)
544         {
545             HttpField f=_fields.get(i);
546             if (f.isSame(field))
547             {
548                 if (put)
549                     _fields.remove(i);
550                 else
551                 {
552                     _fields.set(i,field);
553                     put=true;
554                 }
555             }
556         }
557         if (!put)
558             _fields.add(field);
559     }
560     
561     /**
562      * Set a field.
563      *
564      * @param name the name of the field
565      * @param value the value of the field. If null the field is cleared.
566      */
567     public void put(String name, String value)
568     {
569         if (value == null)
570             remove(name);
571         else
572             put(new HttpField(name, value));
573     }
574 
575     public void put(HttpHeader header, HttpHeaderValue value)
576     {
577         put(header,value.toString());
578     }
579 
580     /**
581      * Set a field.
582      *
583      * @param header the header name of the field
584      * @param value the value of the field. If null the field is cleared.
585      */
586     public void put(HttpHeader header, String value)
587     {
588         if (value == null)
589             remove(header);
590         else
591             put(new HttpField(header, value));
592     }
593 
594     /**
595      * Set a field.
596      *
597      * @param name the name of the field
598      * @param list the List value of the field. If null the field is cleared.
599      */
600     public void put(String name, List<String> list)
601     {
602         remove(name);
603         for (String v : list)
604             if (v!=null)
605                 add(name,v);
606     }
607 
608     /**
609      * Add to or set a field. If the field is allowed to have multiple values, add will add multiple
610      * headers of the same name.
611      *
612      * @param name the name of the field
613      * @param value the value of the field.
614      * @exception IllegalArgumentException If the name is a single valued field and already has a
615      *                value.
616      */
617     public void add(String name, String value) throws IllegalArgumentException
618     {
619         if (value == null)
620             return;
621 
622         HttpField field = new HttpField(name, value);
623         _fields.add(field);
624     }
625 
626     public void add(HttpHeader header, HttpHeaderValue value) throws IllegalArgumentException
627     {
628         add(header,value.toString());
629     }
630 
631     /**
632      * Add to or set a field. If the field is allowed to have multiple values, add will add multiple
633      * headers of the same name.
634      *
635      * @param header the header
636      * @param value the value of the field.
637      * @exception IllegalArgumentException 
638      */
639     public void add(HttpHeader header, String value) throws IllegalArgumentException
640     {
641         if (value == null) throw new IllegalArgumentException("null value");
642 
643         HttpField field = new HttpField(header, value);
644         _fields.add(field);
645     }
646 
647     /**
648      * Remove a field.
649      *
650      * @param name the field to remove
651      */
652     public void remove(HttpHeader name)
653     {
654         for (int i=_fields.size();i-->0;)
655         {
656             HttpField f=_fields.get(i);
657             if (f.getHeader()==name)
658                 _fields.remove(i);
659         }
660     }
661 
662     /**
663      * Remove a field.
664      *
665      * @param name the field to remove
666      */
667     public void remove(String name)
668     {
669         for (int i=_fields.size();i-->0;)
670         {
671             HttpField f=_fields.get(i);
672             if (f.getName().equalsIgnoreCase(name))
673                 _fields.remove(i);
674         }
675     }
676 
677     /**
678      * Get a header as an long value. Returns the value of an integer field or -1 if not found. The
679      * case of the field name is ignored.
680      *
681      * @param name the case-insensitive field name
682      * @exception NumberFormatException If bad long found
683      */
684     public long getLongField(String name) throws NumberFormatException
685     {
686         HttpField field = getField(name);
687         return field==null?-1L:field.getLongValue();
688     }
689 
690     /**
691      * Get a header as a date value. Returns the value of a date field, or -1 if not found. The case
692      * of the field name is ignored.
693      *
694      * @param name the case-insensitive field name
695      */
696     public long getDateField(String name)
697     {
698         HttpField field = getField(name);
699         if (field == null)
700             return -1;
701 
702         String val = valueParameters(field.getValue(), null);
703         if (val == null)
704             return -1;
705 
706         final long date = __dateParser.get().parse(val);
707         if (date==-1)
708             throw new IllegalArgumentException("Cannot convert date: " + val);
709         return date;
710     }
711 
712 
713     /**
714      * Sets the value of an long field.
715      *
716      * @param name the field name
717      * @param value the field long value
718      */
719     public void putLongField(HttpHeader name, long value)
720     {
721         String v = Long.toString(value);
722         put(name, v);
723     }
724 
725     /**
726      * Sets the value of an long field.
727      *
728      * @param name the field name
729      * @param value the field long value
730      */
731     public void putLongField(String name, long value)
732     {
733         String v = Long.toString(value);
734         put(name, v);
735     }
736 
737 
738     /**
739      * Sets the value of a date field.
740      *
741      * @param name the field name
742      * @param date the field date value
743      */
744     public void putDateField(HttpHeader name, long date)
745     {
746         String d=formatDate(date);
747         put(name, d);
748     }
749 
750     /**
751      * Sets the value of a date field.
752      *
753      * @param name the field name
754      * @param date the field date value
755      */
756     public void putDateField(String name, long date)
757     {
758         String d=formatDate(date);
759         put(name, d);
760     }
761 
762     /**
763      * Sets the value of a date field.
764      *
765      * @param name the field name
766      * @param date the field date value
767      */
768     public void addDateField(String name, long date)
769     {
770         String d=formatDate(date);
771         add(name,d);
772     }
773 
774     /**
775      * Format a set cookie value
776      *
777      * @param cookie The cookie.
778      */
779     public void addSetCookie(HttpCookie cookie)
780     {
781         addSetCookie(
782                 cookie.getName(),
783                 cookie.getValue(),
784                 cookie.getDomain(),
785                 cookie.getPath(),
786                 cookie.getMaxAge(),
787                 cookie.getComment(),
788                 cookie.isSecure(),
789                 cookie.isHttpOnly(),
790                 cookie.getVersion());
791     }
792 
793     /**
794      * Format a set cookie value
795      *
796      * @param name the name
797      * @param value the value
798      * @param domain the domain
799      * @param path the path
800      * @param maxAge the maximum age
801      * @param comment the comment (only present on versions > 0)
802      * @param isSecure true if secure cookie
803      * @param isHttpOnly true if for http only
804      * @param version version of cookie logic to use (0 == default behavior)
805      */
806     public void addSetCookie(
807             final String name,
808             final String value,
809             final String domain,
810             final String path,
811             final long maxAge,
812             final String comment,
813             final boolean isSecure,
814             final boolean isHttpOnly,
815             int version)
816     {
817         // Check arguments
818         if (name == null || name.length() == 0)
819             throw new IllegalArgumentException("Bad cookie name");
820 
821         // Format value and params
822         StringBuilder buf = new StringBuilder(128);
823         
824         // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting
825         boolean quote_name=isQuoteNeededForCookie(name);
826         quoteOnlyOrAppend(buf,name,quote_name);
827         
828         buf.append('=');
829         
830         // Remember name= part to look for other matching set-cookie
831         String name_equals=buf.toString();
832 
833         // Append the value
834         boolean quote_value=isQuoteNeededForCookie(value);
835         quoteOnlyOrAppend(buf,value,quote_value);
836 
837         // Look for domain and path fields and check if they need to be quoted
838         boolean has_domain = domain!=null && domain.length()>0;
839         boolean quote_domain = has_domain && isQuoteNeededForCookie(domain);
840         boolean has_path = path!=null && path.length()>0;
841         boolean quote_path = has_path && isQuoteNeededForCookie(path);
842         
843         // Upgrade the version if we have a comment or we need to quote value/path/domain or if they were already quoted
844         if (version==0 && ( comment!=null || quote_name || quote_value || quote_domain || quote_path || isQuoted(name) || isQuoted(value) || isQuoted(path) || isQuoted(domain)))
845             version=1;
846 
847         // Append version
848         if (version==1)
849             buf.append (";Version=1");
850         else if (version>1)
851             buf.append (";Version=").append(version);
852         
853         // Append path
854         if (has_path)
855         {
856             buf.append(";Path=");
857             quoteOnlyOrAppend(buf,path,quote_path);
858         }
859         
860         // Append domain
861         if (has_domain)
862         {
863             buf.append(";Domain=");
864             quoteOnlyOrAppend(buf,domain,quote_domain);
865         }
866 
867         // Handle max-age and/or expires
868         if (maxAge >= 0)
869         {
870             // Always use expires
871             // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies
872             buf.append(";Expires=");
873             if (maxAge == 0)
874                 buf.append(__01Jan1970_COOKIE);
875             else
876                 formatCookieDate(buf, System.currentTimeMillis() + 1000L * maxAge);
877             
878             // for v1 cookies, also send max-age
879             if (version>=1)
880             {
881                 buf.append(";Max-Age=");
882                 buf.append(maxAge);
883             }
884         }
885 
886         // add the other fields
887         if (isSecure)
888             buf.append(";Secure");
889         if (isHttpOnly)
890             buf.append(";HttpOnly");
891         if (comment != null)
892         {
893             buf.append(";Comment=");
894             quoteOnlyOrAppend(buf,comment,isQuoteNeededForCookie(comment));
895         }
896 
897         // remove any existing set-cookie fields of same name
898         Iterator<HttpField> i=_fields.iterator();
899         while (i.hasNext())
900         {
901             HttpField field=i.next();
902             if (field.getHeader()==HttpHeader.SET_COOKIE)
903             {
904                 String val = field.getValue();
905                 if (val!=null && val.startsWith(name_equals))
906                 {
907                     //existing cookie has same name, does it also match domain and path?
908                     if (((!has_domain && !val.contains("Domain")) || (has_domain && val.contains(domain))) &&
909                         ((!has_path && !val.contains("Path")) || (has_path && val.contains(path))))
910                     {
911                         i.remove();
912                     }
913                 }
914             }
915         }
916         
917         // add the set cookie
918         add(HttpHeader.SET_COOKIE.toString(), buf.toString());
919 
920         // Expire responses with set-cookie headers so they do not get cached.
921         put(HttpHeader.EXPIRES.toString(), __01Jan1970);
922     }
923 
924     public void putTo(ByteBuffer bufferInFillMode) 
925     {
926         for (HttpField field : _fields)
927         {
928             if (field != null)
929                 field.putTo(bufferInFillMode);
930         }
931         BufferUtil.putCRLF(bufferInFillMode);
932     }
933 
934     @Override
935     public String
936     toString()
937     {
938         try
939         {
940             StringBuilder buffer = new StringBuilder();
941             for (HttpField field : _fields)
942             {
943                 if (field != null)
944                 {
945                     String tmp = field.getName();
946                     if (tmp != null) buffer.append(tmp);
947                     buffer.append(": ");
948                     tmp = field.getValue();
949                     if (tmp != null) buffer.append(tmp);
950                     buffer.append("\r\n");
951                 }
952             }
953             buffer.append("\r\n");
954             return buffer.toString();
955         }
956         catch (Exception e)
957         {
958             LOG.warn(e);
959             return e.toString();
960         }
961     }
962 
963     /**
964      * Clear the header.
965      */
966     public void clear()
967     {
968         _fields.clear();
969     }
970 
971     public void add(HttpField field)
972     {
973         _fields.add(field);
974     }
975 
976     
977     
978     /**
979      * Add fields from another HttpFields instance. Single valued fields are replaced, while all
980      * others are added.
981      *
982      * @param fields the fields to add
983      */
984     public void add(HttpFields fields)
985     {
986         if (fields == null) return;
987 
988         Enumeration<String> e = fields.getFieldNames();
989         while (e.hasMoreElements())
990         {
991             String name = e.nextElement();
992             Enumeration<String> values = fields.getValues(name);
993             while (values.hasMoreElements())
994                 add(name, values.nextElement());
995         }
996     }
997 
998     /**
999      * Get field value parameters. Some field values can have parameters. This method separates the
1000      * value from the parameters and optionally populates a map with the parameters. For example:
1001      *
1002      * <PRE>
1003      *
1004      * FieldName : Value ; param1=val1 ; param2=val2
1005      *
1006      * </PRE>
1007      *
1008      * @param value The Field value, possibly with parameteres.
1009      * @param parameters A map to populate with the parameters, or null
1010      * @return The value.
1011      */
1012     public static String valueParameters(String value, Map<String,String> parameters)
1013     {
1014         if (value == null) return null;
1015 
1016         int i = value.indexOf(';');
1017         if (i < 0) return value;
1018         if (parameters == null) return value.substring(0, i).trim();
1019 
1020         StringTokenizer tok1 = new QuotedStringTokenizer(value.substring(i), ";", false, true);
1021         while (tok1.hasMoreTokens())
1022         {
1023             String token = tok1.nextToken();
1024             StringTokenizer tok2 = new QuotedStringTokenizer(token, "= ");
1025             if (tok2.hasMoreTokens())
1026             {
1027                 String paramName = tok2.nextToken();
1028                 String paramVal = null;
1029                 if (tok2.hasMoreTokens()) paramVal = tok2.nextToken();
1030                 parameters.put(paramName, paramVal);
1031             }
1032         }
1033 
1034         return value.substring(0, i).trim();
1035     }
1036 
1037     private static final Float __one = new Float("1.0");
1038     private static final Float __zero = new Float("0.0");
1039     private static final Trie<Float> __qualities = new ArrayTernaryTrie<>();
1040     static
1041     {
1042         __qualities.put("*", __one);
1043         __qualities.put("1.0", __one);
1044         __qualities.put("1", __one);
1045         __qualities.put("0.9", new Float("0.9"));
1046         __qualities.put("0.8", new Float("0.8"));
1047         __qualities.put("0.7", new Float("0.7"));
1048         __qualities.put("0.66", new Float("0.66"));
1049         __qualities.put("0.6", new Float("0.6"));
1050         __qualities.put("0.5", new Float("0.5"));
1051         __qualities.put("0.4", new Float("0.4"));
1052         __qualities.put("0.33", new Float("0.33"));
1053         __qualities.put("0.3", new Float("0.3"));
1054         __qualities.put("0.2", new Float("0.2"));
1055         __qualities.put("0.1", new Float("0.1"));
1056         __qualities.put("0", __zero);
1057         __qualities.put("0.0", __zero);
1058     }
1059 
1060     public static Float getQuality(String value)
1061     {
1062         if (value == null) return __zero;
1063 
1064         int qe = value.indexOf(";");
1065         if (qe++ < 0 || qe == value.length()) return __one;
1066 
1067         if (value.charAt(qe++) == 'q')
1068         {
1069             qe++;
1070             Float q = __qualities.get(value, qe, value.length() - qe);
1071             if (q != null)
1072                 return q;
1073         }
1074 
1075         Map<String,String> params = new HashMap<>(4);
1076         valueParameters(value, params);
1077         String qs = params.get("q");
1078         if (qs==null)
1079             qs="*";
1080         Float q = __qualities.get(qs);
1081         if (q == null)
1082         {
1083             try
1084             {
1085                 q = new Float(qs);
1086             }
1087             catch (Exception e)
1088             {
1089                 q = __one;
1090             }
1091         }
1092         return q;
1093     }
1094 
1095     /**
1096      * List values in quality order.
1097      *
1098      * @param e Enumeration of values with quality parameters
1099      * @return values in quality order.
1100      */
1101     public static List<String> qualityList(Enumeration<String> e)
1102     {
1103         if (e == null || !e.hasMoreElements())
1104             return Collections.emptyList();
1105 
1106         Object list = null;
1107         Object qual = null;
1108 
1109         // Assume list will be well ordered and just add nonzero
1110         while (e.hasMoreElements())
1111         {
1112             String v = e.nextElement();
1113             Float q = getQuality(v);
1114 
1115             if (q >= 0.001)
1116             {
1117                 list = LazyList.add(list, v);
1118                 qual = LazyList.add(qual, q);
1119             }
1120         }
1121 
1122         List<String> vl = LazyList.getList(list, false);
1123         if (vl.size() < 2) 
1124             return vl;
1125 
1126         List<Float> ql = LazyList.getList(qual, false);
1127 
1128         // sort list with swaps
1129         Float last = __zero;
1130         for (int i = vl.size(); i-- > 0;)
1131         {
1132             Float q = ql.get(i);
1133             if (last.compareTo(q) > 0)
1134             {
1135                 String tmp = vl.get(i);
1136                 vl.set(i, vl.get(i + 1));
1137                 vl.set(i + 1, tmp);
1138                 ql.set(i, ql.get(i + 1));
1139                 ql.set(i + 1, q);
1140                 last = __zero;
1141                 i = vl.size();
1142                 continue;
1143             }
1144             last = q;
1145         }
1146         ql.clear();
1147         return vl;
1148     }
1149 
1150 
1151 
1152     /* ------------------------------------------------------------ */
1153     /** Does a cookie value need to be quoted?
1154      * @param s value string
1155      * @return true if quoted;
1156      * @throws IllegalArgumentException If there a control characters in the string
1157      */
1158     public static boolean isQuoteNeededForCookie(String s)
1159     {
1160         if (s==null || s.length()==0)
1161             return true;
1162         
1163         if (QuotedStringTokenizer.isQuoted(s))
1164             return false;
1165 
1166         for (int i=0;i<s.length();i++)
1167         {
1168             char c = s.charAt(i);
1169             if (__COOKIE_DELIM.indexOf(c)>=0)
1170                 return true;
1171             
1172             if (c<0x20 || c>=0x7f)
1173                 throw new IllegalArgumentException("Illegal character in cookie value");
1174         }
1175 
1176         return false;
1177     }
1178     
1179     
1180     private static void quoteOnlyOrAppend(StringBuilder buf, String s, boolean quote)
1181     {
1182         if (quote)
1183             QuotedStringTokenizer.quoteOnly(buf,s);
1184         else
1185             buf.append(s);
1186     }
1187 }