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