HttpCookie.java revision 3bb69fa0b8fe5119c3f19cd7f5d725118aa506af
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.util.ArrayList; 20import java.util.Arrays; 21import java.util.Date; 22import java.util.HashSet; 23import java.util.List; 24import java.util.Locale; 25import java.util.Set; 26import libcore.net.http.HttpDate; 27import 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 */ 65public 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 231 CookieParser(String input) { 232 this.input = input; 233 this.inputLowerCase = input.toLowerCase(Locale.US); 234 } 235 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 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 hasMaxAge = true; 339 cookie.maxAge = Long.parseLong(value); 340 } else if (name.equals("path") && cookie.path == null) { 341 cookie.path = value; 342 } else if (name.equals("port") && cookie.portList == null) { 343 cookie.portList = value != null ? value : ""; 344 } else if (name.equals("secure")) { 345 cookie.secure = true; 346 } else if (name.equals("httponly")) { 347 cookie.httpOnly = true; 348 } else if (name.equals("version") && !hasVersion) { 349 cookie.version = Integer.parseInt(value); 350 } 351 } 352 353 /** 354 * Returns the next attribute name, or null if the input has been 355 * exhausted. Returns wth the cursor on the delimiter that follows. 356 */ 357 private String readAttributeName(boolean returnLowerCase) { 358 skipWhitespace(); 359 int c = find(ATTRIBUTE_NAME_TERMINATORS); 360 String forSubstring = returnLowerCase ? inputLowerCase : input; 361 String result = pos < c ? forSubstring.substring(pos, c) : null; 362 pos = c; 363 return result; 364 } 365 366 /** 367 * Returns true if an equals sign was read and consumed. 368 */ 369 private boolean readEqualsSign() { 370 skipWhitespace(); 371 if (pos < input.length() && input.charAt(pos) == '=') { 372 pos++; 373 return true; 374 } 375 return false; 376 } 377 378 /** 379 * Reads an attribute value, by parsing either a quoted string or until 380 * the next character in {@code terminators}. The terminator character 381 * is not consumed. 382 */ 383 private String readAttributeValue(String terminators) { 384 skipWhitespace(); 385 386 /* 387 * Quoted string: read 'til the close quote. The spec mentions only "double quotes" 388 * but RI bug 6901170 claims that 'single quotes' are also used. 389 */ 390 if (pos < input.length() && (input.charAt(pos) == '"' || input.charAt(pos) == '\'')) { 391 char quoteCharacter = input.charAt(pos++); 392 int closeQuote = input.indexOf(quoteCharacter, pos); 393 if (closeQuote == -1) { 394 throw new IllegalArgumentException("Unterminated string literal in " + input); 395 } 396 String result = input.substring(pos, closeQuote); 397 pos = closeQuote + 1; 398 return result; 399 } 400 401 int c = find(terminators); 402 String result = input.substring(pos, c); 403 pos = c; 404 return result; 405 } 406 407 /** 408 * Returns the index of the next character in {@code chars}, or the end 409 * of the string. 410 */ 411 private int find(String chars) { 412 for (int c = pos; c < input.length(); c++) { 413 if (chars.indexOf(input.charAt(c)) != -1) { 414 return c; 415 } 416 } 417 return input.length(); 418 } 419 420 private void skipWhitespace() { 421 for (; pos < input.length(); pos++) { 422 if (WHITESPACE.indexOf(input.charAt(pos)) == -1) { 423 break; 424 } 425 } 426 } 427 } 428 429 private String comment; 430 private String commentURL; 431 private boolean discard; 432 private String domain; 433 private long maxAge = -1l; 434 private final String name; 435 private String path; 436 private String portList; 437 private boolean secure; 438 private boolean httpOnly; 439 private String value; 440 private int version = 1; 441 442 /** 443 * Creates a new cookie. 444 * 445 * @param name a non-empty string that contains only printable ASCII, no 446 * commas or semicolons, and is not prefixed with {@code $}. May not be 447 * an HTTP attribute name. 448 * @param value an opaque value from the HTTP server. 449 * @throws IllegalArgumentException if {@code name} is invalid. 450 */ 451 public HttpCookie(String name, String value) { 452 String ntrim = name.trim(); // erase leading and trailing whitespace 453 if (!isValidName(ntrim)) { 454 throw new IllegalArgumentException("Invalid name: " + name); 455 } 456 457 this.name = ntrim; 458 this.value = value; 459 } 460 461 462 private boolean isValidName(String n) { 463 // name cannot be empty or begin with '$' or equals the reserved 464 // attributes (case-insensitive) 465 boolean isValid = !(n.length() == 0 || n.startsWith("$") 466 || RESERVED_NAMES.contains(n.toLowerCase(Locale.US))); 467 if (isValid) { 468 for (int i = 0; i < n.length(); i++) { 469 char nameChar = n.charAt(i); 470 // name must be ASCII characters and cannot contain ';', ',' and 471 // whitespace 472 if (nameChar < 0 473 || nameChar >= 127 474 || nameChar == ';' 475 || nameChar == ',' 476 || (Character.isWhitespace(nameChar) && nameChar != ' ')) { 477 isValid = false; 478 break; 479 } 480 } 481 } 482 return isValid; 483 } 484 485 /** 486 * Returns the {@code Comment} attribute. 487 */ 488 public String getComment() { 489 return comment; 490 } 491 492 /** 493 * Returns the value of {@code CommentURL} attribute. 494 */ 495 public String getCommentURL() { 496 return commentURL; 497 } 498 499 /** 500 * Returns the {@code Discard} attribute. 501 */ 502 public boolean getDiscard() { 503 return discard; 504 } 505 506 /** 507 * Returns the {@code Domain} attribute. 508 */ 509 public String getDomain() { 510 return domain; 511 } 512 513 /** 514 * Returns the {@code Max-Age} attribute, in delta-seconds. 515 */ 516 public long getMaxAge() { 517 return maxAge; 518 } 519 520 /** 521 * Returns the name of this cookie. 522 */ 523 public String getName() { 524 return name; 525 } 526 527 /** 528 * Returns the {@code Path} attribute. This cookie is visible to all 529 * subpaths. 530 */ 531 public String getPath() { 532 return path; 533 } 534 535 /** 536 * Returns the {@code Port} attribute, usually containing comma-separated 537 * port numbers. A null port indicates that the cookie may be sent to any 538 * port. The empty string indicates that the cookie should only be sent to 539 * the port of the originating request. 540 */ 541 public String getPortlist() { 542 return portList; 543 } 544 545 /** 546 * Returns the {@code Secure} attribute. 547 */ 548 public boolean getSecure() { 549 return secure; 550 } 551 552 /** 553 * Returns the {@code HttpOnly} attribute. If {@code true} the cookie should not be accessible 554 * to scripts in a browser. 555 * 556 * @since 1.7 557 * @hide Until ready for an API update 558 */ 559 public boolean isHttpOnly() { 560 return httpOnly; 561 } 562 563 /** 564 * Returns the {@code HttpOnly} attribute. If {@code true} the cookie should not be accessible 565 * to scripts in a browser. 566 * 567 * @since 1.7 568 * @hide Until ready for an API update 569 */ 570 public void setHttpOnly(boolean httpOnly) { 571 this.httpOnly = httpOnly; 572 } 573 574 /** 575 * Returns the value of this cookie. 576 */ 577 public String getValue() { 578 return value; 579 } 580 581 /** 582 * Returns the version of this cookie. 583 */ 584 public int getVersion() { 585 return version; 586 } 587 588 /** 589 * Returns true if this cookie's Max-Age is 0. 590 */ 591 public boolean hasExpired() { 592 // -1 indicates the cookie will persist until browser shutdown 593 // so the cookie is not expired. 594 if (maxAge == -1l) { 595 return false; 596 } 597 598 boolean expired = false; 599 if (maxAge <= 0l) { 600 expired = true; 601 } 602 return expired; 603 } 604 605 /** 606 * Set the {@code Comment} attribute of this cookie. 607 */ 608 public void setComment(String comment) { 609 this.comment = comment; 610 } 611 612 /** 613 * Set the {@code CommentURL} attribute of this cookie. 614 */ 615 public void setCommentURL(String commentURL) { 616 this.commentURL = commentURL; 617 } 618 619 /** 620 * Set the {@code Discard} attribute of this cookie. 621 */ 622 public void setDiscard(boolean discard) { 623 this.discard = discard; 624 } 625 626 /** 627 * Set the {@code Domain} attribute of this cookie. HTTP clients send 628 * cookies only to matching domains. 629 */ 630 public void setDomain(String pattern) { 631 domain = pattern == null ? null : pattern.toLowerCase(Locale.US); 632 } 633 634 /** 635 * Sets the {@code Max-Age} attribute of this cookie. 636 */ 637 public void setMaxAge(long deltaSeconds) { 638 maxAge = deltaSeconds; 639 } 640 641 private void setExpires(Date expires) { 642 maxAge = (expires.getTime() - System.currentTimeMillis()) / 1000; 643 } 644 645 /** 646 * Set the {@code Path} attribute of this cookie. HTTP clients send cookies 647 * to this path and its subpaths. 648 */ 649 public void setPath(String path) { 650 this.path = path; 651 } 652 653 /** 654 * Set the {@code Port} attribute of this cookie. 655 */ 656 public void setPortlist(String portList) { 657 this.portList = portList; 658 } 659 660 /** 661 * Sets the {@code Secure} attribute of this cookie. 662 */ 663 public void setSecure(boolean secure) { 664 this.secure = secure; 665 } 666 667 /** 668 * Sets the opaque value of this cookie. 669 */ 670 public void setValue(String value) { 671 // FIXME: According to spec, version 0 cookie value does not allow many 672 // symbols. But RI does not implement it. Follow RI temporarily. 673 this.value = value; 674 } 675 676 /** 677 * Sets the {@code Version} attribute of the cookie. 678 * 679 * @throws IllegalArgumentException if v is neither 0 nor 1 680 */ 681 public void setVersion(int newVersion) { 682 if (newVersion != 0 && newVersion != 1) { 683 throw new IllegalArgumentException("Bad version: " + newVersion); 684 } 685 version = newVersion; 686 } 687 688 @Override public Object clone() { 689 try { 690 return super.clone(); 691 } catch (CloneNotSupportedException e) { 692 throw new AssertionError(); 693 } 694 } 695 696 /** 697 * Returns true if {@code object} is a cookie with the same domain, name and 698 * path. Domain and name use case-insensitive comparison; path uses a 699 * case-sensitive comparison. 700 */ 701 @Override public boolean equals(Object object) { 702 if (object == this) { 703 return true; 704 } 705 if (object instanceof HttpCookie) { 706 HttpCookie that = (HttpCookie) object; 707 return name.equalsIgnoreCase(that.getName()) 708 && (domain != null ? domain.equalsIgnoreCase(that.domain) : that.domain == null) 709 && Objects.equal(path, that.path); 710 } 711 return false; 712 } 713 714 /** 715 * Returns the hash code of this HTTP cookie: <pre> {@code 716 * name.toLowerCase(Locale.US).hashCode() 717 * + (domain == null ? 0 : domain.toLowerCase(Locale.US).hashCode()) 718 * + (path == null ? 0 : path.hashCode()) 719 * }</pre> 720 */ 721 @Override public int hashCode() { 722 return name.toLowerCase(Locale.US).hashCode() 723 + (domain == null ? 0 : domain.toLowerCase(Locale.US).hashCode()) 724 + (path == null ? 0 : path.hashCode()); 725 } 726 727 /** 728 * Returns a string representing this cookie in the format used by the 729 * {@code Cookie} header line in an HTTP request as specified by RFC 2965 section 3.3.4. 730 * 731 * <p>The resulting string does not include a "Cookie:" prefix or any version information. 732 * The returned {@code String} is not suitable for passing to {@link #parse(String)}: Several of 733 * the attributes that would be needed to preserve all of the cookie's information are omitted. 734 * The String is formatted for an HTTP request not an HTTP response. 735 * 736 * <p>The attributes included and the format depends on the cookie's {@code version}: 737 * <ul> 738 * <li>Version 0: Includes only the name and value. Conforms to RFC 2965 (for 739 * version 0 cookies). This should also be used to conform with RFC 6265. 740 * </li> 741 * <li>Version 1: Includes the name and value, and Path, Domain and Port attributes. 742 * Conforms to RFC 2965 (for version 1 cookies).</li> 743 * </ul> 744 */ 745 @Override public String toString() { 746 if (version == 0) { 747 return name + "=" + value; 748 } 749 750 StringBuilder result = new StringBuilder() 751 .append(name) 752 .append("=") 753 .append("\"") 754 .append(value) 755 .append("\""); 756 appendAttribute(result, "Path", path); 757 appendAttribute(result, "Domain", domain); 758 appendAttribute(result, "Port", portList); 759 return result.toString(); 760 } 761 762 private void appendAttribute(StringBuilder builder, String name, String value) { 763 if (value != null && builder != null) { 764 builder.append(";$"); 765 builder.append(name); 766 builder.append("=\""); 767 builder.append(value); 768 builder.append("\""); 769 } 770 } 771} 772