/*
 * 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 java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.StringTokenizer;
import net.jini.security.Security;

/**
 * Provides utility methods for creating and using HTTPMD URLs.
 *
 * 
 * @since 2.0
 */
public class HttpmdUtil {

    /**
     * A URL handler to use for HTTPMD URLs so we don't require the protocol
     * handler to be installed.
     */
    private static final Handler handler = new Handler();

    /** Not instantiable. */
    private HttpmdUtil() { }

    /**
     * Computes the message digest of data specified by a URL.
     *
     * @param url the URL of the data
     * @param algorithm the message digest algorithm to use
     * @return the message digest, as a <code>String</code> in hexadecimal
     *	       format
     * @throws IOException if an I/O exception occurs while reading data from
     *	       the URL
     * @throws NoSuchAlgorithmException if no provider is found for the message
     *	       digest algorithm
     * @throws NullPointerException if either argument is <code>null</code>
     */
    public static String computeDigest(URL url, String algorithm)
	throws IOException, NoSuchAlgorithmException
    {
	return computeDigest(url.openStream(), algorithm);
    }

    /** Computes the message digest for an input stream. */
    private static String computeDigest(InputStream in, String algorithm)
	throws IOException, NoSuchAlgorithmException
    {
	try {
	    if (!(in instanceof BufferedInputStream)) {
		in = new BufferedInputStream(in, 2048);
	    }
	    MessageDigest md = MessageDigest.getInstance(algorithm);
	    byte[] buf = new byte[2048];
	    while (true) {
		int n = in.read(buf);
		if (n < 0) {
		    break;
		}
		md.update(buf, 0, n);
	    }
	    return digestString(md.digest());
	} finally {
	    try {
		in.close();
	    } catch (IOException e) {
	    }
	}
    }

    /**
     * Computes the message digests for a codebase with HTTPMD URLs.
     * <p>
     * Do not use a directory on a remote filesystem, or a directory URL, if
     * the underlying network access protocol does not provide adequate data
     * integrity or authentication of the remote host.
     *
     * @param sourceDirectory the filename or URL of the directory containing
     *	      the source files corresponding to the URLs in
     *	      <code>codebase</code>
     * @param codebase a space-separated list of HTTPMD URLs. The digest
     *	      values specified in the URLs will be ignored. The path portion of
     *	      the URLs, without the message digest parameters, will be used to
     *	      specify the source files, relative to
     *	      <code>sourceDirectory</code>, to use for computing message
     *	      digests.
     * @return the codebase updated to include computed digests
     * @throws IllegalArgumentException if any of the URLs in
     *	       <code>codebase</code> fail to specify the HTTPMD protocol
     * @throws IOException if an I/O exception occurs while reading data from
     *	       the source files
     * @throws MalformedURLException if any of the URLs in
     *	       <code>codebase</code> have incorrect syntax
     * @throws NullPointerException if either argument is <code>null</code>
     */
    public static String computeDigestCodebase(String sourceDirectory,
					       String codebase)
	throws IOException, MalformedURLException, NullPointerException
    {
	boolean isURL;
	try {
	    new URL(sourceDirectory);
	    isURL = true;
	} catch (MalformedURLException e) {
	    isURL = false;
	}
	if (sourceDirectory.endsWith(isURL ? "/" : File.separator)) {
	    sourceDirectory =
		sourceDirectory.substring(0, sourceDirectory.length() - 1);
	}
	StringTokenizer specs = new StringTokenizer(codebase);
	StringBuffer sb = new StringBuffer();
	boolean first = true;
	while (specs.hasMoreTokens()) {
	    final String spec = specs.nextToken();
	    if (!"httpmd:".regionMatches(true, 0, spec, 0, 7)) {
		throw new IllegalArgumentException(
		    "Codebase URL does not specify HTTPMD protocol: " + spec);
	    }
	    /*
	     * Use doPrivileged so caller doesn't need
	     * java.net.NetPermission("specifyStreamHandler") to specify the
	     * URL stream handler.
	     */
	    URL url;
	    try {
		url = (URL) Security.doPrivileged(
		    new PrivilegedExceptionAction() {
			public Object run() throws MalformedURLException {
			    return new URL(null, spec, handler);
			}
		    });
	    } catch (PrivilegedActionException e) {
		throw (MalformedURLException) e.getCause();
	    }
	    String path = url.getPath();
	    int paramIndex = path.lastIndexOf(';');
	    int equalsIndex = path.indexOf('=', paramIndex);
	    int commentIndex = path.indexOf(',', equalsIndex);
	    String algorithm = path.substring(paramIndex + 1, equalsIndex);
	    URI relSourceURI;
	    try {
		relSourceURI = new URI(
		    "file:" + (path.startsWith("/") ? "" : "/") +
		    path.substring(0, path.indexOf(';')));
	    } catch (URISyntaxException e) {
		throw new MalformedURLException(
		    "Problem with codebase URL " + spec + ": " +
		    e.getMessage());
	    }
	    InputStream in;
	    if (isURL) {
		String relSource = relSourceURI.getRawPath();
		in = new URL(sourceDirectory + relSource).openStream();
	    } else {
		String relSource = relSourceURI.getPath();
		if ('/' != File.separatorChar) {
		    relSource = relSource.replace('/', File.separatorChar);
		}
		in = new FileInputStream(sourceDirectory + relSource);
	    }
	    String digest;
	    try {
		digest = computeDigest(in, algorithm);
	    } catch (NoSuchAlgorithmException e) {
		/* Shouldn't happen -- URL constructor should find this */
		throw new RuntimeException("Shouldn't happen: " + e);
	    }
	    URL result = new URL(
		url,
		path.substring(0, equalsIndex + 1) + digest +
		(commentIndex < 0 ? "" : path.substring(commentIndex)) +
		(url.getQuery() == null ? "" : '?' + url.getQuery()) +
		(url.getRef() == null ? "" : '#' + url.getRef()));
	    if (!first) {
		sb.append(' ');
	    } else {
		first = false;
	    }
	    sb.append(result);
	}
	return sb.toString();
    }

    /** Converts a message digest to a String in hexadecimal format. */
    static String digestString(byte[] digest) {
	StringBuffer sb = new StringBuffer(digest.length * 2);
	for (int i = 0; i < digest.length; i++) {
	    byte b = digest[i];
	    sb.append(Character.forDigit((b >> 4) & 0xf, 16));
	    sb.append(Character.forDigit(b & 0xf, 16));
	}
	return sb.toString();
    }

    /** Converts a String in hexadecimal format to a message digest. */
    static byte[] stringDigest(String s) throws NumberFormatException {
	byte[] result = new byte[(s.length() + 1) / 2];
	int rpos = result.length;
	int last = -1;
	for (int spos = s.length(); --spos >= 0; ) {
	    int digit = Character.digit(s.charAt(spos), 16);
	    if (digit < 0) {
		throw new NumberFormatException(
		    "Illegal hex digit: '" + s.charAt(spos) + "'");
	    }
	    if (last < 0) {
		last = digit;
	    } else {
		result[--rpos] = (byte) (last + (digit << 4));
		last = -1;
	    }
	}
	if (last >= 0) {
	    result[--rpos] = (byte) last;
	}
	return result;
    }

    /**
     * Returns true if the character is permitted in an HTTPMD comment.  Legal
     * comment characters are ASCII letters and numbers, plus '-', '_', '.',
     * '~', '*', ''', '(', ')', ':', '@', '&', '=', '+', '$', and ','.
     */
    static boolean commentChar(char c) {
	return (c >= 'a' && c <= 'z')
	    || (c >= 'A' && c <= 'Z')
	    || (c >= '0' && c <= '9')
	    || ("-_.~*'():@&=+$,".indexOf(c) >= 0);
    }
}
