content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing,
   * software distributed under the License is distributed on an
   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   * KIND, either express or implied.  See the License for the
   * specific language governing permissions and limitations
   * under the License.
   */
  /*
   * This class has been taken from Apache Harmony (http://harmony.apache.org/) 
   * and has been modified to work with OpenCMIS.
   */
  package org.apache.chemistry.opencmis.client.bindings.spi.cookies;
  
  import java.io.Serializable;
  import java.util.ArrayList;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import org.apache.chemistry.opencmis.commons.impl.DateTimeHelper;
  
  /**
   * This class represents a http cookie, which indicates the status information
   * between the client agent side and the server side. According to RFC, there
   * are 4 http cookie specifications. This class is compatible with the original
   * Netscape specification, RFC 2109, RFC 2965 and party compatible with RFC
   * 6265. HttpCookie class can accept all syntax forms.
   */
  public final class CmisHttpCookie implements Cloneable, Serializable {
  
      private static final long serialVersionUID = 1L;
  
      private static final String DOT_STR = ".";
      private static final String LOCAL_STR = ".local";
      private static final String QUOTE_STR = "\"";
      private static final String COMMA_STR = ",";
      private static final Pattern HEAD_PATTERN = Pattern.compile("Set-Cookie2?:", Pattern.CASE_INSENSITIVE);
      private static final Pattern NAME_PATTERN = Pattern.compile(
              "([^$=,\u0085\u2028\u2029][^,\n\t\r\r\n\u0085\u2028\u2029]*?)=([^;]*)(;)?", Pattern.DOTALL
                      | Pattern.CASE_INSENSITIVE);
      private static final Pattern ATTR_PATTERN0 = Pattern.compile("([^;=]*)(?:=([^;]*))?");
      private static final Pattern ATTR_PATTERN1 = Pattern.compile("(,?[^;=]*)(?:=([^;,]*))?((?=.))?");
  
      private abstract static class Setter {
          private boolean set;
  
          Setter() {
              set = false;
          }
  
          boolean isSet() {
              return set;
          }
  
          void set(boolean isSet) {
              set = isSet;
          }
  
          abstract void setValue(String value, CmisHttpCookie cookie);
  
          void validate(String value, CmisHttpCookie cookie) {
              if (cookie.getVersion() == 1 && value != null && value.contains(COMMA_STR)) {
                  throw new IllegalArgumentException();
              }
          }
      }
  
      private Map<String, Setter> attributeSet = new HashMap<String, Setter>();
  
      private String comment;
      private String commentURL;
      private boolean discard;
      private String domain;
      private long maxAge = -1L;
      private String name;
      private String path;
      private String portList;
      private boolean secure;
      private String value;
      private int version = 1;
  
      /**
       * A utility method used to check whether the host name is in a domain or
       * not.
       * 
       * @param domain
       *            the domain to be checked against
       * @param host
       *            the host to be checked
       * @return true if the host is in the domain, false otherwise
       */
      public static boolean domainMatches(String domain, String host) {
          if (domain == null || host == null) {
              return false;
          }
          String newDomain = domain.toLowerCase(Locale.ENGLISH);
          String newHost = host.toLowerCase(Locale.ENGLISH);
  
          return newDomain.equals(newHost)
                  || (isValidDomain(newDomain) && effDomainMatches(newDomain, newHost) && isValidHost(newDomain, newHost));
      }
  
      private static boolean effDomainMatches(String domain, String host) {
          // calculate effective host name
          String effHost = host.indexOf(DOT_STR) != -1 ? host : (host + LOCAL_STR);
  
          // Rule 2: domain and host are string-compare equal, or A = NB, B = .B'
          // and N is a non-empty name string
          boolean inDomain = domain.equals(effHost);
          inDomain = inDomain
                  || (effHost.endsWith(domain) && effHost.length() > domain.length() && domain.startsWith(DOT_STR));
  
          return inDomain;
      }
  
      private static boolean isCommaDelim(CmisHttpCookie cookie) {
          String value = cookie.getValue();
          if (value.startsWith(QUOTE_STR) && value.endsWith(QUOTE_STR)) {
              cookie.setValue(value.substring(1, value.length() - 1));
              return false;
          }
  
          if (cookie.getVersion() == 1 && value.contains(COMMA_STR)) {
              cookie.setValue(value.substring(0, value.indexOf(COMMA_STR)));
              return true;
          }
  
          return false;
      }
  
      private static boolean isValidDomain(String domain) {
          // Rule 1: The value for Domain contains embedded dots, or is .local
          if (domain.length() <= 2) {
              return false;
          }
  
          return domain.substring(1, domain.length() - 1).indexOf(DOT_STR) != -1 || domain.equals(LOCAL_STR);
      }
  
      private static boolean isValidHost(String domain, String host) {
          // Rule 3: host does not end with domain, or the remainder does not
          // contain "."
          boolean matches = !host.endsWith(domain);
          if (!matches) {
              String hostSub = host.substring(0, host.length() - domain.length());
              matches = hostSub.indexOf(DOT_STR) == -1;
          }
  
          return matches;
      }
  
      /**
       * Constructs a cookie from a string. The string should comply with
       * set-cookie or set-cookie2 header format as specified in RFC 2965. Since
       * set-cookies2 syntax allows more than one cookie definitions in one
       * header, the returned object is a list.
       * 
       * @param header
       *            a set-cookie or set-cookie2 header.
       * @return a list of constructed cookies
       * @throws IllegalArgumentException
       *             if the string does not comply with cookie specification, or
       *             the cookie name contains illegal characters, or reserved
       *             tokens of cookie specification appears
       * @throws NullPointerException
       *             if header is null
       */
      public static List<CmisHttpCookie> parse(String header) {
          Matcher matcher = HEAD_PATTERN.matcher(header);
          // Parse cookie name & value
          List<CmisHttpCookie> list = null;
          CmisHttpCookie cookie = null;
          String headerString = header;
          int version = 0;
          // process set-cookie | set-cookie2 head
          if (matcher.find()) {
              String cookieHead = matcher.group();
              if ("set-cookie2:".equalsIgnoreCase(cookieHead)) {
                  version = 1;
              }
              headerString = header.substring(cookieHead.length());
          }
  
          // parse cookie name/value pair
          matcher = NAME_PATTERN.matcher(headerString);
          if (matcher.lookingAt()) {
              list = new ArrayList<CmisHttpCookie>();
              cookie = new CmisHttpCookie(matcher.group(1), matcher.group(2));
              cookie.setVersion(version);
  
              /*
               * Comma is a delimiter in cookie spec 1.1. If find comma in version
               * 1 cookie header, part of matched string need to be spitted out.
               */
              String nameGroup = matcher.group();
              if (isCommaDelim(cookie)) {
                  headerString = headerString.substring(nameGroup.indexOf(COMMA_STR));
              } else {
                  headerString = headerString.substring(nameGroup.length());
              }
              list.add(cookie);
          } else {
              throw new IllegalArgumentException();
          }
  
          // parse cookie headerString
          while (!(headerString.length() == 0)) {
              matcher = cookie.getVersion() == 1 ? ATTR_PATTERN1.matcher(headerString) : ATTR_PATTERN0
                      .matcher(headerString);
  
              if (matcher.lookingAt()) {
                  String attrName = matcher.group(1).trim();
  
                  // handle special situation like: <..>;;<..>
                  if (attrName.length() == 0) {
                      headerString = headerString.substring(1);
                      continue;
                  }
  
                  // If port is the attribute, then comma will not be used as a
                  // delimiter
                  if (attrName.equalsIgnoreCase("port") || attrName.equalsIgnoreCase("expires")) {
                      int start = matcher.regionStart();
                      matcher = ATTR_PATTERN0.matcher(headerString);
                      matcher.region(start, headerString.length());
                      matcher.lookingAt();
                  } else if (cookie.getVersion() == 1 && attrName.startsWith(COMMA_STR)) {
                      // If the last encountered token is comma, and the parsed
                      // attribute is not port, then this attribute/value pair
                      // ends.
                      headerString = headerString.substring(1);
                      matcher = NAME_PATTERN.matcher(headerString);
                      if (matcher.lookingAt()) {
                          cookie = new CmisHttpCookie(matcher.group(1), matcher.group(2));
                          list.add(cookie);
                          headerString = headerString.substring(matcher.group().length());
                          continue;
                      }
                  }
  
                  Setter setter = cookie.attributeSet.get(attrName.toLowerCase(Locale.ENGLISH));
                  if (setter != null && !setter.isSet()) {
                      String attrValue = matcher.group(2);
                      setter.validate(attrValue, cookie);
                      setter.setValue(matcher.group(2), cookie);
                  }
                  headerString = headerString.substring(matcher.end());
              }
          }
  
          return list;
      }
  
      {
          attributeSet.put("comment", new Setter() {
              @Override
              void setValue(String value, CmisHttpCookie cookie) {
                  cookie.setComment(value);
                  if (cookie.getComment() != null) {
                      set(true);
                  }
              }
          });
          attributeSet.put("commenturl", new Setter() {
              @Override
              void setValue(String value, CmisHttpCookie cookie) {
                  cookie.setCommentURL(value);
                  if (cookie.getCommentURL() != null) {
                      set(true);
                  }
              }
          });
          attributeSet.put("discard", new Setter() {
              @Override
              void setValue(String value, CmisHttpCookie cookie) {
                  cookie.setDiscard(true);
                  set(true);
              }
          });
          attributeSet.put("domain", new Setter() {
              @Override
              void setValue(String value, CmisHttpCookie cookie) {
                  cookie.setDomain(value);
                  if (cookie.getDomain() != null) {
                      set(true);
                  }
              }
          });
          attributeSet.put("max-age", new Setter() {
              @Override
              void setValue(String value, CmisHttpCookie cookie) {
                  try {
                      cookie.setMaxAge(Long.parseLong(value));
                  } catch (NumberFormatException e) {
                      throw new IllegalArgumentException("Invalid max-age!", e);
                  }
                  set(true);
  
                  if (!attributeSet.get("version").isSet()) {
                      cookie.setVersion(1);
                  }
              }
          });
  
          attributeSet.put("path", new Setter() {
              @Override
              void setValue(String value, CmisHttpCookie cookie) {
                  cookie.setPath(value);
                  if (cookie.getPath() != null) {
                      set(true);
                  }
              }
          });
          attributeSet.put("port", new Setter() {
              @Override
              void setValue(String value, CmisHttpCookie cookie) {
                  cookie.setPortlist(value);
                  if (cookie.getPortlist() != null) {
                      set(true);
                  }
              }
  
              @Override
              void validate(String v, CmisHttpCookie cookie) {
              }
          });
          attributeSet.put("secure", new Setter() {
              @Override
              void setValue(String value, CmisHttpCookie cookie) {
                  cookie.setSecure(true);
                  set(true);
              }
          });
          attributeSet.put("version", new Setter() {
              @Override
              void setValue(String value, CmisHttpCookie cookie) {
                  try {
                      int v = Integer.parseInt(value);
                      if (v > cookie.getVersion()) {
                          cookie.setVersion(v);
                      }
                  } catch (NumberFormatException e) {
                      throw new IllegalArgumentException("Invalid version!", e);
                  }
                  if (cookie.getVersion() != 0) {
                      set(true);
                  }
              }
          });
  
          attributeSet.put("expires", new Setter() {
              @Override
              void setValue(String value, CmisHttpCookie cookie) {
                  cookie.setVersion(0);
                  attributeSet.get("version").set(true);
                  if (!attributeSet.get("max-age").isSet()) {
                      attributeSet.get("max-age").set(true);
                      if (!"en".equalsIgnoreCase(Locale.getDefault().getLanguage())) {
                          cookie.setMaxAge(0);
                          return;
                      }
  
                      Date date = DateTimeHelper.parseHttpDateTime(value);
                      if (date != null) {
                          cookie.setMaxAge((date.getTime() - System.currentTimeMillis()) / 1000);
                      } else {
                          cookie.setMaxAge(0);
                      }
                  }
              }
  
              @Override
              void validate(String v, CmisHttpCookie cookie) {
              }
          });
      }
  
      /**
       * Initializes a cookie with the specified name and value.
       * 
       * The name attribute can just contain ASCII characters, which is immutable
       * after creation. Commas, white space and semicolons are not allowed. The $
       * character is also not allowed to be the beginning of the name.
       * 
       * The value attribute depends on what the server side is interested. The
       * setValue method can be used to change it.
       * 
       * RFC 2965 is the default cookie specification of this class. If one wants
       * to change the version of the cookie, the setVersion method is available.
       * 
       * @param name
       *            - the specific name of the cookie
       * @param value
       *            - the specific value of the cookie
       * 
       * @throws IllegalArgumentException
       *             - if the name contains not-allowed or reserved characters
       * 
       * @throws NullPointerException
       *             if the value of name is null
       */
      public CmisHttpCookie(String name, String value) {
          String ntrim = name.trim(); // erase leading and trailing whitespaces
          if (!isValidName(ntrim)) {
              throw new IllegalArgumentException("Invalid name!");
          }
  
          this.name = ntrim;
          this.value = value;
      }
  
      private void attrToString(StringBuilder builder, String attrName, String attrValue) {
          if (attrValue != null && builder != null) {
              builder.append(';');
              builder.append('$');
              builder.append(attrName);
              builder.append("=\"");
              builder.append(attrValue);
              builder.append(QUOTE_STR);
          }
      }
  
      /**
       * Answers a copy of this object.
       * 
       * @return a copy of this cookie
       */
      @Override
      public Object clone() {
          try {
              return super.clone();
          } catch (CloneNotSupportedException e) {
              return null;
          }
      }
  
      /**
       * Answers whether two cookies are equal. Two cookies are equal if they have
       * the same domain and name in a case-insensitive mode and path in a
       * case-sensitive mode.
       * 
       * @param obj
       *            the object to be compared.
       * @return true if two cookies equals, false otherwise
       */
      @Override
      public boolean equals(Object obj) {
          if (obj == this) {
              return true;
          }
          if (obj instanceof CmisHttpCookie) {
              CmisHttpCookie anotherCookie = (CmisHttpCookie) obj;
              if (name.equalsIgnoreCase(anotherCookie.getName())) {
                  String anotherDomain = anotherCookie.getDomain();
                  boolean equals = domain == null ? anotherDomain == null : domain.equalsIgnoreCase(anotherDomain);
                  if (equals) {
                      String anotherPath = anotherCookie.getPath();
                      return path == null ? anotherPath == null : path.equals(anotherPath);
                  }
              }
          }
          return false;
      }
  
      /**
       * Answers the value of comment attribute(specified in RFC 2965) of this
       * cookie.
       * 
       * @return the value of comment attribute
       */
      public String getComment() {
          return comment;
      }
  
      /**
       * Answers the value of commentURL attribute(specified in RFC 2965) of this
       * cookie.
       * 
       * @return the value of commentURL attribute
       */
      public String getCommentURL() {
          return commentURL;
      }
  
      /**
       * Answers the value of discard attribute(specified in RFC 2965) of this
       * cookie.
       * 
       * @return discard value of this cookie
       */
      public boolean getDiscard() {
          return discard;
      }
  
      /**
       * Answers the domain name for this cookie in the format specified in RFC
       * 2965
       * 
       * @return the domain value of this cookie
       */
      public String getDomain() {
          return domain;
      }
  
      /**
       * Returns the Max-Age value as specified in RFC 2965 of this cookie.
       * 
       * @return the Max-Age value
       */
      public long getMaxAge() {
          return maxAge;
      }
  
      /**
       * Answers the name for this cookie.
       * 
       * @return the name for this cookie
       */
      public String getName() {
          return name;
      }
  
      /**
       * Answers the path part of a request URL to which this cookie is returned.
       * This cookie is visible to all subpaths.
       * 
       * @return the path used to return the cookie
       */
      public String getPath() {
          return path;
      }
  
      /**
       * Answers the value of port attribute(specified in RFC 2965) of this
       * cookie.
       * 
       * @return port list of this cookie
       */
      public String getPortlist() {
          return portList;
      }
  
      /**
       * Answers true if the browser only sends cookies over a secure protocol.
       * False if can send cookies through any protocols.
       * 
       * @return true if sends cookies only through secure protocol, false
       *         otherwise
       */
      public boolean getSecure() {
          return secure;
      }
  
      /**
       * Answers the value of this cookie.
       * 
       * @return the value of this cookie
       */
      public String getValue() {
          return value;
      }
  
      /**
       * Get the version of this cookie
       * 
       * @return 0 indicates the original Netscape cookie specification, while 1
       *         indicates RFC 2965/2109 specification.
       */
      public int getVersion() {
          return version;
      }
  
      /**
       * Answers whether the cookie has expired.
       * 
       * @return true is the cookie has expired, false otherwise
       */
      public boolean hasExpired() {
          // -1 indicates the cookie will persist until browser shutdown
          // so the cookie is not expired.
          if (maxAge == -1L) {
              return false;
          }
  
          boolean expired = false;
          if (maxAge <= 0L) {
              expired = true;
          }
          return expired;
      }
  
      /**
       * Answers hash code of this http cookie. The result is calculated as below:
       * 
       * getName().toLowerCase(Locale.ENGLISH).hashCode() +
       * getDomain().toLowerCase(Locale.ENGLISH).hashCode() + getPath().hashCode()
       * 
       * @return the hash code of this cookie
       */
      @Override
      public int hashCode() {
          int hashCode = name.toLowerCase(Locale.ENGLISH).hashCode();
          hashCode += domain == null ? 0 : domain.toLowerCase(Locale.ENGLISH).hashCode();
          hashCode += path == null ? 0 : path.hashCode();
          return hashCode;
      }
  
      private boolean isValidName(String n) {
          // name cannot be empty or begin with '$' or equals the reserved
          // attributes (case-insensitive)
          boolean isValid = !(n.length() == 0 || n.charAt(0) == '$' || attributeSet.containsKey(n
                  .toLowerCase(Locale.ENGLISH)));
          if (isValid) {
              for (int i = 0; i < n.length(); i++) {
                  char nameChar = n.charAt(i);
                  // name must be ASCII characters and cannot contain ';', ',' and
                  // whitespace
                  if (nameChar < 0 || nameChar >= 127 || nameChar == ';' || nameChar == ','
                          || (Character.isWhitespace(nameChar) && nameChar != ' ')) {
                      isValid = false;
                      break;
                  }
              }
          }
  
          return isValid;
      }
  
      /**
       * Set the value of comment attribute(specified in RFC 2965) of this cookie.
       * 
       * @param purpose
       *            the comment value to be set
       */
      public void setComment(String purpose) {
          comment = purpose;
      }
  
      /**
       * Set the value of commentURL attribute(specified in RFC 2965) of this
       * cookie.
       * 
       * @param purpose
       *            the value of commentURL attribute to be set
       */
      public void setCommentURL(String purpose) {
          commentURL = purpose;
      }
  
      /**
       * Set the value of discard attribute(specified in RFC 2965) of this cookie.
       * 
       * @param discard
       *            the value for discard attribute
       */
      public void setDiscard(boolean discard) {
          this.discard = discard;
      }
  
      /**
       * Set the domain value for this cookie. Browsers send the cookie to the
       * domain specified by this value. The form of the domain is specified in
       * RFC 2965.
       * 
       * @param pattern
       *            the domain pattern
       */
      public void setDomain(String pattern) {
          domain = pattern == null ? null : pattern.toLowerCase(Locale.ENGLISH);
      }
  
      /**
       * Sets the Max-Age value as specified in RFC 2965 of this cookie to expire.
       * 
       * @param expiry
       *            the value used to set the Max-Age value of this cookie
       */
      public void setMaxAge(long expiry) {
          maxAge = expiry;
      }
  
      /**
       * Set the path to which this cookie is returned. This cookie is visible to
       * all the pages under the path and all subpaths.
       * 
       * @param path
       *            the path to which this cookie is returned
       */
      public void setPath(String path) {
          this.path = path;
      }
  
      /**
       * Set the value of port attribute(specified in RFC 2965) of this cookie.
       * 
       * @param ports
       *            the value for port attribute
       */
      public void setPortlist(String ports) {
          portList = ports;
      }
  
      /*
       * Handle 2 special cases: 1. value is wrapped by a quotation 2. value
       * contains comma
       */
  
      /**
       * Tells the browser whether the cookies should be sent to server through
       * secure protocols.
       * 
       * @param flag
       *            tells browser to send cookie to server only through secure
       *            protocol if flag is true
       */
      public void setSecure(boolean flag) {
          secure = flag;
      }
  
      /**
       * Sets the value for this cookie after it has been instantiated. String
       * newValue can be in BASE64 form. If the version of the cookie is 0,
       * special value as: white space, brackets, parentheses, equals signs,
       * commas, double quotes, slashes, question marks, at signs, colons, and
       * semicolons are not recommended. Empty values may lead to different
       * behavior on different browsers.
       * 
       * @param newValue
       *            the value for this cookie
       */
      public void setValue(String newValue) {
          // FIXME: According to spec, version 0 cookie value does not allow many
          // symbols. But RI does not implement it. Follow RI temporarily.
          value = newValue;
      }
  
      /**
       * Sets the version of the cookie. 0 indicates the original Netscape cookie
       * specification, while 1 indicates RFC 2965/2109 specification.
       * 
       * @param v
       *            0 or 1 as stated above
       * @throws IllegalArgumentException
       *             if v is neither 0 nor 1
       */
      public void setVersion(int v) {
          if (v != 0 && v != 1) {
              throw new IllegalArgumentException("Unknown version!");
          }
          version = v;
      }
  
      /**
       * Returns a string to represent the cookie. The format of string follows
       * the cookie specification. The leading token "Cookie" is not included
       * 
       * @return the string format of the cookie object
       */
      @Override
      public String toString() {
          StringBuilder cookieStr = new StringBuilder(128);
          cookieStr.append(name);
          cookieStr.append('=');
          if (version == 0) {
              cookieStr.append(value);
          } else if (version == 1) {
              cookieStr.append(QUOTE_STR);
              cookieStr.append(value);
              cookieStr.append(QUOTE_STR);
  
              attrToString(cookieStr, "Path", path);
              attrToString(cookieStr, "Domain", domain);
              attrToString(cookieStr, "Port", portList);
          }
  
          return cookieStr.toString();
      }
  }
  