• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /* Licensed to the Apache Software Foundation (ASF) under one or more
2  * contributor license agreements.  See the NOTICE file distributed with
3  * this work for additional information regarding copyright ownership.
4  * The ASF licenses this file to You under the Apache License, Version 2.0
5  * (the "License"); you may not use this file except in compliance with
6  * the License.  You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package java.net;
18 
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.Date;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Locale;
25 import java.util.Set;
26 import libcore.net.http.HttpDate;
27 import libcore.util.Objects;
28 
29 /**
30  * An opaque key-value value pair held by an HTTP client to permit a stateful
31  * session with an HTTP server. This class parses cookie headers for all three
32  * commonly used HTTP cookie specifications:
33  *
34  * <ul>
35  *     <li>The Netscape cookie spec is officially obsolete but widely used in
36  *         practice. Each cookie contains one key-value pair and the following
37  *         attributes: {@code Domain}, {@code Expires}, {@code Path}, and
38  *         {@code Secure}. The {@link #getVersion() version} of cookies in this
39  *         format is {@code 0}.
40  *         <p>There are no accessors for the {@code Expires} attribute. When
41  *         parsed, expires attributes are assigned to the {@link #getMaxAge()
42  *         Max-Age} attribute as an offset from {@link System#currentTimeMillis()
43  *         now}.
44  *     <li><a href="http://www.ietf.org/rfc/rfc2109.txt">RFC 2109</a> formalizes
45  *         the Netscape cookie spec. It replaces the {@code Expires} timestamp
46  *         with a {@code Max-Age} duration and adds {@code Comment} and {@code
47  *         Version} attributes. The {@link #getVersion() version} of cookies in
48  *         this format is {@code 1}.
49  *     <li><a href="http://www.ietf.org/rfc/rfc2965.txt">RFC 2965</a> refines
50  *         RFC 2109. It adds {@code Discard}, {@code Port}, and {@code
51  *         CommentURL} attributes and renames the header from {@code Set-Cookie}
52  *         to {@code Set-Cookie2}. The {@link #getVersion() version} of cookies
53  *         in this format is {@code 1}.
54  * </ul>
55  *
56  * <p>Support for the "HttpOnly" attribute specified in
57  * <a href="http://tools.ietf.org/html/rfc6265">RFC 6265</a> is also included. RFC 6265 is intended
58  * to obsolete RFC 2965. Support for features from RFC 2965 that have been deprecated by RFC 6265
59  * such as Cookie2, Set-Cookie2 headers and version information remain supported by this class.
60  *
61  * <p>This implementation silently discards unrecognized attributes.
62  *
63  * @since 1.6
64  */
65 public final class HttpCookie implements Cloneable {
66 
67     private static final Set<String> RESERVED_NAMES = new HashSet<String>();
68 
69     static {
70         RESERVED_NAMES.add("comment");    //           RFC 2109  RFC 2965  RFC 6265
71         RESERVED_NAMES.add("commenturl"); //                     RFC 2965  RFC 6265
72         RESERVED_NAMES.add("discard");    //                     RFC 2965  RFC 6265
73         RESERVED_NAMES.add("domain");     // Netscape  RFC 2109  RFC 2965  RFC 6265
74         RESERVED_NAMES.add("expires");    // Netscape
75         RESERVED_NAMES.add("httponly");   //                               RFC 6265
76         RESERVED_NAMES.add("max-age");    //           RFC 2109  RFC 2965  RFC 6265
77         RESERVED_NAMES.add("path");       // Netscape  RFC 2109  RFC 2965  RFC 6265
78         RESERVED_NAMES.add("port");       //                     RFC 2965  RFC 6265
79         RESERVED_NAMES.add("secure");     // Netscape  RFC 2109  RFC 2965  RFC 6265
80         RESERVED_NAMES.add("version");    //           RFC 2109  RFC 2965  RFC 6265
81     }
82 
83     /**
84      * Returns true if {@code host} matches the domain pattern {@code domain}.
85      *
86      * @param domainPattern a host name (like {@code android.com} or {@code
87      *     localhost}), or a pattern to match subdomains of a domain name (like
88      *     {@code .android.com}). A special case pattern is {@code .local},
89      *     which matches all hosts without a TLD (like {@code localhost}).
90      * @param host the host name or IP address from an HTTP request.
91      */
domainMatches(String domainPattern, String host)92     public static boolean domainMatches(String domainPattern, String host) {
93         if (domainPattern == null || host == null) {
94             return false;
95         }
96 
97         String a = host.toLowerCase(Locale.US);
98         String b = domainPattern.toLowerCase(Locale.US);
99 
100         /*
101          * From the spec: "both host names are IP addresses and their host name strings match
102          * exactly; or both host names are FQDN strings and their host name strings match exactly"
103          */
104         if (a.equals(b) && (isFullyQualifiedDomainName(a, 0) || InetAddress.isNumeric(a))) {
105             return true;
106         }
107         if (!isFullyQualifiedDomainName(a, 0)) {
108             return b.equals(".local");
109         }
110 
111         /*
112          * Not in the spec! If prefixing a hostname with "." causes it to equal the domain pattern,
113          * then it should match. This is necessary so that the pattern ".google.com" will match the
114          * host "google.com".
115          */
116         if (b.length() == 1 + a.length()
117                 && b.startsWith(".")
118                 && b.endsWith(a)
119                 && isFullyQualifiedDomainName(b, 1)) {
120             return true;
121         }
122 
123         /*
124          * From the spec: "A is a HDN string and has the form NB, where N is a
125          * non-empty name string, B has the form .B', and B' is a HDN string.
126          * (So, x.y.com domain-matches .Y.com but not Y.com.)
127          */
128         return a.length() > b.length()
129                 && a.endsWith(b)
130                 && ((b.startsWith(".") && isFullyQualifiedDomainName(b, 1)) || b.equals(".local"));
131     }
132 
133     /**
134      * Returns true if {@code cookie} should be sent to or accepted from {@code uri} with respect
135      * to the cookie's path. Cookies match by directory prefix: URI "/foo" matches cookies "/foo",
136      * "/foo/" and "/foo/bar", but not "/" or "/foobar".
137      */
pathMatches(HttpCookie cookie, URI uri)138     static boolean pathMatches(HttpCookie cookie, URI uri) {
139         String uriPath = matchablePath(uri.getPath());
140         String cookiePath = matchablePath(cookie.getPath());
141         return uriPath.startsWith(cookiePath);
142     }
143 
144     /**
145      * Returns true if {@code cookie} should be sent to {@code uri} with respect to the cookie's
146      * secure attribute. Secure cookies should not be sent in insecure (ie. non-HTTPS) requests.
147      */
secureMatches(HttpCookie cookie, URI uri)148     static boolean secureMatches(HttpCookie cookie, URI uri) {
149         return !cookie.getSecure() || "https".equalsIgnoreCase(uri.getScheme());
150     }
151 
152     /**
153      * Returns true if {@code cookie} should be sent to {@code uri} with respect to the cookie's
154      * port list.
155      */
portMatches(HttpCookie cookie, URI uri)156     static boolean portMatches(HttpCookie cookie, URI uri) {
157         if (cookie.getPortlist() == null) {
158             return true;
159         }
160         return Arrays.asList(cookie.getPortlist().split(","))
161                 .contains(Integer.toString(uri.getEffectivePort()));
162     }
163 
164     /**
165      * Returns a non-null path ending in "/".
166      */
matchablePath(String path)167     private static String matchablePath(String path) {
168         if (path == null) {
169             return "/";
170         } else if (path.endsWith("/")) {
171             return path;
172         } else {
173             return path + "/";
174         }
175     }
176 
177     /**
178      * Returns true if {@code s.substring(firstCharacter)} contains a dot
179      * between its first and last characters, exclusive. This considers both
180      * {@code android.com} and {@code co.uk} to be fully qualified domain names,
181      * but not {@code android.com.}, {@code .com}. or {@code android}.
182      *
183      * <p>Although this implements the cookie spec's definition of FQDN, it is
184      * not general purpose. For example, this returns true for IPv4 addresses.
185      */
isFullyQualifiedDomainName(String s, int firstCharacter)186     private static boolean isFullyQualifiedDomainName(String s, int firstCharacter) {
187         int dotPosition = s.indexOf('.', firstCharacter + 1);
188         return dotPosition != -1 && dotPosition < s.length() - 1;
189     }
190 
191     /**
192      * Constructs a cookie from a string. The string should comply with
193      * set-cookie or set-cookie2 header format as specified in
194      * <a href="http://www.ietf.org/rfc/rfc2965.txt">RFC 2965</a>. Since
195      * set-cookies2 syntax allows more than one cookie definitions in one
196      * header, the returned object is a list.
197      *
198      * @param header
199      *            a set-cookie or set-cookie2 header.
200      * @return a list of constructed cookies
201      * @throws IllegalArgumentException
202      *             if the string does not comply with cookie specification, or
203      *             the cookie name contains illegal characters, or reserved
204      *             tokens of cookie specification appears
205      * @throws NullPointerException
206      *             if header is null
207      */
parse(String header)208     public static List<HttpCookie> parse(String header) {
209         return new CookieParser(header).parse();
210     }
211 
212     static class CookieParser {
213         private static final String ATTRIBUTE_NAME_TERMINATORS = ",;= \t";
214         private static final String WHITESPACE = " \t";
215         private final String input;
216         private final String inputLowerCase;
217         private int pos = 0;
218 
219         /*
220          * The cookie's version is set based on an overly complex heuristic:
221          * If it has an expires attribute, the version is 0.
222          * Otherwise, if it has a max-age attribute, the version is 1.
223          * Otherwise, if the cookie started with "Set-Cookie2", the version is 1.
224          * Otherwise, if it has any explicit version attributes, use the first one.
225          * Otherwise, the version is 0.
226          */
227         boolean hasExpires = false;
228         boolean hasMaxAge = false;
229         boolean hasVersion = false;
230 
CookieParser(String input)231         CookieParser(String input) {
232             this.input = input;
233             this.inputLowerCase = input.toLowerCase(Locale.US);
234         }
235 
parse()236         public List<HttpCookie> parse() {
237             List<HttpCookie> cookies = new ArrayList<HttpCookie>(2);
238 
239             // The RI permits input without either the "Set-Cookie:" or "Set-Cookie2" headers.
240             boolean pre2965 = true;
241             if (inputLowerCase.startsWith("set-cookie2:")) {
242                 pos += "set-cookie2:".length();
243                 pre2965 = false;
244                 hasVersion = true;
245             } else if (inputLowerCase.startsWith("set-cookie:")) {
246                 pos += "set-cookie:".length();
247             }
248 
249             /*
250              * Read a comma-separated list of cookies. Note that the values may contain commas!
251              *   <NAME> "=" <VALUE> ( ";" <ATTR NAME> ( "=" <ATTR VALUE> )? )*
252              */
253             while (true) {
254                 String name = readAttributeName(false);
255                 if (name == null) {
256                     if (cookies.isEmpty()) {
257                         throw new IllegalArgumentException("No cookies in " + input);
258                     }
259                     return cookies;
260                 }
261 
262                 if (!readEqualsSign()) {
263                     throw new IllegalArgumentException(
264                             "Expected '=' after " + name + " in " + input);
265                 }
266 
267                 String value = readAttributeValue(pre2965 ? ";" : ",;");
268                 HttpCookie cookie = new HttpCookie(name, value);
269                 cookie.version = pre2965 ? 0 : 1;
270                 cookies.add(cookie);
271 
272                 /*
273                  * Read the attributes of the current cookie. Each iteration of this loop should
274                  * enter with input either exhausted or prefixed with ';' or ',' as in ";path=/"
275                  * and ",COOKIE2=value2".
276                  */
277                 while (true) {
278                     skipWhitespace();
279                     if (pos == input.length()) {
280                         break;
281                     }
282 
283                     if (input.charAt(pos) == ',') {
284                         pos++;
285                         break; // a true comma delimiter; the current cookie is complete.
286                     } else if (input.charAt(pos) == ';') {
287                         pos++;
288                     }
289 
290                     String attributeName = readAttributeName(true);
291                     if (attributeName == null) {
292                         continue; // for empty attribute as in "Set-Cookie: foo=Foo;;path=/"
293                     }
294 
295                     /*
296                      * Since expires and port attributes commonly include comma delimiters, always
297                      * scan until a semicolon when parsing these attributes.
298                      */
299                     String terminators = pre2965
300                             || "expires".equals(attributeName) || "port".equals(attributeName)
301                             ? ";"
302                             : ";,";
303                     String attributeValue = null;
304                     if (readEqualsSign()) {
305                         attributeValue = readAttributeValue(terminators);
306                     }
307                     setAttribute(cookie, attributeName, attributeValue);
308                 }
309 
310                 if (hasExpires) {
311                     cookie.version = 0;
312                 } else if (hasMaxAge) {
313                     cookie.version = 1;
314                 }
315             }
316         }
317 
setAttribute(HttpCookie cookie, String name, String value)318         private void setAttribute(HttpCookie cookie, String name, String value) {
319             if (name.equals("comment") && cookie.comment == null) {
320                 cookie.comment = value;
321             } else if (name.equals("commenturl") && cookie.commentURL == null) {
322                 cookie.commentURL = value;
323             } else if (name.equals("discard")) {
324                 cookie.discard = true;
325             } else if (name.equals("domain") && cookie.domain == null) {
326                 cookie.domain = value;
327             } else if (name.equals("expires")) {
328                 hasExpires = true;
329                 if (cookie.maxAge == -1L) {
330                     Date date = HttpDate.parse(value);
331                     if (date != null) {
332                         cookie.setExpires(date);
333                     } else {
334                         cookie.maxAge = 0;
335                     }
336                 }
337             } else if (name.equals("max-age") && cookie.maxAge == -1L) {
338                 // RFCs 2109 and 2965 suggests a zero max-age as a way of deleting a cookie.
339                 // RFC 6265 specifies the value must be > 0 but also describes what to do if the
340                 // value is negative, zero or non-numeric in section 5.2.2. The RI does none of this
341                 // and accepts negative, positive values and throws an IllegalArgumentException
342                 // if the value is non-numeric.
343                 try {
344                     long maxAge = Long.parseLong(value);
345                     hasMaxAge = true;
346                     cookie.maxAge = maxAge;
347                 } catch (NumberFormatException e) {
348                     throw new IllegalArgumentException("Invalid max-age: " + value);
349                 }
350             } else if (name.equals("path") && cookie.path == null) {
351                 cookie.path = value;
352             } else if (name.equals("port") && cookie.portList == null) {
353                 cookie.portList = value != null ? value : "";
354             } else if (name.equals("secure")) {
355                 cookie.secure = true;
356             } else if (name.equals("httponly")) {
357                 cookie.httpOnly = true;
358             } else if (name.equals("version") && !hasVersion) {
359                 cookie.version = Integer.parseInt(value);
360             }
361         }
362 
363         /**
364          * Returns the next attribute name, or null if the input has been
365          * exhausted. Returns wth the cursor on the delimiter that follows.
366          */
readAttributeName(boolean returnLowerCase)367         private String readAttributeName(boolean returnLowerCase) {
368             skipWhitespace();
369             int c = find(ATTRIBUTE_NAME_TERMINATORS);
370             String forSubstring = returnLowerCase ? inputLowerCase : input;
371             String result = pos < c ? forSubstring.substring(pos, c) : null;
372             pos = c;
373             return result;
374         }
375 
376         /**
377          * Returns true if an equals sign was read and consumed.
378          */
379         private boolean readEqualsSign() {
380             skipWhitespace();
381             if (pos < input.length() && input.charAt(pos) == '=') {
382                 pos++;
383                 return true;
384             }
385             return false;
386         }
387 
388         /**
389          * Reads an attribute value, by parsing either a quoted string or until
390          * the next character in {@code terminators}. The terminator character
391          * is not consumed.
392          */
393         private String readAttributeValue(String terminators) {
394             skipWhitespace();
395 
396             /*
397              * Quoted string: read 'til the close quote. The spec mentions only "double quotes"
398              * but RI bug 6901170 claims that 'single quotes' are also used.
399              */
400             if (pos < input.length() && (input.charAt(pos) == '"' || input.charAt(pos) == '\'')) {
401                 char quoteCharacter = input.charAt(pos++);
402                 int closeQuote = input.indexOf(quoteCharacter, pos);
403                 if (closeQuote == -1) {
404                     throw new IllegalArgumentException("Unterminated string literal in " + input);
405                 }
406                 String result = input.substring(pos, closeQuote);
407                 pos = closeQuote + 1;
408                 return result;
409             }
410 
411             int c = find(terminators);
412             String result = input.substring(pos, c);
413             pos = c;
414             return result;
415         }
416 
417         /**
418          * Returns the index of the next character in {@code chars}, or the end
419          * of the string.
420          */
421         private int find(String chars) {
422             for (int c = pos; c < input.length(); c++) {
423                 if (chars.indexOf(input.charAt(c)) != -1) {
424                     return c;
425                 }
426             }
427             return input.length();
428         }
429 
430         private void skipWhitespace() {
431             for (; pos < input.length(); pos++) {
432                 if (WHITESPACE.indexOf(input.charAt(pos)) == -1) {
433                     break;
434                 }
435             }
436         }
437     }
438 
439     private String comment;
440     private String commentURL;
441     private boolean discard;
442     private String domain;
443     private long maxAge = -1l;
444     private final String name;
445     private String path;
446     private String portList;
447     private boolean secure;
448     private boolean httpOnly;
449     private String value;
450     private int version = 1;
451 
452     /**
453      * Creates a new cookie.
454      *
455      * @param name a non-empty string that contains only printable ASCII, no
456      *     commas or semicolons, and is not prefixed with  {@code $}. May not be
457      *     an HTTP attribute name.
458      * @param value an opaque value from the HTTP server.
459      * @throws IllegalArgumentException if {@code name} is invalid.
460      */
461     public HttpCookie(String name, String value) {
462         String ntrim = name.trim(); // erase leading and trailing whitespace
463         if (!isValidName(ntrim)) {
464             throw new IllegalArgumentException("Invalid name: " + name);
465         }
466 
467         this.name = ntrim;
468         this.value = value;
469     }
470 
471 
472     private boolean isValidName(String n) {
473         // name cannot be empty or begin with '$' or equals the reserved
474         // attributes (case-insensitive)
475         boolean isValid = !(n.length() == 0 || n.startsWith("$")
476                 || RESERVED_NAMES.contains(n.toLowerCase(Locale.US)));
477         if (isValid) {
478             for (int i = 0; i < n.length(); i++) {
479                 char nameChar = n.charAt(i);
480                 // name must be ASCII characters and cannot contain ';', ',' and
481                 // whitespace
482                 if (nameChar < 0
483                         || nameChar >= 127
484                         || nameChar == ';'
485                         || nameChar == ','
486                         || (Character.isWhitespace(nameChar) && nameChar != ' ')) {
487                     isValid = false;
488                     break;
489                 }
490             }
491         }
492         return isValid;
493     }
494 
495     /**
496      * Returns the {@code Comment} attribute.
497      */
498     public String getComment() {
499         return comment;
500     }
501 
502     /**
503      * Returns the value of {@code CommentURL} attribute.
504      */
505     public String getCommentURL() {
506         return commentURL;
507     }
508 
509     /**
510      * Returns the {@code Discard} attribute.
511      */
512     public boolean getDiscard() {
513         return discard;
514     }
515 
516     /**
517      * Returns the {@code Domain} attribute.
518      */
519     public String getDomain() {
520         return domain;
521     }
522 
523     /**
524      * Returns the {@code Max-Age} attribute, in delta-seconds.
525      */
526     public long getMaxAge() {
527         return maxAge;
528     }
529 
530     /**
531      * Returns the name of this cookie.
532      */
533     public String getName() {
534         return name;
535     }
536 
537     /**
538      * Returns the {@code Path} attribute. This cookie is visible to all
539      * subpaths.
540      */
541     public String getPath() {
542         return path;
543     }
544 
545     /**
546      * Returns the {@code Port} attribute, usually containing comma-separated
547      * port numbers. A null port indicates that the cookie may be sent to any
548      * port. The empty string indicates that the cookie should only be sent to
549      * the port of the originating request.
550      */
551     public String getPortlist() {
552         return portList;
553     }
554 
555     /**
556      * Returns the {@code Secure} attribute.
557      */
558     public boolean getSecure() {
559         return secure;
560     }
561 
562     /**
563      * Returns the value of this cookie.
564      */
565     public String getValue() {
566         return value;
567     }
568 
569     /**
570      * Returns the version of this cookie.
571      */
572     public int getVersion() {
573         return version;
574     }
575 
576     /**
577      * Returns true if this cookie's Max-Age is 0.
578      */
579     public boolean hasExpired() {
580         // -1 indicates the cookie will persist until browser shutdown
581         // so the cookie is not expired.
582         if (maxAge == -1l) {
583             return false;
584         }
585 
586         boolean expired = false;
587         if (maxAge <= 0l) {
588             expired = true;
589         }
590         return expired;
591     }
592 
593     /**
594      * Set the {@code Comment} attribute of this cookie.
595      */
596     public void setComment(String comment) {
597         this.comment = comment;
598     }
599 
600     /**
601      * Set the {@code CommentURL} attribute of this cookie.
602      */
603     public void setCommentURL(String commentURL) {
604         this.commentURL = commentURL;
605     }
606 
607     /**
608      * Set the {@code Discard} attribute of this cookie.
609      */
610     public void setDiscard(boolean discard) {
611         this.discard = discard;
612     }
613 
614     /**
615      * Set the {@code Domain} attribute of this cookie. HTTP clients send
616      * cookies only to matching domains.
617      */
618     public void setDomain(String pattern) {
619         domain = pattern == null ? null : pattern.toLowerCase(Locale.US);
620     }
621 
622     /**
623      * Sets the {@code Max-Age} attribute of this cookie.
624      */
625     public void setMaxAge(long deltaSeconds) {
626         maxAge = deltaSeconds;
627     }
628 
629     private void setExpires(Date expires) {
630         maxAge = (expires.getTime() - System.currentTimeMillis()) / 1000;
631     }
632 
633     /**
634      * Set the {@code Path} attribute of this cookie. HTTP clients send cookies
635      * to this path and its subpaths.
636      */
637     public void setPath(String path) {
638         this.path = path;
639     }
640 
641     /**
642      * Set the {@code Port} attribute of this cookie.
643      */
644     public void setPortlist(String portList) {
645         this.portList = portList;
646     }
647 
648     /**
649      * Sets the {@code Secure} attribute of this cookie.
650      */
651     public void setSecure(boolean secure) {
652         this.secure = secure;
653     }
654 
655     /**
656      * Sets the opaque value of this cookie.
657      */
658     public void setValue(String value) {
659         // FIXME: According to spec, version 0 cookie value does not allow many
660         // symbols. But RI does not implement it. Follow RI temporarily.
661         this.value = value;
662     }
663 
664     /**
665      * Sets the {@code Version} attribute of the cookie.
666      *
667      * @throws IllegalArgumentException if v is neither 0 nor 1
668      */
669     public void setVersion(int newVersion) {
670         if (newVersion != 0 && newVersion != 1) {
671             throw new IllegalArgumentException("Bad version: " + newVersion);
672         }
673         version = newVersion;
674     }
675 
676     @Override public Object clone() {
677         try {
678             return super.clone();
679         } catch (CloneNotSupportedException e) {
680             throw new AssertionError();
681         }
682     }
683 
684     /**
685      * Returns true if {@code object} is a cookie with the same domain, name and
686      * path. Domain and name use case-insensitive comparison; path uses a
687      * case-sensitive comparison.
688      */
689     @Override public boolean equals(Object object) {
690         if (object == this) {
691             return true;
692         }
693         if (object instanceof HttpCookie) {
694             HttpCookie that = (HttpCookie) object;
695             return name.equalsIgnoreCase(that.getName())
696                     && (domain != null ? domain.equalsIgnoreCase(that.domain) : that.domain == null)
697                     && Objects.equal(path, that.path);
698         }
699         return false;
700     }
701 
702     /**
703      * Returns the hash code of this HTTP cookie: <pre>   {@code
704      *   name.toLowerCase(Locale.US).hashCode()
705      *       + (domain == null ? 0 : domain.toLowerCase(Locale.US).hashCode())
706      *       + (path == null ? 0 : path.hashCode())
707      * }</pre>
708      */
709     @Override public int hashCode() {
710         return name.toLowerCase(Locale.US).hashCode()
711                 + (domain == null ? 0 : domain.toLowerCase(Locale.US).hashCode())
712                 + (path == null ? 0 : path.hashCode());
713     }
714 
715     /**
716      * Returns a string representing this cookie in the format used by the
717      * {@code Cookie} header line in an HTTP request as specified by RFC 2965 section 3.3.4.
718      *
719      * <p>The resulting string does not include a "Cookie:" prefix or any version information.
720      * The returned {@code String} is not suitable for passing to {@link #parse(String)}: Several of
721      * the attributes that would be needed to preserve all of the cookie's information are omitted.
722      * The String is formatted for an HTTP request not an HTTP response.
723      *
724      * <p>The attributes included and the format depends on the cookie's {@code version}:
725      * <ul>
726      *     <li>Version 0: Includes only the name and value. Conforms to RFC 2965 (for
727      *     version 0 cookies). This should also be used to conform with RFC 6265.
728      *     </li>
729      *     <li>Version 1: Includes the name and value, and Path, Domain and Port attributes.
730      *     Conforms to RFC 2965 (for version 1 cookies).</li>
731      * </ul>
732      */
733     @Override public String toString() {
734         if (version == 0) {
735             return name + "=" + value;
736         }
737 
738         StringBuilder result = new StringBuilder()
739                 .append(name)
740                 .append("=")
741                 .append("\"")
742                 .append(value)
743                 .append("\"");
744         appendAttribute(result, "Path", path);
745         appendAttribute(result, "Domain", domain);
746         appendAttribute(result, "Port", portList);
747         return result.toString();
748     }
749 
750     private void appendAttribute(StringBuilder builder, String name, String value) {
751         if (value != null && builder != null) {
752             builder.append(";$");
753             builder.append(name);
754             builder.append("=\"");
755             builder.append(value);
756             builder.append("\"");
757         }
758     }
759 }
760