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 // 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 */ 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