/*
 * 
 * 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 com.sun.jini.loader.pref.internal;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;
import java.util.TreeSet;

/**
 * Internal data structure which holds preference information for a
 * preferred class loader.  This utility is used only by the preferred
 * class loader provider and is not intended to be a public API.
 *
 * A preferred resources object is created from an input stream which
 * is formatted according to the Preferred List Syntax which is
 * defined in the specification for
 * <code>net.jini.loader.pref.PreferredClassProvider</code>
 *
 * Preferred resources instances hold preferred list expression data
 * and the preferred state for the resources contained in a given
 * preferred class loader.
 *
 * @author Sun Microsystems, Inc.
 * @since 2.0
 */
public final class PreferredResources {
    /**
     * Constant value that indicates that there is no preference value
     * for a given name.
     */
    public static final int NAME_NO_PREFERENCE = 0;

    /**
     * Constant value that indicates that the resource for a given
     * name is known to be not preferred.  This state is applicable
     * when a preference expression declared that the name is not
     * preferred or when the resource for the name (preferred or not)
     * is known to not exist.
     */
    public static final int NAME_NOT_PREFERRED = 1;

    /**
     * Constant value that indicates that a given name is preferred.
     * This state is applicable when a preference expression declares
     * the name to be preferred but it is uncertain whether the
     * resource for the name the exists.
     */
    public static final int NAME_PREFERRED = 2;

    /**
     * Constant value that indicates that a given resource is
     * preferred.  This state is applicable when a preference
     * expression declares the name to be preferred and the resource
     * for the name is known to exist.
     */
    public static final int NAME_PREFERRED_RESOURCE_EXISTS = 3;
    
    /** string that starts preference specification headers */
    private static final String HEADER_TITLE = "PreferredResources-Version: ";
    private static final String HEADER_MAJOR_VERSION = "1";
    private static final String HEADER_MINOR_VERSION = "0";
    
    /** preference syntax keywords */
    private static final String PREF_PREFIX = "Preferred:";
    private static final String NAME_PREFIX = "Name:";

    /* tables which contain relevant components of preferred names */
    private Map namespacePrefs;
    private Map packagePrefs;
    private Map completeNamePrefs;

    /** flag to signal if this preference object is empty */
    private boolean isEmpty = true;

    /** default preference setting */
    private Boolean defaultPreference = Boolean.FALSE;

    /**
     * Create a preference object from a stream of formatted
     * preference syntax.
     *
     * @see PreferredResources
     */
    public PreferredResources(InputStream in) throws IOException {
	read(in);
    }

    /**
     * Read and parse preference information from the parameter input
     * stream <code>in</code>.  When the method completes, the
     * preference expression maps contain preference settings for
     * preference names contained in the input stream.
     */
    private void read(InputStream in) throws IOException {
        BufferedReader br = new BufferedReader
	    (new InputStreamReader(in, "UTF8"));	
        String line = null;
	String name = null;
	Boolean preference = null;

	/* clear old contents */
	isEmpty = true;
	completeNamePrefs = new HashMap(53);
	packagePrefs = new HashMap(23);
	namespacePrefs = new HashMap(11);
	
	if ((line = readLineTrimComments(br)) != null) {
	    if (!line.startsWith(HEADER_TITLE)) {
		throw new IOException("unsupported preferred list header: " +
				      line);
	    } else {
		String version =
		    line.substring(HEADER_TITLE.length(),
				   line.length()).trim();
		if (!version.startsWith("1.")) {
		    throw new IOException("preferred list major version " +
					  "not supported");
		}
	    }
	    for (line = readLineTrimComments(br); line != null;
		 line = readLineTrimComments(br))
	    {
		if (line.length() == 0) {
		    continue;
		} else if (line.startsWith(NAME_PREFIX)) {
		    if (name != null) {
			throw new IOException("Preferred name without " +
					      "preference value");
		    }
		    name = line.substring(NAME_PREFIX.length()).trim();
		    continue;
		    
		} else if (line.startsWith(PREF_PREFIX)) {
		    String value = line.substring(PREF_PREFIX.length()).trim();
		    if (value.equals("")) {
			throw new IOException("Empty preference value not " +
					      "permitted");
		    }
		    preference = Boolean.valueOf(value);
		} else {
		    throw new IOException("unrecognized preference entry: " +
					  line);
		}
		if (name == null) {
		    if (preference != null) {
			if (!isEmpty) {
			    throw new IOException("default preference must " +
				"be the first expression and can not " +
				"be redefined");
			}
			defaultPreference = preference;
			preference = null;
			isEmpty = false;
		    }
		    
		    /* it is ok for both name and preference to be null */
		    
		} else if (preference != null) {
		    if (name.startsWith("/") ||
			name.startsWith("*") ||
			name.startsWith("-") ||			
			name.startsWith("."))
		    {
			throw new IOException("Invalid character " +
					      "at name beginning: " + name);
		    } else if (name.endsWith("/*")) {
			mapPut(packagePrefs,
			    name.substring(0, name.length() - 2), preference);
		    } else if (name.endsWith("/-")) {
			mapPut(namespacePrefs,
			    name.substring(0, name.length() - 2), preference);
		    } else if (name.endsWith("/")) {
			mapPut(packagePrefs,
			    name.substring(0, name.length() - 1), preference);
		    } else {
			/* no wildcard; must be a full resource name */
			int state = (preference.booleanValue() ?
				     NAME_PREFERRED : NAME_NOT_PREFERRED);
			mapPut(completeNamePrefs, name, new Integer(state));
		    }
		    preference = null;
		    name = null;
		}
	    }
	    if (name != null) {
		throw new IOException("Preferred name without " +
				      "preference value");
	    }
	}
	if (isEmpty) {
	    throw new IOException("Empty preferences list is invalid");
	}
    }

    /**
     * Reads the next line from the specified BufferedReader, removing
     * leading and trailing whitespace and comments.  Null is returned
     * on EOF.
     **/
    private String readLineTrimComments(BufferedReader br) throws IOException {
	String line = br.readLine();
	if (line != null) {
	    line = line.trim();
	    if (line.indexOf('#') == 0) {
		line = "";
	    }
	}
	return line;
    }

    /**
     * Insert a preference expression and value into a given map.
     */
    private void mapPut(Map map, String name, Object preference)
	throws IOException
    {
	isEmpty = false;
	
	if ((name.length() == 0) || (name == null)) {
	    throw new IOException("no name specified in preference" +
				  " expression");
	}
	if (map.put(name, preference) != null) {
	    throw new IOException("duplicate map entry: " + name);
	}
    }

    /**
     * Write the preferences to the specified OutputStream using the
     * preference list syntax. Preference expressions are written in
     * the following order:
     *
     * Complete name expressions
     *
     * Package expressions
     * 
     * Namespace expressions
     *
     * @param out the stream to which formatted preference information
     * will be written
     * @throws IOException if an error occurs while writing to the stream
     */
    public void write(OutputStream out) throws IOException {
        BufferedWriter bw = new BufferedWriter
            (new OutputStreamWriter(out, "UTF8"));

	/* write the specification header */
        bw.write(HEADER_TITLE + "1.0\n");
	bw.write(PREF_PREFIX + " " + defaultPreference + "\n\n");

	/* write out most specific preferences first */
	writeMap(completeNamePrefs, bw, "");
	writeMap(packagePrefs, bw, "/*");
	writeMap(namespacePrefs, bw, "/-");

	bw.flush();
    }

    /**
     * Write the contents of the map into <code>out</code> using the
     * preference syntax.
     */
    private void writeMap(Map prefs, Writer out, String suffix)
	throws IOException
    {
	Iterator i = (new TreeSet(prefs.keySet())).iterator();
	while (i.hasNext()) {
	    Object current = i.next();
	    out.write(NAME_PREFIX + " " + current +
		      suffix + "\n");
	    Object value = prefs.get(current);
	    if (value instanceof Boolean) {
		out.write(PREF_PREFIX + " " + value + "\n\n");
	    } else if (value instanceof Integer) {
		int state = ((Integer) value).intValue();
		if ((state == NAME_PREFERRED) ||
		    (state == NAME_PREFERRED_RESOURCE_EXISTS))
		{
		    out.write(PREF_PREFIX + " " + true + "\n\n");
		} else {
		    out.write(PREF_PREFIX + " " + false + "\n\n");   
		}
	    }
	}
    }

    /**
     * Returns the preference setting that will be applied to names
     * which have no explicit preference setting in contained
     * preference settings.  The default preference is set by the
     * first preference setting with no associated name in a
     * preferences list file.
     *
     * @return default boolean preference value for these preferences
     */
    public Boolean getDefaultPreference() {
	return defaultPreference;
    }
    
    /**
     * Enable MarshalInputStream to optimize preference information:
     * permits complete name expressions to be added for names that
     * only match wild-card expressions.  These added expressions hold
     * data that tells if a resource has been loaded.
     *
     * This method makes this object mutable.  Synchronization must be
     * used to ensure consistent preference state when this method is
     * called.
     *
     * @param name the name for which preferred state will be set
     * @param prefState the preferred state for the given name
     * @throws IOException if the name length is zero length
     */
    public void setNameState(String name, int prefState)
	throws IOException
    {
	isEmpty = false;
	
	if (name.length() == 0) {
	    throw new IOException("no name specified in preference" +
				  " expression");
	}

 	completeNamePrefs.put(name, new Integer(prefState));
    }

    /**
     * Searches the preference maps to determine the preference state
     * of the named resource.  The preference state for the given
     * resource name is returned.  The preference state is an integer
     * that is equal to one of the preference state values defined
     * above.  This state integer tells the preference value of name
     * and whether its resource is known to exist.
     *
     * @param name
     * @param isClass whether the given <code>name</code> refers to a
     *        class resource
     *
     * @return the state for the given name which will be set to one
     *         of the following values: NAME_NO_PREFERENCE,
     *         NAME_NOT_PREFERRED, NAME_PREFERRED,
     *         NAME_PREFERRED_RESOURCE_EXISTS
     * 
     * @throws IOException if an error occurs getting the state for
     *         the supplied name
     */
    public int getNameState(String name, boolean isClass)
	throws IOException
    {
	Integer state = null;

	if (isClass) {
	    state = getClassNameState(name);
	} else {
	    state = (Integer) completeNamePrefs.get(name);
	}
	if (state != null) {
	    return state.intValue();
	}
	return NAME_NO_PREFERENCE;
    }

    /**
     * Returns the preference state for a given name (as is done in
     * getNonclassNameState) but also interprets the notation for
     * inner classes as a wild card so that the preference value for a
     * container class propagates to the classes it contains.  More
     * specific inner classes names override more general container
     * preference.
     */
    private Integer getClassNameState(String name) throws IOException {
	if (!name.endsWith(".class")) {
	    throw new IOException("requested name state on a " +
				  "non-class resource: " + name);
	}
	
	Integer state = null;	
	String container = name;
	int lastDollar = -1;
	do {
	    state = (Integer) completeNamePrefs.get(container);
	    if (state == null) {
		lastDollar = container.lastIndexOf("$");
		if (lastDollar >= 0) {
		    container =
			container.substring(0, lastDollar) + ".class";
		}
	    }
	} while ((lastDollar >= 0) && (state == null));

	return state;
    }
    
    /**
     * Return the boolean value of the most specific wild card
     * (package and namespace) expression which matches
     * <code>name</code>.  Package preferences are always more
     * specific than namespace preferences.
     *
     * @param name the resource name to which the returned boolean
     * value will apply
     *
     * @return <code>Boolean.TRUE/code> if <code>name</code> is
     * preferred. <code>Boolean.FALSE</code> is it is
     * not. <code>null</code> if there is no wildcard preference for
     * the name.
     */
    public Boolean getWildcardPreference(String name) {
	Boolean wildcardPref = null;
	
	int lastSlash = name.lastIndexOf("/");
	if (lastSlash >= 0) {
	    String mostSpecific = name.substring(0, lastSlash);
	    if (!mostSpecific.equals("")) {
		if (!packagePrefs.isEmpty()) {
		    wildcardPref = (Boolean)
			packagePrefs.get(mostSpecific);
		}
		if (wildcardPref == null) {
		    wildcardPref = getNamespacePreference(mostSpecific);
		}
	    }
	}

	return wildcardPref;
    }

    /**
     * Return a Boolean for the most specific namespace expression
     * which matches <code>name</code>. null if the name does not
     * match a namespace preference expression.
     */
    private Boolean getNamespacePreference(String namespace) {
	Boolean namespacePref = null;
	
	if (!namespacePrefs.isEmpty()) {
	    int lastSlash;
	    do {
		namespacePref =
		    (Boolean) namespacePrefs.get(namespace);
		
		lastSlash = namespace.lastIndexOf("/");
		if (lastSlash >= 0) {
		    namespace = namespace.substring(0, lastSlash);
		}

	    } while ((lastSlash >= 0) &&
		     (!namespace.equals("")) &&
		     namespacePref == null);
	}
	return namespacePref;
    }
}
