1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18 package org.apache.river.tool;
19
20 import java.io.BufferedReader;
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.FileOutputStream;
24 import java.io.InputStreamReader;
25 import java.io.IOException;
26 import java.io.PrintWriter;
27
28 import java.lang.Comparable;
29 import java.lang.reflect.Constructor;
30 import java.lang.reflect.Field;
31 import java.lang.reflect.Method;
32 import java.lang.reflect.Modifier;
33
34 import java.net.URL;
35 import java.net.URLClassLoader;
36
37 import java.text.MessageFormat;
38
39 import java.util.ArrayList;
40 import java.util.Collection;
41 import java.util.Enumeration;
42 import java.util.HashSet;
43 import java.util.Iterator;
44 import java.util.MissingResourceException;
45 import java.util.ResourceBundle;
46 import java.util.StringTokenizer;
47 import java.util.TreeSet;
48
49 import java.util.jar.JarEntry;
50 import java.util.jar.JarFile;
51 import java.util.jar.JarInputStream;
52 import java.util.jar.JarOutputStream;
53 import java.util.jar.Manifest;
54
55 import java.util.regex.Matcher;
56 import java.util.regex.Pattern;
57
58 /**
59 * Tool used to generate the preferred class information for downloadable JAR
60 * files in the form of a META-INF/PREFERRED.LIST required for use by the <code>
61 * net.jini.loader.pref.PreferredClassLoader</code>. The list is generated by
62 * examining the dependencies of classes contained within a target JAR file and
63 * zero or more additional supporting JAR files. Through various command-line
64 * options, a set of "root" classes are identified as belonging to a public API.
65 * These root classes provide the starting point for recursively computing a
66 * dependency graph, finding all of the classes referenced in the public API of
67 * the root classes, finding all of the classes referenced in turn by the public
68 * API of those classes, and so on, until no new classes are found. The results
69 * of the dependency analysis are combined with the preferred list information
70 * in the additional supporting JAR files to compute a preferred list having the
71 * smallest number of entries that describes the preferred state of the classes
72 * and resources contained in all of the JAR files. The output of the tool is a
73 * new version of the target JAR file containing the generated preferred list,
74 * and/or a copy of the list printed to <code>System.out</code>.
75 * <p>
76 * This tool implements the first guideline described in <code>
77 * net.jini.loader.pref</code>. In many cases it is sufficient to specify
78 * the roots via the <code>-proxy</code> option. The <code>-api</code> and
79 * <code>-impl</code> options are used to generate lists for JAR files
80 * in which the roots are not completely defined by the
81 * proxy classes, or for non-service JAR files. Since there is no definitive
82 * set of rules for determining whether a class should be preferred,
83 * the developer should verify the correctness of the generated list.
84 * <p>
85 * The following items are discussed below:
86 * <ul>
87 * <li><a href="#running">Running the Tool</a>
88 * <li><a href="#processing">Processing Options</a>
89 * <li><a href="#examples">Examples</a>
90 * </ul>
91 *
92 * <a name="running"></a>
93 * <h3>Running the Tool</h3>
94 *
95 * To run the tool on UNIX platforms:
96 * <blockquote><pre>
97 * java -jar <var><b>install_dir</b></var>/lib/preferredlistgen.jar <var><b>processing_options</b></var>
98 * </pre></blockquote>
99 * To run the tool on Microsoft Windows platforms:
100 * <blockquote><pre>
101 * java -jar <var><b>install_dir</b></var>\lib\preferredlistgen.jar <var><b>processing_options</b></var>
102 * </pre></blockquote>
103 * <p>
104 * Note that the options for this tool can be specified in any order, and
105 * can be intermixed.
106 *
107 * <a name="processing"></a>
108 * <h3>Processing Options</h3>
109 * <p>
110 * <dl>
111 * <dt><b><code>-cp</code> <var>input_classpath</var></b>
112 * <dd>Identifies the classpath for all of the classes that might need to be
113 * included in the dependency analysis. Typically this will include all of your
114 * application classes, classes from the JGDMS release, and any other classes on
115 * which your classes might depend. It is safe to include more classes than are
116 * actually necessary because the tool limits the scope of the preferred list to
117 * those classes actually included in the JAR files being analyzed. It is not
118 * necessary to include the JAR files being analyzed in the classpath as they
119 * will be appended automatically. It is also unnecessary to include any classes
120 * that are part of the Java(TM) 2 SDK. The class path should be in the form of a
121 * list of directories or JAR files, delimited by a colon (":") on UNIX
122 * platforms and a semi-colon (";") on Microsoft Windows platforms. The order of
123 * locations in the path does not matter.
124 * </dd>
125 * </dl>
126 * <dl>
127 * <dt><b><code>-jar</code> <var>file</var></b>
128 * <dd>Identifies a JAR file containing the classes to analyze. If the JAR
129 * manifest includes a <code>Class-Path</code> attribute, then these JAR files
130 * will also be processed recursively. The default behavior is to replace the
131 * original JAR file with a new file containing the generated preferred list. If
132 * the original target JAR file contained a preferred list, that list is ignored
133 * and is replaced by the newly generated list. This option may be specified
134 * zero or more times. If multiple <code>-jar</code> options are specified, the
135 * first file specified is considered the target JAR file.
136 * </dd>
137 *
138 * <dt><b><code>-proxy</code> <var>classname</var></b>
139 * <dd>Identifies the class name of a proxy in the target JAR file. All of the
140 * public interfaces implemented by the proxy, and all of the public super
141 * interfaces of any non-public interfaces implemented by the proxy, are
142 * included in the set of roots for performing dependency analysis. This option
143 * may be specified zero or more times.
144 * </dd>
145 *
146 * <dt><b><code>-api</code> <var>name-expression</var></b>
147 * <dd>
148 * This option identifies a class or a JAR entry, package, or namespace that is
149 * to be considered public and therefore <b>not</b> preferred. If
150 * <var>name-expression</var> ends with ".class", it represents a class whose
151 * name is <var>name-expression</var> without the ".class" suffix and with each
152 * '/' character replaced with a '.'. Otherwise, if <var>name-expression</var>
153 * ends with "/" or "/*", it represents a directory wildcard matching all
154 * entries in the named directory. Otherwise, if <var>name-expression</var> ends
155 * with "/-", it represents a namespace wildcard that matches all entries in the
156 * named directory and all of its subdirectories. Otherwise
157 * <var>name-expression</var> represents a non-class resource in the JAR
158 * file. Alternatively, <var>name-expression</var> may be expressed directly as
159 * a class name. A nested (including inner) class must be expressed as a binary
160 * class name; if <code>Bar</code> is a nested class of <code>Foo,</code> then
161 * <code>Bar</code> would be expressed as <code>Foo$Bar</code>. The most
162 * specific <var>name-expression</var> is used to match an entry. By default,
163 * any public class in the JAR file that matches <var>name-expression</var> will
164 * be included in the set of roots for dependency analysis. If
165 * <var>name-expression</var> is a class name, then that class will be included
166 * in the set of roots irregardless of its access modifier. If the
167 * <code>-nonpublic</code> option is also present, then matching non-public
168 * classes will also be included in the set of roots. The <code>-api</code>
169 * option may be specified zero or more times.
170 * <p>
171 * As an example, presuming the class <code>org.apache.river.example.Foo</code>
172 * was included in the target JAR file, then the following would all cause
173 * that class to be included in the public API:
174 * <blockquote><pre>
175 * -api org/apache/river/example/Foo.class
176 * -api org.apache.river.example.Foo
177 * -api org/apache/river/example/*
178 * -api org/apache/river/example/-
179 * </pre></blockquote>
180 * and the last example would also apply to, for instance,
181 * <code>org.apache.river.example.gui.FooPanel</code>.
182 * </dd>
183 *
184 * <dt><b><code>-impl</code> <var>name-expression</var></b>
185 * <dd>This option identifies a class or a JAR entry, package, or namespace that
186 * is to be considered private and therefore preferred.
187 * <var>name-expression</var> is interpreted as described for the
188 * <code>-api</code> option. If <var>name-expression</var> is a class name or a
189 * class JAR entry name, that class will be considered preferred and will not be
190 * selected by or included in the dependency analysis even if it was included in
191 * the set of roots as a result of processing the <code>-proxy</code> and
192 * <code>-api</code> options. This option may be specified zero or more times.
193 * </dd>
194 *
195 * <dt><b><code>-nonpublic</code></b>
196 * <dd>This option forces any non-public classes matched by the
197 * <code>-api</code> <var>name-expression</var>s to be included in the set of
198 * roots for dependency analysis.
199 * </dd>
200 *
201 * <dt><b><code>-nomerge</code></b>
202 * <dd>Causes the classes in JAR files which do not contain preferred
203 * lists to be considered not preferred. If this option is not specified, all classes in
204 * JAR files which do not contain preferred lists are merged with the classes supplied by
205 * the target JAR file for purposes of dependency
206 * analysis; the additional classes are not included in the generated target JAR file.
207 * The <code>-impl</code> and <code>-api</code> options may be used to initialize
208 * the preferred state of the merged classes.
209 * </dd>
210 *
211 * <dt><b><code>-default</code> <var>false|true</var></b>
212 * <dd>Specifies the default preferred value to use when generating the
213 * preferred list and forces the generation of an explicit default preferred
214 * entry in the preferred list. If this option is not provided, the default
215 * that produces a list with the fewest entries is used; an explicit entry for
216 * the default <code>false</code> case will not be generated (except when no
217 * single entry is found, in which case a default preferred value of
218 * <code>false</code> is written). In the event of optimization ties, a default
219 * value of <var>false</var> is used.
220 * </dd>
221 *
222 * <dt><b><code>-noreplace</code></b>
223 * <dd>
224 * Inhibits the replacement of the original JAR file with a new updated JAR
225 * file. If this option is specified, the preferred list is printed on
226 * <code>System.out</code>.
227 * </dd>
228 *
229 * <dt><b><code>-print</code></b>
230 * <dd>
231 * Causes the preferred list to be printed to <code>System.out</code>, even if
232 * the list is also placed in an updated JAR file.
233 * </dd>
234 *
235 * <dt><b><code>-tell</code> <var>classname</var></b>
236 * <dd>Specifies the fully qualified name of a class for which dependency
237 * information is desired. This option causes the tool to display information
238 * about every class in the dependency graph that references the specified
239 * class. If no class references the specified class, it will be identified as a
240 * root class. This information is sent to the error stream of the tool, not to
241 * the normal output stream. This option can be specified zero or more
242 * times. If this option is used, all other output options are ignored, and the
243 * normal class output is not produced. This option is useful for debugging.
244 * </dd>
245 * </dl>
246 *
247 * Using values from the <code>-api</code> and <code>-impl</code> options, a
248 * graph is constructed that defines initial preferred values to be inherited by
249 * the target JAR entries as they are loaded into the graph. If there were no
250 * such options specified, all entries from the target JAR file loaded into the
251 * graph initially will be marked as preferred. The classes and resources
252 * identified by the first <code>-jar</code> option (the target JAR file) are
253 * then loaded into this graph and are assigned their initial preferred
254 * values. The remaining JAR files that include preferred lists are loaded into
255 * the graph and the entries assigned preferred values based on the preferred
256 * list contained in the JAR file being loaded. If a non-target JAR file does
257 * not contain a preferred list, the default behavior is to merge the classes
258 * and resources in the file with those of the target JAR file (for purposes of
259 * dependency analysis only), making them subject to the <code>-api</code> and
260 * <code>-impl</code> options. The <code>-nomerge</code> option can be used to
261 * override the default behavior, causing all such classes to be assigned a
262 * value of not preferred. The set of root classes is constructed by finding
263 * all of the classes from the target JAR file that are marked as not preferred
264 * in the graph, and by adding all of the public interfaces, or any public
265 * superinterfaces of non-public interfaces implemented by the proxy classes
266 * specified via the <code>-proxy</code> option. Starting with the root classes,
267 * dependent classes are identified by examining the compiled class file for the
268 * class, finding all of the public and protected fields, methods, constructors,
269 * interfaces, and super classes it references, and then in turn examining those
270 * classes. Any dependent classes found that also exist in the graph will be
271 * marked not preferred, unless that class was explicitly named by a
272 * <code>-impl</code> option. Any root class or dependent class named by a
273 * <code>-impl</code> option retains its original preferred value and no further
274 * dependency analysis is performed for the class. The range of the dependency
275 * analysis is restricted to the set of classes included in the graph.
276 * <p>
277 * The tool then processes the graph to find the smallest number of preferred
278 * list entries that describes the preferred state of all classes and resources
279 * in the graph. The resulting preferred list may be printed or included in a
280 * JAR file that replaces the original (first) JAR file.
281 * <p>
282 * <a name="examples"></a>
283 * <h3>Examples</h3>
284 *
285 * The following example generates the preferred list for the codebase JAR file
286 * for reggie, replacing the original reggie-dl.jar with a new file containing
287 * the preferred list. The reggie implementation includes four proxy classes,
288 * however <code>org.apache.river.reggie.RegistrarProxy</code> and
289 * <code>org.apache.river.reggie.AdminProxy</code> are not identified on the command
290 * line because they are parent classes of
291 * <code>org.apache.river.reggie.ConstrainableRegistrarProxy</code> and
292 * <code>org.apache.river.reggie.ConstrainableAdminProxy</code>.
293 * <p>
294 * <blockquote><pre>
295 * java -jar <var><b>install_dir</b></var>/lib/preferredlistgen.jar \
296 * -cp <var><b>install_dir</b></var>/lib/jsk-platform.jar \
297 * -jar <var><b>install_dir</b></var>/lib-dl/reggie-dl.jar \
298 * -jar <var><b>install_dir</b></var>/lib-dl/jsk-dl.jar \
299 * -proxy org.apache.river.reggie.ConstrainableRegistrarProxy \
300 * -proxy org.apache.river.reggie.ConstrainableAdminProxy
301 * </pre></blockquote>
302 * <p>
303 *
304 * @author Sun Microsystems, Inc.
305 */
306
307 public class PreferredListGen {
308
309 /** remember classes processed to avoid redundant work or loops */
310 private final Collection seen;
311
312 /*
313 * NOTE: the Boolean class is used extensively to represent the three
314 * possible states of a preferred value of true/false/undefined
315 * (for instance, if there is no default preferred value).
316 */
317
318 /** Boolean equivalent of true */
319 private final Boolean TRUE;
320
321 /** Boolean equivalent of false */
322 private final Boolean FALSE;
323
324 /** the names of proxies supplied by the -proxy option */
325 private final Collection proxies;
326
327 /** the set of classes to report based on the -tell options */
328 private final Collection tells;
329
330 /** the first JAR in the set of files loaded via the -jar option */
331 private File targetJar;
332
333 /** replace the first JAR with a copy containing the preferred list */
334 private boolean replaceJar;
335
336 /** print the preferred list, only meaningful with the -jar option */
337 private boolean printResults;
338
339 /** ordered list of JAR files names specified by the -jar options */
340 private final Collection jarList;
341
342 /** the classpath specified by the -cp option */
343 private String classpath;
344
345 /** the ordered graph containing the JAR class info */
346 private final Graph listGraph;
347
348 /** if true, use defaultToForce for default, otherwise optimize */
349 private boolean forceDefault;
350
351 /** the default preference value to force */
352 private boolean defaultToForce;
353
354 /** the class loader for resolving class names */
355 private ClassLoader loader;
356
357 /** I18N resource bundle */
358 private static ResourceBundle resources;
359
360 /** flag to indicate that initialization of resources has been attempted */
361 private static boolean resinit = false;
362
363 /** union of the entries in all JAR files for -api/-impl existence check */
364 private final HashSet jarEntries;
365
366 /** loaded JAR names, to avoid infinite loops due to circular definitions */
367 private final HashSet jarFiles;
368
369 /** if true, non-public roots are allowed */
370 private boolean keepNonPublicRoots;
371
372 /** if true, load JARs without preferred lists directly into listGraph */
373 private boolean doMerge;
374
375 /**
376 * Get the strings from our resource localization bundle.
377 */
378 private synchronized static String getString(String key, Object v1, Object v2, Object v3) {
379 String fmt = "no text found: \"" + key + "\"";
380 if (!resinit) {
381 try {
382 resinit = true;
383 resources = ResourceBundle.getBundle
384 ("org.apache.river.tool.resources.preflistgen");
385 } catch (MissingResourceException e) {
386 e.printStackTrace();
387 }
388 }
389 if (resources != null) {
390 try {
391 fmt = resources.getString(key);
392 } catch (MissingResourceException e) {
393 }
394 }
395 return MessageFormat.format(fmt, new Object[]{v1, v2, v3});
396 }
397
398 /**
399 * Return the string according to resourceBundle format.
400 */
401 private static String getString(String key) {
402 return getString(key, null, null, null);
403 }
404
405 /**
406 * Return the string according to resourceBundle format.
407 */
408 private static String getString(String key, Object v1) {
409 return getString(key, v1, null, null);
410 }
411
412 /**
413 * Return the string according to resourceBundle format.
414 */
415 private static String getString(String key, Object v1, Object v2) {
416 return getString(key, v1, v2, null);
417 }
418
419 /**
420 * Print out string according to resourceBundle format.
421 */
422 private static void print(String key, Object v1) {
423 System.err.println(getString(key, v1));
424 }
425
426 /**
427 * Print out string according to resourceBundle format.
428 */
429 private static void print(String key, Object v1, Object v2) {
430 System.err.println(getString(key, v1, v2));
431 }
432
433 /**
434 * Print out string according to resourceBundle format.
435 */
436 private static void print(String key, Object v1, Object v2, Object v3) {
437 System.err.println(getString(key, v1, v2, v3));
438 }
439
440 /**
441 * Create a preferred list generator and process the command line arguments.
442 *
443 * @param args the command line arguments
444 */
445 private PreferredListGen(String[] args) {
446 this.FALSE = Boolean.FALSE;
447 this.TRUE = Boolean.TRUE;
448 this.doMerge = true;
449 this.keepNonPublicRoots = false;
450 this.jarFiles = new HashSet();
451 this.jarEntries = new HashSet();
452 this.loader = getClass().getClassLoader();
453 this.defaultToForce = false;
454 this.forceDefault = false;
455 this.forceDefault = false;
456 this.listGraph = new Graph();
457 this.jarList = new ArrayList();
458 this.printResults = false;
459 this.replaceJar = true;
460 this.tells = new HashSet();
461 this.proxies = new HashSet();
462 this.seen = new HashSet();
463 seen.add("int"); // mark primitives as already seen or stack traces fly
464 seen.add("long");
465 seen.add("float");
466 seen.add("double");
467 seen.add("short");
468 seen.add("void");
469 seen.add("char");
470 seen.add("byte");
471 seen.add("boolean");
472 if (args.length == 0) {
473 throw new IllegalArgumentException(getString("preflistgen.noargs"));
474 }
475 for (int i = 0; i < args.length ; i++ ) {
476 String arg = args[i];
477 if (arg.equals("-print")) {
478 setPrint(true);
479 } else if (arg.equals("-noreplace")) {
480 setReplaceJar(false);
481 setPrint(true);
482 } else if (arg.equals("-jar")) {
483 addJar(args[++i]);
484 } else if (arg.equals("-tell")) {
485 addTell(args[++i]);
486 } else if (arg.equals("-impl")) {
487 addImpl(args[++i]);
488 } else if (arg.equals("-api")) {
489 addApi(args[++i]);
490 } else if (arg.equals("-nonpublic")) {
491 setKeepNonPublicRoots(true);
492 } else if (arg.equals("-nomerge")) {
493 setMerge(false);
494 } else if (arg.equals("-default")) {
495 String def = args[++i];
496 if (def.equalsIgnoreCase("true")
497 || def.equalsIgnoreCase("false"))
498 {
499 setDefault(def.equalsIgnoreCase("true"));
500 } else {
501 String msg = getString("preflistgen.baddefault", def);
502 throw new IllegalArgumentException(msg);
503 }
504 } else if (arg.equals("-cp")) {
505 setClasspath(args[++i]);
506 } else if (arg.equals("-proxy")) {
507 addProxy(args[++i]);
508 } else {
509 String msg = getString("preflistgen.badoption", arg);
510 throw new IllegalArgumentException(msg);
511 }
512 }
513 }
514
515 /**
516 * Constructor for programmatic access. The public <code>set</code> and
517 * <code>add</code> methods must be called to supply the argument
518 * values. Then <code>compute</code> and <code>generatePreferredList</code>
519 * must be called to perform the dependency analysis and to generate the
520 * preferred list.
521 */
522 public PreferredListGen() {
523 this.doMerge = true;
524 this.keepNonPublicRoots = false;
525 this.jarFiles = new HashSet();
526 this.jarEntries = new HashSet();
527 this.loader = (ClassLoader) getClass().getClassLoader();
528 this.defaultToForce = false;
529 this.forceDefault = false;
530 this.forceDefault = false;
531 this.listGraph = new Graph();
532 this.jarList = new ArrayList();
533 this.printResults = false;
534 this.replaceJar = true;
535 this.tells = new HashSet();
536 this.proxies = new HashSet();
537 this.FALSE = Boolean.FALSE;
538 this.TRUE = Boolean.TRUE;
539 this.seen = new HashSet();
540 seen.add("int"); // mark primitives as already seen or stack traces fly
541 seen.add("long");
542 seen.add("float");
543 seen.add("double");
544 seen.add("short");
545 seen.add("void");
546 seen.add("char");
547 seen.add("byte");
548 seen.add("boolean");
549 }
550
551 /**
552 * Set the flag controlling whether a preferred list is to be printed.
553 * This flag is ignored if the <code>PrintWriter</code> supplied in
554 * the call to <code>generatePreferredList</code> is non-<code>null</code>.
555 * The default value is <code>false</code>.
556 *
557 * @param printResults if <code>true</code>, print the preferred list
558 */
559 public final synchronized void setPrint(boolean printResults) {
560 this.printResults = printResults;
561 }
562
563 /**
564 * Set the flag controlling whether non-public classes should be retained
565 * in the set of roots used for performing dependency analysis. By default,
566 * non-public classes are discarded.
567 *
568 * @param keepNonPublicRoots if <code>true</code>, non-public root classes
569 * are retained
570 */
571 public final synchronized void setKeepNonPublicRoots(boolean keepNonPublicRoots) {
572 this.keepNonPublicRoots = keepNonPublicRoots;
573 }
574
575 /**
576 * Select the behavior for processing non-target JAR files which do not
577 * contain preferred lists. If <code>doMerge</code> is <code>true</code>, the
578 * classes contained in these JAR files are merged with the target JAR
579 * file for purposes of dependency analysis. The <code>-impl</code> and
580 * <code>-api</code> options may be used to initialize the preferred state
581 * of the merged classes. If <code>doMerge</code> is <code>false</code>,
582 * the classes in non-target JAR files which do not contain preferred lists are
583 * initialized with a preferred state of 'not preferred'. The default behavior
584 * corresponds to calling <code>setMerge(true)</code>.
585 *
586 * @param doMerge if <code>true</code>, perform the merge
587 */
588 public final synchronized void setMerge(boolean doMerge) {
589 this.doMerge = doMerge;
590 }
591
592 /**
593 * Set the flag controlling whether a preferred list is to be placed
594 * in the target JAR file. The default value is <code>true</code>.
595 *
596 * @param replaceJar if <code>true</code>, update the target JAR file
597 */
598 public final synchronized void setReplaceJar(boolean replaceJar) {
599 this.replaceJar = replaceJar;
600 }
601
602 /**
603 * Add <code>jarName</code> to the list of JAR files to process.
604 * The first call identifies the target JAR file. This method must
605 * be called at least once.
606 *
607 * @param jarName the name of the JAR file to add to the set.
608 */
609 public final void addJar(String jarName) {
610 jarList.add(jarName);
611 }
612
613 /**
614 * Add <code>tellName</code> to the tell list. If a class is identified
615 * as not preferred through the dependency analysis, and if that class
616 * name is in the tell list, then the source dependency causing the class to
617 * be included is printed. This is for debugging purposes.
618 *
619 * @param tellName the name of the JAR file to add to the tell set.
620 */
621 public final void addTell(String tellName) {
622 String tellClass = fileToClass(tellName);
623 tells.add(tellClass);
624 }
625
626 /**
627 * Initialize the dependency graph with a private API entry.
628 * <code>implName</code> identifies a class or a JAR entry, package, or
629 * namespace that is to be considered private and therefore preferred. If
630 * <code>implName</code> ends with ".class", it represents a class whose
631 * name is <code>implName</code> without the ".class" suffix and with each
632 * '/' character replaced with a '.'. Otherwise, if <code>implName</code>
633 * ends with "/" or "/*", it represents a directory wildcard matching all
634 * entries in the named directory. Otherwise, if <code>implName</code> ends
635 * with "/-", it represents a namespace wildcard that matches all entries in
636 * the named directory and all of its subdirectories. Otherwise
637 * <code>implName</code> represents a non-class resource in the JAR
638 * file. Alternatively, <code>implName</code> may be expressed directly as a
639 * class name. The most specific <code>implName</code> is used to match an
640 * entry found in the JAR files being analyzed. If <code>implName</code> is
641 * either of the class name forms, then that class is forced to be preferred
642 * and is not included in the public API even it is found by the dependency
643 * analysis.
644 *
645 * @param implName the identifier for the private API entry
646 *
647 * @throws IllegalArgumentException if <code>implName</code> does not match
648 * any of the criteria above.
649 */
650 public final void addImpl(String implName) {
651 listGraph.initialize(implName, true, null); // preferred
652 }
653
654 /**
655 * Initialize the dependency graph with a public API entry.
656 * <code>apiName</code> identifies a class or a JAR entry, package, or
657 * namespace that is to be considered public and therefore <b>not</b>
658 * preferred. If <code>apiName</code> ends with ".class", it represents a
659 * class whose name is <code>apiName</code> without the ".class" suffix and
660 * with each '/' character replaced with a '.'. Otherwise, if
661 * <code>apiName</code> ends with "/" or "/*", it represents a directory
662 * wildcard matching all entries in the named directory. Otherwise, if
663 * <code>apiName</code> ends with "/-", it represents a namespace wildcard
664 * that matches all entries in the named directory and all of its
665 * subdirectories. Otherwise <code>apiName</code> represents a non-class
666 * resource in the JAR file. Alternatively, <code>apiName</code> may be
667 * expressed directly as a class name. The most specific
668 * <code>apiName</code> is used to match an entry found in the JAR files
669 * being analyzed. Any class in the JAR file that matches
670 * <code>apiName</code> will be included in the set of roots for dependency
671 * analysis. This method may be called zero or more times.
672 *
673 * @param apiName the identifier for the public API entry
674 *
675 * @throws IllegalArgumentException if <code>apiName</code> does not match
676 * any of the criteria above.
677 */
678 public final void addApi(String apiName) {
679 listGraph.initialize(apiName, false, null); // not preferred
680 }
681
682 /**
683 * Set the default value to use for the preferred list. If this method
684 * is not called, the default will be chosen which results in a preferred
685 * list with the smallest number of entries. In the event of optimization
686 * ties, a default value of <code>false</code> is used.
687 *
688 * @param def the default value to use for the list
689 */
690 public final synchronized void setDefault(boolean def) {
691 forceDefault = true;
692 defaultToForce = def;
693 }
694
695 /**
696 * Set the classpath of the classes to include in the analysis. It is
697 * not necessary to include the JAR files supplied via calls to the
698 * <code>addJar</code> method.
699 *
700 * @param path the classpath for the classes to include in the analysis
701 */
702 public final synchronized void setClasspath(String path) {
703 this.classpath = path;
704 }
705
706 /**
707 * Add <code>proxy</code> to the set of proxies used to identify
708 * roots. This method may be called zero or more times.
709 *
710 * @param proxy the name of the proxy class
711 */
712 public final void addProxy(String proxy) {
713 proxies.add(proxy);
714 }
715
716
717 /**
718 * Load all of the JAR files named on the command line or referenced in
719 * Class-Path manifest entries of those JAR files.
720 *
721 * @throws IOException if an error occurs reading the contents
722 * of any of the JAR files.
723 */
724 private void loadJars() throws IOException {
725 Iterator it = jarList.iterator();
726 while (it.hasNext()) {
727 String jarName = (String) it.next();
728 File jarFile = new File(jarName);
729 loadJar(jarFile);
730 }
731 }
732
733 /**
734 * Load the given JAR <code>File</code>, adding every class or resource in
735 * the file a graph. The first time this method is called,
736 * <code>listGraph</code> is loaded directly and all non-preferred classes
737 * found in the graph are marked as roots. On subsequent calls, a new
738 * temporary graph is created and initialized based on the preferred list in
739 * the JAR. After entries are loaded into the graph, it is merged into
740 * <code>listGraph</code>. The manifest is also read, and any JARs specified
741 * by the manifest <code>Class-Path</code> attribute are loaded
742 * recursively. If the given JAR file does not exist or is a directory, an
743 * error message is printed.
744 *
745 * @param jar the <code>File</code> representing the JAR file
746 * @throws IOException if an error occured reading the contents
747 * of the JAR or any of the JARs in the Class-Path.
748 * @throws IllegalArgumentException if the JAR file does not exist
749 * or is a directory
750 */
751 private synchronized void loadJar(File jar) throws IOException {
752 if (jarFiles.contains(jar)) {
753 return; // short-circuit circular definitions
754 }
755 jarFiles.add(jar);
756 if (!jar.exists()) {
757 String msg = getString("preflistgen.nojarfound", jar);
758 throw new IllegalArgumentException(msg);
759 }
760 if (jar.isDirectory()) {
761 String msg = getString("preflistgen.jarisdir", jar);
762 throw new IllegalArgumentException(msg);
763 }
764 if (targetJar == null) {
765 targetJar = jar;
766 populateGraph(listGraph, jar);
767 } else if (doMerge && noPreferredList(jar)) {
768 populateGraph(listGraph, jar);
769 } else {
770 Graph g = createGraph(jar); // read pref list to init graph
771 populateGraph(g, jar);
772 listGraph.merge(g);
773 }
774 }
775
776 /**
777 * Return <code>true</code> if the given JAR file does not contain a preferred list.
778 *
779 * @param jar the file to examine
780 * @return true if no preferred list is present
781 * @throws IOException if an error occured reading the contents of the JAR
782 */
783 private boolean noPreferredList(File jar) throws IOException {
784 JarFile jarFile = new JarFile(jar);
785 return jarFile.getJarEntry("META-INF/PREFERRED.LIST") == null;
786 }
787
788 /**
789 * Load the given JAR <code>File</code>, adding every class or resource in
790 * the file to the given graph. The manifest is also read, and any JARs
791 * specified by the manifest <code>Class-Path</code> attribute are loaded
792 * recursively.
793 *
794 * @param g the graph to populate
795 * @param jar the <code>File</code> representing the JAR file
796 */
797 private void populateGraph(Graph g, File jar) {
798 try {
799 JarFile jarFile = new JarFile(jar);
800 Enumeration en = jarFile.entries();
801 while (en.hasMoreElements()) {
802 JarEntry entry = (JarEntry) en.nextElement();
803 String id = entry.getName();
804 if (id.startsWith("META-INF") || id.endsWith("/")) {
805 continue; // ignore directories and anything in meta-inf
806 }
807 jarEntries.add(id);
808 if (id.endsWith(".class")) {
809 g.add(fileToClass(id), Graph.CLASS, jar);
810 } else {
811 g.add(fileToClass(id), Graph.RESOURCE, jar);
812 }
813 }
814 Manifest manifest = jarFile.getManifest();
815 if (manifest == null) {
816 return; // processed a zip file
817 }
818 String classPath =
819 manifest.getMainAttributes().getValue("Class-Path");
820 if (classPath != null) {
821 StringTokenizer tok = new StringTokenizer(classPath);
822 while (tok.hasMoreTokens()) {
823 String fileName = tok.nextToken();
824 File nextJar = new File(jar.getParentFile(), fileName);
825 loadJar(nextJar);
826 }
827 }
828 } catch (IOException e) {
829 e.printStackTrace();
830 }
831 }
832
833 /**
834 * Create a <code>Graph</code> initialized from the preferred list of
835 * the given JAR file.
836 *
837 * @param jarFile the JAR file from which an initialized graph is
838 * to be derived
839 * @return an initialized <code>Graph</code>
840 * @throws IOException if an error occurs reading <code>jarFile</code>
841 */
842 private Graph createGraph(File jarFile) throws IOException {
843
844 final Pattern headerPattern =
845 Pattern.compile("^PreferredResources-Version:\\s*(.*?)$");
846 final Pattern versionPattern =
847 Pattern.compile("^1\\.\\d+$");
848 final Pattern namePattern =
849 Pattern.compile("^Name:\\s*(.*)$");
850 final Pattern preferredPattern =
851 Pattern.compile("^Preferred:\\s*(.*)$");
852
853 /**
854 * Parses the given JAR file's preferred list, if any.
855 */
856 Graph graph = new Graph();
857 JarFile jar = new JarFile(jarFile);
858 JarEntry ent = jar.getJarEntry("META-INF/PREFERRED.LIST");
859 if (ent == null) {
860 graph.setDefaultPref(false);
861 return graph;
862 }
863 BufferedReader r = new BufferedReader(
864 new InputStreamReader(jar.getInputStream(ent), "UTF8"));
865
866 String s = r.readLine();
867 if (s == null) {
868 throw new IOException("missing preferred list header");
869 }
870 s = s.trim();
871 Matcher m = headerPattern.matcher(s);
872 if (!m.matches()) {
873 throw new IOException("illegal preferred list header: " + s);
874 }
875 s = m.group(1);
876 if (!versionPattern.matcher(s).matches()) {
877 throw new IOException(
878 "unsupported preferred list version: " + s);
879 }
880
881 s = nextNonBlankLine(r);
882 if (s == null) {
883 throw new IOException("empty preferred list");
884 }
885 if ((m = preferredPattern.matcher(s)).matches()) {
886 graph.setDefaultPref(Boolean.valueOf(m.group(1)).booleanValue());
887 s = nextNonBlankLine(r);
888 } else {
889 graph.setDefaultPref(false);
890 }
891
892 while (s != null) {
893 if (!(m = namePattern.matcher(s)).matches()) {
894 throw new IOException(
895 "expected preferred entry name: " + s);
896 }
897 String name = m.group(1);
898
899 s = nextNonBlankLine(r);
900 if (s == null) {
901 throw new IOException("EOF before preferred entry");
902 }
903 if (!(m = preferredPattern.matcher(s)).matches()) {
904 throw new IOException("expected preferred entry: " + s);
905 }
906 boolean pref = Boolean.valueOf(m.group(1)).booleanValue();
907 graph.initialize(name, pref, jarFile);
908 s = nextNonBlankLine(r);
909 }
910 return graph;
911 }
912
913 /**
914 * Returns next non-blank, non-comment line, or null if end of file has
915 * been reached.
916 *
917 * @param reader the input stream
918 * @return a <code>String</code> containing the next line,
919 * or <code>null</code>
920 * @throws IOException if an error occurs reading the stream
921 */
922 private static String nextNonBlankLine(BufferedReader reader)
923 throws IOException
924 {
925 String s;
926 while ((s = reader.readLine()) != null) {
927 s = s.trim();
928 if (s.length() > 0 && s.charAt(0) != '#') {
929 return s;
930 }
931 }
932 return null;
933 }
934
935 /**
936 * Convert the given class reference to a class name. If the argument is
937 * already a class name, the value is returned unchanged. If the argument is
938 * a file name, the equivalent class name is returned. Path names are in JAR
939 * format: path separators must be '/' characters and the first character
940 * must not be a path separator.
941 *
942 * @param classID the class identifier, which may be a class file
943 * reference or a class name
944 * @return the associated class name
945 */
946 private String fileToClass(String classID) {
947 classID = classID.replace('/', '.');
948 if (classID.endsWith(".class")) {
949 classID = classID.substring(0, classID.length() - 6);
950 }
951 return classID;
952 }
953
954 /**
955 * Inspect the given class for dependencies. If the class is an
956 * array, it's component type is inspected. Inspection consists
957 * of processing the classes name.
958 *
959 * @param from the dependent of the given class, or null if this
960 * is the top-level class
961 * @param c the class to inspect
962 */
963 private void process(String from, Class c) {
964 while (c.isArray())
965 c = c.getComponentType();
966 process(from, c.getName());
967 }
968
969 /**
970 * Inspect the given array of classes for dependencies.
971 *
972 * @param from the dependent of the given array, or null if this
973 * is the top-level class
974 * @param arr the array of classes to inspect
975 */
976 private void process(String from, Class[] arr) {
977 for (int i = arr.length; --i >= 0; )
978 process(from, arr[i]);
979 }
980
981 /**
982 * Determine whether the given modifier imply inclusion in the public api.
983 * Return <code>true</code> if the modifiers include the public or protected
984 * flags.
985 *
986 * @param modifiers the modifier value
987 * @return true if a public api member is implied
988 */
989 private boolean include(int modifiers) {
990 return (modifiers & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0;
991 }
992
993 /**
994 * Inspect the given constructor for dependencies. Any parameter types
995 * and exception types declared for the constructor are inspected.
996 *
997 * @param from the dependent of the constructor, or null if this
998 * is the top-level class
999 * @param c the constructor to inspect
1000 */
1001 private void process(String from, Constructor c) {
1002 if (!include(c.getModifiers()))
1003 return;
1004 process(from, c.getParameterTypes());
1005 process(from, c.getExceptionTypes());
1006 }
1007
1008 /**
1009 * Inspect the given method for dependencies. Any parameter types,
1010 * exception types, and the return type declared for the method
1011 * are inspected.
1012 *
1013 * @param from the dependent of the method, or null if this
1014 * is the top-level class
1015 * @param m the method to inspect
1016 */
1017 private void process(String from, Method m) {
1018 if (!include(m.getModifiers()))
1019 return;
1020 process(from, m.getReturnType());
1021 process(from, m.getParameterTypes());
1022 process(from, m.getExceptionTypes());
1023 }
1024
1025 /**
1026 * Inspect the given field for dependencies. The field type is inspected.
1027 *
1028 * @param from the dependent of the field, or null if this
1029 * is the top-level class
1030 * @param f the field to inspect
1031 */
1032 private void process(String from, Field f) {
1033 if (!include(f.getModifiers()))
1034 return;
1035 process(from, f.getType());
1036 }
1037
1038 /**
1039 * Mark the given class in the <code>Graph</code> as 'not preferred'
1040 * unless overridden by the command line <code>-impl</code> argument.
1041 * If so marked, then the public and private methods, fields,
1042 * constructors, interfaces, and superclasses declared by the class are
1043 * also inspected recursively. Only classes classes contained within
1044 * <code>listGraph</code> are processed.
1045 *
1046 * @param from the class referencing the given class, or null if this
1047 * is the top-level class
1048 * @param clazz the class to add to the public api
1049 */
1050 private void process(String from, String clazz) {
1051 if (!seen.contains(clazz)) {
1052 seen.add(clazz);
1053 if ( ! listGraph.contains(clazz)) {
1054 return;
1055 }
1056 for (Iterator it = tells.iterator(); it.hasNext();) {
1057 if (clazz.equals((String)it.next())) {
1058 if (from != null) {
1059 System.err.println(clazz + " caused by " + from);
1060 } else {
1061 System.err.println(clazz + " is a root class");
1062 }
1063 }
1064 }
1065 if (!listGraph.setPreferred(clazz, false, false)) {
1066 return;
1067 }
1068 try {
1069 inspectClass(clazz);
1070 } catch (NoClassDefFoundError e){
1071 System.err.println(e.toString());
1072 }
1073 }
1074 }
1075
1076 /**
1077 * Process all public and protected fields, methods, constructors,
1078 * interfaces, and superclasses declared by the given class.
1079 *
1080 * @param className the name of the class to process
1081 */
1082 private void inspectClass(String className) {
1083 Class c;
1084 try {
1085 c = Class.forName(className, false, loader);
1086 } catch (Exception e) {
1087 e.printStackTrace();
1088 return;
1089 }
1090 Class sup = c.getSuperclass();
1091 if (sup != null)
1092 process(className, sup);
1093 Class[] ifaces = c.getInterfaces();
1094 for (int i = ifaces.length; --i >= 0; )
1095 process(className, ifaces[i]);
1096 Constructor[] cons = c.getDeclaredConstructors();
1097 for (int i = cons.length; --i >= 0; ) {
1098 process(className, cons[i]);
1099 }
1100 Method[] methods = c.getDeclaredMethods();
1101 for (int i = methods.length; --i >= 0; ) {
1102 process(className, methods[i]);
1103 }
1104 Field[] fields = c.getDeclaredFields();
1105 for (int i = fields.length; --i >= 0; ) {
1106 process(className, fields[i]);
1107 }
1108 }
1109
1110 /**
1111 * Load JAR files, initialize the dependency graph, and perform the
1112 * dependency analysis.
1113 *
1114 * @throws IOException if an error occurs constructing the class loader
1115 * or reading any of the JAR files.
1116 * @throws IllegalArgumentException in the following cases:
1117 * <ul>
1118 * <li>if <code>addJar</code> was never
1119 * called or <code>addJar</code> was called with a file which
1120 * does not exist or is a directory
1121 * <li>if any proxies supplied via the <code>addProxy</code>
1122 * method could not be found
1123 * <li>if any component in the classpath does not exist
1124 * </ul>
1125 */
1126 public synchronized void compute() throws IOException {
1127 if (jarList.isEmpty()) {
1128 throw new IllegalArgumentException(getString("preflistgen.nojars"));
1129 }
1130 ArrayList list = new ArrayList();
1131 if (classpath != null) {
1132 StringTokenizer st = new StringTokenizer(classpath,
1133 File.pathSeparator);
1134 while (st.hasMoreTokens()) {
1135 String fileName = st.nextToken();
1136 File cpFile = new File(fileName);
1137 if (!cpFile.exists()) {
1138 String msg = getString("preflistgen.badcp", fileName);
1139 throw new IllegalArgumentException(msg);
1140 }
1141 list.add(cpFile.getCanonicalFile().toURI().toURL());
1142 }
1143 }
1144 for (Iterator it = jarList.iterator(); it.hasNext(); ) {
1145 String fileName = (String) it.next();
1146 list.add(new File(fileName).getCanonicalFile().toURI().toURL());
1147 }
1148 if (list.size() > 0) {
1149 URL[] urls = (URL[]) list.toArray(new URL[list.size()]);
1150 ClassLoader cl = ClassLoader.getSystemClassLoader();
1151 if (cl != null) {
1152 cl = cl.getParent(); // the extension classloader
1153 }
1154 loader = new URLClassLoader(urls, cl);
1155 }
1156 loadJars();
1157 Collection roots = getRoots();
1158 for (Iterator it = roots.iterator(); it.hasNext(); ) {
1159 String clazz = (String) it.next();
1160 Class c = null;
1161 try {
1162 c = Class.forName(clazz, false, loader);
1163 } catch (ClassNotFoundException e) {
1164 throw new IllegalStateException("could not load root class " + clazz);
1165 }
1166 if ((c.getModifiers() & Modifier.PUBLIC) == 0) {
1167 if (keepNonPublicRoots) {
1168 print("preflistgen.rootNotPublic", c);
1169 } else {
1170 if (!listGraph.isFrozen(clazz)) {
1171 //remove non public root and force preferred
1172 it.remove();
1173 listGraph.setPreferred(clazz, true, true);
1174 continue;
1175 }
1176 }
1177 }
1178 process(null, clazz);
1179 }
1180 }
1181
1182 /**
1183 * Generate the <code>Collection</code> of roots to use for the dependency
1184 * analysis. All classes from the first JAR with a preferred state of false
1185 * (which will only be true due to the -api option, or because the same
1186 * class in another JAR is marked not-preferred) are added to the set;
1187 * Also, all interfaces of classes in the set of <code>-proxy</code>
1188 * arguments are added to the set of roots.
1189 *
1190 * @return the <code>Collection</code> of roots
1191 * @throws IllegalArgumentException if any of the proxies supplied via
1192 * the <code>addProxy</code> method could not be found.
1193 */
1194 private Collection getRoots() {
1195 HashSet roots = new HashSet();
1196 listGraph.addRoots(roots);
1197 Iterator proxyIt = proxies.iterator();
1198 while (proxyIt.hasNext()) {
1199 String proxy = (String) proxyIt.next();
1200 try {
1201 Class proxyClass = Class.forName(proxy, false, loader);
1202 addProxyRoot(proxyClass, roots);
1203 } catch (ClassNotFoundException e) {
1204 String msg = getString("preflistgen.badproxyclass", proxy);
1205 throw new IllegalArgumentException(msg, e);
1206 }
1207 }
1208 return roots;
1209 }
1210
1211 /**
1212 * Add to the given set of roots the public interfaces implemented by
1213 * the <code>proxyClass</code> and all of its superclasses.
1214 * If any non-public interfaces are implemented, then any parent
1215 * interfaces which are public are added.
1216 *
1217 * @param proxyClass the proxy to inspect
1218 * @param roots the set of root class names
1219 */
1220 private void addProxyRoot(Class proxyClass, Collection roots) {
1221 Class[] interfaces = proxyClass.getInterfaces();
1222 for (int j = 0; j < interfaces.length; j++) {
1223 addIfPublic(interfaces[j], roots);
1224 }
1225 Class superClass = proxyClass.getSuperclass();
1226 if (superClass != null) {
1227 addProxyRoot(superClass, roots);
1228 }
1229 }
1230
1231 /**
1232 * Add to the given set of roots the interface named <code>intFace</code> if
1233 * that interface is public. Otherwise, add any superinterfaces which are
1234 * public.
1235 *
1236 * @param intFace the interface to add
1237 * @param roots the set of roots
1238 */
1239 private void addIfPublic(Class intFace, Collection roots) {
1240 if ((intFace.getModifiers() & Modifier.PUBLIC) != 0) {
1241 roots.add(intFace.getName());
1242 return;
1243 }
1244 Class[] parents = intFace.getInterfaces();
1245 int l = parents.length;
1246 for (int i = 0; i < l; i++) {
1247 addIfPublic(parents[i], roots);
1248 }
1249 }
1250
1251 /**
1252 * Print the usage message.
1253 */
1254 private static void usage() {
1255 print("preflistgen.usage", null);
1256 }
1257
1258 /**
1259 * Generate the preferred list from the dependency graph. If a default value
1260 * was specified, the optimal list using that default value is
1261 * generated. Otherwise, the optimal list among the two possibilities
1262 * (<code>false/true</code> in order of precedence for 'optimization ties')
1263 * is generated. An explicit default entry is generated only for the
1264 * default <code>true</code> case.
1265 * <p>
1266 * The preferred list is sorted such that more specific
1267 * definitions precede less specific definitions; ties are broken with
1268 * an alphabetic secondary sort.
1269 * <p>
1270 * The preferred list will be placed in the target JAR file unless
1271 * <code>setReplaceJar(false)</code> was called. The preferred list will be
1272 * written to <code>writer</code> if it is non-<code>null</code>. If
1273 * <code>writer</code> is <code>null</code> and <code>setPrint(true)</code>
1274 * was called, the preferred list will be written to
1275 * <code>System.out</code>.
1276 *
1277 * @param writer the <code>PrintWriter</code> to write the preferred list
1278 * to.
1279 *
1280 * @throws IOException if an error occurs updating the target JAR file.
1281 */
1282 public synchronized void generatePreferredList(PrintWriter writer) throws IOException {
1283 if (writer == null && printResults) {
1284 writer = new PrintWriter(System.out);
1285 }
1286 String newLine = System.getProperty("line.separator");
1287 if (!tells.isEmpty()) {
1288 return;
1289 }
1290 StringBuilder sb = new StringBuilder();
1291 sb.append("PreferredResources-Version: 1.0");
1292 sb.append(newLine);
1293 Collection entries = new TreeSet();
1294 boolean pref;
1295 listGraph.markStaleInnerClasses(); // throw away inners matching outers
1296 listGraph.deleteStaleNodes(); // discard all stale nodes
1297 if (forceDefault) {
1298 pref = defaultToForce;
1299 listGraph.setDefaultPref(pref);
1300 listGraph.addEntries(entries); // create sorted list of entries
1301 } else {
1302 // create list using optimal default
1303 HashSet bestEntries = new HashSet();
1304 // first create entries for default=false
1305 pref = false;
1306 listGraph.setDefaultPref(pref);
1307 listGraph.addEntries(bestEntries);
1308 // create entries for default=true and remember the best result
1309 HashSet test = new HashSet();
1310 listGraph.setDefaultPref(true);
1311 listGraph.addEntries(test);
1312 if (test.size() < bestEntries.size()) {
1313 bestEntries = test;
1314 pref = true;
1315 }
1316 entries = new TreeSet(bestEntries); // do the sort
1317 }
1318 if (pref || forceDefault || entries.isEmpty()) {
1319 sb.append(newLine);
1320 sb.append("Preferred: ");
1321 sb.append(pref);
1322 sb.append(newLine);
1323 }
1324 // write the individual entries
1325 Iterator it = entries.iterator();
1326 while (it.hasNext()) {
1327 PrefData entry = (PrefData) it.next();
1328 sb.append(newLine);
1329 sb.append("Name: ");
1330 sb.append(entry.name);
1331 sb.append(newLine);
1332 sb.append("Preferred: ");
1333 sb.append(entry.preferred);
1334 sb.append(newLine);
1335 }
1336 // output the resulting list or JAR file or both
1337 String preferredList = sb.toString();
1338 if (writer != null) {
1339 writer.print(preferredList);
1340 writer.flush();
1341 }
1342 if (replaceJar) {
1343 buildJarFile(preferredList);
1344 }
1345 }
1346
1347 /**
1348 * Replace the first JAR file in the set of JARs with a new copy containing
1349 * the given preferred list. If the JAR contained a preferred list, it is
1350 * replaced.
1351 *
1352 * @param preferredList the new preferred list
1353 * @throws IOException if an error occurs updating the target JAR file.
1354 */
1355 private void buildJarFile(String preferredList) throws IOException {
1356 // prepare to create a temp JAR file from the original
1357 File newJar = File.createTempFile("pref", "jar");
1358 newJar.deleteOnExit();
1359 JarInputStream ji = new JarInputStream(new FileInputStream(targetJar));
1360 JarOutputStream jo =
1361 new JarOutputStream(new FileOutputStream(newJar));
1362 JarEntry entry;
1363 byte[] ba;
1364
1365 // write the META-INF directory entry, probably not really necessary
1366 entry = new JarEntry("META-INF/");
1367 jo.putNextEntry(entry);
1368 jo.closeEntry();
1369
1370 // copy the original manifest entry to the new file
1371 entry = new JarEntry("META-INF/MANIFEST.MF");
1372 jo.putNextEntry(entry);
1373 ji.getManifest().write(jo);
1374 jo.closeEntry();
1375
1376 // write the new preferred list entry to the new file
1377 entry = new JarEntry("META-INF/PREFERRED.LIST");
1378 jo.putNextEntry(entry);
1379 ba = preferredList.getBytes();
1380 jo.write(ba, 0, ba.length);
1381
1382 // copy the remaining entries, discarding the old preferred list
1383 while ((entry = ji.getNextJarEntry()) != null) {
1384 if (entry.getName().equals("META-INF/PREFERRED.LIST")) {
1385 ji.closeEntry();
1386 continue;
1387 }
1388 jo.putNextEntry(entry);
1389 ba = new byte[1000];
1390 int size;
1391 while ((size = ji.read(ba, 0, 1000)) >= 0) {
1392 jo.write(ba, 0, size);
1393 }
1394 jo.closeEntry();
1395 }
1396 ji.close();
1397 jo.close();
1398
1399 // copy the new JAR file over the old one
1400 FileInputStream fi = new FileInputStream(newJar);
1401 FileOutputStream fo = new FileOutputStream(targetJar);
1402 ba = new byte[1000];
1403 int size;
1404 while ((size = fi.read(ba)) >= 0) {
1405 fo.write(ba, 0, size);
1406 }
1407 fi.close();
1408 fo.close();
1409 }
1410
1411 /**
1412 * The command line interface to the tool. Parses the command line arguments,
1413 * computes the dependency graph, and generates the preferred list.
1414 *
1415 * @param args the command line arguments
1416 */
1417 public static void main(String[] args) {
1418 try {
1419 PreferredListGen dep = new PreferredListGen(args);
1420 dep.compute();
1421 dep.generatePreferredList(null);
1422 System.exit(0);
1423 } catch (IllegalArgumentException e) {
1424 print("preflistgen.badarg", e.getMessage());
1425 usage();
1426 } catch (IOException e) {
1427 print("preflistgen.ioproblem", e.getMessage());
1428 e.printStackTrace();
1429 } catch (Error e){
1430 print("preflistgen.error", e.getMessage());
1431 e.printStackTrace();
1432 }
1433 System.exit(1);
1434 }
1435
1436 /**
1437 * A representation of a class path, package path wildcard, or namespace
1438 * path wildcard preferred list entry. This class implements
1439 * <code>Comparable</code> to provide a primary sort key for the
1440 * ordering of class/path/namespace, and a secondary sort key which
1441 * is alphabetic.
1442 */
1443 private class PrefData implements Comparable {
1444
1445 /** the preferred list entry name string */
1446 final String name;
1447
1448 /** the preferred value for this entry */
1449 final boolean preferred;
1450
1451 /** the sourceJar, used when merging two graphs */
1452 final File sourceJar;
1453
1454 PrefData(String name, boolean preferred) {
1455 this(name, preferred , null);
1456 }
1457
1458 PrefData(String name, boolean preferred, File sourceJar) {
1459 this.name = name;
1460 this.preferred = preferred;
1461 this.sourceJar = sourceJar;
1462 }
1463
1464 public int compareTo(Object o) {
1465 String oPrefixed = ((PrefData) o).prefixedString();
1466 return prefixedString().compareTo(oPrefixed);
1467 }
1468
1469 private String prefixedString() {
1470 if (name.endsWith(".class")) {
1471 return "a" + name;
1472 } else if (name.endsWith("/*")) {
1473 return "b" + name;
1474 } else {
1475 return "c" + name;
1476 }
1477 }
1478 }
1479
1480 /**
1481 * A representation of the graph of classes contained in the set of
1482 * JARs being analyzed. This class implements the search algorithm to
1483 * find the optimal (smallest) set of preferred list entries which
1484 * describes the preferred state of entries in the graph. Nodes in
1485 * the graph can be one of seven types:
1486 * <ul>
1487 * <li><code>PKG</code> nodes represent package wildcards
1488 * <li><code>NAMESPACE</code> nodes represent namespace wildcards
1489 * <li><code>DEFAULT</code> nodes represent the default preference. Only
1490 * the root node of the graph can be of this type.
1491 * <li><code>INHERIT</code> nodes inherit their preference values from
1492 * their parent nodes. For the case where there is no default
1493 * preferred value, the root of the graph will be of this type.
1494 * <li> <code>CLASS</code> leaf node representing a class
1495 * <li> <code>RESOURCE</code> leaf node representing a non-class resource
1496 * <li> <code>STALE</code> an entry which has been marked for deletion
1497 * </ul>
1498 * All non-terminal nodes represent a component in the package name
1499 * of a class. All leaf nodes represent classes or resources.
1500 */
1501 private class Graph {
1502
1503 /** type value when non-leaf node inherits preference from its parent */
1504 static final int INHERIT = 0;
1505
1506 /** type value for package wildcard nodes */
1507 static final int PKG = 1;
1508
1509 /** type value for namespace wildcard nodes */
1510 static final int NAMESPACE = 2;
1511
1512 /** type value for the default preference (root) node */
1513 static final int DEFAULT = 3;
1514
1515 /** type value for a class node */
1516 static final int CLASS = 4;
1517
1518 /** type value for a resource (non-class JAR entry) node */
1519 static final int RESOURCE = 5;
1520
1521 /** type value for a stale node (marked for deletion) */
1522 static final int STALE = 6;
1523
1524 /** the name of this node (e.g. com or sun, not com.sun) */
1525 String name;
1526
1527 /** the set of child nodes of this node */
1528 HashSet nodes;
1529
1530 /** the preferred state, only meaningful to leaf (class) nodes */
1531 boolean preferred;
1532
1533 /** the parent of this node, or null for the root node */
1534 Graph parent;
1535
1536 /** the type of the node */
1537 int type;
1538
1539 /** the preferred value implied for child nodes */
1540 Boolean impliedPref = null;
1541
1542 /** the source JAR causing creation of this node, or null */
1543 File sourceJar;
1544
1545 /**
1546 * Create the root node of the graph, setting implied pref to TRUE
1547 */
1548 Graph() {
1549 this.type = INHERIT;
1550 this.sourceJar = null;
1551 this.parent = null;
1552 this.preferred = true;
1553 this.nodes = new HashSet();
1554 name = "";
1555 impliedPref = TRUE;
1556 type = DEFAULT;
1557 }
1558
1559 /**
1560 * Create a node of the graph having the given name and parent.
1561 * If the name contains multiple '.' separated tokens, the name
1562 * is taken from the first token, and another node is constructed
1563 * using the remainder of the name and this node as the parent.
1564 * The final (leaf) node created is given a preferred value inherited
1565 * from its ancestors.
1566 *
1567 * @param parent the parent of this node, or <code>null</code> if
1568 * this is the root node.
1569 * @param name the (possibly dot-separated) name for this node (and
1570 * child nodes).
1571 * @param type the node type, must be CLASS or RESOURCE
1572 * @param sourceJar source of this node definition, or null defined
1573 * through the -api or -impl options
1574 */
1575 Graph(Graph parent, String name, int type, File sourceJar) {
1576 this.type = INHERIT;
1577 this.nodes = new HashSet();
1578 if (type != CLASS && type != RESOURCE) {
1579 throw new IllegalStateException("type must be CLASS or "
1580 + "RESOURCE");
1581 }
1582 this.parent = parent;
1583 this.sourceJar = sourceJar;
1584 int firstDot = name.indexOf(".");
1585 if (firstDot < 0) {
1586 this.name = name;
1587 this.type = type;
1588 preferred = parent.childPref();
1589 if (type == CLASS) {
1590 Graph outer = getOuter();
1591 /* if outer == null, then either this node does not represent
1592 * a nested class, or it represents a nested class who's outer
1593 * class hasn't been loaded yet. In either case, inheriting the
1594 * parents preference is the right thing to do because when the
1595 * outer class get loaded later it will also inherit the parents
1596 * preference value.
1597 */
1598 if (outer != null) {
1599 preferred = outer.preferred;
1600 }
1601 }
1602 } else {
1603 this.name = name.substring(0, firstDot);
1604 nodes.add(new Graph(this,
1605 name.substring(firstDot + 1),
1606 type,
1607 sourceJar));
1608 }
1609 }
1610
1611 /**
1612 * Create a node of the graph having the given name, parent, type and
1613 * preferred value. If the name contains multiple '.' separated tokens,
1614 * the name is taken from the first token, and another node is
1615 * constructed using the remainder of the name and this node as the
1616 * parent. The type and preferred value are only set on the last node
1617 * created in the chain.
1618 *
1619 * @param parent the parent of this node, or <code>null</code> if
1620 * this is the root node.
1621 * @param name the (possibly dot-separated) name for this node (and
1622 * child nodes).
1623 * @param type the node type, must not be INHERIT
1624 * @param preferred the preferred state of this node
1625 * @param sourceJar source of this node definition, or null defined
1626 * through the -api or -impl options
1627 *
1628 * @throws IllegalStateException if type is INHERIT
1629 */
1630 Graph(Graph parent,
1631 String name,
1632 int type,
1633 boolean preferred,
1634 File sourceJar)
1635 {
1636 this.type = INHERIT;
1637 this.preferred = true;
1638 this.nodes = new HashSet();
1639 if (type == INHERIT) {
1640 throw new IllegalStateException("Cannot create a preference "
1641 + "node of type INHERIT");
1642 }
1643 this.parent = parent;
1644 this.sourceJar = sourceJar;
1645 int firstDot = name.indexOf(".");
1646 if (firstDot < 0) {
1647 this.name = name;
1648 this.type = type;
1649 if (type == CLASS || type == RESOURCE) {
1650 this.preferred = preferred;
1651 } else {
1652 impliedPref = Boolean.valueOf(preferred);
1653 }
1654 } else {
1655 this.name = name.substring(0, firstDot);
1656 String childName = name.substring(firstDot + 1);
1657 nodes.add(new Graph(this,
1658 childName,
1659 type,
1660 preferred,
1661 sourceJar));
1662 }
1663 }
1664
1665 /**
1666 * Determines whether <code>arg</code> represents a path reference to a
1667 * class, a resource, a package wildcard, or a namespace wild card, and
1668 * add the value, converted to a '.' separated name, to the graph with
1669 * the given preferred value and appropriate type. If <code>arg</code>
1670 * contains at least one '.' character and no '/' characters, assume it
1671 * is a class name and add the value to the graph with the given
1672 * preferred value and a type of <code>Graph.CLASS</code>.
1673 *
1674 * @param arg the name of the component to add to the graph
1675 * @param preferred the preferred value of the component
1676 * @throws IllegalArgumentException if <code>arg</code> is not
1677 * a recognized form.
1678 */
1679 void initialize(String arg, boolean preferred, File sourceJar) {
1680 if (name.length() > 0) {
1681 throw new IllegalStateException("must initialize "
1682 + "only the root");
1683 }
1684 if (arg.endsWith("/-")) {
1685 String pkgName = fileToClass(arg.substring(0, arg.length() - 2));
1686 addWithPreference(pkgName, NAMESPACE, preferred, sourceJar);
1687 } else if (arg.endsWith("/*")) {
1688 String pkgName =
1689 fileToClass(arg.substring(0, arg.length() - 2));
1690 addWithPreference(pkgName, PKG, preferred, sourceJar);
1691 } else if (arg.endsWith("/")) {
1692 String pkgName =
1693 fileToClass(arg.substring(0, arg.length() - 1));
1694 addWithPreference(pkgName, PKG, preferred, sourceJar);
1695 } else if (arg.endsWith(".class")) { // class file reference
1696 String leafName = fileToClass(arg);
1697 addWithPreference(leafName, CLASS, preferred, sourceJar);
1698 } else if (arg.indexOf('/') != -1) { // resource file reference
1699 String leafName = fileToClass(arg);
1700 addWithPreference(leafName, RESOURCE, preferred, sourceJar);
1701 } else if (arg.indexOf('.') != -1) { // class name reference
1702 addWithPreference(arg, Graph.CLASS, preferred, sourceJar);
1703 } else {
1704 String msg = getString("preflistgen.badapi", arg);
1705 throw new IllegalArgumentException(msg);
1706 }
1707 }
1708
1709 /**
1710 * Add roots from this subtree of the graph to the given
1711 * collection of roots. If this is a CLASS node and this node
1712 * has a preferred value of <code>false</code> and was defined
1713 * by a command line option or was supplied by the first JAR file,
1714 * add this node to the collection.
1715 *
1716 * @param roots the collection to add to
1717 */
1718 synchronized void addRoots(Collection roots) {
1719 if (type == CLASS && sourceJar == null) {
1720 String entryName = getFullName() + ".class";
1721 if (!jarEntries.contains(entryName)) {
1722 System.err.println("Class entry " + entryName
1723 + " identified by the -"
1724 + (preferred ? "impl" : "api")
1725 + " option does not exist in any JAR file. "
1726 + "That entry has been discarded");
1727 return;
1728 }
1729 }
1730 if (type == CLASS
1731 && !preferred
1732 && (sourceJar == null || sourceJar == targetJar))
1733 {
1734 roots.add(fileToClass(getFullName()));
1735 } else {
1736 for (Iterator it = nodes.iterator();it.hasNext(); ) {
1737 Graph g = (Graph) it.next();
1738 g.addRoots(roots);
1739 }
1740 }
1741 }
1742
1743 /**
1744 * For non-leaf nodes,
1745 * Reset the type and implied preference value for this node
1746 * and all child nodes recursively. If this is a leaf node,
1747 * do nothing.
1748 */
1749 synchronized void reset() {
1750 if (type != CLASS && type != RESOURCE) {
1751 type = INHERIT;
1752 impliedPref = null;
1753 Iterator it = nodes.iterator();
1754 while (it.hasNext()) {
1755 Graph g = (Graph) it.next();
1756 g.reset();
1757 }
1758 }
1759 }
1760
1761 /**
1762 * Return the implied preference as determined by the nearest
1763 * parent namespace wildcard, or the default preference if
1764 * there are no parent namespace wildcards. This method should
1765 * only be called indirectly as a side-effect of calling
1766 * <code>childPref</code>.
1767 *
1768 * @return the implied preferred value
1769 */
1770 synchronized boolean impliedChildPref() {
1771 if (type == NAMESPACE || type == DEFAULT) {
1772 if (!(impliedPref instanceof Boolean)) {
1773 throw new IllegalStateException("impliedPref is null");
1774 }
1775 return impliedPref.booleanValue();
1776 }
1777 if (parent == null) {
1778 throw new IllegalStateException("default undefined in graph");
1779 }
1780 return parent.impliedChildPref();
1781 }
1782
1783 /**
1784 * Return the implied preference of the parent node if the parent is a
1785 * package wildcard. Otherwise, return the implied preference as
1786 * determined by the nearest parent namespace wildcard, or the default
1787 * preference if there are no parent namespace wildcards.
1788 *
1789 * @return the implied preferred value
1790 */
1791 synchronized boolean childPref() {
1792 if (type == NAMESPACE || type == DEFAULT || type == PKG) {
1793 if (impliedPref == null) {
1794 throw new IllegalStateException("impliedPref is null");
1795 }
1796 return impliedPref.booleanValue();
1797 }
1798 if (parent == null) {
1799 throw new IllegalStateException("default undefined in graph");
1800 }
1801 return parent.impliedChildPref();
1802 }
1803
1804 /**
1805 * Set the default preferred value for the graph. This method
1806 * may only be called on the root node of the graph.
1807 *
1808 * @param value the default preferred value, which may be
1809 * <code>null</code> to indicate that no default is defined
1810 */
1811 synchronized void setDefaultPref(boolean value) {
1812 if (name.length() > 0) {
1813 throw new IllegalStateException("must set default on root");
1814 }
1815 type = DEFAULT;
1816 impliedPref = Boolean.valueOf(value);
1817 }
1818
1819 // inherit javadoc
1820 public String toString() {
1821 return "Graph[" + name + ", Parent = " + parent + "]";
1822 }
1823
1824 /**
1825 * Add a child node to this node having the given name. the name
1826 * may have multiple '.' separated components, in which case a
1827 * hierarchy of child nodes will be added. Only child nodes which
1828 * do not already exist are created. The preferred value for
1829 * the child will be set based on the value inherited from its
1830 * ancestors. Nodes added by this method must always be leaf nodes.
1831 *
1832 * @param name the name of the child node to add
1833 * @param type the node type, must be CLASS or RESOURCE
1834 * @param sourceJar the JAR file causing this class or resource to be added
1835 */
1836 synchronized void add(String name, int type, File sourceJar) {
1837 if (type != CLASS && type != RESOURCE) {
1838 throw new IllegalStateException("type must be CLASS or "
1839 + "RESOURCE");
1840 }
1841 if (name == null) { // added previously - noop
1842 return;
1843 }
1844 String nodeName;
1845 String childName = null;
1846 int firstDot = name.indexOf(".");
1847 if (firstDot < 0) {
1848 nodeName = name;
1849 } else {
1850 nodeName = name.substring(0, firstDot);
1851 childName = name.substring(firstDot + 1);
1852 }
1853 Iterator it = nodes.iterator();
1854 // search for a match to an existing child and recursively add
1855 while (it.hasNext()) {
1856 Graph g = (Graph) it.next();
1857 if (g.name.equals(nodeName)) {
1858 g.add(childName, type, sourceJar); // noop if null
1859 return;
1860 }
1861 }
1862 // if here, name doesn't exist in collection of child nodes
1863 nodes.add(new Graph(this, name, type, sourceJar));
1864 }
1865
1866 /**
1867 * Return <code>true</code> if the graph contains a node having
1868 * the given name.
1869 *
1870 * @param name the name of the node to test for
1871 * @return true if the node is found
1872 */
1873 synchronized boolean contains(String name) {
1874 String nodeName;
1875 String childName = null;
1876 int firstDot = name.indexOf(".");
1877 if (firstDot < 0) {
1878 nodeName = name;
1879 } else {
1880 nodeName = name.substring(0, firstDot);
1881 childName = name.substring(firstDot + 1);
1882 }
1883 Iterator it = nodes.iterator();
1884 // search for a match to an existing child and recursively add
1885 while (it.hasNext()) {
1886 Graph g = (Graph) it.next();
1887 if (g.name.equals(nodeName)) {
1888 if (childName == null) {
1889 return true;
1890 } else {
1891 return g.contains(childName);
1892 }
1893 }
1894 }
1895 // if here, name doesn't exist in collection of child nodes
1896 return false;
1897 }
1898
1899 /**
1900 * Return the frozen state of the class node having the
1901 * given name. A node is frozen if it was created through
1902 * the <code>-api</code> or <code>-impl</code> options.
1903 *
1904 * @param name the dot-separate name
1905 * @return true if the node is frozen
1906 */
1907 synchronized boolean isFrozen(String name) {
1908 String nodeName;
1909 String childName = null;
1910 int firstDot = name.indexOf(".");
1911 if (firstDot < 0) {
1912 nodeName = name;
1913 } else {
1914 nodeName = name.substring(0, firstDot);
1915 childName = name.substring(firstDot + 1);
1916 }
1917 Iterator it = nodes.iterator();
1918 while (it.hasNext()) {
1919 Graph g = (Graph) it.next();
1920 if (g.name.equals(nodeName)) {
1921 if (childName == null) {
1922 if (g.type != CLASS) {
1923 throw new IllegalStateException("Not a class: "
1924 + g.getFullName());
1925 }
1926 return g.sourceJar == null; // set by -api/-impl
1927 } else {
1928 return g.isFrozen(childName);
1929 }
1930 }
1931 }
1932 // if here, name doesn't exist in collection of child nodes
1933 return false;
1934 }
1935
1936
1937
1938 /**
1939 * Add a child node to this node having the given name. the name may
1940 * have multiple '.' separated components, in which case a hierarchy of
1941 * child nodes will be added. Only child nodes which do not already
1942 * exist are created. The final node created will be assigned the given
1943 * type and preferred value. If the node already exists, new type and
1944 * preferred values will be assigned unless the original source JAR is
1945 * null, indicating node was created via the -api or -impl options. The
1946 * source JAR is not changed from it's original value. If the node
1947 * already exists, the original source JAR is non-null and is also not
1948 * the first JAR, this is a leaf node, and preferred values differ, a
1949 * warning message is written to System.err to identify the conflicting
1950 * definitions; in this case, the state of the node is set to
1951 * not-preferred. The first JAR is ignored in this case because the
1952 * correct value for the preferred state of these classes is not known
1953 * until the dependency analysis is done. Therefore in this special case
1954 * the preferred state is reset to the new state.
1955 *
1956 * @param name the name of the child node to add
1957 */
1958 synchronized void addWithPreference(String name,
1959 int type,
1960 boolean preferred,
1961 File sourceJar)
1962 {
1963 if (type == INHERIT) {
1964 throw new IllegalStateException("Cannot add preference node "
1965 + "of type INHERIT");
1966 }
1967 // allow type to be redefined - should probably do an analysis
1968 if (name == null) {
1969 if (this.sourceJar != null) {
1970 this.type = type;
1971 if (type == CLASS || type == RESOURCE) {
1972 if(this.preferred != preferred
1973 && this.sourceJar != targetJar)
1974 {
1975 print("preflistgen.prefconflict",
1976 getFullName(),
1977 this.sourceJar,
1978 sourceJar);
1979 this.preferred = false;
1980 } else {
1981 this.preferred = preferred;
1982 }
1983 } else {
1984 impliedPref = Boolean.valueOf(preferred);
1985 }
1986 }
1987 return;
1988 }
1989 String nodeName;
1990 String childName = null;
1991 int firstDot = name.indexOf(".");
1992 if (firstDot < 0) {
1993 nodeName = name;
1994 } else {
1995 nodeName = name.substring(0, firstDot);
1996 childName = name.substring(firstDot + 1);
1997 }
1998 Iterator it = nodes.iterator();
1999 // search for a match to an existing child and recursively add
2000 while (it.hasNext()) {
2001 Graph g = (Graph) it.next();
2002 if (g.name.equals(nodeName)) {
2003 g.addWithPreference(childName, type, preferred, sourceJar);
2004 return;
2005 }
2006 }
2007 // if here, name doesn't exist in collection of child nodes
2008 nodes.add(new Graph(this, name, type, preferred, sourceJar));
2009 }
2010
2011 /**
2012 * Set the preferred state of the leaf (class) child node having the
2013 * given '.' qualified name to the given value. If <code>name</code>
2014 * is <code>null</code>, set the preferred value for this node.
2015 * If the target name does not exist in the graph, this method
2016 * is a noop.
2017 * <p>
2018 * This method enforces the requirement that the ultimate target
2019 * node for the call must be a leaf node.
2020 *
2021 * @param name the name of the child node to set, or <code>null</code>
2022 * to represent this node
2023 * @param value the preferred value to set.
2024 * @param force if true, ignore frozen state
2025 * @return true if the preferred value matches <code>value</code>
2026 * or is changed to <code>value</code> or if the named
2027 * node does not exist in the graph. Returns
2028 * <code>false</code> if the values don't match and
2029 * the node is frozen and force is false.
2030 */
2031 synchronized boolean setPreferred(String name, boolean value, boolean force) {
2032 if (name == null) {
2033 if (type != CLASS && type != RESOURCE) {
2034 throw new IllegalStateException("only leaf node preferred"
2035 + " state may be set");
2036 }
2037 if (sourceJar != null || force) { // frozen if defined by -api or -impl
2038 preferred = value;
2039 }
2040 return preferred == value;
2041 }
2042 String childName = name;
2043 String targetName = null;
2044 int firstDot = name.indexOf(".");
2045 if (firstDot >= 0) { // must have reached the leaf node
2046 childName = name.substring(0, firstDot);
2047 targetName = name.substring(firstDot + 1);
2048 }
2049 Iterator it = nodes.iterator();
2050 while (it.hasNext()) {
2051 Graph g = (Graph) it.next();
2052 if (g.name.equals(childName)) {
2053 return g.setPreferred(targetName, value, force);
2054 }
2055 }
2056 // if here, name doesn't exist in the graph - noop
2057 return true;
2058 }
2059
2060 /**
2061 * Count the number of child leaf nodes which
2062 * have a preferred state matching the given value. If this
2063 * is a leaf node, return 1 if the value matches the preferred
2064 * state of this node.
2065 *
2066 * @param value the preferred value to search for
2067 * @return the number of leaf nodes having the given value
2068 */
2069 synchronized int countPreferred(boolean value) {
2070 if (type == CLASS || type == RESOURCE) { // leaf
2071 return (value == preferred) ? 1 : 0;
2072 } else {
2073 int total = 0;
2074 Iterator it = nodes.iterator();
2075 while (it.hasNext()) {
2076 Graph g = (Graph) it.next();
2077 total += g.countPreferred(value);
2078 }
2079 return total;
2080 }
2081 }
2082
2083 /**
2084 * Count the number of child leaf (class) nodes which have a preferred
2085 * state which differs from the value implied by * its ancestors. If
2086 * this is a leaf node and is also a nested class, return 1 if the outer
2087 * class has a different preferred state than this class. If this is a
2088 * leaf node and is not a nested class, return 1 if the implied value
2089 * differs the preferred state for this node. If no value is implied
2090 * by the ancestors, this is interpreted as a mismatch.
2091 *
2092 * @return the number of leaf nodes having preferred values which differ
2093 * from that implied by its ancestors
2094 */
2095 synchronized int countImpliedFailures() {
2096 if (type == CLASS || type == RESOURCE) { // leaf
2097 if (type == CLASS) {
2098 Graph outer = getOuter();
2099 if (outer != null) {
2100 return (outer.preferred == preferred) ? 0 : 1;
2101 }
2102 }
2103 return (parent.childPref() != preferred) ? 1 : 0;
2104 } else {
2105 int total = 0;
2106 Iterator it = nodes.iterator();
2107 while (it.hasNext()) {
2108 Graph g = (Graph) it.next();
2109 total += g.countImpliedFailures();
2110 }
2111 return total;
2112 }
2113 }
2114
2115 /**
2116 * Count the number of child leaf nodes.
2117 *
2118 * @return the number of leaf nodes which have this node
2119 * as an ancestor.
2120 */
2121 synchronized int countLeafNodes() {
2122 int total = 0;
2123 Iterator it = nodes.iterator();
2124 while (it.hasNext()) {
2125 Graph g = (Graph) it.next();
2126 if (g.type == CLASS || g.type == RESOURCE) { // leaf node
2127 total++;
2128 } else {
2129 total += g.countLeafNodes();
2130 }
2131 }
2132 return total;
2133 }
2134
2135 /**
2136 * Return the full name of this node expressed as a relative
2137 * path. No suffixes (".class", "/-", "/*") are appended.
2138 *
2139 * @return the full path name representing this node
2140 */
2141 String getFullName() {
2142 if (name.length() == 0) {
2143 return "";
2144 }
2145 String prefix = "";
2146 if (parent != null) {
2147 prefix = parent.getFullName();
2148 }
2149 if (prefix.length() > 0) {
2150 prefix += "/";
2151 }
2152 return prefix + name;
2153 }
2154
2155 /**
2156 * If this node is of type CLASS and is an inner class, mark this node
2157 * as STALE if the outer class has the same preferred value. Print a
2158 * warning to <code>System.err</code> if they do not have the same
2159 * value. If there is no outer class corresponding to the inner class,
2160 * this node is retained. Call this method on all children.
2161 */
2162 synchronized void markStaleInnerClasses() {
2163 if (type == CLASS) {
2164 Graph outer = getOuter();
2165 if (outer != null) {
2166 if (outer.preferred == preferred) {
2167 type = STALE;
2168 } else {
2169 print("preflistgen.innerwarn", outer.name, name);
2170 }
2171 }
2172 } else {
2173 for (Iterator it = nodes.iterator(); it.hasNext(); ) {
2174 Graph g = (Graph) it.next();
2175 g.markStaleInnerClasses();
2176 }
2177 }
2178 }
2179
2180 /**
2181 * Return the node for the outer class for this class if this is a nested
2182 * class.
2183 *
2184 * @return the graph for the outer class, or <code>null</code> if this is
2185 * not a nested class, or if the outer class is not in the graph
2186 */
2187 final synchronized Graph getOuter() {
2188 if (type != CLASS) {
2189 throw new IllegalStateException("Attempt to get outer of a non-class");
2190 }
2191 Graph g = null;
2192 int lastDollar = name.lastIndexOf('$');
2193 if (lastDollar > 0) {
2194 String outerName = name.substring(0, lastDollar);
2195 g = parent.getChild(outerName);
2196 }
2197 return g;
2198 }
2199
2200 /**
2201 * Recursively remove child nodes of type STALE.
2202 */
2203 synchronized void deleteStaleNodes() {
2204 for (Iterator it = nodes.iterator(); it.hasNext(); ) {
2205 Graph g = (Graph) it.next();
2206 if (g.type == STALE) {
2207 it.remove();
2208 } else {
2209 g.deleteStaleNodes();
2210 }
2211 }
2212 }
2213
2214 /**
2215 * Get the child node having the given name.
2216 *
2217 * @param name the child name
2218 * @return the child <code>Graph</code> object, or null if the
2219 * no child named <code>name</code> exists
2220 */
2221 Graph getChild(String name) {
2222 for (Iterator it = nodes.iterator(); it.hasNext(); ) {
2223 Graph g = (Graph) it.next();
2224 if (g.name.equals(name)) {
2225 return g;
2226 }
2227 }
2228 return null;
2229 }
2230
2231 /**
2232 * Test whether all of the immediate child nodes of this
2233 * node are leaf (class) nodes.
2234 *
2235 * @return <code>true</code> if all a the nodes are leaf nodes
2236 */
2237 synchronized boolean containsLeavesOnly() {
2238 Iterator it = nodes.iterator();
2239 while (it.hasNext()) {
2240 Graph g = (Graph) it.next();
2241 if (g.type != CLASS && g.type != RESOURCE) { // must be subpkg
2242 return false;
2243 }
2244 }
2245 return true;
2246 }
2247
2248 synchronized void addLeaves(Collection leaves) {
2249 if (type == CLASS) {
2250 leaves.add(new PrefData(getFullName() + ".class",
2251 preferred,
2252 sourceJar));
2253 return;
2254 }
2255 if (type == RESOURCE) {
2256 leaves.add(new PrefData(getFullName(), preferred, sourceJar));
2257 return;
2258 }
2259 Iterator it = nodes.iterator();
2260 while (it.hasNext()) {
2261 Graph g = (Graph) it.next();
2262 g.addLeaves(leaves);
2263 }
2264 return;
2265 }
2266
2267 synchronized void merge(Graph g) {
2268 Collection leaves = new HashSet();
2269 g.addLeaves(leaves);
2270 Iterator it = leaves.iterator();
2271 while (it.hasNext()) {
2272 PrefData data = (PrefData) it.next();
2273 int type = ((data.name.endsWith(".class") ? CLASS : RESOURCE));
2274 addWithPreference(fileToClass(data.name),
2275 type,
2276 data.preferred,
2277 data.sourceJar);
2278 }
2279 }
2280
2281 /**
2282 * Add preferred list entries (represented by <code>PrefData</code>
2283 * objects) to the given <code>Collection</code>. One of the following
2284 * cases is handled:
2285 * <ul>
2286 * <li>if this is the root node, then <code>addEntries</code> is
2287 * called on each of the child nodes. Otherwise:
2288 * <li>if the preferred state of all leaf nodes (including possibly
2289 * this one) matches the implied state provided by the ancestors,
2290 * then return without adding any entries. Otherwise:
2291 * <li>if this is a leaf node and its preferred state does not
2292 * match the state implied by its parent, add a class style
2293 * entry to the collection and return. Otherwise:
2294 * <li>if this is not a leaf node, and there is a single child
2295 * which is also not a leaf node, then this node represents
2296 * an intermediate component in a package name. Call
2297 * <code>addEntries</code> on the child to generate a more
2298 * qualified entry. Otherwise:
2299 * <li>if this is not a leaf node and all of the child leaf nodes
2300 * have the same preferred value, then make this a package
2301 * wildcard having that value (if all children are leaves)
2302 * or make this a namespace wildcard having that value (if
2303 * some children are not leaves). Otherwise:
2304 * <li>try all variations of package/namespace wildcards, as well
2305 * as no wildcard setting, for this node. Add an entry
2306 * to the collection giving the optimal (minimal) result, then
2307 * call <code>addEntries</code> on all children.
2308 * </ul>
2309 *
2310 */
2311 synchronized void addEntries(Collection entries) {
2312 if (parent == null) {
2313 Iterator it = nodes.iterator();
2314 while (it.hasNext()) {
2315 Graph g = (Graph) it.next();
2316 g.addEntries(entries);
2317 }
2318 return;
2319 }
2320 reset(); // set all non-leaf nodes to type = INHERIT
2321 if (countImpliedFailures() == 0) {
2322 return;
2323 }
2324 if (type == CLASS) { // entry failed previous count test
2325 entries.add(new PrefData(getFullName() + ".class", preferred));
2326 return;
2327 }
2328 if (type == RESOURCE) { // entry failed previous count test
2329 entries.add(new PrefData(getFullName(), preferred));
2330 return;
2331 }
2332 // special case - find max qualified variant of pkg name
2333 if (nodes.size() == 1) {
2334 Iterator it = nodes.iterator();
2335 if (! it.hasNext()) {
2336 throw new IllegalStateException("Inconsistent iterator");
2337 }
2338 Graph g = (Graph) it.next();
2339 if (g.type != CLASS && g.type != RESOURCE) { // must be subpkg
2340 g.addEntries(entries);
2341 return;
2342 }
2343 }
2344 /*
2345 * if here, some children have actual preferences which don't match
2346 * their implied preferences. Check whether all children have
2347 * the same preference value, and if so, make this node a
2348 * wildcard package (if all immediate children are leaves)
2349 * or a wildcard namespace otherwise (if at least one child
2350 * is a non-leaf)
2351 */
2352 boolean gotMatch = false;
2353 boolean matchValue = false;
2354 int leafCount = countLeafNodes();
2355 if (leafCount == countPreferred(true)) {
2356 gotMatch = true;
2357 matchValue = true;
2358 } else if (leafCount == countPreferred(false)) {
2359 gotMatch = true;
2360 matchValue = false;
2361 }
2362 if (gotMatch) {
2363 if (containsLeavesOnly()) {
2364 entries.add(new PrefData(getFullName() + "/*", matchValue));
2365 } else {
2366 entries.add(new PrefData(getFullName() + "/-", matchValue));
2367 }
2368 return;
2369 }
2370 /*
2371 * if here, children have a mix of preferred values. Find
2372 * which variation of wildcard type (if any) to assign to
2373 * this node to optimize the entries generated by children
2374 */
2375 int lowestValue = countEntries(INHERIT, null);
2376 int bestType = INHERIT;
2377 Boolean bestImpliedPref = null;
2378 // do PKG before NAMESPACE so 'leaf package' always get pkg wildcard
2379 int count = countEntries(PKG, TRUE);
2380 if (count < lowestValue) {
2381 lowestValue = count;
2382 bestType = PKG;
2383 bestImpliedPref = TRUE;
2384 }
2385
2386 count = countEntries(PKG, FALSE);
2387 if (count < lowestValue) {
2388 lowestValue = count;
2389 bestType = PKG;
2390 bestImpliedPref = FALSE;
2391 }
2392
2393 count = countEntries(NAMESPACE, TRUE);
2394 if (count < lowestValue) {
2395 lowestValue = count;
2396 bestType = NAMESPACE;
2397 bestImpliedPref = TRUE;
2398 }
2399
2400 count = countEntries(NAMESPACE, FALSE);
2401 if (count < lowestValue) {
2402 lowestValue = count;
2403 bestType = NAMESPACE;
2404 bestImpliedPref = FALSE;
2405 }
2406
2407 type = bestType;
2408 impliedPref = bestImpliedPref;
2409 if (type == NAMESPACE) {
2410 entries.add(new PrefData(getFullName() + "/-",
2411 impliedPref.booleanValue()));
2412 }
2413 if (type == PKG) {
2414 entries.add(new PrefData(getFullName() + "/*",
2415 impliedPref.booleanValue()));
2416 }
2417 for (Iterator it = nodes.iterator(); it.hasNext(); ) {
2418 Graph g = (Graph) it.next();
2419 g.addEntries(entries);
2420 }
2421 }
2422
2423 /**
2424 * Count the number of entries which would be created
2425 * by this node for the given node type and preference. The
2426 * <code>type</code> and <code>impliedPref</code> class attributes
2427 * are side-affected by this method.
2428 *
2429 * @param type the node type to assign
2430 * @param pref the node's implied preference value
2431 * @return the number of entries generated by children
2432 */
2433 synchronized int countEntries(int type, Boolean pref) {
2434 HashSet test = new HashSet();
2435 this.type = type;
2436 impliedPref = pref;
2437 for (Iterator it = nodes.iterator(); it.hasNext(); ) {
2438 Graph g = (Graph) it.next();
2439 g.addEntries(test);
2440 }
2441 return test.size();
2442 }
2443 }
2444 }