/*
 * 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 com.sun.jini.tool;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintWriter;

import java.lang.Comparable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import java.net.URL;
import java.net.URLClassLoader;

import java.text.MessageFormat;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.TreeSet;

import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Tool used to generate the preferred class information for downloadable JAR
 * files in the form of a META-INF/PREFERRED.LIST required for use by the {@link
 * net.jini.loader.pref.PreferredClassLoader}. The list is generated by
 * examining the dependencies of classes contained within a target JAR file and
 * zero or more additional supporting JAR files.  Through various command-line
 * options, a set of "root" classes are identified as belonging to a public API.
 * These root classes provide the starting point for recursively computing a
 * dependency graph, finding all of the classes referenced in the public API of
 * the root classes, finding all of the classes referenced in turn by the public
 * API of those classes, and so on, until no new classes are found.  The results
 * of the dependency analysis are combined with the preferred list information
 * in the additional supporting JAR files to compute a preferred list having the
 * smallest number of entries that describes the preferred state of the classes
 * and resources contained in all of the JAR files. The output of the tool is a
 * new version of the target JAR file containing the generated preferred list,
 * and/or a copy of the list printed to <code>System.out</code>.
 * <p>
 * This tool implements the first guideline described in {@link
 * net.jini.loader.pref}.  In many cases it is sufficient to specify
 * the roots via the <code>-proxy</code> option. The <code>-api</code> and 
 * <code>-impl</code> options are used to generate lists for JAR files
 * in which the roots are not completely defined by the
 * proxy classes, or for non-service JAR files. Since there is no definitive 
 * set of rules for determining whether a class should be preferred, 
 * the developer should verify the correctness of the generated list.
 * <p>
 * The following items are discussed below:
 * <ul>
 * <li><a href="#running">Running the Tool</a>
 * <li><a href="#processing">Processing Options</a>
 * <li><a href="#examples">Examples</a>
 * </ul>
 *
 * <a name="running"></a>
 * <h3>Running the Tool</h3>
 *
 * To run the tool on UNIX platforms:
 * <blockquote><pre>
 * java -jar <var><b>install_dir</b></var>/lib/preferredlistgen.jar <var><b>processing_options</b></var>
 * </pre></blockquote>
 * To run the tool on Microsoft Windows platforms:
 * <blockquote><pre>
 * java -jar <var><b>install_dir</b></var>\lib\preferredlistgen.jar <var><b>processing_options</b></var>
 * </pre></blockquote>
 * <p>
 * Note that the options for this tool can be specified in any order, and
 * can be intermixed.
 *
 * <a name="processing"></a>
 * <h3>Processing Options</h3>
 * <p>
 * <dl>
 * <dt><b><code>-cp</code> <var>input_classpath</var></b>
 * <dd>Identifies the classpath for all of the classes that might need to be
 * included in the dependency analysis. Typically this will include all of your
 * application classes, classes from the Apache River release, and any other classes on
 * which your classes might depend. It is safe to include more classes than are
 * actually necessary because the tool limits the scope of the preferred list to
 * those classes actually included in the JAR files being analyzed.  It is not
 * necessary to include the JAR files being analyzed in the classpath as they
 * will be appended automatically. It is also unnecessary to include any classes
 * that are part of the Java(TM) 2 SDK.  The class path should be in the form of a
 * list of directories or JAR files, delimited by a colon (":") on UNIX
 * platforms and a semi-colon (";") on Microsoft Windows platforms. The order of
 * locations in the path does not matter.
 * </dd>
 * <p>
 * <dl>
 * <dt><b><code>-jar</code> <var>file</var></b>
 * <dd>Identifies a JAR file containing the classes to analyze. If the JAR
 * manifest includes a <code>Class-Path</code> attribute, then these JAR files
 * will also be processed recursively. The default behavior is to replace the
 * original JAR file with a new file containing the generated preferred list. If
 * the original target JAR file contained a preferred list, that list is ignored
 * and is replaced by the newly generated list. This option may be specified
 * zero or more times. If multiple <code>-jar</code> options are specified, the
 * first file specified is considered the target JAR file.
 * </dd>
 * <p>
 * <dt><b><code>-proxy</code> <var>classname</var></b>
 * <dd>Identifies the class name of a proxy in the target JAR file. All of the
 * public interfaces implemented by the proxy, and all of the public super
 * interfaces of any non-public interfaces implemented by the proxy, are
 * included in the set of roots for performing dependency analysis. This option
 * may be specified zero or more times.
 * </dd>
 * <p>
 * <dt><b><code>-api</code> <var>name-expression</var></b>
 * <dd>
 * This option identifies a class or a JAR entry, package, or namespace that is
 * to be considered public and therefore <b>not</b> preferred. If
 * <var>name-expression</var> ends with ".class", it represents a class whose
 * name is <var>name-expression</var> without the ".class" suffix and with each
 * '/' character replaced with a '.'. Otherwise, if <var>name-expression</var>
 * ends with "/" or "/*", it represents a directory wildcard matching all
 * entries in the named directory. Otherwise, if <var>name-expression</var> ends
 * with "/-", it represents a namespace wildcard that matches all entries in the
 * named directory and all of its subdirectories. Otherwise
 * <var>name-expression</var> represents a non-class resource in the JAR
 * file. Alternatively, <var>name-expression</var> may be expressed directly as
 * a class name. A nested (including inner) class must be expressed as a binary
 * class name; if <code>Bar</code> is a nested class of <code>Foo,</code> then
 * <code>Bar</code> would be expressed as <code>Foo$Bar</code>. The most
 * specific <var>name-expression</var> is used to match an entry. By default,
 * any public class in the JAR file that matches <var>name-expression</var> will
 * be included in the set of roots for dependency analysis. If
 * <var>name-expression</var> is a class name, then that class will be included
 * in the set of roots irregardless of its access modifier. If the
 * <code>-nonpublic</code> option is also present, then matching non-public
 * classes will also be included in the set of roots. The <code>-api</code>
 * option may be specified zero or more times.
 * <p>
 * As an example, presuming the class <code>com.sun.jini.example.Foo</code>
 * was included in the target JAR file, then the following would all cause
 * that class to be included in the public API:
 * <blockquote><pre>
 *    -api com/sun/jini/example/Foo.class
 *    -api com.sun.jini.example.Foo
 *    -api com/sun/jini/example/*
 *    -api com/sun/jini/example/-
 * </pre></blockquote>
 * and the last example would also apply to, for instance,
 * <code>com.sun.jini.example.gui.FooPanel</code>.
 * </dd>
 * <p>
 * <dt><b><code>-impl</code> <var>name-expression</var></b>
 * <dd>This option identifies a class or a JAR entry, package, or namespace that
 * is to be considered private and therefore preferred.
 * <var>name-expression</var> is interpreted as described for the
 * <code>-api</code> option. If <var>name-expression</var> is a class name or a
 * class JAR entry name, that class will be considered preferred and will not be
 * selected by or included in the dependency analysis even if it was included in
 * the set of roots as a result of processing the <code>-proxy</code> and
 * <code>-api</code> options. This option may be specified zero or more times.
 * </dd>
 * <p>
 * <dt><b><code>-nonpublic</code></b>
 * <dd>This option forces any non-public classes matched by the
 * <code>-api</code> <var>name-expression</var>s to be included in the set of
 * roots for dependency analysis.
 * </dd>
 * <p>
 * <dt><b><code>-nomerge</code></b>
 * <dd>Causes the classes in JAR files which do not contain preferred
 * lists to be considered not preferred. If this option is not specified, all classes in 
 * JAR files which do not contain preferred lists are merged with the classes supplied by 
 * the target JAR file for purposes of dependency
 * analysis; the additional classes are not included in the generated target JAR file.
 * The <code>-impl</code> and <code>-api</code> options may be used to initialize
 * the preferred state of the merged classes. 
 * </dd>
 * <p>
 * <dt><b><code>-default</code> <var>false|true</var></b>
 * <dd>Specifies the default preferred value to use when generating the
 * preferred list and forces the generation of an explicit default preferred
 * entry in the preferred list.  If this option is not provided, the default
 * that produces a list with the fewest entries is used; an explicit entry for
 * the default <code>false</code> case will not be generated (except when no
 * single entry is found, in which case a default preferred value of
 * <code>false</code> is written). In the event of optimization ties, a default
 * value of <var>false</var> is used.
 * </dd>
 * <p>
 * <dt><b><code>-noreplace</code></b>
 * <dd>
 * Inhibits the replacement of the original JAR file with a new updated JAR
 * file. If this option is specified, the preferred list is printed on
 * <code>System.out</code>.
 * </dd>
 * <p>
 * <dt><b><code>-print</code></b>
 * <dd>
 * Causes the preferred list to be printed to <code>System.out</code>, even if
 * the list is also placed in an updated JAR file.
 * </dd>
 * <p>
 * <dt><b><code>-tell</code> <var>classname</var></b>
 * <dd>Specifies the fully qualified name of a class for which dependency
 * information is desired. This option causes the tool to display information
 * about every class in the dependency graph that references the specified
 * class. If no class references the specified class, it will be identified as a
 * root class. This information is sent to the error stream of the tool, not to
 * the normal output stream.  This option can be specified zero or more
 * times. If this option is used, all other output options are ignored, and the
 * normal class output is not produced. This option is useful for debugging.
 * </dd>
 * </dl>
 * <p>
 * Using values from the <code>-api</code> and <code>-impl</code> options, a
 * graph is constructed that defines initial preferred values to be inherited by
 * the target JAR entries as they are loaded into the graph. If there were no
 * such options specified, all entries from the target JAR file loaded into the
 * graph initially will be marked as preferred.  The classes and resources
 * identified by the first <code>-jar</code> option (the target JAR file) are
 * then loaded into this graph and are assigned their initial preferred
 * values. The remaining JAR files that include preferred lists are loaded into
 * the graph and the entries assigned preferred values based on the preferred
 * list contained in the JAR file being loaded. If a non-target JAR file does
 * not contain a preferred list, the default behavior is to merge the classes
 * and resources in the file with those of the target JAR file (for purposes of
 * dependency analysis only), making them subject to the <code>-api</code> and
 * <code>-impl</code> options.  The <code>-nomerge</code> option can be used to
 * override the default behavior, causing all such classes to be assigned a
 * value of not preferred.  The set of root classes is constructed by finding
 * all of the classes from the target JAR file that are marked as not preferred
 * in the graph, and by adding all of the public interfaces, or any public
 * superinterfaces of non-public interfaces implemented by the proxy classes
 * specified via the <code>-proxy</code> option. Starting with the root classes,
 * dependent classes are identified by examining the compiled class file for the
 * class, finding all of the public and protected fields, methods, constructors,
 * interfaces, and super classes it references, and then in turn examining those
 * classes. Any dependent classes found that also exist in the graph will be
 * marked not preferred, unless that class was explicitly named by a
 * <code>-impl</code> option. Any root class or dependent class named by a
 * <code>-impl</code> option retains its original preferred value and no further
 * dependency analysis is performed for the class.  The range of the dependency
 * analysis is restricted to the set of classes included in the graph.
 * <p> 
 * The tool then processes the graph to find the smallest number of preferred
 * list entries that describes the preferred state of all classes and resources
 * in the graph.  The resulting preferred list may be printed or included in a
 * JAR file that replaces the original (first) JAR file.
 * <p>
 * <a name="examples"></a>
 * <h3>Examples</h3>
 *
 * The following example generates the preferred list for the codebase JAR file
 * for reggie, replacing the original reggie-dl.jar with a new file containing
 * the preferred list. The reggie implementation includes four proxy classes,
 * however <code>com.sun.jini.reggie.RegistrarProxy</code> and
 * <code>com.sun.jini.reggie.AdminProxy</code> are not identified on the command
 * line because they are parent classes of
 * <code>com.sun.jini.reggie.ConstrainableRegistrarProxy</code> and
 * <code>com.sun.jini.reggie.ConstrainableAdminProxy</code>.
 * <p>
 * <blockquote><pre>
 * java -jar <var><b>install_dir</b></var>/lib/preferredlistgen.jar \
 *      -cp <var><b>install_dir</b></var>/lib/jsk-platform.jar \
 *      -jar <var><b>install_dir</b></var>/lib-dl/reggie-dl.jar \
 *      -jar <var><b>install_dir</b></var>/lib-dl/jsk-dl.jar \
 *      -proxy com.sun.jini.reggie.ConstrainableRegistrarProxy \
 *      -proxy com.sun.jini.reggie.ConstrainableAdminProxy 
 * </pre></blockquote>
 * <p>
 *
 * 
 */

public class PreferredListGen {

    /** remember classes processed to avoid redundant work or loops */
    private final Collection seen = new HashSet();
    {
	seen.add("int"); // mark primitives as already seen or stack traces fly
	seen.add("long");
	seen.add("float");
	seen.add("double");
	seen.add("short");
	seen.add("void");
	seen.add("char");
	seen.add("byte");
	seen.add("boolean");
    }

    /* 
     * NOTE: the Boolean class is used extensively to represent the three
     *       possible states of a preferred value of true/false/undefined
     *       (for instance, if there is no default preferred value).
     */

    /** Boolean equivalent of true */
    private Boolean TRUE = new Boolean(true);

    /** Boolean equivalent of false */
    private Boolean FALSE = new Boolean(false);

    /** the names of proxies supplied by the -proxy option */
    private final Collection proxies = new HashSet();

    /** the set of classes to report based on the -tell options */
    private final Collection tells = new HashSet();

    /** the first JAR in the set of files loaded via the -jar option */
    private File targetJar;

    /** replace the first JAR with a copy containing the preferred list */
    private boolean replaceJar = true;

    /** print the preferred list, only meaningful with the -jar option */
    private boolean printResults = false;

    /** ordered list of JAR files names specified by the -jar options */
    private final Collection jarList = new ArrayList();

    /** the classpath specified by the -cp option */
    private String classpath;

    /** the ordered graph containing the JAR class info */
    private final Graph listGraph = new Graph();

    /** if true, use defaultToForce for default, otherwise optimize */
    private boolean forceDefault = false;

    /** the default preference value to force */
    private boolean defaultToForce = false;

    /** the class loader for resolving class names */
    private ClassLoader loader = 
	(ClassLoader) getClass().getClassLoader();

    /** I18N resource bundle */
    private static ResourceBundle resources;

    /** flag to indicate that initialization of resources has been attempted */
    private static boolean resinit = false;

    /** union of the entries in all JAR files for -api/-impl existence check */
    private HashSet jarEntries = new HashSet();

    /** loaded JAR names, to avoid infinite loops due to circular definitions */
    private HashSet jarFiles = new HashSet();

    /** if true, non-public roots are allowed */
    private boolean keepNonPublicRoots = false;

    /** if true, load JARs without preferred lists directly into listGraph */
    private boolean doMerge = true;

    /**
     * Get the strings from our resource localization bundle.
     */
    private static String getString(String key, Object v1, Object v2, Object v3) {
        String fmt = "no text found: \"" + key + "\"";
	if (!resinit) {
	    try {
		resinit = true;
		resources = ResourceBundle.getBundle
		    ("com.sun.jini.tool.resources.preflistgen");
	    } catch (MissingResourceException e) {
		e.printStackTrace();
	    }
	}
	if (resources != null) {
	    try {
		fmt = resources.getString(key);
	    } catch (MissingResourceException e) {
	    }
	}
	return MessageFormat.format(fmt, new Object[]{v1, v2, v3});
    }

    /**
     * Return  the string according to resourceBundle format.
     */
    private static String getString(String key) {
	return getString(key, null, null, null);
    }

    /**
     * Return  the string according to resourceBundle format.
     */
    private static String getString(String key, Object v1) {
	return getString(key, v1, null, null);
    }

    /**
     * Return  the string according to resourceBundle format.
     */
    private static String getString(String key, Object v1, Object v2) {
	return getString(key, v1, v2, null);
    }

    /**
     * Print out string according to resourceBundle format.
     */
    private static void print(String key, Object v1) {
	System.err.println(getString(key, v1));
    }

    /**
     * Print out string according to resourceBundle format.
     */
    private static void print(String key, Object v1, Object v2) {
	System.err.println(getString(key, v1, v2));
    }

    /**
     * Print out string according to resourceBundle format.
     */
    private static void print(String key, Object v1, Object v2, Object v3) {
	System.err.println(getString(key, v1, v2, v3));
    }

     /**
     * Create a preferred list generator and process the command line arguments.
     *
     * @param args the command line arguments
     */
    private PreferredListGen(String[] args) {
	if (args.length == 0) {
	    throw new IllegalArgumentException(getString("preflistgen.noargs"));
	}
	for (int i = 0; i < args.length ; i++ ) {
	    String arg = args[i];
	    if (arg.equals("-print")) {
		setPrint(true);
	    } else if (arg.equals("-noreplace")) {
		setReplaceJar(false);
		setPrint(true);
	    } else if (arg.equals("-jar")) {
		addJar(args[++i]);
	    } else if (arg.equals("-tell")) {
		addTell(args[++i]);
	    } else if (arg.equals("-impl")) {
		addImpl(args[++i]);
	    } else if (arg.equals("-api")) {
		addApi(args[++i]);
	    } else if (arg.equals("-nonpublic")) {
		setKeepNonPublicRoots(true);
	    } else if (arg.equals("-nomerge")) {
		setMerge(false);
	    } else if (arg.equals("-default")) {
		String def = args[++i];
		if (def.equalsIgnoreCase("true") 
		    || def.equalsIgnoreCase("false")) 
		{
		    setDefault(def.equalsIgnoreCase("true"));
		} else {
		    String msg = getString("preflistgen.baddefault", def);
		    throw new IllegalArgumentException(msg);
		}
	    } else if (arg.equals("-cp")) {
		setClasspath(args[++i]);
	    } else if (arg.equals("-proxy")) {
		addProxy(args[++i]);
	    } else {
		String msg = getString("preflistgen.badoption", arg);
		throw new IllegalArgumentException(msg);
	    }
	}
    }
    
    /**
     * Constructor for programmatic access. The public <code>set</code> and
     * <code>add</code> methods must be called to supply the argument
     * values. Then <code>compute</code> and <code>generatePreferredList</code>
     * must be called to perform the dependency analysis and to generate the
     * preferred list.
     */
    public PreferredListGen() {
    }

    /**
     * Set the flag controlling whether a preferred list is to be printed.
     * This flag is ignored if the <code>PrintWriter</code> supplied in
     * the call to <code>generatePreferredList</code> is non-<code>null</code>.
     * The default value is <code>false</code>.
     *
     * @param printResults if <code>true</code>, print the preferred list
     */
    public void setPrint(boolean printResults) {
	this.printResults = printResults;
    }

    /**
     * Set the flag controlling whether non-public classes should be retained
     * in the set of roots used for performing dependency analysis. By default,
     * non-public classes are discarded.
     *
     * @param keepNonPublicRoots if <code>true</code>, non-public root classes
     *        are retained
     */
    public void setKeepNonPublicRoots(boolean keepNonPublicRoots) {
	this.keepNonPublicRoots = keepNonPublicRoots;
    }

    /**
     * Select the behavior for processing non-target JAR files which do not
     * contain preferred lists. If <code>doMerge</code> is <code>true</code>, the
     * classes contained in these JAR files are merged with the target JAR
     * file for purposes of dependency analysis. The <code>-impl</code> and
     * <code>-api</code> options may be used to initialize the preferred state
     * of the merged classes. If <code>doMerge</code> is <code>false</code>,
     * the classes in non-target JAR files which do not contain preferred lists are
     * initialized with a preferred state of 'not preferred'. The default behavior
     * corresponds to calling <code>setMerge(true)</code>.
     *
     * @param doMerge if <code>true</code>, perform the merge
     */
    public void setMerge(boolean doMerge) {
	this.doMerge = doMerge;
    }

    /**
     * Set the flag controlling whether a preferred list is to be placed
     * in the target JAR file. The default value is <code>true</code>.
     *
     * @param replaceJar if <code>true</code>, update the target JAR file
     */
    public void setReplaceJar(boolean replaceJar) {
	this.replaceJar = replaceJar;
    }

    /**
     * Add <code>jarName</code> to the list of JAR files to process.
     * The first call identifies the target JAR file. This method must
     * be called at least once.
     *
     * @param jarName the name of the JAR file to add to the set.
     */
    public void addJar(String jarName) {
	jarList.add(jarName);
    }

    /**
     * Add <code>tellName</code> to the tell list. If a class is identified
     * as not preferred through the dependency analysis, and if that class
     * name is in the tell list, then the source dependency causing the class to
     * be included is printed. This is for debugging purposes.
     *
     * @param tellName the name of the JAR file to add to the tell set.
     */
    public void addTell(String tellName) {
	String tellClass = fileToClass(tellName);
	tells.add(tellClass);
    }
    
    /**
     * Initialize the dependency graph with a private API entry.
     * <code>implName</code> identifies a class or a JAR entry, package, or
     * namespace that is to be considered private and therefore preferred. If
     * <code>implName</code> ends with ".class", it represents a class whose
     * name is <code>implName</code> without the ".class" suffix and with each
     * '/' character replaced with a '.'. Otherwise, if <code>implName</code>
     * ends with "/" or "/*", it represents a directory wildcard matching all
     * entries in the named directory. Otherwise, if <code>implName</code> ends
     * with "/-", it represents a namespace wildcard that matches all entries in
     * the named directory and all of its subdirectories. Otherwise
     * <code>implName</code> represents a non-class resource in the JAR
     * file. Alternatively, <code>implName</code> may be expressed directly as a
     * class name. The most specific <code>implName</code> is used to match an
     * entry found in the JAR files being analyzed. If <code>implName</code> is
     * either of the class name forms, then that class is forced to be preferred
     * and is not included in the public API even it is found by the dependency
     * analysis.
     *
     * @param implName the identifier for the private API entry
     *
     * @throws IllegalArgumentException if <code>implName</code> does not match
     *         any of the criteria above.
     */
    public void addImpl(String implName) {
	listGraph.initialize(implName, true, null); // preferred 
    }

    /**
     * Initialize the dependency graph with a public API entry.
     * <code>apiName</code> identifies a class or a JAR entry, package, or
     * namespace that is to be considered public and therefore <b>not</b>
     * preferred. If <code>apiName</code> ends with ".class", it represents a
     * class whose name is <code>apiName</code> without the ".class" suffix and
     * with each '/' character replaced with a '.'. Otherwise, if
     * <code>apiName</code> ends with "/" or "/*", it represents a directory
     * wildcard matching all entries in the named directory. Otherwise, if
     * <code>apiName</code> ends with "/-", it represents a namespace wildcard
     * that matches all entries in the named directory and all of its
     * subdirectories. Otherwise <code>apiName</code> represents a non-class
     * resource in the JAR file. Alternatively, <code>apiName</code> may be
     * expressed directly as a class name. The most specific
     * <code>apiName</code> is used to match an entry found in the JAR files
     * being analyzed. Any class in the JAR file that matches
     * <code>apiName</code> will be included in the set of roots for dependency
     * analysis. This method may be called zero or more times.
     *
     * @param apiName the identifier for the public API entry
     *
     * @throws IllegalArgumentException if <code>apiName</code> does not match
     *         any of the criteria above.
     */
    public void addApi(String apiName) {
	listGraph.initialize(apiName, false, null); // not preferred
    }

    /**
     * Set the default value to use for the preferred list. If this method
     * is not called, the default will be chosen which results in a preferred
     * list with the smallest number of entries. In the event of optimization
     * ties, a default value of <code>false</code> is used.
     *
     * @param def the default value to use for the list
     */
    public void setDefault(boolean def) {
	forceDefault = true;
	defaultToForce = def;
    }

    /**
     * Set the classpath of the classes to include in the analysis. It is
     * not necessary to include the JAR files supplied via calls to the
     * <code>addJar</code> method.
     *
     * @param path the classpath for the classes to include in the analysis
     */
    public void setClasspath(String path) {
	this.classpath = path;
    }
	
    /**
     * Add <code>proxy</code> to the set of proxies used to identify
     * roots. This method may be called zero or more times.
     *
     * @param proxy the name of the proxy class
     */
    public void addProxy(String proxy) {
	proxies.add(proxy);
    }


    /**
     * Load all of the JAR files named on the command line or referenced in
     * Class-Path manifest entries of those JAR files.
     *
     * @throws IOException if an error occurs reading the contents
     *         of any of the JAR files.
     */
    private void loadJars() throws IOException {
	Iterator it = jarList.iterator();
	while (it.hasNext()) {
	    String jarName = (String) it.next();
	    File jarFile = new File(jarName);
	    loadJar(jarFile);
	}
    }

    /**
     * Load the given JAR <code>File</code>, adding every class or resource in
     * the file a graph. The first time this method is called,
     * <code>listGraph</code> is loaded directly and all non-preferred classes
     * found in the graph are marked as roots. On subsequent calls, a new
     * temporary graph is created and initialized based on the preferred list in
     * the JAR. After entries are loaded into the graph, it is merged into
     * <code>listGraph</code>. The manifest is also read, and any JARs specified
     * by the manifest <code>Class-Path</code> attribute are loaded
     * recursively. If the given JAR file does not exist or is a directory, an
     * error message is printed.
     *
     * @param jar the <code>File</code> representing the JAR file
     * @throws IOException if an error occured reading the contents
     *         of the JAR or any of the JARs in the Class-Path.
     * @throws IllegalArgumentException if the JAR file does not exist
     *         or is a directory
     */
    private void loadJar(File jar) throws IOException {
	if (jarFiles.contains(jar)) {
	    return; // short-circuit circular definitions
	}
	jarFiles.add(jar);
	if (!jar.exists()) {
	    String msg = getString("preflistgen.nojarfound", jar);
	    throw new IllegalArgumentException(msg);
	}
	if (jar.isDirectory()) {
	    String msg = getString("preflistgen.jarisdir", jar);
	    throw new IllegalArgumentException(msg);
	}
	if (targetJar == null) {
	    targetJar = jar;
	    populateGraph(listGraph, jar);
	} else if (doMerge && noPreferredList(jar)) {
	    populateGraph(listGraph, jar);
	} else {
	    Graph g = createGraph(jar); // read pref list to init graph
	    populateGraph(g, jar);
	    listGraph.merge(g);
	}
    }

    /**
     * Return <code>true</code> if the given JAR file does not contain a preferred list.
     *
     * @param jar the file to examine
     * @return true if no preferred list is present
     * @throws IOException if an error occured reading the contents of the JAR
     */
    private boolean noPreferredList(File jar) throws IOException {
	JarFile jarFile = new JarFile(jar);
	return jarFile.getJarEntry("META-INF/PREFERRED.LIST") == null;
    }

    /**
     * Load the given JAR <code>File</code>, adding every class or resource in
     * the file to the given graph. The manifest is also read, and any JARs
     * specified by the manifest <code>Class-Path</code> attribute are loaded
     * recursively. 
     *
     * @param g the graph to populate
     * @param jar the <code>File</code> representing the JAR file
     */
    private void populateGraph(Graph g, File jar) {
	try {
	    JarFile jarFile = new JarFile(jar);
	    Enumeration en = jarFile.entries();
	    while (en.hasMoreElements()) {
		JarEntry entry = (JarEntry) en.nextElement();
		String id = entry.getName();
		if (id.startsWith("META-INF") || id.endsWith("/")) {
		    continue; // ignore directories and anything in meta-inf 
		}
		jarEntries.add(id);
		if (id.endsWith(".class")) {
		    g.add(fileToClass(id), Graph.CLASS, jar);
		} else {
		    g.add(fileToClass(id), Graph.RESOURCE, jar);
		}
	    }
	    Manifest manifest = jarFile.getManifest();
	    if (manifest == null) {
		return; // processed a zip file
	    }
	    String classPath = 
		manifest.getMainAttributes().getValue("Class-Path");
	    if (classPath != null) {
		StringTokenizer tok = new StringTokenizer(classPath);
		while (tok.hasMoreTokens()) {
		    String fileName = tok.nextToken();
		    File nextJar = new File(jar.getParentFile(), fileName);
		    loadJar(nextJar);
		}
	    }
	} catch (IOException e) {
	    e.printStackTrace();
	}
    }

    /**
     * Create a <code>Graph</code> initialized from the preferred list of
     * the given JAR file.
     *
     * @param jarFile the JAR file from which an initialized graph is
     *                to be derived
     * @return an initialized <code>Graph</code>
     * @throws IOException if an error occurs reading <code>jarFile</code>
     */
    private Graph createGraph(File jarFile) throws IOException {

	final Pattern headerPattern = 
	    Pattern.compile("^PreferredResources-Version:\\s*(.*?)$");
	final Pattern versionPattern =
	    Pattern.compile("^1\\.\\d+$");
	final Pattern namePattern =
	    Pattern.compile("^Name:\\s*(.*)$");
	final Pattern preferredPattern =
	    Pattern.compile("^Preferred:\\s*(.*)$");

	/**
	 * Parses the given JAR file's preferred list, if any.
	 */
	Graph graph = new Graph();
	JarFile jar = new JarFile(jarFile);
	JarEntry ent = jar.getJarEntry("META-INF/PREFERRED.LIST");
	if (ent == null) {
	    graph.setDefaultPref(false);
	    return graph;
	}
	BufferedReader r = new BufferedReader(
	    new InputStreamReader(jar.getInputStream(ent), "UTF8"));

	String s = r.readLine();
	if (s == null) {
	    throw new IOException("missing preferred list header");
	}
	s = s.trim();
	Matcher m = headerPattern.matcher(s);
	if (!m.matches()) {
	    throw new IOException("illegal preferred list header: " + s);
	}
	s = m.group(1);
	if (!versionPattern.matcher(s).matches()) {
	    throw new IOException(
		"unsupported preferred list version: " + s);
	}

	s = nextNonBlankLine(r);
	if (s == null) {
	    throw new IOException("empty preferred list");
	}
	if ((m = preferredPattern.matcher(s)).matches()) {
	    graph.setDefaultPref(Boolean.valueOf(m.group(1)).booleanValue());
	    s = nextNonBlankLine(r);
	} else {
	    graph.setDefaultPref(false);
	}

	while (s != null) {
	    if (!(m = namePattern.matcher(s)).matches()) {
		throw new IOException(
		    "expected preferred entry name: " + s);
	    }
	    String name = m.group(1);

	    s = nextNonBlankLine(r);
	    if (s == null) {
		throw new IOException("EOF before preferred entry");
	    }
	    if (!(m = preferredPattern.matcher(s)).matches()) {
		throw new IOException("expected preferred entry: " + s);
	    }
	    boolean pref = Boolean.valueOf(m.group(1)).booleanValue();
	    graph.initialize(name, pref, jarFile);
	    s = nextNonBlankLine(r);
	}
	return graph;
    }

    /**
     * Returns next non-blank, non-comment line, or null if end of file has
     * been reached.
     *
     * @param reader the input stream
     * @return a <code>String</code> containing the next line, 
     *         or <code>null</code>
     * @throws IOException if an error occurs reading the stream
     */
    private static String nextNonBlankLine(BufferedReader reader)
	throws IOException
    {
	String s;
	while ((s = reader.readLine()) != null) {
	    s = s.trim();
	    if (s.length() > 0 && s.charAt(0) != '#') {
		return s;
	    }
	}
	return null;
    }

    /**
     * Convert the given class reference to a class name. If the argument is
     * already a class name, the value is returned unchanged. If the argument is
     * a file name, the equivalent class name is returned. Path names are in JAR
     * format: path separators must be '/' characters and the first character
     * must not be a path separator.
     *
     * @param classID the class identifier, which may be a class file
     *                reference or a class name
     * @return the associated class name
     */
    private String fileToClass(String classID) {
	classID = classID.replace('/', '.'); 
	if (classID.endsWith(".class")) {
	    classID = classID.substring(0, classID.length() - 6);
	}
        return classID;
    }

    /**
     * Inspect the given class for dependencies. If the class is an
     * array, it's component type is inspected. Inspection consists
     * of processing the classes name.
     *
     * @param from the dependent of the given class, or null if this
     *             is the top-level class
     * @param c the class to inspect
     */
    private void process(String from, Class c) {
	while (c.isArray())
	    c = c.getComponentType();
	process(from, c.getName());
    }

    /**
     * Inspect the given array of classes for dependencies. 
     *
     * @param from the dependent of the given array, or null if this
     *             is the top-level class
     * @param arr the array of classes to inspect
     */
    private void process(String from, Class[] arr) {
	for (int i = arr.length; --i >= 0; )
	    process(from, arr[i]);
    }

    /**
     * Determine whether the given modifier imply inclusion in the public api.
     * Return <code>true</code> if the modifiers include the public or protected
     * flags.
     *
     * @param modifiers the modifier value
     * @return true if a public api member is implied
     */
    private boolean include(int modifiers) {
	return (modifiers & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0;
    }

    /**
     * Inspect the given constructor for dependencies. Any parameter types
     * and exception types declared for the constructor are inspected.
     *
     * @param from the dependent of the constructor, or null if this
     *             is the top-level class
     * @param c the constructor to inspect
     */
    private void process(String from, Constructor c) {
	if (!include(c.getModifiers()))
	    return;
	process(from, c.getParameterTypes());
	process(from, c.getExceptionTypes());
    }

    /**
     * Inspect the given method for dependencies. Any parameter types,
     * exception types, and the return type declared for the method
     * are inspected.
     *
     * @param from the dependent of the method, or null if this
     *             is the top-level class
     * @param m the method to inspect
     */
    private void process(String from, Method m) {
	if (!include(m.getModifiers()))
	    return;
	process(from, m.getReturnType());
	process(from, m.getParameterTypes());
	process(from, m.getExceptionTypes());
    }

    /**
     * Inspect the given field for dependencies. The field type is inspected.
     *
     * @param from the dependent of the field, or null if this
     *             is the top-level class
     * @param f the field to inspect
     */
    private void process(String from, Field f) {
	if (!include(f.getModifiers()))
	    return;
	process(from, f.getType());
    }

    /**
     * Mark the given class in the <code>Graph</code> as 'not preferred'
     * unless overridden by the command line <code>-impl</code> argument.
     * If so marked, then the public and private methods, fields,
     * constructors, interfaces, and superclasses declared by the class are
     * also inspected recursively. Only classes classes contained within
     * <code>listGraph</code> are processed.
     *
     * @param from the class referencing the given class, or null if this
     *             is the top-level class
     * @param clazz the class to add to the public api
     */
    private void process(String from, String clazz) {
	if (!seen.contains(clazz)) {
	    seen.add(clazz);
	    if ( ! listGraph.contains(clazz)) {
		return;
	    }
	    for (Iterator it = tells.iterator(); it.hasNext();) {
		if (clazz.equals((String)it.next())) {
		    if (from != null) {
			System.err.println(clazz + " caused by " + from);
		    } else {
			System.err.println(clazz + " is a root class");
		    }
		}
	    }
	    if (!listGraph.setPreferred(clazz, false, false)) {
		return;
	    }
	    inspectClass(clazz);
	}
    }

    /**
     * Process all public and protected fields, methods, constructors,
     * interfaces, and superclasses declared by the given class.
     *
     * @param className the name of the class to process
     */
    private void inspectClass(String className) {
	Class c;
	try {
	    c = Class.forName(className, false, loader);
	} catch (Exception e) {
	    e.printStackTrace();
	    return;
	}
	Class sup = c.getSuperclass();
	if (sup != null)
	    process(className, sup);
	Class[] ifaces = c.getInterfaces();
	for (int i = ifaces.length; --i >= 0; )
	    process(className, ifaces[i]);
	Constructor[] cons = c.getDeclaredConstructors();
	for (int i = cons.length; --i >= 0; ) {
	    process(className, cons[i]);
	}
	Method[] methods = c.getDeclaredMethods();
	for (int i = methods.length; --i >= 0; ) {
	    process(className, methods[i]);
	}
	Field[] fields = c.getDeclaredFields();
	for (int i = fields.length; --i >= 0; ) {
	    process(className, fields[i]);
	}
    }

    /**
     * Load JAR files, initialize the dependency graph, and perform the 
     * dependency analysis.
     *
     * @throws IOException if an error occurs constructing the class loader
     *         or reading any of the JAR files.
     * @throws IllegalArgumentException in the following cases:
     *         <ul>
     *         <li>if <code>addJar</code>  was never
     *             called or <code>addJar</code> was called with a file which
     *             does not exist or is a directory
     *         <li>if any proxies supplied via the <code>addProxy</code> 
     *             method could not be found 
     *         <li>if any component in the classpath does not exist 
     *         </ul>
     */
    public void compute() throws IOException {
	if (jarList.size() == 0) {
	    throw new IllegalArgumentException(getString("preflistgen.nojars"));
	}
        ArrayList list = new ArrayList();
	if (classpath != null) {
	    StringTokenizer st = new StringTokenizer(classpath, 
						     File.pathSeparator);
	    while (st.hasMoreTokens()) {
		String fileName = st.nextToken();
		File cpFile = new File(fileName);
		if (!cpFile.exists()) {
		    String msg = getString("preflistgen.badcp", fileName);
		    throw new IllegalArgumentException(msg);
		}
		list.add(cpFile.getCanonicalFile().toURI().toURL());
	    }
	}
	for (Iterator it = jarList.iterator(); it.hasNext(); ) {
	    String fileName = (String) it.next();
	    list.add(new File(fileName).getCanonicalFile().toURI().toURL());
	}
	if (list.size() > 0) {
	    URL[] urls = (URL[]) list.toArray(new URL[list.size()]);
	    ClassLoader cl = ClassLoader.getSystemClassLoader();
	    if (cl != null) {
		cl = cl.getParent(); // the extension classloader
	    }
	    loader = new URLClassLoader(urls, cl);
	}
	loadJars();
	Collection roots = getRoots();
	for (Iterator it = roots.iterator(); it.hasNext(); ) {
	    String clazz = (String) it.next();
	    Class c = null;
            try {
		c = Class.forName(clazz, false, loader);
	    } catch (ClassNotFoundException e) {
		throw new IllegalStateException("could not load root class " + clazz);
	    }
	    if ((c.getModifiers() & Modifier.PUBLIC) == 0) {
		if (keepNonPublicRoots) {
		    print("preflistgen.rootNotPublic", c);
		} else {
		    if (!listGraph.isFrozen(clazz)) {
			//remove non public root and force preferred
			it.remove();
			listGraph.setPreferred(clazz, true, true);
			continue;
		    }
		}
	    }
	    process(null, clazz);
	}
    }
       
    /**
     * Generate the <code>Collection</code> of roots to use for the dependency
     * analysis. All classes from the first JAR with a preferred state of false
     * (which will only be true due to the -api option, or because the same
     * class in another JAR is marked not-preferred) are added to the set;
     * Also, all interfaces of classes in the set of <code>-proxy</code>
     * arguments are added to the set of roots.
     *
     * @return the <code>Collection</code> of roots
     * @throws IllegalArgumentException if any of the proxies supplied via
     *         the <code>addProxy</code> method could not be found.
     */
    private Collection getRoots() {
	HashSet roots = new HashSet();
	listGraph.addRoots(roots);
	Iterator proxyIt = proxies.iterator();
	while (proxyIt.hasNext()) {
	    String proxy = (String) proxyIt.next();
	    try {
		Class proxyClass = Class.forName(proxy, false, loader);
		addProxyRoot(proxyClass, roots);
	    } catch (ClassNotFoundException e) {
		String msg = getString("preflistgen.badproxyclass", proxy);
		IllegalArgumentException iae = new IllegalArgumentException(msg);
		iae.initCause(e);
		throw iae;
	    }
        }
	return roots;
    }

    /**
     * Add to the given set of roots the public interfaces implemented by 
     * the <code>proxyClass</code> and all of its superclasses.
     * If any non-public interfaces are implemented, then any parent
     * interfaces which are public are added.
     *
     * @param proxyClass the proxy to inspect
     * @param roots the set of root class names
     */
    private void addProxyRoot(Class proxyClass, Collection roots) {
	Class[] interfaces = proxyClass.getInterfaces();
	for (int j = 0; j < interfaces.length; j++) {
	    addIfPublic(interfaces[j], roots);
	}
	Class superClass = proxyClass.getSuperclass();
	if (superClass != null) {
	    addProxyRoot(superClass, roots);
	}
    }

    /**
     * Add to the given set of roots the interface named <code>intFace</code> if
     * that interface is public. Otherwise, add any superinterfaces which are
     * public.
     *
     * @param intFace the interface to add
     * @param roots the set of roots
     */
    private void addIfPublic(Class intFace, Collection roots) {
	if ((intFace.getModifiers() & Modifier.PUBLIC) != 0) {
	    roots.add(intFace.getName());
	    return;
	}
	Class[] parents = intFace.getInterfaces();
	for (int i = 0; i < parents.length; i++) {
	    addIfPublic(parents[i], roots);
	}
    }

    /**
     * Print the usage message.
     */
    private static void usage() {
	print("preflistgen.usage", null);
    }

    /**
     * Generate the preferred list from the dependency graph. If a default value
     * was specified, the optimal list using that default value is
     * generated. Otherwise, the optimal list among the two possibilities
     * (<code>false/true</code> in order of precedence for 'optimization ties')
     * is generated. An explicit default entry is generated only for the
     * default <code>true</code> case.
     * <p>
     * The preferred list is sorted such that more specific
     * definitions precede less specific definitions; ties are broken with
     * an alphabetic secondary sort.
     * <p>
     * The preferred list will be placed in the target JAR file unless
     * <code>setReplaceJar(false)</code> was called. The preferred list will be
     * written to <code>writer</code> if it is non-<code>null</code>.  If
     * <code>writer</code> is <code>null</code> and <code>setPrint(true)</code>
     * was called, the preferred list will be written to
     * <code>System.out</code>.
     *
     * @param writer the <code>PrintWriter</code> to write the preferred list 
     *               to. 
     *
     * @throws IOException if an error occurs updating the target JAR file.
     */
    public void generatePreferredList(PrintWriter writer) throws IOException {
	if (writer == null && printResults) {
	    writer = new PrintWriter(System.out);
	}
	String newLine = System.getProperty("line.separator");
	if (!tells.isEmpty()) {
	    return;
        }
	StringBuffer sb = new StringBuffer();
	sb.append("PreferredResources-Version: 1.0");
	sb.append(newLine);
	Collection entries = new TreeSet();
	boolean pref;
	listGraph.markStaleInnerClasses(); // throw away inners matching outers
	listGraph.deleteStaleNodes(); // discard all stale nodes
	if (forceDefault) {
	    pref = defaultToForce;
	    listGraph.setDefaultPref(pref);
	    listGraph.addEntries(entries); // create sorted list of entries
	} else {
	    // create list using optimal default
	    HashSet bestEntries = new HashSet();
	    // first create entries for default=false 
	    pref = false;
	    listGraph.setDefaultPref(pref);
	    listGraph.addEntries(bestEntries);
	    // create entries for default=true and remember the best result
	    HashSet test = new HashSet();
	    listGraph.setDefaultPref(true);
	    listGraph.addEntries(test);
	    if (test.size() < bestEntries.size()) {
		bestEntries = test;
		pref = true;
	    }
	    entries = new TreeSet(bestEntries); // do the sort
	}
	if (pref || forceDefault || entries.isEmpty()) {
	    sb.append(newLine);
	    sb.append("Preferred: ");
	    sb.append(pref);
	    sb.append(newLine);
	}
	// write the individual entries
	Iterator it = entries.iterator();
	while (it.hasNext()) {
	    PrefData entry = (PrefData) it.next();
	    sb.append(newLine);
	    sb.append("Name: ");
	    sb.append(entry.name);
	    sb.append(newLine);
	    sb.append("Preferred: ");
	    sb.append(entry.preferred);
	    sb.append(newLine);
	}
	// output the resulting list or JAR file or both
	String preferredList = sb.toString();
	if (writer != null) {
	    writer.print(preferredList);
	    writer.flush();
	}
	if (replaceJar) {
	    buildJarFile(preferredList);
	}
    }

    /**
     * Replace the first JAR file in the set of JARs with a new copy containing
     * the given preferred list. If the JAR contained a preferred list, it is
     * replaced.
     *
     * @param preferredList the new preferred list
     * @throws IOException if an error occurs updating the target JAR file.
     */
    private void buildJarFile(String preferredList) throws IOException {
	// prepare to create a temp JAR file from the original
	File newJar = File.createTempFile("pref", "jar");
	newJar.deleteOnExit();
	JarInputStream ji = new JarInputStream(new FileInputStream(targetJar));
	JarOutputStream jo = 
	    new JarOutputStream(new FileOutputStream(newJar));
	JarEntry entry;
	byte[] ba;

	// write the META-INF directory entry, probably not really necessary
	entry = new JarEntry("META-INF/");
	jo.putNextEntry(entry);
	jo.closeEntry();

	// copy the original manifest entry to the new file
	entry = new JarEntry("META-INF/MANIFEST.MF");
	jo.putNextEntry(entry);
	ji.getManifest().write(jo);
	jo.closeEntry();

	// write the new preferred list entry to the new file
	entry = new JarEntry("META-INF/PREFERRED.LIST");
	jo.putNextEntry(entry);
	ba = preferredList.getBytes();
	jo.write(ba, 0, ba.length);

	// copy the remaining entries, discarding the old preferred list
	while ((entry = ji.getNextJarEntry()) != null) {
	    if (entry.getName().equals("META-INF/PREFERRED.LIST")) {
		ji.closeEntry();
		continue;
	    }
	    jo.putNextEntry(entry);
	    ba = new byte[1000];
	    int size;
	    while ((size = ji.read(ba, 0, 1000)) >= 0) {
		jo.write(ba, 0, size);
	    }
	    jo.closeEntry();
	}
	ji.close();
	jo.close();

	// copy the new JAR file over the old one
	FileInputStream fi = new FileInputStream(newJar);
	FileOutputStream fo = new FileOutputStream(targetJar);
	ba = new byte[1000];
	int size;
	while ((size = fi.read(ba)) >= 0) {
	    fo.write(ba, 0, size);
	}
	fi.close();
	fo.close();
    }

    /**
     * The command line interface to the tool. Parses the command line arguments,
     * computes the dependency graph, and generates the preferred list.
     *
     * @param args the command line arguments
     */
    public static void main(String[] args) {
	try {
	    PreferredListGen dep = new PreferredListGen(args);
	    dep.compute();
	    dep.generatePreferredList(null);
	    System.exit(0);
	} catch (IllegalArgumentException e) {
	    print("preflistgen.badarg", e.getMessage());
	    usage();
	} catch (IOException e) {
	    print("preflistgen.ioproblem", e.getMessage());
	    e.printStackTrace();
	}
	System.exit(1);
    }

    /**
     * A representation of a class path, package path wildcard, or namespace
     * path wildcard preferred list entry. This class implements
     * <code>Comparable</code> to provide a primary sort key for the
     * ordering of class/path/namespace, and a secondary sort key which
     * is alphabetic.
     */ 
     private class PrefData implements Comparable { 

	 /** the preferred list entry name string */
	 String name; 

	 /** the preferred value for this entry */
	 boolean preferred;

	 /** the sourceJar, used when merging two graphs */
	 File sourceJar;

	PrefData(String name, boolean preferred) {
	    this.name = name;
	    this.preferred = preferred;
	}

	PrefData(String name, boolean preferred, File sourceJar) {
	    this(name, preferred);
	    this.sourceJar = sourceJar;
	}

	public int compareTo(Object o) {
	    String oPrefixed = ((PrefData) o).prefixedString();
	    return prefixedString().compareTo(oPrefixed);
	}

	private String prefixedString() {
	    if (name.endsWith(".class")) {
		return "a" + name;
	    } else if (name.endsWith("/*")) {
		return "b" + name;
	    } else {
		return "c" + name;
	    }
	}
    }

    /**
     * A representation of the graph of classes contained in the set of
     * JARs being analyzed. This class implements the search algorithm to
     * find the optimal (smallest) set of preferred list entries which
     * describes the preferred state of entries in the graph. Nodes in
     * the graph can be one of seven types:
     * <ul>
     *  <li><code>PKG</code> nodes represent package wildcards
     *  <li><code>NAMESPACE</code> nodes represent namespace wildcards
     *  <li><code>DEFAULT</code> nodes represent the default preference. Only
     *      the root node of the graph can be of this type.
     *  <li><code>INHERIT</code> nodes inherit their preference values from
     *      their parent nodes. For the case where there is no default
     *      preferred value, the root of the graph will be of this type.
     *  <li> <code>CLASS</code> leaf node representing a class
     *  <li> <code>RESOURCE</code> leaf node representing a non-class resource
     *  <li> <code>STALE</code> an entry which has been marked for deletion
     * </ul>
     * All non-terminal nodes represent a component in the package name
     * of a class. All leaf nodes represent classes or resources.
     */
    private class Graph {

	/** type value when non-leaf node inherits preference from its parent */
	static final int INHERIT = 0;

	/** type value for package wildcard nodes */
	static final int PKG = 1;

	/** type value for namespace wildcard nodes */
	static final int NAMESPACE = 2;

	/** type value for the default preference (root) node */
	static final int DEFAULT = 3;

	/** type value for a class node */
	static final int CLASS = 4;

	/** type value for a resource (non-class JAR entry) node */
	static final int RESOURCE = 5;

	/** type value for a stale node (marked for deletion) */
	static final int STALE = 6;

	/** the name of this node (e.g. com or sun, not com.sun) */
	String name;

	/** the set of child nodes of this node */
	HashSet nodes = new HashSet();

	/** the preferred state, only meaningful to leaf (class) nodes */
	boolean preferred = true;

	/** the parent of this node, or null for the root node */
	Graph parent;

	/** the type of the node */
	int type = INHERIT;

	/** the preferred value implied for child nodes */
	Boolean impliedPref = null;

	/** the source JAR causing creation of this node, or null */
	File sourceJar = null;

	/**
	 * Create the root node of the graph, setting implied pref to TRUE
	 */
	Graph() {
	    name = "";
	    impliedPref = TRUE;
	    type = DEFAULT;
	}

	/**
	 * Create a node of the graph having the given name and parent.
	 * If the name contains multiple '.' separated tokens, the name
	 * is taken from the first token, and another node is constructed
	 * using the remainder of the name and this node as the parent.
	 * The final (leaf) node created is given a preferred value inherited
	 * from its ancestors.
	 *
	 * @param parent the parent of this node, or <code>null</code> if
	 *        this is the root node.
	 * @param name the (possibly dot-separated) name for this node (and
	 *        child nodes).
	 * @param type the node type, must be CLASS or RESOURCE
	 * @param sourceJar source of this node definition, or null defined
	 *        through the -api or -impl options
	 */
	Graph(Graph parent, String name, int type, File sourceJar) {
	    if (type != CLASS && type != RESOURCE) {
		throw new IllegalStateException("type must be CLASS or "
					        + "RESOURCE");
	    }
	    this.parent = parent;
	    this.sourceJar = sourceJar;
	    int firstDot = name.indexOf(".");
	    if (firstDot < 0) {
		this.name = name;
		this.type = type;
		preferred = parent.childPref();
		if (type == CLASS) {
		    Graph outer = getOuter();
		    /* if outer == null, then either this node does not represent
		     * a nested class, or it represents a nested class who's outer
		     * class hasn't been loaded yet. In either case, inheriting the
		     * parents preference is the right thing to do because when the
		     * outer class get loaded later it will also inherit the parents
		     * preference value. 
		     */
		    if (outer != null) {
			preferred = outer.preferred;
		    }
		}
	    } else {
		this.name = name.substring(0, firstDot);
		nodes.add(new Graph(this, 
				    name.substring(firstDot + 1), 
				    type, 
				    sourceJar));
	    }
	}

	/**
	 * Create a node of the graph having the given name, parent, type and
	 * preferred value.  If the name contains multiple '.' separated tokens,
	 * the name is taken from the first token, and another node is
	 * constructed using the remainder of the name and this node as the
	 * parent.  The type and preferred value are only set on the last node
	 * created in the chain. 
	 *
	 * @param parent the parent of this node, or <code>null</code> if
	 *        this is the root node.
	 * @param name the (possibly dot-separated) name for this node (and
	 *        child nodes).
	 * @param type the node type, must not be INHERIT
	 * @param preferred the preferred state of this node
	 * @param sourceJar source of this node definition, or null defined
	 *        through the -api or -impl options
	 *
	 * @throws IllegalStateException if type is INHERIT
	 */
	Graph(Graph parent, 
	      String name, 
	      int type, 
	      boolean preferred, 
	      File sourceJar) 
	{
	    if (type == INHERIT) {
		throw new IllegalStateException("Cannot create a preference "
					        + "node of type INHERIT");
	    }
	    this.parent = parent;
	    this.sourceJar = sourceJar;
	    int firstDot = name.indexOf(".");
	    if (firstDot < 0) {
		this.name = name;
		this.type = type;
		if (type == CLASS || type == RESOURCE) {
		    this.preferred = preferred;
		} else {
		    impliedPref = new Boolean(preferred);
		}
	    } else {
		this.name = name.substring(0, firstDot);
		String childName = name.substring(firstDot + 1);
		nodes.add(new Graph(this, 
				    childName, 
				    type, 
				    preferred, 
				    sourceJar));
	    }
	}

	/**
	 * Determines whether <code>arg</code> represents a path reference to a
	 * class, a resource, a package wildcard, or a namespace wild card, and
	 * add the value, converted to a '.' separated name, to the graph with
	 * the given preferred value and appropriate type. If <code>arg</code>
	 * contains at least one '.' character and no '/' characters, assume it
	 * is a class name and add the value to the graph with the given
	 * preferred value and a type of <code>Graph.CLASS</code>.
	 *
	 * @param arg the name of the component to add to the graph
	 * @param preferred the preferred value of the component
	 * @throws IllegalArgumentException if <code>arg</code> is not
	 *         a recognized form.
	 */
	void initialize(String arg, boolean preferred, File sourceJar) {
	    if (name.length() > 0) {
		throw new IllegalStateException("must initialize "
						+ "only the root");
	    }
	    if (arg.endsWith("/-")) {
		String pkgName = fileToClass(arg.substring(0, arg.length() - 2));
		addWithPreference(pkgName, NAMESPACE, preferred, sourceJar);
	    } else if (arg.endsWith("/*")) {
		String pkgName = 
		    fileToClass(arg.substring(0, arg.length() - 2));
		addWithPreference(pkgName, PKG, preferred, sourceJar);
	    } else if (arg.endsWith("/")) {
		String pkgName = 
		    fileToClass(arg.substring(0, arg.length() - 1));
		addWithPreference(pkgName, PKG, preferred, sourceJar);
	    } else if (arg.endsWith(".class")) { // class file reference
		String leafName = fileToClass(arg);
		addWithPreference(leafName, CLASS, preferred, sourceJar);
	    } else if (arg.indexOf('/') != -1) { // resource file reference
		String leafName = fileToClass(arg);
		addWithPreference(leafName, RESOURCE, preferred, sourceJar);
	    } else if (arg.indexOf('.') != -1) { // class name reference
		addWithPreference(arg, Graph.CLASS, preferred, sourceJar);
	    } else {
		String msg = getString("preflistgen.badapi", arg);
		throw new IllegalArgumentException(msg);
	    }
	}

	/**
	 * Add roots from this subtree of the graph to the given
	 * collection of roots. If this is a CLASS node and this node
	 * has a preferred value of <code>false</code> and was defined
	 * by a command line option or was supplied by the first JAR file,
	 * add this node to the collection.
	 *
	 * @param roots the collection to add to
	 */
	void addRoots(Collection roots) {
	    if (type == CLASS && sourceJar == null) {
	        String entryName = getFullName() + ".class";
	        if (!jarEntries.contains(entryName)) {
		    System.err.println("Class entry " + entryName
				     + " identified by the -" 
				     + (preferred ? "impl" : "api") 
				     + " option does not exist in any JAR file. "
				     + "That entry has been discarded");
		    return;
		}
	    }
	    if (type == CLASS  
		&& !preferred 
		&& (sourceJar == null || sourceJar == targetJar)) 
	    {
		roots.add(fileToClass(getFullName()));
	    } else {
		for (Iterator it = nodes.iterator();it.hasNext(); ) {
		    Graph g = (Graph) it.next();
		    g.addRoots(roots);
		}
	    }
	}

	/**
	 * For non-leaf nodes,
	 * Reset the type and implied preference value for this node
	 * and all child nodes recursively. If this is a leaf node,
	 * do nothing.
	 */
	void reset() {
	    if (type != CLASS && type != RESOURCE) {
		type = INHERIT;
		impliedPref = null;
		Iterator it = nodes.iterator();
		while (it.hasNext()) {
		    Graph g = (Graph) it.next();
		    g.reset();
		}
	    }
	}

	/**
	 * Return the implied preference as determined by the nearest
	 * parent namespace wildcard, or the default preference if
	 * there are no parent namespace wildcards. This method should
	 * only be called indirectly as a side-effect of calling 
	 * <code>childPref</code>.
	 *
	 * @return the implied preferred value
	 */
	boolean impliedChildPref() {
	    if (type == NAMESPACE || type == DEFAULT) {
		if (impliedPref == null) {
		    throw new IllegalStateException("impliedPref is null");
		}
		return impliedPref.booleanValue();
	    }
	    if (parent == null) {
		throw new IllegalStateException("default undefined in graph");
	    }
	    return parent.impliedChildPref();
	}

	/**
	 * Return the implied preference of the parent node if the parent is a
	 * package wildcard. Otherwise, return the implied preference as
	 * determined by the nearest parent namespace wildcard, or the default
	 * preference if there are no parent namespace wildcards.
	 *
	 * @return the implied preferred value
	 */
	boolean childPref() {
	    if (type == NAMESPACE || type == DEFAULT || type == PKG) {
		if (impliedPref == null) {
		    throw new IllegalStateException("impliedPref is null");
		}
		return impliedPref.booleanValue();
	    }
	    if (parent == null) {
		throw new IllegalStateException("default undefined in graph");
	    }
	    return parent.impliedChildPref();
	}

	/**
	 * Set the default preferred value for the graph. This method
	 * may only be called on the root node of the graph.
	 *
	 * @param value the default preferred value, which may be 
	 *        <code>null</code> to indicate that no default is defined
	 */
	void setDefaultPref(boolean value) {
	    if (name.length() > 0) {
		throw new IllegalStateException("must set default on root");
	    }
	    type = DEFAULT;
	    impliedPref = new Boolean(value);
	}

	// inherit javadoc
	public String toString() {
	    return "Graph[" + name + ", Parent = " + parent + "]";
	}

	/**
	 * Add a child node to this node having the given name. the name
	 * may have multiple '.' separated components, in which case a
	 * hierarchy of child nodes will be added. Only child nodes which
	 * do not already exist are created. The preferred value for
	 * the child will be set based on the value inherited from its
	 * ancestors. Nodes added by this method must always be leaf nodes.
	 * 
	 * @param name the name of the child node to add
	 * @param type the node type, must be CLASS or RESOURCE
	 * @param sourceJar the JAR file causing this class or resource to be added
	 */
	void add(String name, int type, File sourceJar) {
	    if (type != CLASS && type != RESOURCE) {
		throw new IllegalStateException("type must be CLASS or "
						+ "RESOURCE");
	    }
	    if (name == null) { // added previously - noop
		return;
	    }
	    String nodeName;
	    String childName = null;
	    int firstDot = name.indexOf(".");
	    if (firstDot < 0) {
		nodeName = name;
	    } else {
		nodeName = name.substring(0, firstDot);
		childName = name.substring(firstDot + 1);
	    }
	    Iterator it = nodes.iterator();
	    // search for a match to an existing child and recursively add
	    while (it.hasNext()) {
		Graph g = (Graph) it.next();
		if (g.name.equals(nodeName)) {
		    g.add(childName, type, sourceJar); // noop if null
		    return;
		}
	    }
	    // if here, name doesn't exist in collection of child nodes
	    nodes.add(new Graph(this, name, type, sourceJar));
	}

	/**
	 * Return <code>true</code> if the graph contains a node having
         * the given name.
	 * 
	 * @param name the name of the node to test for
         * @return true if the node is found
	 */
	boolean contains(String name) {
	    String nodeName;
	    String childName = null;
	    int firstDot = name.indexOf(".");
	    if (firstDot < 0) {
		nodeName = name;
	    } else {
		nodeName = name.substring(0, firstDot);
		childName = name.substring(firstDot + 1);
	    }
	    Iterator it = nodes.iterator();
	    // search for a match to an existing child and recursively add
	    while (it.hasNext()) {
		Graph g = (Graph) it.next();
		if (g.name.equals(nodeName)) {
		    if (childName == null) {
			return true;
		    } else {
			return g.contains(childName);
		    }
		}
	    }
	    // if here, name doesn't exist in collection of child nodes
	    return false;
	}

	/**
	 * Return the frozen state of the class node having the
	 * given name. A node is frozen if it was created through 
	 * the <code>-api</code> or <code>-impl</code> options.
	 *
	 * @param name the dot-separate name
	 * @return true if the node is frozen
	 */
	boolean isFrozen(String name) {
	    String nodeName;
	    String childName = null;
	    int firstDot = name.indexOf(".");
	    if (firstDot < 0) {
		nodeName = name;
	    } else {
		nodeName = name.substring(0, firstDot);
		childName = name.substring(firstDot + 1);
	    }
	    Iterator it = nodes.iterator();
	    while (it.hasNext()) {
		Graph g = (Graph) it.next();
		if (g.name.equals(nodeName)) {
		    if (childName == null) {
			if (g.type != CLASS) {
			    throw new IllegalStateException("Not a class: "
							    + g.getFullName());
			}
			return g.sourceJar == null; // set by -api/-impl
		    } else {
			return g.isFrozen(childName);
		    }
		}
	    }
	    // if here, name doesn't exist in collection of child nodes
	    return false;
	}



	/**
	 * Add a child node to this node having the given name. the name may
	 * have multiple '.' separated components, in which case a hierarchy of
	 * child nodes will be added. Only child nodes which do not already
	 * exist are created. The final node created will be assigned the given
	 * type and preferred value. If the node already exists, new type and
	 * preferred values will be assigned unless the original source JAR is
	 * null, indicating node was created via the -api or -impl options.  The
	 * source JAR is not changed from it's original value. If the node
	 * already exists, the original source JAR is non-null and is also not
	 * the first JAR, this is a leaf node, and preferred values differ, a
	 * warning message is written to System.err to identify the conflicting
	 * definitions; in this case, the state of the node is set to
	 * not-preferred. The first JAR is ignored in this case because the
	 * correct value for the preferred state of these classes is not known
	 * until the dependency analysis is done. Therefore in this special case
	 * the preferred state is reset to the new state.
	 * 
	 * @param name the name of the child node to add
	 */
	void addWithPreference(String name, 
			       int type, 
			       boolean preferred, 
			       File sourceJar) 
	{
	    if (type == INHERIT) {
		throw new IllegalStateException("Cannot add preference node "
						+ "of type INHERIT");
	    }
	    // allow type to be redefined - should probably do an analysis
	    if (name == null) {
		if (this.sourceJar != null) {
		    this.type = type;
		    if (type == CLASS || type == RESOURCE) {
			if(this.preferred != preferred 
			   && this.sourceJar != targetJar) 
			{
			    print("preflistgen.prefconflict",
				  getFullName(),
				  this.sourceJar,
				  sourceJar);
			    this.preferred = false;
			} else {
			    this.preferred = preferred;
			}
		    } else {
			impliedPref = new Boolean(preferred);
		    }
		}
		return;
	    }
	    String nodeName;
	    String childName = null;
	    int firstDot = name.indexOf(".");
	    if (firstDot < 0) {
		nodeName = name;
	    } else {
		nodeName = name.substring(0, firstDot);
		childName = name.substring(firstDot + 1);
	    }
	    Iterator it = nodes.iterator();
	    // search for a match to an existing child and recursively add
	    while (it.hasNext()) {
		Graph g = (Graph) it.next();
		if (g.name.equals(nodeName)) {
		    g.addWithPreference(childName, type, preferred, sourceJar);
		    return;
		}
	    }
	    // if here, name doesn't exist in collection of child nodes
	    nodes.add(new Graph(this, name, type, preferred, sourceJar));
	}

	/**
	 * Set the preferred state of the leaf (class) child node having the
	 * given '.' qualified name to the given value. If <code>name</code>
	 * is <code>null</code>, set the preferred value for this node.
	 * If the target name does not exist in the graph, this method
	 * is a noop.
	 * <p>
	 * This method enforces the requirement that the ultimate target
	 * node for the call must be a leaf node. 
	 *
	 * @param name the name of the child node to set, or <code>null</code>
	 *             to represent this node
	 * @param value the preferred value to set.
	 * @param force if true, ignore frozen state
	 * @return true if the preferred value matches <code>value</code>
	 *              or is changed to <code>value</code> or if the named
	 *              node does not exist in the graph. Returns
	 *              <code>false</code> if the values don't match and
	 *              the node is frozen and force is false.
  	 */
	boolean setPreferred(String name, boolean value, boolean force) {
	    if (name == null) {
		if (type != CLASS && type != RESOURCE) {
		    throw new IllegalStateException("only leaf node preferred"
						     + " state may be set");
		}
		if (sourceJar != null || force) { // frozen if defined by -api or -impl
		    preferred = value;
		}
		return preferred == value;
	    }
	    String childName = name;
	    String targetName = null;
	    int firstDot = name.indexOf(".");
	    if (firstDot >= 0) { // must have reached the leaf node
		childName = name.substring(0, firstDot);
		targetName = name.substring(firstDot + 1);
	    }
	    Iterator it = nodes.iterator();
	    while (it.hasNext()) {
		Graph g = (Graph) it.next();
		if (g.name.equals(childName)) {
		    return g.setPreferred(targetName, value, force);
		}
	    }
	    // if here, name doesn't exist in the graph - noop
	    return true;
	}

	/**
	 * Count the number of child leaf nodes which
	 * have a preferred state matching the given value. If this
	 * is a leaf node, return 1 if the value matches the preferred
	 * state of this node.
	 *
	 * @param value the preferred value to search for
	 * @return the number of leaf nodes having the given value
	 */
	int countPreferred(boolean value) {
	    if (type == CLASS || type == RESOURCE) { // leaf 
		return (value == preferred) ? 1 : 0;
	    } else {
		int total = 0;
		Iterator it = nodes.iterator();
		while (it.hasNext()) {
		    Graph g = (Graph) it.next();
		    total += g.countPreferred(value);
		}
		return total;
	    }
	}

	/**
	 * Count the number of child leaf (class) nodes which have a preferred
	 * state which differs from the value implied by * its ancestors. If
	 * this is a leaf node and is also a nested class, return 1 if the outer
	 * class has a different preferred state than this class. If this is a
	 * leaf node and is not a nested class, return 1 if the implied value
	 * differs the preferred state for this node. If no value is implied
	 * by the ancestors, this is interpreted as a mismatch.
	 *
	 * @return the number of leaf nodes having preferred values which differ
         *         from that implied by its ancestors
	 */
	int countImpliedFailures() {
	    if (type == CLASS || type == RESOURCE) { // leaf 
		if (type == CLASS) {
		    Graph outer = getOuter();
		    if (outer != null) {
			return (outer.preferred == preferred) ? 0 : 1;
		    }
		}
		return (parent.childPref() != preferred) ? 1 : 0;
	    } else {
		int total = 0;
		Iterator it = nodes.iterator();
		while (it.hasNext()) {
		    Graph g = (Graph) it.next();
		    total += g.countImpliedFailures();
		}
		return total;
	    }
	}

	/**
	 * Count the number of child leaf nodes.
	 *
	 * @return the number of leaf nodes which have this node
	 *         as an ancestor.
	 */
	int countLeafNodes() {
	    int total = 0;
	    Iterator it = nodes.iterator();
	    while (it.hasNext()) {
		Graph g = (Graph) it.next();
		if (g.type == CLASS || g.type == RESOURCE) { // leaf node
		    total++;
		} else {
		    total += g.countLeafNodes();
		}
	    }
	    return total;
	}

	/**
	 * Return the full name of this node expressed as a relative
	 * path. No suffixes (".class", "/-", "/*") are appended.
	 *
	 * @return the full path name representing this node
	 */
	String getFullName() {
	    if (name.length() == 0) {
		return "";
	    }
	    String prefix = "";
	    if (parent != null) {
		prefix = parent.getFullName();
	    }
	    if (prefix.length() > 0) {
		prefix += "/";
	    }
	    return prefix + name;
	}

	/**
	 * If this node is of type CLASS and is an inner class, mark this node
	 * as STALE if the outer class has the same preferred value. Print a
	 * warning to <code>System.err</code> if they do not have the same
	 * value. If there is no outer class corresponding to the inner class,
	 * this node is retained. Call this method on all children.
	 */
	void markStaleInnerClasses() {
	    if (type == CLASS) {
		Graph outer = getOuter();
		if (outer != null) {
		    if (outer.preferred == preferred) {
			type = STALE;
		    } else {
			print("preflistgen.innerwarn", outer.name, name);
		    }
		}
	    } else {
		for (Iterator it = nodes.iterator(); it.hasNext(); ) {
		    Graph g = (Graph) it.next();
		    g.markStaleInnerClasses();
		}
	    }
	}

	/**
	 * Return the node for the outer class for this class if this is a nested
	 * class. 
	 *
	 * @return the graph for the outer class, or <code>null</code> if this is
	 *         not a nested class, or if the outer class is not in the graph
	 */
	Graph getOuter() {
	    if (type != CLASS) {
		throw new IllegalStateException("Attempt to get outer of a non-class");
	    }
	    Graph g = null;
	    int lastDollar = name.lastIndexOf('$');
	    if (lastDollar > 0) {
		String outerName = name.substring(0, lastDollar);
		g = parent.getChild(outerName);
	    }
	    return g;
	}

	/**
	 * Recursively remove child nodes of type STALE.
	 */
	void deleteStaleNodes() {
	    for (Iterator it = nodes.iterator(); it.hasNext(); ) {
		Graph g = (Graph) it.next();
		if (g.type == STALE) {
		    it.remove();
		} else {
		    g.deleteStaleNodes();
		}
	    }
	}

	/**
	 * Get the child node having the given name.
	 *
	 * @param name the child name
	 * @return the child <code>Graph</code> object, or null if the
	 *         no child named <code>name</code> exists
	 */
	Graph getChild(String name) {
	    for (Iterator it = nodes.iterator(); it.hasNext(); ) {
		Graph g = (Graph) it.next();
		if (g.name.equals(name)) {
		    return g;
		}
	    }
	    return null;
	}

	/**
	 * Test whether all of the immediate child nodes of this
	 * node are leaf (class) nodes.
	 *
	 * @return <code>true</code> if all a the nodes are leaf nodes
	 */
	boolean containsLeavesOnly() {
	    Iterator it = nodes.iterator();
	    while (it.hasNext()) {
		Graph g = (Graph) it.next();
		if (g.type != CLASS && g.type != RESOURCE) { // must be subpkg
		    return false;
		}
	    }
	    return true;
	}

	void addLeaves(Collection leaves) {
	    if (type == CLASS) { 
		leaves.add(new PrefData(getFullName() + ".class", 
					preferred, 
					sourceJar));
		return;
	    }
	    if (type == RESOURCE) {
		leaves.add(new PrefData(getFullName(), preferred, sourceJar));
		return;
	    }
	    Iterator it = nodes.iterator();
	    while (it.hasNext()) {
		Graph g = (Graph) it.next();
		g.addLeaves(leaves);
	    }
	    return;
	}
	
	void merge(Graph g) {
	    Collection leaves = new HashSet();
	    g.addLeaves(leaves);
	    Iterator it = leaves.iterator();
	    while (it.hasNext()) {
		PrefData data = (PrefData) it.next();
		int type = ((data.name.endsWith(".class") ? CLASS : RESOURCE));
		addWithPreference(fileToClass(data.name), 
				  type,
				  data.preferred, 
				  data.sourceJar);
	    }
	}

	/**
	 * Add preferred list entries (represented by <code>PrefData</code>
	 * objects) to the given <code>Collection</code>. One of the following
	 * cases is handled:
	 * <ul>
	 *  <li>if this is the root node, then <code>addEntries</code> is 
	 *      called on each of the child nodes. Otherwise:
	 *  <li>if the preferred state of all leaf nodes (including possibly
	 *      this one) matches the implied state provided by the ancestors,
	 *      then return without adding any entries. Otherwise:
	 *  <li>if this is a leaf node and its preferred state does not
	 *      match the state implied by its parent, add a class style
	 *      entry to the collection and return. Otherwise:
	 *  <li>if this is not a leaf node, and there is a single child
	 *      which is also not a leaf node, then this node represents
	 *      an intermediate component in a package name. Call 
	 *      <code>addEntries</code> on the child to generate a more
	 *      qualified entry. Otherwise:
	 *  <li>if this is not a leaf node and all of the child leaf nodes
	 *      have the same preferred value, then make this a package
	 *      wildcard having that value (if all children are leaves)
	 *      or make this a namespace wildcard having that value (if
	 *      some children are not leaves). Otherwise:
	 *  <li>try all variations of package/namespace wildcards, as well
	 *      as no wildcard setting, for this node. Add an entry
	 *      to the collection giving the optimal (minimal) result, then
	 *      call <code>addEntries</code> on all children.
	 * </ul>
	 * 
	 */
	void addEntries(Collection entries) {
	    if (parent == null) {
		Iterator it = nodes.iterator();
		while (it.hasNext()) {
		    Graph g = (Graph) it.next();
		    g.addEntries(entries);
		}
		return;
	    }
	    reset(); // set all non-leaf nodes to type = INHERIT
	    if (countImpliedFailures() == 0) {
		return;
	    }
	    if (type == CLASS) { // entry failed previous count test
		entries.add(new PrefData(getFullName() + ".class", preferred));
		return;
	    }
	    if (type == RESOURCE) { // entry failed previous count test
		entries.add(new PrefData(getFullName(), preferred));
		return;
	    }
	    // special case - find max qualified variant of pkg name
	    if (nodes.size() == 1) {
		Iterator it = nodes.iterator();
		if (! it.hasNext()) {
		    throw new IllegalStateException("Inconsistent iterator");
		}
		Graph g = (Graph) it.next();
		if (g.type != CLASS && g.type != RESOURCE) { // must be subpkg
		    g.addEntries(entries);
		    return;
		}
	    }
	    /*
	     * if here, some children have actual preferences which don't match
	     * their implied preferences. Check whether all children have
	     * the same preference value, and if so, make this node a
	     * wildcard package (if all immediate children are leaves)
	     * or a wildcard namespace otherwise (if at least one child
	     * is a non-leaf)
	     */
	    boolean gotMatch = false;
	    boolean matchValue = false;
	    int leafCount = countLeafNodes();
	    if (leafCount == countPreferred(true)) {
		gotMatch = true;
		matchValue = true;
	    } else if (leafCount == countPreferred(false)) {
		gotMatch = true;
		matchValue = false;
	    }
	    if (gotMatch) {
		if (containsLeavesOnly()) {
		    entries.add(new PrefData(getFullName() + "/*", matchValue));
		} else {
		    entries.add(new PrefData(getFullName() + "/-", matchValue));
		}
		return;
	    }
	    /*
	     * if here, children have a mix of preferred values. Find
	     * which variation of wildcard type (if any) to assign to
	     * this node to optimize the entries generated by children
	     */
	    int lowestValue = countEntries(INHERIT, null);
	    int bestType = INHERIT;
	    Boolean bestImpliedPref = null;
	    // do PKG before NAMESPACE so 'leaf package' always get pkg wildcard
	    int count = countEntries(PKG, TRUE);
	    if (count < lowestValue) {
		lowestValue = count;
		bestType = PKG;
		bestImpliedPref = TRUE;
	    }

	    count = countEntries(PKG, FALSE);
	    if (count < lowestValue) {
		lowestValue = count;
		bestType = PKG;
		bestImpliedPref = FALSE;
	    }

	    count = countEntries(NAMESPACE, TRUE);
	    if (count < lowestValue) {
		lowestValue = count;
		bestType = NAMESPACE;
		bestImpliedPref = TRUE;
	    }

	    count = countEntries(NAMESPACE, FALSE);
	    if (count < lowestValue) {
		lowestValue = count;
		bestType = NAMESPACE;
		bestImpliedPref = FALSE;
	    }

	    type = bestType;
	    impliedPref = bestImpliedPref;
	    if (type == NAMESPACE) {
		entries.add(new PrefData(getFullName() + "/-",
					 impliedPref.booleanValue()));
	    }
	    if (type == PKG) {
		entries.add(new PrefData(getFullName() + "/*",
					 impliedPref.booleanValue()));
	    }
	    for (Iterator it = nodes.iterator(); it.hasNext(); ) {
		Graph g = (Graph) it.next();
		g.addEntries(entries);
	    }
	}

	/**
	 * Count the number of entries which would be created
	 * by this node for the given node type and preference. The
	 * <code>type</code> and <code>impliedPref</code> class attributes
	 * are side-affected by this method.
	 *
	 * @param type the node type to assign
	 * @param pref the node's implied preference value
	 * @return the number of entries generated by children
	 */
	int countEntries(int type, Boolean pref) {
	    HashSet test = new HashSet();
	    this.type = type;
	    impliedPref = pref;
	    for (Iterator it = nodes.iterator(); it.hasNext(); ) {
		Graph g = (Graph) it.next();
		g.addEntries(test);
	    }
	    return test.size();
	}
    }
}
