/*
 * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/cookie/RFC2109Spec.java,v 1.21 2004/06/05 16:49:20 olegk Exp $
 * $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 org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.Cookie;
import org.apache.commons.httpclient.util.ParameterFormatter;

/**
 * <p>RFC 2109 specific cookie management functions
 *
 * @author  B.C. Holmes
 * @author <a href="mailto:jericho@thinkfree.com">Park, Sung-Gu</a>
 * @author <a href="mailto:dsale@us.britannica.com">Doug Sale</a>
 * @author Rod Waldhoff
 * @author dIon Gillard
 * @author Sean C. Sullivan
 * @author <a href="mailto:JEvans@Cyveillance.com">John Evans</a>
 * @author Marc A. Saegesser
 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
 * 
 * @since 2.0 
 */

public class RFC2109Spec extends CookieSpecBase {

    private final ParameterFormatter formatter;

    /**
     * Cookie Response Header  name for cookies processed
     * by this spec.
     */
    public final static String SET_COOKIE_KEY = "set-cookie";

    /** Default constructor */
    public RFC2109Spec() {
        super();
        this.formatter = new ParameterFormatter();
        this.formatter.setAlwaysUseQuotes(true);
    }

    /**
      * Parse RFC 2109 specific cookie attribute and update the corresponsing
      * {@link Cookie} properties.
      *
      * @param attribute {@link NameValuePair} cookie attribute from the
      * <tt>Set- Cookie</tt>
      * @param cookie {@link 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 (cookie == null) {
            throw new IllegalArgumentException("Cookie may not be null.");
        }
        final String paramName = attribute.getName().toLowerCase();
        final String paramValue = attribute.getValue();

        if (paramName.equals("path")) {
            if (paramValue == null) {
                throw new MalformedCookieException(
                    "Missing value for path attribute");
            }
            if (paramValue.trim().equals("")) {
                throw new MalformedCookieException(
                    "Blank value for path attribute");
            }
            cookie.setPath(paramValue);
            cookie.setPathAttributeSpecified(true);
        } else if (paramName.equals("version")) {

            if (paramValue == null) {
                throw new MalformedCookieException(
                    "Missing value for version attribute");
            }
            try {
               cookie.setVersion(Integer.parseInt(paramValue));
            } catch (NumberFormatException e) {
                throw new MalformedCookieException("Invalid version: " 
                    + e.getMessage());
            }

        } else {
            super.parseAttribute(attribute, cookie);
        }
    }

    /**
      * Performs RFC 2109 compliant {@link Cookie} validation
      *
      * @param host the host from which the {@link Cookie} was received
      * @param port the port from which the {@link Cookie} was received
      * @param path the path from which the {@link Cookie} was received
      * @param secure <tt>true</tt> when the {@link Cookie} was received using a
      * secure connection
      * @param cookie The cookie to validate
      * @throws MalformedCookieException if an exception occurs during
      * validation
      */
    public void validate(String host, int port, String path, 
        boolean secure, final Cookie cookie) throws MalformedCookieException {
            
        LOG.trace("enter RFC2109Spec.validate(String, int, String, "
            + "boolean, Cookie)");
            
        // Perform generic validation
        super.validate(host, port, path, secure, cookie);
        // Perform RFC 2109 specific validation
        
        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 $");
        }
        
        if (cookie.isDomainAttributeSpecified() 
            && (!cookie.getDomain().equals(host))) {
                
            // domain must start with dot
            if (!cookie.getDomain().startsWith(".")) {
                throw new MalformedCookieException("Domain attribute \"" 
                    + cookie.getDomain() 
                    + "\" violates RFC 2109: domain must start with a dot");
            }
            // domain must have at least one embedded dot
            int dotIndex = cookie.getDomain().indexOf('.', 1);
            if (dotIndex < 0 || dotIndex == cookie.getDomain().length() - 1) {
                throw new MalformedCookieException("Domain attribute \"" 
                    + cookie.getDomain() 
                    + "\" violates RFC 2109: domain must contain an embedded dot");
            }
            host = host.toLowerCase();
            if (!host.endsWith(cookie.getDomain())) {
                throw new MalformedCookieException(
                    "Illegal domain attribute \"" + cookie.getDomain() 
                    + "\". Domain of origin: \"" + host + "\"");
            }
            // host minus domain may not contain any dots
            String hostWithoutDomain = host.substring(0, host.length() 
                - cookie.getDomain().length());
            if (hostWithoutDomain.indexOf('.') != -1) {
                throw new MalformedCookieException("Domain attribute \"" 
                    + cookie.getDomain() 
                    + "\" violates RFC 2109: host minus domain may not contain any dots");
            }
        }
    }

    /**
     * Performs domain-match as defined by the RFC2109.
     * @param host The target host.
     * @param domain The cookie domain attribute.
     * @return true if the specified host matches the given domain.
     * 
     * @since 3.0
     */
    public boolean domainMatch(String host, String domain) {
        boolean match = host.equals(domain) 
            || (domain.startsWith(".") && host.endsWith(domain));

        return match;
    }

    /**
     * Return a name/value string suitable for sending in a <tt>"Cookie"</tt>
     * header as defined in RFC 2109 for backward compatibility with cookie
     * version 0
     * @param buffer The string buffer to use for output
     * @param param The parameter.
     * @param version The cookie version 
     */
    private void formatParam(final StringBuffer buffer, final NameValuePair param, int version) {
        if (version < 1) {
            buffer.append(param.getName());
            buffer.append("=");
            if (param.getValue() != null) {
                buffer.append(param.getValue());   
            }
        } else {
            this.formatter.format(buffer, param);
        }
    }

    /**
     * Return a string suitable for sending in a <tt>"Cookie"</tt> header 
     * as defined in RFC 2109 for backward compatibility with cookie version 0
     * @param buffer The string buffer to use for output
     * @param cookie The {@link Cookie} to be formatted as string
     * @param version The version to use.
     */
    private void formatCookieAsVer(final StringBuffer buffer, final Cookie cookie, int version) {
        String value = cookie.getValue();
        if (value == null) {
            value = "";
        }
        formatParam(buffer, new NameValuePair(cookie.getName(), value), version);
        if ((cookie.getPath() != null) && cookie.isPathAttributeSpecified()) {
          buffer.append("; ");
          formatParam(buffer, new NameValuePair("$Path", cookie.getPath()), version);
        }
        if ((cookie.getDomain() != null) 
            && cookie.isDomainAttributeSpecified()) {
            buffer.append("; ");
            formatParam(buffer, new NameValuePair("$Domain", cookie.getDomain()), version);
        }
    }

    /**
     * Return a string suitable for sending in a <tt>"Cookie"</tt> header as
     * defined in RFC 2109
     * @param cookie a {@link Cookie} to be formatted as string
     * @return a string suitable for sending in a <tt>"Cookie"</tt> header.
     */
    public String formatCookie(Cookie cookie) {
        LOG.trace("enter RFC2109Spec.formatCookie(Cookie)");
        if (cookie == null) {
            throw new IllegalArgumentException("Cookie may not be null");
        }
        int version = cookie.getVersion();
        StringBuffer buffer = new StringBuffer();
        formatParam(buffer, 
                new NameValuePair("$Version", Integer.toString(version)), 
                version);
        buffer.append("; ");
        formatCookieAsVer(buffer, cookie, version);
        return buffer.toString();
    }

    /**
     * Create a RFC 2109 compliant <tt>"Cookie"</tt> header value containing all
     * {@link Cookie}s in <i>cookies</i> suitable for sending in a <tt>"Cookie"
     * </tt> header
     * @param cookies an array of {@link Cookie}s to be formatted
     * @return a string suitable for sending in a Cookie header.
     */
    public String formatCookies(Cookie[] cookies) {
        LOG.trace("enter RFC2109Spec.formatCookieHeader(Cookie[])");
        int version = Integer.MAX_VALUE;
        // Pick the lowerest common denominator
        for (int i = 0; i < cookies.length; i++) {
            Cookie cookie = cookies[i];
            if (cookie.getVersion() < version) {
                version = cookie.getVersion();
            }
        }
        final StringBuffer buffer = new StringBuffer();
        formatParam(buffer, 
                new NameValuePair("$Version", Integer.toString(version)), 
                version);
        for (int i = 0; i < cookies.length; i++) {
            buffer.append("; ");
            formatCookieAsVer(buffer, cookies[i], version);
        }
        return buffer.toString();
    }

}
