HttpCookie.java revision d138a32a96aef19d6ae3bd7ead3fbfef1a5f8217
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.Date; 21import java.util.HashMap; 22import java.util.List; 23import java.util.Locale; 24import java.util.regex.Matcher; 25import java.util.regex.Pattern; 26 27import org.apache.harmony.luni.util.Msg; 28 29/** 30 * This class represents a http cookie, which indicates the status information 31 * between the client agent side and the server side. According to RFC, there 32 * are 3 http cookie specifications. This class is compatible with all the three 33 * forms. HttpCookie class can accept all these 3 forms of syntax. 34 * 35 * @since 1.6 36 */ 37public final class HttpCookie implements Cloneable { 38 39 private abstract static class Setter { 40 boolean set; 41 42 Setter() { 43 set = false; 44 } 45 46 boolean isSet() { 47 return set; 48 } 49 50 void set(boolean isSet) { 51 set = isSet; 52 } 53 54 abstract void setValue(String value, HttpCookie cookie); 55 56 void validate(String value, HttpCookie cookie) { 57 if (cookie.getVersion() == 1 && value != null 58 && value.contains(COMMA_STR)) { 59 throw new IllegalArgumentException(); 60 } 61 } 62 } 63 64 private static final String DOT_STR = "."; //$NON-NLS-1$ 65 66 private static final String LOCAL_STR = ".local"; //$NON-NLS-1$ 67 68 private static final String QUOTE_STR = "\""; //$NON-NLS-1$ 69 70 private static final String COMMA_STR = ","; //$NON-NLS-1$ 71 72 private static Pattern HEAD_PATTERN = Pattern.compile("Set-Cookie2?:", //$NON-NLS-1$ 73 Pattern.CASE_INSENSITIVE); 74 75 private static Pattern NAME_PATTERN = Pattern 76 .compile( 77 "([^$=,\u0085\u2028\u2029][^,\n\t\r\r\n\u0085\u2028\u2029]*?)=([^;]*)(;)?", //$NON-NLS-1$ 78 Pattern.DOTALL | Pattern.CASE_INSENSITIVE); 79 80 private static Pattern ATTR_PATTERN0 = Pattern 81 .compile("([^;=]*)(?:=([^;]*))?"); //$NON-NLS-1$ 82 83 private static Pattern ATTR_PATTERN1 = Pattern 84 .compile("(,?[^;=]*)(?:=([^;,]*))?((?=.))?"); //$NON-NLS-1$ 85 86 private HashMap<String, Setter> attributeSet = new HashMap<String, Setter>(); 87 88 /** 89 * A utility method used to check whether the host name is in a domain or 90 * not. 91 * 92 * @param domain 93 * the domain to be checked against 94 * @param host 95 * the host to be checked 96 * @return true if the host is in the domain, false otherwise 97 */ 98 public static boolean domainMatches(String domain, String host) { 99 if (domain == null || host == null) { 100 return false; 101 } 102 String newDomain = domain.toLowerCase(); 103 String newHost = host.toLowerCase(); 104 return isValidDomain(newDomain) && effDomainMatches(newDomain, newHost) 105 && isValidHost(newDomain, newHost); 106 } 107 108 private static boolean effDomainMatches(String domain, String host) { 109 // calculate effective host name 110 String effHost = host.indexOf(DOT_STR) != -1 ? host 111 : (host + LOCAL_STR); 112 113 // Rule 2: domain and host are string-compare equal, or A = NB, B = .B' 114 // and N is a non-empty name string 115 boolean inDomain = domain.equals(effHost); 116 inDomain = inDomain 117 || (effHost.endsWith(domain) 118 && effHost.length() > domain.length() && domain 119 .startsWith(DOT_STR)); 120 return inDomain; 121 } 122 123 private static boolean isCommaDelim(HttpCookie cookie) { 124 String value = cookie.getValue(); 125 if (value.startsWith(QUOTE_STR) && value.endsWith(QUOTE_STR)) { 126 cookie.setValue(value.substring(1, value.length() - 1)); 127 return false; 128 } 129 if (cookie.getVersion() == 1 && value.contains(COMMA_STR)) { 130 cookie.setValue(value.substring(0, value.indexOf(COMMA_STR))); 131 return true; 132 } 133 return false; 134 } 135 136 private static boolean isValidDomain(String domain) { 137 // Rule 1: The value for Domain contains embedded dots, or is .local 138 if (domain.length() <= 2) { 139 return false; 140 } 141 return domain.substring(1, domain.length() - 1).indexOf(DOT_STR) != -1 142 || domain.equals(LOCAL_STR); 143 } 144 145 private static boolean isValidHost(String domain, String host) { 146 // Rule 3: host does not end with domain, or the remainder does not 147 // contain "." 148 boolean matches = !host.endsWith(domain); 149 if (!matches) { 150 String hostSub = host.substring(0, host.length() - domain.length()); 151 matches = hostSub.indexOf(DOT_STR) == -1; 152 } 153 return matches; 154 } 155 156 /** 157 * Constructs a cookie from a string. The string should comply with 158 * set-cookie or set-cookie2 header format as specified in RFC 2965. Since 159 * set-cookies2 syntax allows more than one cookie definitions in one 160 * header, the returned object is a list. 161 * 162 * @param header 163 * a set-cookie or set-cookie2 header. 164 * @return a list of constructed cookies 165 * @throws IllegalArgumentException 166 * if the string does not comply with cookie specification, or 167 * the cookie name contains illegal characters, or reserved 168 * tokens of cookie specification appears 169 * @throws NullPointerException 170 * if header is null 171 */ 172 public static List<HttpCookie> parse(String header) { 173 Matcher matcher = HEAD_PATTERN.matcher(header); 174 // Parse cookie name & value 175 List<HttpCookie> list = null; 176 HttpCookie cookie = null; 177 String headerString = header; 178 int version = 0; 179 // process set-cookie | set-cookie2 head 180 if (matcher.find()) { 181 String cookieHead = matcher.group(); 182 if ("set-cookie2:".equalsIgnoreCase(cookieHead)) { //$NON-NLS-1$ 183 version = 1; 184 } 185 headerString = header.substring(cookieHead.length()); 186 } 187 188 // parse cookie name/value pair 189 matcher = NAME_PATTERN.matcher(headerString); 190 if (matcher.lookingAt()) { 191 list = new ArrayList<HttpCookie>(); 192 cookie = new HttpCookie(matcher.group(1), matcher.group(2)); 193 cookie.setVersion(version); 194 195 /* 196 * Comma is a delimiter in cookie spec 1.1. If find comma in version 197 * 1 cookie header, part of matched string need to be spitted out. 198 */ 199 String nameGroup = matcher.group(); 200 if (isCommaDelim(cookie)) { 201 headerString = headerString.substring(nameGroup 202 .indexOf(COMMA_STR)); 203 } else { 204 headerString = headerString.substring(nameGroup.length()); 205 } 206 list.add(cookie); 207 } else { 208 throw new IllegalArgumentException(); 209 } 210 211 // parse cookie headerString 212 while (!(headerString.length() == 0)) { 213 matcher = cookie.getVersion() == 1 ? ATTR_PATTERN1 214 .matcher(headerString) : ATTR_PATTERN0 215 .matcher(headerString); 216 217 if (matcher.lookingAt()) { 218 String attrName = matcher.group(1).trim(); 219 220 // handle special situation like: <..>;;<..> 221 if (attrName.length() == 0) { 222 headerString = headerString.substring(1); 223 continue; 224 } 225 226 // If port is the attribute, then comma will not be used as a 227 // delimiter 228 if (attrName.equalsIgnoreCase("port") //$NON-NLS-1$ 229 || attrName.equalsIgnoreCase("expires")) { //$NON-NLS-1$ 230 int start = matcher.regionStart(); 231 matcher = ATTR_PATTERN0.matcher(headerString); 232 matcher.region(start, headerString.length()); 233 matcher.lookingAt(); 234 } else if (cookie.getVersion() == 1 235 && attrName.startsWith(COMMA_STR)) { 236 // If the last encountered token is comma, and the parsed 237 // attribute is not port, then this attribute/value pair 238 // ends. 239 headerString = headerString.substring(1); 240 matcher = NAME_PATTERN.matcher(headerString); 241 if (matcher.lookingAt()) { 242 cookie = new HttpCookie(matcher.group(1), matcher 243 .group(2)); 244 list.add(cookie); 245 headerString = headerString.substring(matcher.group() 246 .length()); 247 continue; 248 } 249 } 250 251 Setter setter = cookie.attributeSet.get(attrName.toLowerCase()); 252 if (null == setter) { 253 throw new IllegalArgumentException(); 254 } 255 if (!setter.isSet()) { 256 String attrValue = matcher.group(2); 257 setter.validate(attrValue, cookie); 258 setter.setValue(matcher.group(2), cookie); 259 } 260 headerString = headerString.substring(matcher.end()); 261 } 262 } 263 264 return list; 265 } 266 267 private String comment; 268 269 private String commentURL; 270 271 private boolean discard; 272 273 private String domain; 274 275 private long maxAge = -1l; 276 277 private String name; 278 279 private String path; 280 281 private String portList; 282 283 private boolean secure; 284 285 private String value; 286 287 private int version = 1; 288 289 { 290 attributeSet.put("comment", new Setter() { //$NON-NLS-1$ 291 @Override 292 void setValue(String value, HttpCookie cookie) { 293 cookie.setComment(value); 294 if (cookie.getComment() != null) { 295 set(true); 296 } 297 } 298 }); 299 attributeSet.put("commenturl", new Setter() { //$NON-NLS-1$ 300 @Override 301 void setValue(String value, HttpCookie cookie) { 302 cookie.setCommentURL(value); 303 if (cookie.getCommentURL() != null) { 304 set(true); 305 } 306 } 307 }); 308 attributeSet.put("discard", new Setter() { //$NON-NLS-1$ 309 @Override 310 void setValue(String value, HttpCookie cookie) { 311 cookie.setDiscard(true); 312 set(true); 313 } 314 }); 315 attributeSet.put("domain", new Setter() { //$NON-NLS-1$ 316 @Override 317 void setValue(String value, HttpCookie cookie) { 318 cookie.setDomain(value); 319 if (cookie.getDomain() != null) { 320 set(true); 321 } 322 } 323 }); 324 attributeSet.put("max-age", new Setter() { //$NON-NLS-1$ 325 @Override 326 void setValue(String value, HttpCookie cookie) { 327 try { 328 cookie.setMaxAge(Long.parseLong(value)); 329 } catch (NumberFormatException e) { 330 throw new IllegalArgumentException(Msg.getString( 331 "KB001", "max-age")); //$NON-NLS-1$//$NON-NLS-2$ 332 } 333 set(true); 334 335 if (!attributeSet.get("version").isSet()) { //$NON-NLS-1$ 336 cookie.setVersion(1); 337 } 338 } 339 }); 340 341 attributeSet.put("path", new Setter() { //$NON-NLS-1$ 342 @Override 343 void setValue(String value, HttpCookie cookie) { 344 cookie.setPath(value); 345 if (cookie.getPath() != null) { 346 set(true); 347 } 348 } 349 }); 350 attributeSet.put("port", new Setter() { //$NON-NLS-1$ 351 @Override 352 void setValue(String value, HttpCookie cookie) { 353 cookie.setPortlist(value); 354 if (cookie.getPortlist() != null) { 355 set(true); 356 } 357 } 358 359 @Override 360 void validate(String v, HttpCookie cookie) { 361 return; 362 } 363 }); 364 attributeSet.put("secure", new Setter() { //$NON-NLS-1$ 365 @Override 366 void setValue(String value, HttpCookie cookie) { 367 cookie.setSecure(true); 368 set(true); 369 } 370 }); 371 attributeSet.put("version", new Setter() { //$NON-NLS-1$ 372 @Override 373 void setValue(String value, HttpCookie cookie) { 374 try { 375 int v = Integer.parseInt(value); 376 if (v > cookie.getVersion()) { 377 cookie.setVersion(v); 378 } 379 } catch (NumberFormatException e) { 380 throw new IllegalArgumentException(Msg.getString( 381 "KB001", "version"));//$NON-NLS-1$//$NON-NLS-2$ 382 } 383 if (cookie.getVersion() != 0) { 384 set(true); 385 } 386 } 387 }); 388 389 attributeSet.put("expires", new Setter() { //$NON-NLS-1$ 390 @Override 391 void setValue(String value, HttpCookie cookie) { 392 cookie.setVersion(0); 393 attributeSet.get("version").set(true); //$NON-NLS-1$ 394 if (!attributeSet.get("max-age").isSet()) { //$NON-NLS-1$ 395 attributeSet.get("max-age").set(true); //$NON-NLS-1$ 396 if (!"en".equalsIgnoreCase(Locale.getDefault() //$NON-NLS-1$ 397 .getLanguage())) { 398 cookie.setMaxAge(0); 399 return; 400 } 401 try { 402 cookie.setMaxAge((Date.parse(value) - System 403 .currentTimeMillis()) / 1000); 404 } catch (IllegalArgumentException e) { 405 cookie.setMaxAge(0); 406 } 407 } 408 } 409 410 @Override 411 void validate(String v, HttpCookie cookie) { 412 return; 413 } 414 }); 415 } 416 417 /** 418 * Initializes a cookie with the specified name and value. 419 * 420 * The name attribute can just contain ASCII characters, which is immutable 421 * after creation. Commas, white space and semicolons are not allowed. The $ 422 * character is also not allowed to be the beginning of the name. 423 * 424 * The value attribute depends on what the server side is interested. The 425 * setValue method can be used to change it. 426 * 427 * RFC 2965 is the default cookie specification of this class. If one wants 428 * to change the version of the cookie, the setVersion method is available. 429 * 430 * @param name - 431 * the specific name of the cookie 432 * @param value - 433 * the specific value of the cookie 434 * 435 * @throws IllegalArgumentException - 436 * if the name contains not-allowed or reserved characters 437 * 438 * @throws NullPointerException 439 * if the value of name is null 440 */ 441 public HttpCookie(String name, String value) { 442 String ntrim = name.trim(); // erase leading and trailing whitespaces 443 if (!isValidName(ntrim)) { 444 throw new IllegalArgumentException(Msg.getString("KB002")); //$NON-NLS-1$ 445 } 446 447 this.name = ntrim; 448 this.value = value; 449 } 450 451 private void attrToString(StringBuilder builder, String attrName, 452 String attrValue) { 453 if (attrValue != null && builder != null) { 454 builder.append(";"); //$NON-NLS-1$ 455 builder.append("$");//$NON-NLS-1$ 456 builder.append(attrName); 457 builder.append("=\""); //$NON-NLS-1$ 458 builder.append(attrValue); 459 builder.append(QUOTE_STR); 460 } 461 } 462 463 /** 464 * Answers a copy of this object. 465 * 466 * @return a copy of this cookie 467 */ 468 @Override 469 public Object clone() { 470 try { 471 HttpCookie obj = (HttpCookie) super.clone(); 472 return obj; 473 } catch (CloneNotSupportedException e) { 474 return null; 475 } 476 } 477 478 /** 479 * Answers whether two cookies are equal. Two cookies are equal if they have 480 * the same domain and name in a case-insensitive mode and path in a 481 * case-sensitive mode. 482 * 483 * @param obj 484 * the object to be compared. 485 * @return true if two cookies equals, false otherwise 486 */ 487 @Override 488 public boolean equals(Object obj) { 489 if (obj == this) { 490 return true; 491 } 492 if (obj instanceof HttpCookie) { 493 HttpCookie anotherCookie = (HttpCookie) obj; 494 if (name.equalsIgnoreCase(anotherCookie.getName())) { 495 String anotherDomain = anotherCookie.getDomain(); 496 boolean equals = domain == null ? anotherDomain == null 497 : domain.equalsIgnoreCase(anotherDomain); 498 if (equals) { 499 String anotherPath = anotherCookie.getPath(); 500 return path == null ? anotherPath == null : path 501 .equals(anotherPath); 502 } 503 } 504 } 505 return false; 506 } 507 508 /** 509 * Answers the value of comment attribute(specified in RFC 2965) of this 510 * cookie. 511 * 512 * @return the value of comment attribute 513 */ 514 public String getComment() { 515 return comment; 516 } 517 518 /** 519 * Answers the value of commentURL attribute(specified in RFC 2965) of this 520 * cookie. 521 * 522 * @return the value of commentURL attribute 523 */ 524 public String getCommentURL() { 525 return commentURL; 526 } 527 528 /** 529 * Answers the value of discard attribute(specified in RFC 2965) of this 530 * cookie. 531 * 532 * @return discard value of this cookie 533 */ 534 public boolean getDiscard() { 535 return discard; 536 } 537 538 /** 539 * Answers the domain name for this cookie in the format specified in RFC 540 * 2965 541 * 542 * @return the domain value of this cookie 543 */ 544 public String getDomain() { 545 return domain; 546 } 547 548 /** 549 * Returns the Max-Age value as specified in RFC 2965 of this cookie. 550 * 551 * @return the Max-Age value 552 */ 553 public long getMaxAge() { 554 return maxAge; 555 } 556 557 /** 558 * Answers the name for this cookie. 559 * 560 * @return the name for this cookie 561 */ 562 public String getName() { 563 return name; 564 } 565 566 /** 567 * Answers the path part of a request URL to which this cookie is returned. 568 * This cookie is visible to all subpaths. 569 * 570 * @return the path used to return the cookie 571 */ 572 public String getPath() { 573 return path; 574 } 575 576 /** 577 * Answers the value of port attribute(specified in RFC 2965) of this 578 * cookie. 579 * 580 * @return port list of this cookie 581 */ 582 public String getPortlist() { 583 return portList; 584 } 585 586 /** 587 * Answers true if the browser only sends cookies over a secure protocol. 588 * False if can send cookies through any protocols. 589 * 590 * @return true if sends cookies only through secure protocol, false 591 * otherwise 592 */ 593 public boolean getSecure() { 594 return secure; 595 } 596 597 /** 598 * Answers the value of this cookie. 599 * 600 * @return the value of this cookie 601 */ 602 public String getValue() { 603 return value; 604 } 605 606 /** 607 * Get the version of this cookie 608 * 609 * @return 0 indicates the original Netscape cookie specification, while 1 610 * indicates RFC 2965/2109 specification. 611 */ 612 public int getVersion() { 613 return version; 614 } 615 616 /** 617 * Answers whether the cookie has expired. 618 * 619 * @return true is the cookie has expired, false otherwise 620 */ 621 public boolean hasExpired() { 622 // -1 indicates the cookie will persist until browser shutdown 623 // so the cookie is not expired. 624 if (maxAge == -1l) { 625 return false; 626 } 627 628 boolean expired = false; 629 if (maxAge <= 0l) { 630 expired = true; 631 } 632 return expired; 633 } 634 635 /** 636 * Answers hash code of this http cookie. The result is calculated as below: 637 * 638 * getName().toLowerCase().hashCode() + getDomain().toLowerCase().hashCode() + 639 * getPath().hashCode() 640 * 641 * @return the hash code of this cookie 642 */ 643 @Override 644 public int hashCode() { 645 int hashCode = name.toLowerCase().hashCode(); 646 hashCode += domain == null ? 0 : domain.toLowerCase().hashCode(); 647 hashCode += path == null ? 0 : path.hashCode(); 648 return hashCode; 649 } 650 651 private boolean isValidName(String n) { 652 // name cannot be empty or begin with '$' or equals the reserved 653 // attributes (case-insensitive) 654 boolean isValid = !(n.length() == 0 || n.startsWith("$") || attributeSet.containsKey(n.toLowerCase())); //$NON-NLS-1$ 655 if (isValid) { 656 for (int i = 0; i < n.length(); i++) { 657 char nameChar = n.charAt(i); 658 // name must be ASCII characters and cannot contain ';', ',' and 659 // whitespace 660 if (nameChar < 0 661 || nameChar >= 127 662 || nameChar == ';' 663 || nameChar == ',' 664 || (Character.isWhitespace(nameChar) && nameChar != ' ')) { 665 isValid = false; 666 break; 667 } 668 } 669 } 670 return isValid; 671 } 672 673 /** 674 * Set the value of comment attribute(specified in RFC 2965) of this cookie. 675 * 676 * @param purpose 677 * the comment value to be set 678 */ 679 public void setComment(String purpose) { 680 comment = purpose; 681 } 682 683 /** 684 * Set the value of commentURL attribute(specified in RFC 2965) of this 685 * cookie. 686 * 687 * @param purpose 688 * the value of commentURL attribute to be set 689 */ 690 public void setCommentURL(String purpose) { 691 commentURL = purpose; 692 } 693 694 /** 695 * Set the value of discard attribute(specified in RFC 2965) of this cookie. 696 * 697 * @param discard 698 * the value for discard attribute 699 */ 700 public void setDiscard(boolean discard) { 701 this.discard = discard; 702 } 703 704 /** 705 * Set the domain value for this cookie. Browsers send the cookie to the 706 * domain specified by this value. The form of the domain is specified in 707 * RFC 2965. 708 * 709 * @param pattern 710 * the domain pattern 711 */ 712 public void setDomain(String pattern) { 713 domain = pattern == null ? null : pattern.toLowerCase(); 714 } 715 716 /** 717 * Sets the Max-Age value as specified in RFC 2965 of this cookie to expire. 718 * 719 * @param expiry 720 * the value used to set the Max-Age value of this cookie 721 */ 722 public void setMaxAge(long expiry) { 723 maxAge = expiry; 724 } 725 726 /** 727 * Set the path to which this cookie is returned. This cookie is visible to 728 * all the pages under the path and all subpaths. 729 * 730 * @param path 731 * the path to which this cookie is returned 732 */ 733 public void setPath(String path) { 734 this.path = path; 735 } 736 737 /** 738 * Set the value of port attribute(specified in RFC 2965) of this cookie. 739 * 740 * @param ports 741 * the value for port attribute 742 */ 743 public void setPortlist(String ports) { 744 portList = ports; 745 } 746 747 /* 748 * Handle 2 special cases: 1. value is wrapped by a quotation 2. value 749 * contains comma 750 */ 751 752 /** 753 * Tells the browser whether the cookies should be sent to server through 754 * secure protocols. 755 * 756 * @param flag 757 * tells browser to send cookie to server only through secure 758 * protocol if flag is true 759 */ 760 public void setSecure(boolean flag) { 761 secure = flag; 762 } 763 764 /** 765 * Sets the value for this cookie after it has been instantiated. String 766 * newValue can be in BASE64 form. If the version of the cookie is 0, 767 * special value as: white space, brackets, parentheses, equals signs, 768 * commas, double quotes, slashes, question marks, at signs, colons, and 769 * semicolons are not recommended. Empty values may lead to different 770 * behavior on different browsers. 771 * 772 * @param newValue 773 * the value for this cookie 774 */ 775 public void setValue(String newValue) { 776 // FIXME: According to spec, version 0 cookie value does not allow many 777 // symbols. But RI does not implement it. Follow RI temporarily. 778 value = newValue; 779 } 780 781 /** 782 * Sets the version of the cookie. 0 indicates the original Netscape cookie 783 * specification, while 1 indicates RFC 2965/2109 specification. 784 * 785 * @param v 786 * 0 or 1 as stated above 787 * @throws IllegalArgumentException 788 * if v is neither 0 nor 1 789 */ 790 public void setVersion(int v) { 791 if (v != 0 && v != 1) { 792 throw new IllegalArgumentException(Msg.getString("KB003")); //$NON-NLS-1$ 793 } 794 version = v; 795 } 796 797 /** 798 * Returns a string to represent the cookie. The format of string follows 799 * the cookie specification. The leading token "Cookie" is not included 800 * 801 * @return the string format of the cookie object 802 */ 803 @Override 804 public String toString() { 805 StringBuilder cookieStr = new StringBuilder(); 806 cookieStr.append(name); 807 cookieStr.append("="); //$NON-NLS-1$ 808 if (version == 0) { 809 cookieStr.append(value); 810 } else if (version == 1) { 811 cookieStr.append(QUOTE_STR); 812 cookieStr.append(value); 813 cookieStr.append(QUOTE_STR); 814 815 attrToString(cookieStr, "Path", path); //$NON-NLS-1$ 816 attrToString(cookieStr, "Domain", domain); //$NON-NLS-1$ 817 attrToString(cookieStr, "Port", portList);//$NON-NLS-1$ 818 } 819 return cookieStr.toString(); 820 } 821}