This project has retired. For details please refer to its
Attic page.
CmisHttpCookie xref
1/*2 * Licensed to the Apache Software Foundation (ASF) under one3 * or more contributor license agreements. See the NOTICE file4 * distributed with this work for additional information5 * regarding copyright ownership. The ASF licenses this file6 * to you under the Apache License, Version 2.0 (the7 * "License"); you may not use this file except in compliance8 * with the License. You may obtain a copy of the License at9 *10 * http://www.apache.org/licenses/LICENSE-2.011 *12 * Unless required by applicable law or agreed to in writing,13 * software distributed under the License is distributed on an14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY15 * KIND, either express or implied. See the License for the16 * specific language governing permissions and limitations17 * under the License.18 */19/*20 * This class has been taken from Apache Harmony (http://harmony.apache.org/)21 * and has been modified to work with OpenCMIS.22 */23package org.apache.chemistry.opencmis.client.bindings.spi.cookies;
2425import java.io.Serializable;
26import java.util.ArrayList;
27import java.util.Date;
28import java.util.HashMap;
29import java.util.List;
30import java.util.Locale;
31import java.util.regex.Matcher;
32import java.util.regex.Pattern;
3334/**35 * This class represents a http cookie, which indicates the status information36 * between the client agent side and the server side. According to RFC, there37 * are 3 http cookie specifications. This class is compatible with all the three38 * forms. HttpCookie class can accept all these 3 forms of syntax.39 */40publicfinalclassCmisHttpCookie implements Cloneable, Serializable {
4142privatestaticfinallong serialVersionUID = 1L;
4344privatestaticfinal String DOT_STR = ".";
45privatestaticfinal String LOCAL_STR = ".local";
46privatestaticfinal String QUOTE_STR = "\"";
47privatestaticfinal String COMMA_STR = ",";
48privatestatic Pattern HEAD_PATTERN = Pattern.compile("Set-Cookie2?:", Pattern.CASE_INSENSITIVE);
49privatestatic Pattern NAME_PATTERN = Pattern.compile(
50"([^$=,\u0085\u2028\u2029][^,\n\t\r\r\n\u0085\u2028\u2029]*?)=([^;]*)(;)?", Pattern.DOTALL
51 | Pattern.CASE_INSENSITIVE);
52privatestatic Pattern ATTR_PATTERN0 = Pattern.compile("([^;=]*)(?:=([^;]*))?");
53privatestatic Pattern ATTR_PATTERN1 = Pattern.compile("(,?[^;=]*)(?:=([^;,]*))?((?=.))?");
5455privateabstractstaticclassSetter {
56boolean set;
5758Setter() {
59 set = false;
60 }
6162boolean isSet() {
63return set;
64 }
6566void set(boolean isSet) {
67 set = isSet;
68 }
6970abstractvoid setValue(String value, CmisHttpCookie cookie);
7172void validate(String value, CmisHttpCookie cookie) {
73if (cookie.getVersion() == 1 && value != null && value.contains(COMMA_STR)) {
74thrownew IllegalArgumentException();
75 }
76 }
77 }
7879private HashMap<String, Setter> attributeSet = new HashMap<String, Setter>();
8081/**82 * A utility method used to check whether the host name is in a domain or83 * not.84 * 85 * @param domain86 * the domain to be checked against87 * @param host88 * the host to be checked89 * @return true if the host is in the domain, false otherwise90 */91publicstaticboolean domainMatches(String domain, String host) {
92if (domain == null || host == null) {
93return false;
94 }
95 String newDomain = domain.toLowerCase();
96 String newHost = host.toLowerCase();
97return newDomain.equals(newHost)
98 || (isValidDomain(newDomain) && effDomainMatches(newDomain, newHost) && isValidHost(newDomain, newHost));
99 }
100101privatestaticboolean effDomainMatches(String domain, String host) {
102// calculate effective host name103 String effHost = host.indexOf(DOT_STR) != -1 ? host : (host + LOCAL_STR);
104105// Rule 2: domain and host are string-compare equal, or A = NB, B = .B'106// and N is a non-empty name string107boolean inDomain = domain.equals(effHost);
108 inDomain = inDomain
109 || (effHost.endsWith(domain) && effHost.length() > domain.length() && domain.startsWith(DOT_STR));
110111return inDomain;
112 }
113114privatestaticboolean isCommaDelim(CmisHttpCookie cookie) {
115 String value = cookie.getValue();
116if (value.startsWith(QUOTE_STR) && value.endsWith(QUOTE_STR)) {
117 cookie.setValue(value.substring(1, value.length() - 1));
118return false;
119 }
120121if (cookie.getVersion() == 1 && value.contains(COMMA_STR)) {
122 cookie.setValue(value.substring(0, value.indexOf(COMMA_STR)));
123returntrue;
124 }
125126return false;
127 }
128129privatestaticboolean isValidDomain(String domain) {
130// Rule 1: The value for Domain contains embedded dots, or is .local131if (domain.length() <= 2) {
132return false;
133 }
134135return domain.substring(1, domain.length() - 1).indexOf(DOT_STR) != -1 || domain.equals(LOCAL_STR);
136 }
137138privatestaticboolean isValidHost(String domain, String host) {
139// Rule 3: host does not end with domain, or the remainder does not140// contain "."141boolean matches = !host.endsWith(domain);
142if (!matches) {
143 String hostSub = host.substring(0, host.length() - domain.length());
144 matches = hostSub.indexOf(DOT_STR) == -1;
145 }
146147return matches;
148 }
149150/**151 * Constructs a cookie from a string. The string should comply with152 * set-cookie or set-cookie2 header format as specified in RFC 2965. Since153 * set-cookies2 syntax allows more than one cookie definitions in one154 * header, the returned object is a list.155 * 156 * @param header157 * a set-cookie or set-cookie2 header.158 * @return a list of constructed cookies159 * @throws IllegalArgumentException160 * if the string does not comply with cookie specification, or161 * the cookie name contains illegal characters, or reserved162 * tokens of cookie specification appears163 * @throws NullPointerException164 * if header is null165 */166publicstatic List<CmisHttpCookie> parse(String header) {
167 Matcher matcher = HEAD_PATTERN.matcher(header);
168// Parse cookie name & value169 List<CmisHttpCookie> list = null;
170CmisHttpCookie cookie = null;
171 String headerString = header;
172int version = 0;
173// process set-cookie | set-cookie2 head174if (matcher.find()) {
175 String cookieHead = matcher.group();
176if ("set-cookie2:".equalsIgnoreCase(cookieHead)) {
177 version = 1;
178 }
179 headerString = header.substring(cookieHead.length());
180 }
181182// parse cookie name/value pair183 matcher = NAME_PATTERN.matcher(headerString);
184if (matcher.lookingAt()) {
185 list = new ArrayList<CmisHttpCookie>();
186 cookie = newCmisHttpCookie(matcher.group(1), matcher.group(2));
187 cookie.setVersion(version);
188189/*190 * Comma is a delimiter in cookie spec 1.1. If find comma in version191 * 1 cookie header, part of matched string need to be spitted out.192 */193 String nameGroup = matcher.group();
194if (isCommaDelim(cookie)) {
195 headerString = headerString.substring(nameGroup.indexOf(COMMA_STR));
196 } else {
197 headerString = headerString.substring(nameGroup.length());
198 }
199 list.add(cookie);
200 } else {
201thrownew IllegalArgumentException();
202 }
203204// parse cookie headerString205while (!(headerString.length() == 0)) {
206 matcher = cookie.getVersion() == 1 ? ATTR_PATTERN1.matcher(headerString) : ATTR_PATTERN0
207 .matcher(headerString);
208209if (matcher.lookingAt()) {
210 String attrName = matcher.group(1).trim();
211212// handle special situation like: <..>;;<..>213if (attrName.length() == 0) {
214 headerString = headerString.substring(1);
215continue;
216 }
217218// If port is the attribute, then comma will not be used as a219// delimiter220if (attrName.equalsIgnoreCase("port") || attrName.equalsIgnoreCase("expires")) {
221int start = matcher.regionStart();
222 matcher = ATTR_PATTERN0.matcher(headerString);
223 matcher.region(start, headerString.length());
224 matcher.lookingAt();
225 } elseif (cookie.getVersion() == 1 && attrName.startsWith(COMMA_STR)) {
226// If the last encountered token is comma, and the parsed227// attribute is not port, then this attribute/value pair228// ends.229 headerString = headerString.substring(1);
230 matcher = NAME_PATTERN.matcher(headerString);
231if (matcher.lookingAt()) {
232 cookie = newCmisHttpCookie(matcher.group(1), matcher.group(2));
233 list.add(cookie);
234 headerString = headerString.substring(matcher.group().length());
235continue;
236 }
237 }
238239Setter setter = cookie.attributeSet.get(attrName.toLowerCase());
240if (setter != null && !setter.isSet()) {
241 String attrValue = matcher.group(2);
242 setter.validate(attrValue, cookie);
243 setter.setValue(matcher.group(2), cookie);
244 }
245 headerString = headerString.substring(matcher.end());
246 }
247 }
248249return list;
250 }
251252private String comment;
253private String commentURL;
254privateboolean discard;
255private String domain;
256privatelong maxAge = -1l;
257private String name;
258private String path;
259private String portList;
260privateboolean secure;
261private String value;
262privateint version = 1;
263264 {
265 attributeSet.put("comment", newSetter() {
266 @Override
267void setValue(String value, CmisHttpCookie cookie) {
268 cookie.setComment(value);
269if (cookie.getComment() != null) {
270 set(true);
271 }
272 }
273 });
274 attributeSet.put("commenturl", newSetter() {
275 @Override
276void setValue(String value, CmisHttpCookie cookie) {
277 cookie.setCommentURL(value);
278if (cookie.getCommentURL() != null) {
279 set(true);
280 }
281 }
282 });
283 attributeSet.put("discard", newSetter() {
284 @Override
285void setValue(String value, CmisHttpCookie cookie) {
286 cookie.setDiscard(true);
287 set(true);
288 }
289 });
290 attributeSet.put("domain", newSetter() {
291 @Override
292void setValue(String value, CmisHttpCookie cookie) {
293 cookie.setDomain(value);
294if (cookie.getDomain() != null) {
295 set(true);
296 }
297 }
298 });
299 attributeSet.put("max-age", newSetter() {
300 @Override
301void setValue(String value, CmisHttpCookie cookie) {
302try {
303 cookie.setMaxAge(Long.parseLong(value));
304 } catch (NumberFormatException e) {
305thrownew IllegalArgumentException("Invalid max-age!");
306 }
307 set(true);
308309if (!attributeSet.get("version").isSet()) {
310 cookie.setVersion(1);
311 }
312 }
313 });
314315 attributeSet.put("path", newSetter() {
316 @Override
317void setValue(String value, CmisHttpCookie cookie) {
318 cookie.setPath(value);
319if (cookie.getPath() != null) {
320 set(true);
321 }
322 }
323 });
324 attributeSet.put("port", newSetter() {
325 @Override
326void setValue(String value, CmisHttpCookie cookie) {
327 cookie.setPortlist(value);
328if (cookie.getPortlist() != null) {
329 set(true);
330 }
331 }
332333 @Override
334void validate(String v, CmisHttpCookie cookie) {
335return;
336 }
337 });
338 attributeSet.put("secure", newSetter() {
339 @Override
340void setValue(String value, CmisHttpCookie cookie) {
341 cookie.setSecure(true);
342 set(true);
343 }
344 });
345 attributeSet.put("version", newSetter() {
346 @Override
347void setValue(String value, CmisHttpCookie cookie) {
348try {
349int v = Integer.parseInt(value);
350if (v > cookie.getVersion()) {
351 cookie.setVersion(v);
352 }
353 } catch (NumberFormatException e) {
354thrownew IllegalArgumentException("Invalid version!");
355 }
356if (cookie.getVersion() != 0) {
357 set(true);
358 }
359 }
360 });
361362 attributeSet.put("expires", newSetter() {
363 @SuppressWarnings("deprecation")
364 @Override
365void setValue(String value, CmisHttpCookie cookie) {
366 cookie.setVersion(0);
367 attributeSet.get("version").set(true);
368if (!attributeSet.get("max-age").isSet()) {
369 attributeSet.get("max-age").set(true);
370if (!"en".equalsIgnoreCase(Locale.getDefault().getLanguage())) {
371 cookie.setMaxAge(0);
372return;
373 }
374try {
375 cookie.setMaxAge((Date.parse(value) - System.currentTimeMillis()) / 1000);
376 } catch (IllegalArgumentException e) {
377 cookie.setMaxAge(0);
378 }
379 }
380 }
381382 @Override
383void validate(String v, CmisHttpCookie cookie) {
384return;
385 }
386 });
387 }
388389/**390 * Initializes a cookie with the specified name and value.391 * 392 * The name attribute can just contain ASCII characters, which is immutable393 * after creation. Commas, white space and semicolons are not allowed. The $394 * character is also not allowed to be the beginning of the name.395 * 396 * The value attribute depends on what the server side is interested. The397 * setValue method can be used to change it.398 * 399 * RFC 2965 is the default cookie specification of this class. If one wants400 * to change the version of the cookie, the setVersion method is available.401 * 402 * @param name403 * - the specific name of the cookie404 * @param value405 * - the specific value of the cookie406 * 407 * @throws IllegalArgumentException408 * - if the name contains not-allowed or reserved characters409 * 410 * @throws NullPointerException411 * if the value of name is null412 */413publicCmisHttpCookie(String name, String value) {
414 String ntrim = name.trim(); // erase leading and trailing whitespaces415if (!isValidName(ntrim)) {
416thrownew IllegalArgumentException("Invalid name!");
417 }
418419this.name = ntrim;
420this.value = value;
421 }
422423privatevoid attrToString(StringBuilder builder, String attrName, String attrValue) {
424if (attrValue != null && builder != null) {
425 builder.append(";");
426 builder.append("$");
427 builder.append(attrName);
428 builder.append("=\"");
429 builder.append(attrValue);
430 builder.append(QUOTE_STR);
431 }
432 }
433434/**435 * Answers a copy of this object.436 * 437 * @return a copy of this cookie438 */439 @Override
440public Object clone() {
441try {
442CmisHttpCookie obj = (CmisHttpCookie) super.clone();
443return obj;
444 } catch (CloneNotSupportedException e) {
445returnnull;
446 }
447 }
448449/**450 * Answers whether two cookies are equal. Two cookies are equal if they have451 * the same domain and name in a case-insensitive mode and path in a452 * case-sensitive mode.453 * 454 * @param obj455 * the object to be compared.456 * @return true if two cookies equals, false otherwise457 */458 @Override
459publicboolean equals(Object obj) {
460if (obj == this) {
461returntrue;
462 }
463if (obj instanceof CmisHttpCookie) {
464CmisHttpCookie anotherCookie = (CmisHttpCookie) obj;
465if (name.equalsIgnoreCase(anotherCookie.getName())) {
466 String anotherDomain = anotherCookie.getDomain();
467boolean equals = domain == null ? anotherDomain == null : domain.equalsIgnoreCase(anotherDomain);
468if (equals) {
469 String anotherPath = anotherCookie.getPath();
470return path == null ? anotherPath == null : path.equals(anotherPath);
471 }
472 }
473 }
474return false;
475 }
476477/**478 * Answers the value of comment attribute(specified in RFC 2965) of this479 * cookie.480 * 481 * @return the value of comment attribute482 */483public String getComment() {
484return comment;
485 }
486487/**488 * Answers the value of commentURL attribute(specified in RFC 2965) of this489 * cookie.490 * 491 * @return the value of commentURL attribute492 */493public String getCommentURL() {
494return commentURL;
495 }
496497/**498 * Answers the value of discard attribute(specified in RFC 2965) of this499 * cookie.500 * 501 * @return discard value of this cookie502 */503publicboolean getDiscard() {
504return discard;
505 }
506507/**508 * Answers the domain name for this cookie in the format specified in RFC509 * 2965510 * 511 * @return the domain value of this cookie512 */513public String getDomain() {
514return domain;
515 }
516517/**518 * Returns the Max-Age value as specified in RFC 2965 of this cookie.519 * 520 * @return the Max-Age value521 */522publiclong getMaxAge() {
523return maxAge;
524 }
525526/**527 * Answers the name for this cookie.528 * 529 * @return the name for this cookie530 */531public String getName() {
532return name;
533 }
534535/**536 * Answers the path part of a request URL to which this cookie is returned.537 * This cookie is visible to all subpaths.538 * 539 * @return the path used to return the cookie540 */541public String getPath() {
542return path;
543 }
544545/**546 * Answers the value of port attribute(specified in RFC 2965) of this547 * cookie.548 * 549 * @return port list of this cookie550 */551public String getPortlist() {
552return portList;
553 }
554555/**556 * Answers true if the browser only sends cookies over a secure protocol.557 * False if can send cookies through any protocols.558 * 559 * @return true if sends cookies only through secure protocol, false560 * otherwise561 */562publicboolean getSecure() {
563return secure;
564 }
565566/**567 * Answers the value of this cookie.568 * 569 * @return the value of this cookie570 */571public String getValue() {
572return value;
573 }
574575/**576 * Get the version of this cookie577 * 578 * @return 0 indicates the original Netscape cookie specification, while 1579 * indicates RFC 2965/2109 specification.580 */581publicint getVersion() {
582return version;
583 }
584585/**586 * Answers whether the cookie has expired.587 * 588 * @return true is the cookie has expired, false otherwise589 */590publicboolean hasExpired() {
591// -1 indicates the cookie will persist until browser shutdown592// so the cookie is not expired.593if (maxAge == -1l) {
594return false;
595 }
596597boolean expired = false;
598if (maxAge <= 0l) {
599 expired = true;
600 }
601return expired;
602 }
603604/**605 * Answers hash code of this http cookie. The result is calculated as below:606 * 607 * getName().toLowerCase().hashCode() + getDomain().toLowerCase().hashCode()608 * + getPath().hashCode()609 * 610 * @return the hash code of this cookie611 */612 @Override
613publicint hashCode() {
614int hashCode = name.toLowerCase().hashCode();
615 hashCode += domain == null ? 0 : domain.toLowerCase().hashCode();
616 hashCode += path == null ? 0 : path.hashCode();
617return hashCode;
618 }
619620privateboolean isValidName(String n) {
621// name cannot be empty or begin with '$' or equals the reserved622// attributes (case-insensitive)623boolean isValid = !(n.length() == 0 || n.startsWith("$") || attributeSet.containsKey(n.toLowerCase()));
624if (isValid) {
625for (int i = 0; i < n.length(); i++) {
626char nameChar = n.charAt(i);
627// name must be ASCII characters and cannot contain ';', ',' and628// whitespace629if (nameChar < 0 || nameChar >= 127 || nameChar == ';' || nameChar == ','
630 || (Character.isWhitespace(nameChar) && nameChar != ' ')) {
631 isValid = false;
632break;
633 }
634 }
635 }
636637return isValid;
638 }
639640/**641 * Set the value of comment attribute(specified in RFC 2965) of this cookie.642 * 643 * @param purpose644 * the comment value to be set645 */646publicvoid setComment(String purpose) {
647 comment = purpose;
648 }
649650/**651 * Set the value of commentURL attribute(specified in RFC 2965) of this652 * cookie.653 * 654 * @param purpose655 * the value of commentURL attribute to be set656 */657publicvoid setCommentURL(String purpose) {
658 commentURL = purpose;
659 }
660661/**662 * Set the value of discard attribute(specified in RFC 2965) of this cookie.663 * 664 * @param discard665 * the value for discard attribute666 */667publicvoid setDiscard(boolean discard) {
668this.discard = discard;
669 }
670671/**672 * Set the domain value for this cookie. Browsers send the cookie to the673 * domain specified by this value. The form of the domain is specified in674 * RFC 2965.675 * 676 * @param pattern677 * the domain pattern678 */679publicvoid setDomain(String pattern) {
680 domain = pattern == null ? null : pattern.toLowerCase();
681 }
682683/**684 * Sets the Max-Age value as specified in RFC 2965 of this cookie to expire.685 * 686 * @param expiry687 * the value used to set the Max-Age value of this cookie688 */689publicvoid setMaxAge(long expiry) {
690 maxAge = expiry;
691 }
692693/**694 * Set the path to which this cookie is returned. This cookie is visible to695 * all the pages under the path and all subpaths.696 * 697 * @param path698 * the path to which this cookie is returned699 */700publicvoid setPath(String path) {
701this.path = path;
702 }
703704/**705 * Set the value of port attribute(specified in RFC 2965) of this cookie.706 * 707 * @param ports708 * the value for port attribute709 */710publicvoid setPortlist(String ports) {
711 portList = ports;
712 }
713714/*715 * Handle 2 special cases: 1. value is wrapped by a quotation 2. value716 * contains comma717 */718719/**720 * Tells the browser whether the cookies should be sent to server through721 * secure protocols.722 * 723 * @param flag724 * tells browser to send cookie to server only through secure725 * protocol if flag is true726 */727publicvoid setSecure(boolean flag) {
728 secure = flag;
729 }
730731/**732 * Sets the value for this cookie after it has been instantiated. String733 * newValue can be in BASE64 form. If the version of the cookie is 0,734 * special value as: white space, brackets, parentheses, equals signs,735 * commas, double quotes, slashes, question marks, at signs, colons, and736 * semicolons are not recommended. Empty values may lead to different737 * behavior on different browsers.738 * 739 * @param newValue740 * the value for this cookie741 */742publicvoid setValue(String newValue) {
743// FIXME: According to spec, version 0 cookie value does not allow many744// symbols. But RI does not implement it. Follow RI temporarily.745 value = newValue;
746 }
747748/**749 * Sets the version of the cookie. 0 indicates the original Netscape cookie750 * specification, while 1 indicates RFC 2965/2109 specification.751 * 752 * @param v753 * 0 or 1 as stated above754 * @throws IllegalArgumentException755 * if v is neither 0 nor 1756 */757publicvoid setVersion(int v) {
758if (v != 0 && v != 1) {
759thrownew IllegalArgumentException("Unknown version!");
760 }
761 version = v;
762 }
763764/**765 * Returns a string to represent the cookie. The format of string follows766 * the cookie specification. The leading token "Cookie" is not included767 * 768 * @return the string format of the cookie object769 */770 @Override
771public String toString() {
772 StringBuilder cookieStr = new StringBuilder();
773 cookieStr.append(name);
774 cookieStr.append("=");
775if (version == 0) {
776 cookieStr.append(value);
777 } elseif (version == 1) {
778 cookieStr.append(QUOTE_STR);
779 cookieStr.append(value);
780 cookieStr.append(QUOTE_STR);
781782 attrToString(cookieStr, "Path", path);
783 attrToString(cookieStr, "Domain", domain);
784 attrToString(cookieStr, "Port", portList);
785 }
786787return cookieStr.toString();
788 }
789 }