You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@river.apache.org by ni...@apache.org on 2008/12/10 06:09:43 UTC

svn commit: r724973 [5/9] - in /incubator/river/jtsk/skunk/niclas1/tools: ./ src/ src/main/ src/main/java/ src/main/java/com/ src/main/java/com/sun/ src/main/java/com/sun/jini/ src/main/java/com/sun/jini/tool/ src/main/java/com/sun/jini/tool/envcheck/ ...

Added: incubator/river/jtsk/skunk/niclas1/tools/src/main/java/com/sun/jini/tool/PreferredListGen.java
URL: http://svn.apache.org/viewvc/incubator/river/jtsk/skunk/niclas1/tools/src/main/java/com/sun/jini/tool/PreferredListGen.java?rev=724973&view=auto
==============================================================================
--- incubator/river/jtsk/skunk/niclas1/tools/src/main/java/com/sun/jini/tool/PreferredListGen.java (added)
+++ incubator/river/jtsk/skunk/niclas1/tools/src/main/java/com/sun/jini/tool/PreferredListGen.java Tue Dec  9 21:09:41 2008
@@ -0,0 +1,2815 @@
+/*
+ * 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.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+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>
+ *
+ * @author Sun Microsystems, Inc.
+ */
+
+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().toURL() );
+            }
+        }
+        for( Iterator it = jarList.iterator(); it.hasNext(); )
+        {
+            String fileName = (String) it.next();
+            list.add( new File( fileName ).getCanonicalFile().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;

[... 478 lines stripped ...]