/*
* 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>jsk_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>jsk_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. 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>jsk_install_dir</b></var>/lib/preferredlistgen.jar
* -cp <var><b>jsk_install_dir</b></var>/lib/jsk-platform.jar
* -jar <var><b>jsk_install_dir</b></var>/lib-dl/reggie-dl.jar
* -jar <var><b>jsk_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) {
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();
}
}
}