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