/*
 * 
 * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source
 * License v3.0/Jini Technology Specific Attachment v 1.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://wwws.sun.com/software/jini/licensing/SCSL3_JiniTSA1.html.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Reference Code is Jini Technology Core Platform code, v2.0. The
 * Developer of the Reference Code is Sun Microsystems, Inc.
 * 
 * Contributor(s): Sun Microsystems, Inc.
 * 
 * The contents of this file comply with the Jini Technology Core
 * Platform Compatibility Kit, v2.0A.
 * 
 * Tester(s): Sun Microsystems, Inc.
 * 
 * Test Platform(s):
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Solaris(TM)SPARC/x86
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Linux (Intel x86)
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Windows (Intel Platform)
 * 
 */

package net.jini.url.httpmd;

import com.sun.jini.logging.Levels;
import com.sun.jini.logging.LogManager;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.logging.Logger;

/**
 * A stream handler for URLs with the HTTPMD protocol. HTTPMD URLs provide a
 * way to insure the integrity of data retrieved from an HTTP URL. The HTTPMD
 * URL includes a message digest for the data to be retrieved from the URL. The
 * URL input stream insures that the data has the correct message digest when
 * the end of file for the stream is reached. If the data has the wrong
 * message digest, a {@link WrongMessageDigestException} is thrown. <p>
 *
 * HTTPMD URLs may be used to guarantee the integrity of a downloaded object's
 * codebase if used when specifying the URLs for the JAR files containing the
 * object's classes. Because HTTPMD URLs specify a message digest for a single
 * item, they should not be used for directories of classes. <p>
 *
 * HTTPMD URLs have a syntax similar to that of HTTP URLs, but include a
 * message digest as the last parameter stored in the last segment of the
 * path. The parameter is introduced by the '<code>;</code>' character, and
 * includes the name of the message digest algorithm, a '<code>=</code>', the
 * message digest, and an optional comment introduced by the '<code>,</code>'
 * character. In addition, a comment by itself may be specified in a relative
 * HTTPMD URL. Comments are ignored when using <code>equals</code> to compare
 * HTTPMD URLs. The comment specified in the context URL is ignored when
 * parsing a relative HTTPMD URL. Adding a comment to an HTTPMD URL is useful
 * in cases where the URL is required to have a particular suffix, for example
 * the ".jar" file extension. A comment-only relative HTTPMD URL is useful when
 * specifying the URL of the containing document from within the contents of
 * the document, where the message digest cannot be specified because it is not
 * yet known. <p>
 *
 * The message digest algorithm is case-insensitive, and may include ASCII
 * letters and numbers, as well as the following characters:
 *
 * <pre>
 * - _ . ~ * ' ( ) : @ & + $ ,
 * </pre> <p>
 *
 * The value specifies the name of the {@link MessageDigest} algorithm to
 * use. For the URL syntax to be valid, the value must be the name of a
 * <code>MessageDigest</code> algorithm as determined by calling
 * {@link MessageDigest#getInstance(String)}. <p>
 *
 * The message digest is represented as a positive hexadecimal integer, using
 * digits, and the letters '<code>a</code>' through '<code>f</code>', in either
 * lowercase or uppercase. <p>
 * 
 * The characters following the '<code>,</code>' comment character may include
 * ASCII letters and numbers, as well as the following characters:
 *
 * <pre>
 * - _ . ~ * ' ( ) : @ & = + $ ,
 * </pre> <p>
 *
 * Here are some examples of HTTPMD URLs: <p> <ul>
 *
 * <li> An absolute URL: <br>
 *      <code>
 *	httpmd://www.sun.com/index.html;md5=7be207c7111e459eeea1c9b3d04f1667
 *	</code>
 *
 * <li> A relative URL: <br>
 *	    <code>index.html;sha=99f6837808c0a79398bf69d83cfb1b82d20cf0cf,Comment
 *	    </code>
 *
 * <li> A comment-only relative URL: <br>
 *	    <code>,.jar</code>
 * </ul>
 *
 * @author Sun Microsystems, Inc.
 * @see HttpmdUtil
 * @since 2.0
 *
 * @com.sun.jini.impl <!-- Implementation Specifics -->
 *
 * This implementation of HTTPMD URLs uses the {@link Logger} named
 * <code>net.jini.url.httpmd</code> to log information at the following logging
 * levels: <p>
 *
 * <table border="1" cellpadding="5" summary="Describes logging performed
 *	  by the HTTPMD URL handler at different logging levels">
 *
 * <caption halign="center" valign="top"><b><code>
 *	    net.jini.url.httpmd</code></b></caption>
 *
 * <tr> <th scope="col"> Level <th scope="col"> Description
 *
 * <tr> <td> {@link Levels#FAILED FAILED} <td> URL input stream detects an
 *	incorrect message digest
 *
 * </table> <p>
 *
 * See the {@link LogManager} class for one way to use the <code>FAILED</code>
 * logging level in standard logging configuration files.
 */
public class Handler extends URLStreamHandler {

    /** Creates a URL stream handler for HTTPMD URLs. */
    public Handler() { }

    /**
     * Returns the default port for a URL parsed by this handler, which is
     * <code>80</code>.
     *
     * @return the default port for a URL parsed by this handler
     */
    protected int getDefaultPort() {
        return 80;
    }

    /** Creates a HTTP URL connection for an HTTPMD URL. */
    protected URLConnection openConnection(URL u) throws IOException {
	return new HttpmdURLConnection(u);
    }

    /**
     * Parses the string representation of an HTTPMD URL object.
     *
     * @throws IllegalArgumentException if the URL is malformed
     */
    protected void parseURL(URL url, String spec, int start, int limit) {
	/* Check for a relative URL that only specifies a comment */
	if (start < limit && spec.charAt(start) == ',') {
	    String query = url.getQuery();
	    int queryStart = spec.indexOf('?', start);
	    if (queryStart != -1) {
		query = spec.substring(queryStart + 1, limit);
		limit = queryStart;
	    }
	    String path = url.getPath() == null ? "" : url.getPath();
	    /* Remove the comment from the context path */
	    int param = path.lastIndexOf(';');
	    if (param != -1) {
		int comment = path.indexOf(',', param);
		if (comment != -1) {
		    path = path.substring(0, comment);
		}
	    }
	    /* Append the new comment */
	    path += spec.substring(start, limit);
	    setURL(url, url.getProtocol(), url.getHost(), url.getPort(),
		   url.getAuthority(), url.getUserInfo(), path, query,
		   url.getRef());
	} else {
	    /* Otherwise, combine spec with the context URL */
	    super.parseURL(url, spec, start, limit);
	}

	/* Check syntax of message digest parameter */
	String path = url.getPath() == null ? "" : url.getPath();
	int semiIndex = path.lastIndexOf(';');
	if (semiIndex < 0) {
	    throw new IllegalArgumentException(
		"Message digest parameter is missing");
	}
	int equalsIndex = path.indexOf('=', semiIndex);
	if (equalsIndex < 0) {
	    throw new IllegalArgumentException(
		"Message digest parameter is missing a '='");
	}
	String algorithm = path.substring(semiIndex + 1, equalsIndex);
	try {
	    MessageDigest.getInstance(algorithm);
	} catch (NoSuchAlgorithmException e) {
	    throw new IllegalArgumentException(
		"Message digest parameter algorithm is not found: " +
		algorithm);
	}
	String digest = path.substring(equalsIndex + 1);
	int comment = digest.indexOf(',');
	if (comment != -1) {
	    for (int i = digest.length(); --i > comment; ) {
		char c = digest.charAt(i);
		if (!HttpmdUtil.commentChar(c)) {
		    throw new IllegalArgumentException(
			"Comment contains illegal character: '" + c + "'");
		}
	    }
	    digest = digest.substring(0, comment);
	}
	int length = digest.length();
	if (length == 0) {
	    throw new IllegalArgumentException(
		"Message digest parameter digest is empty");
	}
	for (int i = length; --i >= 0; ) {
	    char c = digest.charAt(i);
	    if (Character.digit(c, 16) < 0) {
		throw new IllegalArgumentException(
		    "Message digest parameter has invalid hex character: " +
		    c);
	    }
	}
    }

    /**
     * Compares two HTTPMD URLs to see if they refer to the same file. Performs
     * case-insensitive comparison of the protocols and of the message digest
     * parameters (ignoring the comment), calls <code>hostsEqual</code> to
     * compare the hosts, compares the ports, with <code>-1</code> matching the
     * default HTTP port (<code>80</code>), and performs case-sensitive
     * comparison on the remaining portions.
     */
    protected boolean sameFile(URL u1, URL u2) {
        /* Compare the protocols */
        if (!((u1.getProtocol() == u2.getProtocol()) ||
              (u1.getProtocol() != null &&
               u1.getProtocol().equalsIgnoreCase(u2.getProtocol()))))
	{
            return false;
	}
	/* Compare the hosts */
	if (!hostsEqual(u1, u2)) {
            return false;
	}
	/* Compare the paths */
	String path1 = u1.getPath();
	String path2 = u2.getPath();
	if (path1 == path2) {
	    /* Paths are OK */
	} else if (path1 == null || path2 == null) {
	    return false;
	} else if (!path1.equals(path2)) {
	    /*
	     * Perform case insensitive matching on the message digest
	     * parameters, ignoring comments.
	     */
	    int param = path1.lastIndexOf(';');
	    if (param < 0 || param != path2.lastIndexOf(';')) {
		return false;
	    }
	    /* Case-sensitive match for non-parameter part */
	    if (!path1.regionMatches(0, path2, 0, param)) {
		return false;
	    }
	    /* Ignore comments */
	    int comment1 = path1.indexOf(',', param + 1);
	    int len = (comment1 != -1) ? comment1 : path1.length();
	    int comment2 = path2.indexOf(',', param + 1);
	    int len2 = (comment2 != -1) ? comment2 : path2.length();
	    if (len != len2) {
		return false;
	    }
	    /* Case-insensitive match for algorithm and digest */
	    if (!path1.regionMatches(true, param + 1,
				     path2, param + 1, len - param - 1))
	    {
		return false;
	    }
	}
	/* Compare queries */
	if (u1.getQuery() == null
	    ? u2.getQuery() != null
	    : !u1.getQuery().equals(u2.getQuery()))
	{
	    return false;
	}
	/* Compare the ports */
        int port1 = (u1.getPort() != -1) ? u1.getPort() : u1.getDefaultPort();
        int port2 = (u2.getPort() != -1) ? u2.getPort() : u2.getDefaultPort();
	if (port1 != port2) {
	    return false;
	}
        return true;
    }

    /**
     * Computes the hash code for the specified URL. This method ignores the
     * comment portion of the message digest parameter, and ignores the
     * case of characters in the message digest and algorithm.
     */
    protected int hashCode(URL u) {
        int h = 0;

        /* Generate the protocol part */
        String protocol = u.getProtocol();
        if (protocol != null) {
	    h += protocol.hashCode();
	}

        /* Generate the host part */
	InetAddress addr = getHostAddress(u);
	if (addr != null) {
	    h += addr.hashCode();
	} else {
            String host = u.getHost();
            if (host != null) {
	        h += host.toLowerCase().hashCode();
	    }
        }

	/*
	 * Generate the path part, ignoring case in the message digest and
	 * algorithm, and ignoring comment.
	 */
	String path = u.getPath();
	if (path != null) {
	    int param = path.lastIndexOf(';');
	    if (param == -1) {
		h += path.hashCode();
	    } else {
		h += path.substring(0, param).hashCode();
		int comment = path.indexOf(',', param);
		if (comment != -1) {
		    path = path.substring(0, comment);
		}
		h += path.substring(param).toLowerCase().hashCode();
	    }
	}

        /* Generate the query part */
        String query = u.getQuery();
	if (query != null) {
	    h += query.hashCode();
	}

        /* Generate the port part */
	if (u.getPort() == -1) {
            h += getDefaultPort();
	} else {
            h += u.getPort();
	}

        /* Generate the ref part */
        String ref = u.getRef();
	if (ref != null)
            h += ref.hashCode();

	return h;
    }
}
