/*
 * 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.
 */

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>
 *
 * 
 * @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 equalsIndex = path.indexOf('=', param);
		if (equalsIndex < 0) {
		    throw new IllegalArgumentException(
		        "Message digest parameter is missing a '='");
		}
		int comment = path.indexOf(',', equalsIndex);
		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);
	    }
	}
    }
    
    /**
     * The default superclass implementation performs dns lookup to determine
     * if hosts are equal, this allows two URL's with different hashCode's
     * to be equal, breaking the hashCode equals contract.
     * 
     * It also causes a test failure in the jtreg test suite.
     * 
     * 
     * *** Start test: Mon Jan 23 08:11:26 EST 2012
     * [jtreg] Test 9: TestEqual: httpmd://foo:88/bar/baz;p1=v1;md5=abcd?q#r, httpmd://alpha:88/bar/baz;p1=v1;md5=abcd?q#r
     * [jtreg] FAIL: Should be: false
     * [jtreg]       Result: true
     * 
     * URL.implies(URL url) is better suited to perform this function, why
     * it was originally implemented in equals is unknown.
     */
    protected boolean hostsEqual(URL u1, URL u2) {
	if (u1.getHost() != null && u2.getHost() != null) 
            return u1.getHost().equalsIgnoreCase(u2.getHost());
	 else
            return u1.getHost() == null && u2.getHost() == null;
    }

    /**
     * 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() != 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 == null || path2 == null) {
	    return false;
        } else if ( path1.equals(path2)){
            /* Paths are OK */
	} else {
	    /*
	     * 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 equalsIndex = path1.indexOf('=', param + 1);
	    if (equalsIndex < 0 || equalsIndex != path2.lastIndexOf('=')) {
		return false;
	    }
	    int comment1 = path1.indexOf(',', equalsIndex + 1);
	    int len = (comment1 != -1) ? comment1 : path1.length();
	    int comment2 = path2.indexOf(',', equalsIndex + 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 equalsIndex = path.indexOf('=', param + 1);
		int comment = path.indexOf(',', equalsIndex != -1 ? equalsIndex
								  : param + 1);
		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;
    }
}
