/*
 * $HeadURL: https://svn.apache.org/repos/asf/jakarta/httpcomponents/oac.hc3x/tags/HTTPCLIENT_3_1/src/java/org/apache/commons/httpclient/cookie/RFC2965Spec.java $
 * $Revision: 507134 $
 * $Date: 2007-02-13 19:18:05 +0100 (Tue, 13 Feb 2007) $
 * 
 * ====================================================================
 *
 *  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 software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.commons.httpclient.cookie;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.commons.httpclient.Cookie;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HeaderElement;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.util.ParameterFormatter;

/**
 * <p>RFC 2965 specific cookie management functions.</p>
 * 
 * @author jain.samit@gmail.com (Samit Jain)
 *
 * @since 3.1
 */
public class RFC2965Spec extends CookieSpecBase implements CookieVersionSupport {

    private static final Comparator PATH_COMPOARATOR = new CookiePathComparator();
    
    /**
    * Cookie Response Header  name for cookies processed
    * by this spec.
    */
    public final static String SET_COOKIE2_KEY = "set-cookie2";
    
    /**
    * used for formatting RFC 2956 style cookies
    */
    private final ParameterFormatter formatter;
     
    /**
     * Stores the list of attribute handlers
     */
    private final List attribHandlerList;
    
    /**
    * Stores attribute name -> attribute handler mappings
    */
    private final Map attribHandlerMap;

    /**
     * Fallback cookie spec (RFC 2109)
     */
    private final CookieSpec rfc2109;
    
    /** 
     * Default constructor 
     * */
    public RFC2965Spec() {
        super();
        this.formatter = new ParameterFormatter();
        this.formatter.setAlwaysUseQuotes(true);
        this.attribHandlerMap = new HashMap(10);        
        this.attribHandlerList = new ArrayList(10);
        this.rfc2109 = new RFC2109Spec();
        
        registerAttribHandler(Cookie2.PATH, new Cookie2PathAttributeHandler());
        registerAttribHandler(Cookie2.DOMAIN, new Cookie2DomainAttributeHandler());
        registerAttribHandler(Cookie2.PORT, new Cookie2PortAttributeHandler());
        registerAttribHandler(Cookie2.MAXAGE, new Cookie2MaxageAttributeHandler());
        registerAttribHandler(Cookie2.SECURE, new CookieSecureAttributeHandler());
        registerAttribHandler(Cookie2.COMMENT, new CookieCommentAttributeHandler());
        registerAttribHandler(Cookie2.COMMENTURL, new CookieCommentUrlAttributeHandler());
        registerAttribHandler(Cookie2.DISCARD, new CookieDiscardAttributeHandler());
        registerAttribHandler(Cookie2.VERSION, new Cookie2VersionAttributeHandler());
    }

    protected void registerAttribHandler(
            final String name, final CookieAttributeHandler handler) {
        if (name == null) {
            throw new IllegalArgumentException("Attribute name may not be null");
        }
        if (handler == null) {
            throw new IllegalArgumentException("Attribute handler may not be null");
        }
        if (!this.attribHandlerList.contains(handler)) {
            this.attribHandlerList.add(handler);
        }
        this.attribHandlerMap.put(name, handler);
    }
    
    /**
     * Finds an attribute handler {@link CookieAttributeHandler} for the
     * given attribute. Returns <tt>null</tt> if no attribute handler is
     * found for the specified attribute.
     *
     * @param name attribute name. e.g. Domain, Path, etc.
     * @return an attribute handler or <tt>null</tt>
     */
    protected CookieAttributeHandler findAttribHandler(final String name) {
        return (CookieAttributeHandler) this.attribHandlerMap.get(name);
    }
    
    /**
     * Gets attribute handler {@link CookieAttributeHandler} for the
     * given attribute.
     *
     * @param name attribute name. e.g. Domain, Path, etc.
     * @throws IllegalStateException if handler not found for the
     *          specified attribute.
     */
    protected CookieAttributeHandler getAttribHandler(final String name) {
        CookieAttributeHandler handler = findAttribHandler(name);
        if (handler == null) {
            throw new IllegalStateException("Handler not registered for " +
                                            name + " attribute.");
        } else {
            return handler;
        }
    }

    protected Iterator getAttribHandlerIterator() {
        return this.attribHandlerList.iterator();
    }
    
    /**
     * Parses the Set-Cookie2 value into an array of <tt>Cookie</tt>s.
     *
     * <P>The syntax for the Set-Cookie2 response header is:
     *
     * <PRE>
     * set-cookie      =    "Set-Cookie2:" cookies
     * cookies         =    1#cookie
     * cookie          =    NAME "=" VALUE * (";" cookie-av)
     * NAME            =    attr
     * VALUE           =    value
     * cookie-av       =    "Comment" "=" value
     *                 |    "CommentURL" "=" <"> http_URL <">
     *                 |    "Discard"
     *                 |    "Domain" "=" value
     *                 |    "Max-Age" "=" value
     *                 |    "Path" "=" value
     *                 |    "Port" [ "=" <"> portlist <"> ]
     *                 |    "Secure"
     *                 |    "Version" "=" 1*DIGIT
     * portlist        =       1#portnum
     * portnum         =       1*DIGIT
     * </PRE>
     *
     * @param host the host from which the <tt>Set-Cookie2</tt> value was
     * received
     * @param port the port from which the <tt>Set-Cookie2</tt> value was
     * received
     * @param path the path from which the <tt>Set-Cookie2</tt> value was
     * received
     * @param secure <tt>true</tt> when the <tt>Set-Cookie2</tt> value was
     * received over secure conection
     * @param header the <tt>Set-Cookie2</tt> <tt>Header</tt> received from the server
     * @return an array of <tt>Cookie</tt>s parsed from the Set-Cookie2 value
     * @throws MalformedCookieException if an exception occurs during parsing
     */
    public Cookie[] parse(
            String host, int port, String path, boolean secure, final Header header)
            throws MalformedCookieException {
        LOG.trace("enter RFC2965.parse("
                  + "String, int, String, boolean, Header)");

        if (header == null) {
            throw new IllegalArgumentException("Header may not be null.");
        }
        if (header.getName() == null) {
            throw new IllegalArgumentException("Header name may not be null.");
        }

        if (header.getName().equalsIgnoreCase(SET_COOKIE2_KEY)) {
            // parse cookie2 cookies
            return parse(host, port, path, secure, header.getValue());
        } else if (header.getName().equalsIgnoreCase(RFC2109Spec.SET_COOKIE_KEY)) {
            // delegate parsing of old-style cookies to rfc2109Spec
            return this.rfc2109.parse(host, port, path, secure, header.getValue());
        } else {
            throw new MalformedCookieException("Header name is not valid. " +
                                               "RFC 2965 supports \"set-cookie\" " +
                                               "and \"set-cookie2\" headers.");
        }
    }

    /**
     * @see #parse(String, int, String, boolean, org.apache.commons.httpclient.Header)
     */
    public Cookie[] parse(String host, int port, String path,
                          boolean secure, final String header)
            throws MalformedCookieException {
        LOG.trace("enter RFC2965Spec.parse("
                  + "String, int, String, boolean, String)");

        // before we do anything, lets check validity of arguments
        if (host == null) {
            throw new IllegalArgumentException(
                    "Host of origin may not be null");
        }
        if (host.trim().equals("")) {
            throw new IllegalArgumentException(
                    "Host of origin may not be blank");
        }
        if (port < 0) {
            throw new IllegalArgumentException("Invalid port: " + port);
        }
        if (path == null) {
            throw new IllegalArgumentException(
                    "Path of origin may not be null.");
        }
        if (header == null) {
            throw new IllegalArgumentException("Header may not be null.");
        }

        if (path.trim().equals("")) {
            path = PATH_DELIM;
        }
        host = getEffectiveHost(host);

        HeaderElement[] headerElements =
                HeaderElement.parseElements(header.toCharArray());

        List cookies = new LinkedList();
        for (int i = 0; i < headerElements.length; i++) {
            HeaderElement headerelement = headerElements[i];
            Cookie2 cookie = null;
            try {
                cookie = new Cookie2(host,
                                    headerelement.getName(),
                                    headerelement.getValue(),
                                    path,
                                    null,
                                    false,
                                    new int[] {port});
            } catch (IllegalArgumentException ex) {
                throw new MalformedCookieException(ex.getMessage());
            }
            NameValuePair[] parameters = headerelement.getParameters();
            // could be null. In case only a header element and no parameters.
            if (parameters != null) {
                // Eliminate duplicate attribues. The first occurence takes precedence
                Map attribmap = new HashMap(parameters.length); 
                for (int j = parameters.length - 1; j >= 0; j--) {
                    NameValuePair param = parameters[j];
                    attribmap.put(param.getName().toLowerCase(), param);
                }
                for (Iterator it = attribmap.entrySet().iterator(); it.hasNext(); ) {
                    Map.Entry entry = (Map.Entry) it.next();
                    parseAttribute((NameValuePair) entry.getValue(), cookie);
                }
            }
            cookies.add(cookie);
            // cycle through the parameters
        }
        return (Cookie[]) cookies.toArray(new Cookie[cookies.size()]);
    }

    /**
     * Parse RFC 2965 specific cookie attribute and update the corresponsing
     * {@link org.apache.commons.httpclient.Cookie} properties.
     *
     * @param attribute {@link org.apache.commons.httpclient.NameValuePair} cookie attribute from the
     * <tt>Set-Cookie2</tt> header.
     * @param cookie {@link org.apache.commons.httpclient.Cookie} to be updated
     * @throws MalformedCookieException if an exception occurs during parsing
     */
    public void parseAttribute(
            final NameValuePair attribute, final Cookie cookie)
            throws MalformedCookieException {
        if (attribute == null) {
            throw new IllegalArgumentException("Attribute may not be null.");
        }
        if (attribute.getName() == null) {
            throw new IllegalArgumentException("Attribute Name may not be null.");
        }
        if (cookie == null) {
            throw new IllegalArgumentException("Cookie may not be null.");
        }
        final String paramName = attribute.getName().toLowerCase();
        final String paramValue = attribute.getValue();

        CookieAttributeHandler handler = findAttribHandler(paramName);
        if (handler == null) {
            // ignore unknown attribute-value pairs
            if (LOG.isDebugEnabled())
                LOG.debug("Unrecognized cookie attribute: " +
                          attribute.toString());
        } else {
            handler.parse(cookie, paramValue);
        }
    }

    /**
     * Performs RFC 2965 compliant {@link org.apache.commons.httpclient.Cookie} validation
     *
     * @param host the host from which the {@link org.apache.commons.httpclient.Cookie} was received
     * @param port the port from which the {@link org.apache.commons.httpclient.Cookie} was received
     * @param path the path from which the {@link org.apache.commons.httpclient.Cookie} was received
     * @param secure <tt>true</tt> when the {@link org.apache.commons.httpclient.Cookie} was received using a
     * secure connection
     * @param cookie The cookie to validate
     * @throws MalformedCookieException if an exception occurs during
     * validation
     */
    public void validate(final String host, int port, final String path,
                         boolean secure, final Cookie cookie)
            throws MalformedCookieException {

        LOG.trace("enter RFC2965Spec.validate(String, int, String, "
                  + "boolean, Cookie)");

        if (cookie instanceof Cookie2) {
            if (cookie.getName().indexOf(' ') != -1) {
                throw new MalformedCookieException("Cookie name may not contain blanks");
            }
            if (cookie.getName().startsWith("$")) {
                throw new MalformedCookieException("Cookie name may not start with $");
            }
            CookieOrigin origin = new CookieOrigin(getEffectiveHost(host), port, path, secure); 
            for (Iterator i = getAttribHandlerIterator(); i.hasNext(); ) {
              CookieAttributeHandler handler = (CookieAttributeHandler) i.next();
              handler.validate(cookie, origin);
            }
        } else {
            // old-style cookies are validated according to the old rules
            this.rfc2109.validate(host, port, path, secure, cookie);
        }
    }

    /**
     * Return <tt>true</tt> if the cookie should be submitted with a request
     * with given attributes, <tt>false</tt> otherwise.
     * @param host the host to which the request is being submitted
     * @param port the port to which the request is being submitted (ignored)
     * @param path the path to which the request is being submitted
     * @param secure <tt>true</tt> if the request is using a secure connection
     * @return true if the cookie matches the criterium
     */
    public boolean match(String host, int port, String path,
                         boolean secure, final Cookie cookie) {

        LOG.trace("enter RFC2965.match("
                  + "String, int, String, boolean, Cookie");
        if (cookie == null) {
            throw new IllegalArgumentException("Cookie may not be null");
        }
        if (cookie instanceof Cookie2) {
            // check if cookie has expired
            if (cookie.isPersistent() && cookie.isExpired()) {
                return false;
            }
            CookieOrigin origin = new CookieOrigin(getEffectiveHost(host), port, path, secure); 
            for (Iterator i = getAttribHandlerIterator(); i.hasNext(); ) {
                CookieAttributeHandler handler = (CookieAttributeHandler) i.next();
                if (!handler.match(cookie, origin)) {
                    return false;
                }
            }
            return true;
        } else {
            // old-style cookies are matched according to the old rules
            return this.rfc2109.match(host, port, path, secure, cookie);
        }
    }

    private void doFormatCookie2(final Cookie2 cookie, final StringBuffer buffer) {
        String name = cookie.getName();
        String value = cookie.getValue();
        if (value == null) {
            value = "";
        }
        this.formatter.format(buffer, new NameValuePair(name, value));
        // format domain attribute
        if (cookie.getDomain() != null && cookie.isDomainAttributeSpecified()) {
            buffer.append("; ");
            this.formatter.format(buffer, new NameValuePair("$Domain", cookie.getDomain()));
        }
        // format path attribute
        if ((cookie.getPath() != null) && (cookie.isPathAttributeSpecified())) {
            buffer.append("; ");
            this.formatter.format(buffer, new NameValuePair("$Path", cookie.getPath()));
        }
        // format port attribute
        if (cookie.isPortAttributeSpecified()) {
            String portValue = "";
            if (!cookie.isPortAttributeBlank()) {
                portValue = createPortAttribute(cookie.getPorts());
            }
            buffer.append("; ");
            this.formatter.format(buffer, new NameValuePair("$Port", portValue));
        }
    }
    
    /**
     * Return a string suitable for sending in a <tt>"Cookie"</tt> header as
     * defined in RFC 2965
     * @param cookie a {@link org.apache.commons.httpclient.Cookie} to be formatted as string
     * @return a string suitable for sending in a <tt>"Cookie"</tt> header.
     */
    public String formatCookie(final Cookie cookie) {
        LOG.trace("enter RFC2965Spec.formatCookie(Cookie)");

        if (cookie == null) {
            throw new IllegalArgumentException("Cookie may not be null");
        }
        if (cookie instanceof Cookie2) {
            Cookie2 cookie2 = (Cookie2) cookie;
            int version = cookie2.getVersion();
            final StringBuffer buffer = new StringBuffer();
            this.formatter.format(buffer, new NameValuePair("$Version", Integer.toString(version)));
            buffer.append("; ");
            doFormatCookie2(cookie2, buffer);
            return buffer.toString();
        } else {
            // old-style cookies are formatted according to the old rules
            return this.rfc2109.formatCookie(cookie);
        }
    }

    /**
     * Create a RFC 2965 compliant <tt>"Cookie"</tt> header value containing all
     * {@link org.apache.commons.httpclient.Cookie}s suitable for
     * sending in a <tt>"Cookie"</tt> header
     * @param cookies an array of {@link org.apache.commons.httpclient.Cookie}s to be formatted
     * @return a string suitable for sending in a Cookie header.
     */
    public String formatCookies(final Cookie[] cookies) {
        LOG.trace("enter RFC2965Spec.formatCookieHeader(Cookie[])");

        if (cookies == null) {
            throw new IllegalArgumentException("Cookies may not be null");
        }
        // check if cookies array contains a set-cookie (old style) cookie
        boolean hasOldStyleCookie = false;
        int version = -1;
        for (int i = 0; i < cookies.length; i++) {
            Cookie cookie = cookies[i];
            if (!(cookie instanceof Cookie2)) {
                hasOldStyleCookie = true;
                break;
            }
            if (cookie.getVersion() > version) {
                version = cookie.getVersion();
            }
        }
        if (version < 0) {
            version = 0;
        }
        if (hasOldStyleCookie || version < 1) {
            // delegate old-style cookie formatting to rfc2109Spec
            return this.rfc2109.formatCookies(cookies);
        }
        // Arrange cookies by path
        Arrays.sort(cookies, PATH_COMPOARATOR);
        
        final StringBuffer buffer = new StringBuffer();
        // format cookie version
        this.formatter.format(buffer, new NameValuePair("$Version", Integer.toString(version)));
        for (int i = 0; i < cookies.length; i++) {
            buffer.append("; ");
            Cookie2 cookie = (Cookie2) cookies[i];
            // format cookie attributes
            doFormatCookie2(cookie, buffer);
        }
        return buffer.toString();
    }

    /**
     * Retrieves valid Port attribute value for the given ports array.
     * e.g. "8000,8001,8002"
     *
     * @param ports int array of ports
     */
    private String createPortAttribute(int[] ports) {
        StringBuffer portValue = new StringBuffer();
        for (int i = 0, len = ports.length; i < len; i++) {
            if (i > 0) {
                portValue.append(",");
            }
            portValue.append(ports[i]);
        }
        return portValue.toString();
    }

    /**
     * Parses the given Port attribute value (e.g. "8000,8001,8002")
     * into an array of ports.
     *
     * @param portValue port attribute value
     * @return parsed array of ports
     * @throws MalformedCookieException if there is a problem in
     *          parsing due to invalid portValue.
     */
    private int[] parsePortAttribute(final String portValue)
            throws MalformedCookieException {
        StringTokenizer st = new StringTokenizer(portValue, ",");
        int[] ports = new int[st.countTokens()];
        try {
            int i = 0;
            while(st.hasMoreTokens()) {
                ports[i] = Integer.parseInt(st.nextToken().trim());
                if (ports[i] < 0) {
                  throw new MalformedCookieException ("Invalid Port attribute.");
                }
                ++i;
            }
        } catch (NumberFormatException e) {
            throw new MalformedCookieException ("Invalid Port "
                                                + "attribute: " + e.getMessage());
        }
        return ports;
    }

    /**
     * Gets 'effective host name' as defined in RFC 2965.
     * <p>
     * If a host name contains no dots, the effective host name is
     * that name with the string .local appended to it.  Otherwise
     * the effective host name is the same as the host name.  Note
     * that all effective host names contain at least one dot.
     *
     * @param host host name where cookie is received from or being sent to.
     * @return
     */
    private static String getEffectiveHost(final String host) {
        String effectiveHost = host.toLowerCase();
        if (host.indexOf('.') < 0) {
            effectiveHost += ".local";
        }
        return effectiveHost;
    }

    /**
     * Performs domain-match as defined by the RFC2965.
     * <p>
     * Host A's name domain-matches host B's if
     * <ol>
     *   <ul>their host name strings string-compare equal; or</ul>
     *   <ul>A is a HDN string and has the form NB, where N is a non-empty
     *       name string, B has the form .B', and B' is a HDN string.  (So,
     *       x.y.com domain-matches .Y.com but not Y.com.)</ul>
     * </ol>
     *
     * @param host host name where cookie is received from or being sent to.
     * @param domain The cookie domain attribute.
     * @return true if the specified host matches the given domain.
     */
    public boolean domainMatch(String host, String domain) {
        boolean match = host.equals(domain)
                        || (domain.startsWith(".") && host.endsWith(domain));

        return match;
    }

    /**
     * Returns <tt>true</tt> if the given port exists in the given
     * ports list.
     *
     * @param port port of host where cookie was received from or being sent to.
     * @param ports port list
     * @return true returns <tt>true</tt> if the given port exists in
     *         the given ports list; <tt>false</tt> otherwise.
     */
    private boolean portMatch(int port, int[] ports) {
        boolean portInList = false;
        for (int i = 0, len = ports.length; i < len; i++) {
            if (port == ports[i]) {
                portInList = true;
                break;
            }
        }
        return portInList;
    }

    /**
     * <tt>"Path"</tt> attribute handler for RFC 2965 cookie spec.
     */
    private class Cookie2PathAttributeHandler
            implements CookieAttributeHandler {

        /**
         * Parse cookie path attribute.
         */
        public void parse(final Cookie cookie, final String path)
                throws MalformedCookieException {
            if (cookie == null) {
                throw new IllegalArgumentException("Cookie may not be null");
            }
            if (path == null) {
                throw new MalformedCookieException(
                        "Missing value for path attribute");
            }
            if (path.trim().equals("")) {
                throw new MalformedCookieException(
                        "Blank value for path attribute");
            }
            cookie.setPath(path);
            cookie.setPathAttributeSpecified(true);
        }

        /**
         * Validate cookie path attribute. The value for the Path attribute must be a
         * prefix of the request-URI (case-sensitive matching).
         */
        public void validate(final Cookie cookie, final CookieOrigin origin)
                throws MalformedCookieException {
            if (cookie == null) {
                throw new IllegalArgumentException("Cookie may not be null");
            }
            if (origin == null) {
                throw new IllegalArgumentException("Cookie origin may not be null");
            }
            String path = origin.getPath();
            if (path == null) {
                throw new IllegalArgumentException(
                        "Path of origin host may not be null.");
            }
            if (cookie.getPath() == null) {
                throw new MalformedCookieException("Invalid cookie state: " +
                                                   "path attribute is null.");
            }
            if (path.trim().equals("")) {
                path = PATH_DELIM;
            }

            if (!pathMatch(path, cookie.getPath())) {
                throw new MalformedCookieException(
                        "Illegal path attribute \"" + cookie.getPath()
                        + "\". Path of origin: \"" + path + "\"");
            }
        }

        /**
         * Match cookie path attribute. The value for the Path attribute must be a
         * prefix of the request-URI (case-sensitive matching).
         */
        public boolean match(final Cookie cookie, final CookieOrigin origin) {
            if (cookie == null) {
                throw new IllegalArgumentException("Cookie may not be null");
            }
            if (origin == null) {
                throw new IllegalArgumentException("Cookie origin may not be null");
            }
            String path = origin.getPath();
            if (cookie.getPath() == null) {
                LOG.warn("Invalid cookie state: path attribute is null.");
                return false;
            }
            if (path.trim().equals("")) {
                path = PATH_DELIM;
            }

            if (!pathMatch(path, cookie.getPath())) {
                return false;
            }
            return true;
        }
    }

    /**
     * <tt>"Domain"</tt> cookie attribute handler for RFC 2965 cookie spec.
     */
    private class Cookie2DomainAttributeHandler
            implements CookieAttributeHandler {

        /**
         * Parse cookie domain attribute.
         */
        public void parse(final Cookie cookie, String domain)
                throws MalformedCookieException {
            if (cookie == null) {
                throw new IllegalArgumentException("Cookie may not be null");
            }
            if (domain == null) {
                throw new MalformedCookieException(
                        "Missing value for domain attribute");
            }
            if (domain.trim().equals("")) {
                throw new MalformedCookieException(
                        "Blank value for domain attribute");
            }
            domain = domain.toLowerCase();
            if (!domain.startsWith(".")) {
                // Per RFC 2965 section 3.2.2
                // "... If an explicitly specified value does not start with
                // a dot, the user agent supplies a leading dot ..."
                // That effectively implies that the domain attribute 
                // MAY NOT be an IP address of a host name
                domain = "." + domain;
            }
            cookie.setDomain(domain);
            cookie.setDomainAttributeSpecified(true);
        }

        /**
         * Validate cookie domain attribute.
         */
        public void validate(final Cookie cookie, final CookieOrigin origin)
                throws MalformedCookieException {
            if (cookie == null) {
                throw new IllegalArgumentException("Cookie may not be null");
            }
            if (origin == null) {
                throw new IllegalArgumentException("Cookie origin may not be null");
            }
            String host = origin.getHost().toLowerCase();
            if (cookie.getDomain() == null) {
                throw new MalformedCookieException("Invalid cookie state: " +
                                                   "domain not specified");
            }
            String cookieDomain = cookie.getDomain().toLowerCase();

            if (cookie.isDomainAttributeSpecified()) {
                // Domain attribute must start with a dot
                if (!cookieDomain.startsWith(".")) {
                    throw new MalformedCookieException("Domain attribute \"" +
                        cookie.getDomain() + "\" violates RFC 2109: domain must start with a dot");
                }

                // Domain attribute must contain atleast one embedded dot,
                // or the value must be equal to .local.
                int dotIndex = cookieDomain.indexOf('.', 1);
                if (((dotIndex < 0) || (dotIndex == cookieDomain.length() - 1))
                    && (!cookieDomain.equals(".local"))) {
                    throw new MalformedCookieException(
                            "Domain attribute \"" + cookie.getDomain()
                            + "\" violates RFC 2965: the value contains no embedded dots "
                            + "and the value is not .local");
                }

                // The effective host name must domain-match domain attribute.
                if (!domainMatch(host, cookieDomain)) {
                    throw new MalformedCookieException(
                            "Domain attribute \"" + cookie.getDomain()
                            + "\" violates RFC 2965: effective host name does not "
                            + "domain-match domain attribute.");
                }

                // effective host name minus domain must not contain any dots
                String effectiveHostWithoutDomain = host.substring(
                        0, host.length() - cookieDomain.length());
                if (effectiveHostWithoutDomain.indexOf('.') != -1) {
                    throw new MalformedCookieException("Domain attribute \""
                                                       + cookie.getDomain() + "\" violates RFC 2965: "
                                                       + "effective host minus domain may not contain any dots");
                }
            } else {
                // Domain was not specified in header. In this case, domain must
                // string match request host (case-insensitive).
                if (!cookie.getDomain().equals(host)) {
                    throw new MalformedCookieException("Illegal domain attribute: \""
                                                       + cookie.getDomain() + "\"."
                                                       + "Domain of origin: \""
                                                       + host + "\"");
                }
            }
        }

        /**
         * Match cookie domain attribute.
         */
        public boolean match(final Cookie cookie, final CookieOrigin origin) {
            if (cookie == null) {
                throw new IllegalArgumentException("Cookie may not be null");
            }
            if (origin == null) {
                throw new IllegalArgumentException("Cookie origin may not be null");
            }
            String host = origin.getHost().toLowerCase();
            String cookieDomain = cookie.getDomain();

            // The effective host name MUST domain-match the Domain
            // attribute of the cookie.
            if (!domainMatch(host, cookieDomain)) {
                return false;
            }
            // effective host name minus domain must not contain any dots
            String effectiveHostWithoutDomain = host.substring(
                    0, host.length() - cookieDomain.length());
            if (effectiveHostWithoutDomain.indexOf('.') != -1) {
                return false;
            }
            return true;
        }

    }

    /**
     * <tt>"Port"</tt> cookie attribute handler for RFC 2965 cookie spec.
     */
    private class Cookie2PortAttributeHandler
            implements CookieAttributeHandler {

        /**
         * Parse cookie port attribute.
         */
        public void parse(final Cookie cookie, final String portValue)
                throws MalformedCookieException {
            if (cookie == null) {
                throw new IllegalArgumentException("Cookie may not be null");
            }
            if (cookie instanceof Cookie2) {
                Cookie2 cookie2 = (Cookie2) cookie;
                if ((portValue == null) || (portValue.trim().equals(""))) {
                    // If the Port attribute is present but has no value, the
                    // cookie can only be sent to the request-port.
                    // Since the default port list contains only request-port, we don't
                    // need to do anything here.
                    cookie2.setPortAttributeBlank(true);
                } else {
                    int[] ports = parsePortAttribute(portValue);
                    cookie2.setPorts(ports);
                }
                cookie2.setPortAttributeSpecified(true);
            }
        }

        /**
         * Validate cookie port attribute. If the Port attribute was specified
         * in header, the request port must be in cookie's port list.
         */
        public void validate(final Cookie cookie, final CookieOrigin origin)
                throws MalformedCookieException {
            if (cookie == null) {
                throw new IllegalArgumentException("Cookie may not be null");
            }
            if (origin == null) {
                throw new IllegalArgumentException("Cookie origin may not be null");
            }
            if (cookie instanceof Cookie2) {
                Cookie2 cookie2 = (Cookie2) cookie;
                int port = origin.getPort();
                if (cookie2.isPortAttributeSpecified()) {
                    if (!portMatch(port, cookie2.getPorts())) {
                        throw new MalformedCookieException(
                                "Port attribute violates RFC 2965: "
                                + "Request port not found in cookie's port list.");
                    }
                }
            }
        }

        /**
         * Match cookie port attribute. If the Port attribute is not specified
         * in header, the cookie can be sent to any port. Otherwise, the request port
         * must be in the cookie's port list.
         */
        public boolean match(final Cookie cookie, final CookieOrigin origin) {
            if (cookie == null) {
                throw new IllegalArgumentException("Cookie may not be null");
            }
            if (origin == null) {
                throw new IllegalArgumentException("Cookie origin may not be null");
            }
            if (cookie instanceof Cookie2) {
                Cookie2 cookie2 = (Cookie2) cookie;
                int port = origin.getPort();
                if (cookie2.isPortAttributeSpecified()) {
                    if (cookie2.getPorts() == null) {
                        LOG.warn("Invalid cookie state: port not specified");
                        return false;
                    }
                    if (!portMatch(port, cookie2.getPorts())) {
                        return false;
                    }
                }
                return true;
            } else {
                return false;
            }
        }
    }

  /**
   * <tt>"Max-age"</tt> cookie attribute handler for RFC 2965 cookie spec.
   */
  private class Cookie2MaxageAttributeHandler
          implements CookieAttributeHandler {

      /**
       * Parse cookie max-age attribute.
       */
      public void parse(final Cookie cookie, final String value)
              throws MalformedCookieException {
          if (cookie == null) {
              throw new IllegalArgumentException("Cookie may not be null");
          }
          if (value == null) {
              throw new MalformedCookieException(
                      "Missing value for max-age attribute");
          }
          int age = -1;
          try {
              age = Integer.parseInt(value);
          } catch (NumberFormatException e) {
              age = -1;
          }
          if (age < 0) {
              throw new MalformedCookieException ("Invalid max-age attribute.");
          }
          cookie.setExpiryDate(new Date(System.currentTimeMillis() + age * 1000L));
      }

      /**
       * validate cookie max-age attribute.
       */
      public void validate(final Cookie cookie, final CookieOrigin origin) {
      }

      /**
       * @see CookieAttributeHandler#match(org.apache.commons.httpclient.Cookie, String)
       */
      public boolean match(final Cookie cookie, final CookieOrigin origin) {
          return true;
      }

  }

  /**
   * <tt>"Secure"</tt> cookie attribute handler for RFC 2965 cookie spec.
   */
  private class CookieSecureAttributeHandler
          implements CookieAttributeHandler {

      public void parse(final Cookie cookie, final String secure)
              throws MalformedCookieException {
          cookie.setSecure(true);
      }

      public void validate(final Cookie cookie, final CookieOrigin origin)
              throws MalformedCookieException {
      }

      public boolean match(final Cookie cookie, final CookieOrigin origin) {
          if (cookie == null) {
              throw new IllegalArgumentException("Cookie may not be null");
          }
          if (origin == null) {
              throw new IllegalArgumentException("Cookie origin may not be null");
          }
          return cookie.getSecure() == origin.isSecure();
      }
      
  }

  /**
   * <tt>"Commant"</tt> cookie attribute handler for RFC 2965 cookie spec.
   */
  private class CookieCommentAttributeHandler
          implements CookieAttributeHandler {

      public void parse(final Cookie cookie, final String comment)
              throws MalformedCookieException {
          cookie.setComment(comment);
      }

      public void validate(final Cookie cookie, final CookieOrigin origin)
              throws MalformedCookieException {
      }

      public boolean match(final Cookie cookie, final CookieOrigin origin) {
          return true;
      }
      
  }

  /**
   * <tt>"CommantURL"</tt> cookie attribute handler for RFC 2965 cookie spec.
   */
  private class CookieCommentUrlAttributeHandler
          implements CookieAttributeHandler {

      public void parse(final Cookie cookie, final String commenturl)
              throws MalformedCookieException {
          if (cookie instanceof Cookie2) {
              Cookie2 cookie2 = (Cookie2) cookie;
              cookie2.setCommentURL(commenturl);
          }
      }

      public void validate(final Cookie cookie, final CookieOrigin origin)
              throws MalformedCookieException {
      }

      public boolean match(final Cookie cookie, final CookieOrigin origin) {
          return true;
      }
      
  }

  /**
   * <tt>"Discard"</tt> cookie attribute handler for RFC 2965 cookie spec.
   */
  private class CookieDiscardAttributeHandler
          implements CookieAttributeHandler {

      public void parse(final Cookie cookie, final String commenturl)
              throws MalformedCookieException {
          if (cookie instanceof Cookie2) {
              Cookie2 cookie2 = (Cookie2) cookie;
              cookie2.setDiscard(true);
          }
      }

      public void validate(final Cookie cookie, final CookieOrigin origin)
              throws MalformedCookieException {
      }

      public boolean match(final Cookie cookie, final CookieOrigin origin) {
          return true;
      }
      
  }

  /**
     * <tt>"Version"</tt> cookie attribute handler for RFC 2965 cookie spec.
     */
    private class Cookie2VersionAttributeHandler
            implements CookieAttributeHandler {

        /**
         * Parse cookie version attribute.
         */
        public void parse(final Cookie cookie, final String value)
                throws MalformedCookieException {
            if (cookie == null) {
                throw new IllegalArgumentException("Cookie may not be null");
            }
            if (cookie instanceof Cookie2) {
                Cookie2 cookie2 = (Cookie2) cookie;
                if (value == null) {
                    throw new MalformedCookieException(
                            "Missing value for version attribute");
                }
                int version = -1;
                try {
                    version = Integer.parseInt(value);
                } catch (NumberFormatException e) {
                    version = -1;
                }
                if (version < 0) {
                    throw new MalformedCookieException("Invalid cookie version.");
                }
                cookie2.setVersion(version);
                cookie2.setVersionAttributeSpecified(true);
            }
        }

        /**
         * validate cookie version attribute. Version attribute is REQUIRED.
         */
        public void validate(final Cookie cookie, final CookieOrigin origin)
                throws MalformedCookieException {
            if (cookie == null) {
                throw new IllegalArgumentException("Cookie may not be null");
            }
            if (cookie instanceof Cookie2) {
                Cookie2 cookie2 = (Cookie2) cookie;
                if (!cookie2.isVersionAttributeSpecified()) {
                    throw new MalformedCookieException(
                            "Violates RFC 2965. Version attribute is required.");
                }
            }
        }

        public boolean match(final Cookie cookie, final CookieOrigin origin) {
            return true;
        }

    }

    public int getVersion() {
        return 1;
    }

    public Header getVersionHeader() {
        ParameterFormatter formatter = new ParameterFormatter();
        StringBuffer buffer = new StringBuffer();
        formatter.format(buffer, new NameValuePair("$Version",
                Integer.toString(getVersion())));
        return new Header("Cookie2", buffer.toString(), true);
    }
    
}

