View Javadoc
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  
19  package org.apache.river.tool;
20  
21  import java.io.BufferedReader;
22  import java.io.BufferedWriter;
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.InputStreamReader;
28  import java.io.OutputStreamWriter;
29  import java.io.Writer;
30  import java.net.URI;
31  import java.net.URISyntaxException;
32  import java.security.MessageDigest;
33  import java.security.NoSuchAlgorithmException;
34  import java.text.MessageFormat;
35  import java.util.ArrayList;
36  import java.util.Collections;
37  import java.util.Enumeration;
38  import java.util.HashMap;
39  import java.util.HashSet;
40  import java.util.Iterator;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.MissingResourceException;
44  import java.util.ResourceBundle;
45  import java.util.Set;
46  import java.util.StringTokenizer;
47  import java.util.jar.Attributes;
48  import java.util.jar.Attributes.Name;
49  import java.util.jar.JarEntry;
50  import java.util.jar.JarFile;
51  import java.util.jar.JarOutputStream;
52  import java.util.jar.Manifest;
53  import java.util.logging.ConsoleHandler;
54  import java.util.logging.Handler;
55  import java.util.logging.Level;
56  import java.util.logging.Logger;
57  import java.util.regex.Matcher;
58  import java.util.regex.Pattern;
59  
60  /**
61   * A tool for generating "wrapper" JAR files.  A wrapper JAR file contains a
62   * <code>Class-Path</code> manifest attribute listing a group of JAR files to
63   * be loaded from a common codebase.  It may also, depending on applicability
64   * and selected options, contain a JAR index file, a preferred class list
65   * and/or a <code>Main-Class</code> manifest entry for the grouped JAR files.
66   * <p>
67   * The following items are discussed below:
68   * <ul>
69   *   <li> <a href="#applicability">Applicability</a>
70   *   <li> <a href="#running">Using the Tool</a>
71   *   <li> <a href="#logging">Logging</a>
72   *   <li> {@linkplain #main Processing Options}
73   * </ul>
74   * <p>
75   * <h3><a name="applicability">Applicability</a></h3>
76   * <p>
77   * The <code>JarWrapper</code> tool is applicable in the following deployment
78   * situations, which may overlap:
79   * <ul>
80   *   <li> If a codebase contains multiple JAR files which declare preferred
81   *	  resources, <code>JarWrapper</code> can be used to produce a wrapper
82   *	  JAR file with a combined preferred list.  Preferred resources are
83   *	  described in the documentation for the <code> net.jini.loader.pref</code>
84   *	  package.
85   *        <p>
86   *   <li> If a codebase contains multiple JAR files and requires integrity
87   *	  protection, <code>JarWrapper</code> can be used to produce a wrapper
88   *	  JAR file with a <code>Class-Path</code> attribute that uses HTTPMD
89   *	  URLs.  HTTPMD URLs are described in the documentation for the
90   *	  <code> net.jini.url.httpmd</code> package.
91   *        <p>
92   *   <li> If an application or service packaged as an executable JAR file
93   *	  refers to classes specified at deployment time (e.g., via a
94   *	  <code> net.jini.config.Configuration Configuration </code>) which are not
95   *	  present in the JAR file or its <code>Class-Path</code>,
96   *	  <code>JarWrapper</code> can be used to produce a wrapper JAR file
97   *	  which includes the extra classes in its <code>Class-Path</code> while
98   *	  retaining the original <code>Main-Class</code> declaration; the
99   *	  wrapper JAR file can then be executed in place of the original JAR
100  *	  file.
101  * </ul>
102  * <p>
103  * <h3><a name="running">Using the Tool</a></h3>
104  * <code>JarWrapper</code> can be run directly from the
105  * {@linkplain #main command line} or can be invoked programmatically using the
106  * {@link #wrap wrap} method.
107  * <p>
108  * To run the tool on UNIX platforms:
109  * <blockquote><pre>
110  * java -jar <var><b>install_dir</b></var>/lib/jarwrapper.jar <var><b>processing_options</b></var>
111  * </pre></blockquote>
112  * To run the tool on Microsoft Windows platforms:
113  * <blockquote><pre>
114  * java -jar <var><b>install_dir</b></var>\lib\jarwrapper.jar <var><b>processing_options</b></var>
115  * </pre></blockquote>
116  * <p>
117  * A more specific example with options for running directly from a Unix command
118  * line might be:
119  * <blockquote><pre>
120  * % java -jar <var><b>install_dir</b></var>/lib/jarwrapper.jar \
121  *        -httpmd=SHA-1 wrapper.jar base_dir src1.jar src2.jar
122  * </pre></blockquote>
123  * where <var><b>install_dir</b></var> is the directory where the Apache
124  * River release is installed. This command line would result in the creation
125  * of a wrapper JAR file, <code>wrapper.jar</code>, in the current working
126  * directory, whose contents would be based on the source JAR files
127  * <code>src1.jar</code> and <code>src2.jar</code> (as well as any other JAR
128  * files referenced transitively through their <code>Class-Path</code>
129  * attributes or JAR indexes).  The paths for <code>src1.jar</code> and
130  * <code>src2.jar</code>, as well as any transitively referenced JAR files,
131  * would be resolved relative to the <code>base_dir</code> directory.  The
132  * <code>Class-Path</code> attribute of <code>wrapper.jar</code> would use
133  * HTTPMD URLs with SHA-1 digests.  If any of the HTTPMD URLs encountered is
134  * found to be invalid and can not be resolved, the <code>JarWrapper</code>
135  * operation will fail.
136  * <p>
137  * The equivalent programmatic invocation of <code>JarWrapper</code> would be:
138  * <blockquote><pre>
139  * JarWrapper.wrap("wrapper.jar", "base_dir", new String[]{ "src1.jar", "src2.jar" }, "SHA-1", true, "manifest.mf" );
140  * </pre></blockquote>
141  *
142  * <p>
143  * <h3><a name="logging">Logging</a></h3>
144  * <p>
145  * <code>JarWrapper</code> uses the {@link Logger} named
146  * <code>org.apache.river.tool.JarWrapper</code> to log information at the
147  * following logging levels:
148  * <p>
149  * <table border="1" cellpadding="5"
150  * 	  summary="Describes logging performed by JarWrapper at different
151  *		   logging levels">
152  * <caption><b><code>
153  *    org.apache.river.tool.JarWrapper</code></b></caption>
154  *
155  *   <tr> <th scope="col"> Level   <th scope="col"> Description </tr>
156  *   <tr>
157  *     <td> {@link Level#WARNING WARNING}
158  *     <td> Generated JAR index entries that do not end in <code>".jar"</code>
159  *   </tr>
160  *   <tr>
161  *     <td> {@link Level#FINE FINE}
162  *     <td> Names of processed source JAR files and output wrapper JAR file
163  *   </tr>
164  *   <tr>
165  *     <td> {@link Level#FINER FINER}
166  *     <td> Processing of <code>Main-Class</code> and <code>Class-Path</code>
167  *          attributes, and presence of preferred lists and JAR indexes
168  *   </tr>
169  *   <tr>
170  *     <td> {@link Level#FINEST FINEST}
171  *     <td> Processing and compilation of preferred lists and JAR indexes
172  *   </tr>
173  * </table>
174  *
175  * @author Sun Microsystems, Inc.
176  * @since 2.0
177  */
178 public class JarWrapper {
179 
180     private static ResourceBundle resources;
181     private static final Object resourcesLock = new Object();
182     private static final Logger logger =
183 	Logger.getLogger(JarWrapper.class.getName());
184 
185     private final File destJar;
186     /**
187      * Base directory, <code>null</code> in case the JAR files are specified as
188      * absolute files, the so called flatten classpath option.
189      */
190     private final File baseDir;
191     private final SourceJarURL[] srcJars;
192     private final Manifest manifest;
193     private final MessageDigest digest;
194     private final JarIndexWriter indexWriter;
195     private final PreferredListWriter prefWriter;
196     private final StringBuffer classPath = new StringBuffer();
197     private String mainClass = null;
198     private final Set seenJars = new HashSet();
199 
200     static private final String DEFAULT_HTTPMD_ALGORITHM = "SHA-1";
201 
202     /**
203      * Initializes JarWrapper based on the given values.
204      */
205     private JarWrapper(String destJar,
206 		       String baseDir,
207 		       String[] srcJars,
208 		       String httpmdAlg,
209 		       boolean index,
210 		       Manifest mf,
211 		       List apiClasses)
212     {
213 	this.destJar = new File(destJar);
214 	if (this.destJar.exists()) {
215 	    throw new LocalizedIllegalArgumentException(
216 		"jarwrapper.fileexists", destJar);
217 	}
218 	if (baseDir != null) {
219 	this.baseDir = new File(baseDir);
220 	if (!this.baseDir.isDirectory()) {
221 	    throw new LocalizedIllegalArgumentException(
222 		"jarwrapper.invalidbasedir", baseDir);
223 	}
224 	}
225 	else {
226 	    this.baseDir = null;
227 	}
228 	this.srcJars = new SourceJarURL[srcJars.length];
229 	for (int i = 0; i < srcJars.length; i++) {
230 	    try {
231 		SourceJarURL url;
232 		if (baseDir == null) {
233 		    File file = new File(srcJars[i]);
234 		    url = new SourceJarURL(file.getName(),
235 			file.getParentFile());
236 		}
237 		else {
238 		    url = new SourceJarURL(srcJars[i]);
239 		}
240 		if (url.algorithm != null) {
241 		    throw new LocalizedIllegalArgumentException(
242 			"jarwrapper.urlhasdigest", url);
243 		}
244 		this.srcJars[i] = url;
245 	    } catch (LocalizedIOException e) {
246 		throw new LocalizedIllegalArgumentException(e);
247 	    } catch (IOException e) {
248 		throw (IllegalArgumentException)
249 		    new IllegalArgumentException(e.getMessage()).initCause(e);
250 	    }
251 	}
252 	if (httpmdAlg != null) {
253 	    try {
254 		digest = MessageDigest.getInstance(httpmdAlg);
255 	    } catch (NoSuchAlgorithmException e) {
256 		throw (IllegalArgumentException)
257 		    new LocalizedIllegalArgumentException(
258 			"jarwrapper.invalidhttpmdalg", httpmdAlg).initCause(e);
259 	    }
260 	} else {
261 	    digest = null;
262 	}
263         manifest = mf != null ? new Manifest(mf) : new Manifest();
264 	indexWriter = index ? new JarIndexWriter() : null;
265 	List classes = new ArrayList();
266 	if (apiClasses != null) {
267 	    for (Iterator classNames = apiClasses.iterator();
268 		    classNames.hasNext();) {
269 		String className = (String) classNames.next();
270 		if (className != null) {
271 		    classes.add(className.replace('.', '/') + ".class");
272 		}
273 	    }
274 	}
275 	prefWriter = new PreferredListWriter(classes);
276     }
277 
278     /**
279      * Generates a wrapper JAR file for the specified JAR files.  The command
280      * line arguments are:
281      * <pre>
282      * [ <var>options</var> ] <var>dest-jar</var> <var>base-dir</var> <var>src-jar</var> [ <var>src-jar</var> ...]
283      * </pre>
284      * The <var>dest-jar</var> argument specifies the name of the wrapper JAR
285      * file to generate.  The <var>base-dir</var> argument specifies the base
286      * directory from which to locate source JAR files to wrap.  The
287      * <var>src-jar</var> arguments are non-absolute URLs to "top-level" source
288      * JAR files relative to <var>base-dir</var>; they also constitute the
289      * basis of the <code>Class-Path</code> attribute included in the generated
290      * wrapper JAR file.  JAR files not present in the command line but
291      * indirectly referenced via JAR index or <code>Class-Path</code> entries
292      * in source JAR files will themselves be used as source JAR files, and
293      * will appear alongside the top-level source JAR files in the
294      * <code>Class-Path</code> attribute of the wrapper JAR file in depth-first
295      * order, with JAR index references appearing before
296      * <code>Class-Path</code> references.  This utility does not modify any
297      * source JAR files.
298      * <p>
299      * If any of the top-level source JAR files contain preferred resources (as
300      * indicated by a preferred list in the JAR file), then a preferred list
301      * describing resource preferences across all source JAR files will be
302      * included in the wrapper JAR file.  The preferred list of a top-level
303      * source JAR file is interpreted as applying to that JAR file along with
304      * all JAR files transitively referenced by it through JAR index or
305      * <code>Class-Path</code> entries, excluding JAR files that have already
306      * been encountered in the processing of preceding top-level JAR files.  If
307      * a given top-level source JAR file does not contain a preferred list,
308      * then all resources contained in it and its transitively referenced JAR
309      * files (again, excluding those previously encountered) are considered not
310      * preferred.  Preferred lists are described further in the documentation
311      * for <code> net.jini.loader.pref.PreferredClassLoader</code>.
312      * <p>
313      * If any of the top-level source JAR files declare a
314      * <code>Main-Class</code> manifest entry, then the wrapper JAR file will
315      * include a <code>Main-Class</code> manifest entry whose value is that of
316      * the first top-level source JAR file listed on the command line which
317      * defines a <code>Main-Class</code> entry.
318      * <p>
319      * Note that attribute values generated by this utility, such as those for
320      * the <code>Class-Path</code> and <code>Main-Class</code> attributes
321      * described above, do not take precedence over values for the same
322      * attributes contained in a manifest file explicitly specified using the
323      * <code>-manifest</code> option (described below).
324      * <p>
325      * Supported options for this tool include:
326      * <p>
327      * <dl>
328      *   <dt> <code>-verbose</code>
329      *   <dd> Sets the level of the <code>org.apache.river.tool.JarWrapper</code>
330      * 	      logger to <code>Level.FINER</code>.
331      *	      <p>
332      *   <dt> <code>-httpmd[=algorithm]</code>
333      *   <dd> Use (relative) HTTPMD URLs in the <code>Class-Path</code>
334      *        attribute of the generated wrapper JAR file.  The default is to
335      *        use HTTP URLs.  Digests for HTTPMD URLs are calculated using the
336      *        given algorithm, or SHA-1 if none is specified.
337      *	      <p>
338      *   <dt> <code>-noindex</code>
339      *   <dd> Do not include a JAR index in the generated wrapper JAR file.  The
340      *	      default is to compile an index based on the contents of the
341      *	      source JAR files.
342      *	      <p>
343      *   <dt> <code>-manifest=<I>file</I></code>
344      *   <dd> Specifies a manifest file containing attribute values to include
345      *        in the manifest file inside the generated wrapper JAR file.
346      * 	      This allows enables users to  add additional metadata or 
347      *        override JarWrapper's generated  values to customize the resulting 
348      *        manifest.  The values contained in this optional file take 
349      *        precedence over the generated content. This flag is conceptually 
350      *        similar to the jar utilities <code>m</code> flag. In the current
351      *        version there are four possible attributes that can be overridden
352      *        in the target Manifest.  These are
353      *        <code>Name.MANIFEST_VERSION</code>,
354      *        <code>Name("Created-By")</code>, <code>Name.CLASS_PATH</code> and
355      *        <code>Name.MAIN_CLASS</code>.  Any additonal attributes beyond
356      *        these four will be appended to the manifest attribute list and
357      *        will appear in the resultant <code>MANIFEST.MF</code> file.
358      * </dl>
359      */
360     public static void main(String[] args) {
361 	String destJar;
362 	String baseDir;
363 	String[] srcJars;
364 	String httpmdAlg = null;
365 	boolean index = true;
366         Manifest mf = null;
367 
368 	int i = 0;
369 	while (i < args.length && args[i].startsWith("-")) {
370 	    String s = args[i++];
371 	    if (s.equals("-help")) {
372 		System.err.println(localize("jarwrapper.usage"));
373 		System.exit(0);
374 	    } else if (s.equals("-verbose")) {
375 		setLoggingLevel(Level.FINER);
376 	    } else if (s.equals("-debug")) {
377 		setLoggingLevel(Level.ALL);
378 	    } else if (s.equals("-httpmd") || s.startsWith("-httpmd=")) {
379 		if (httpmdAlg != null) {
380 		    System.err.println(localize("jarwrapper.multiplehttpmd"));
381 		    System.err.println(localize("jarwrapper.usage"));
382 		    System.exit(1);
383 		}
384 		int split = s.indexOf('=');
385 		httpmdAlg = (split != -1) ? 
386                     s.substring(split + 1) : DEFAULT_HTTPMD_ALGORITHM;
387             } else if (s.startsWith("-manifest=")) {
388                 int split = s.indexOf('=');
389                 String fileName = s.substring(split + 1);
390                 try {
391                     mf = retrieveManifest(fileName);
392                 } catch (IOException ioe) {
393 		    System.err.println(localize("jarwrapper.badmanifest", s));
394 	            System.exit(1);
395                 }
396 	    } else if (s.equals("-noindex")) {
397 		index = false;
398 	    } else {
399 		System.err.println(localize("jarwrapper.badoption", s));
400 		System.err.println(localize("jarwrapper.usage"));
401 		System.exit(1);
402 	    }
403 	}
404 	if (args.length - i < 3) {
405 	    System.err.println(localize("jarwrapper.insufficientargs"));
406 	    System.err.println(localize("jarwrapper.usage"));
407 	    System.exit(1);
408 	}
409 	destJar = args[i++];
410 	baseDir = args[i++];
411 	srcJars = new String[args.length - i];
412 	System.arraycopy(args, i, srcJars, 0, srcJars.length);
413 
414 	try {
415 	    wrap(destJar, baseDir, srcJars, httpmdAlg, index, mf);
416 	} catch (Throwable t) {
417 	    if (t instanceof LocalizedIllegalArgumentException ||
418 		t instanceof LocalizedIOException)
419 	    {
420 		System.err.println(t.getMessage());
421 	    } else {
422 		System.err.println(localize("jarwrapper.fatalexception"));
423 		t.printStackTrace();
424 	    }
425 	    System.exit(1);
426 	}
427     }
428 
429     /**
430      * Invokes {@link #wrap(String, String, String[], String, boolean, Manifest)
431      * wrap} with the provided values and a <code>null</code> manifest.
432      *
433      * @param destJar name of the wrapper JAR file to generate
434      * @param baseDir base directory from which to locate source JAR
435      * files to wrap
436      * @param srcJars list of top-level source JAR files to process
437      * @param httpmdAlg name of algorithm to use for generating HTTPMD URLs, or
438      * <code>null</code> if plain HTTP URLs should be used
439      * @param index if <code>true</code>, generate a JAR index; if
440      * <code>false</code>, do not generate one
441      * @throws IOException if an I/O error occurs while processing source JAR
442      * files or generating the wrapper JAR file
443      * @throws IllegalArgumentException if the provided values are invalid
444      * @throws NullPointerException if <code>destJar</code>,
445      * <code>baseDir</code>, <code>srcJars</code>, or any element of
446      * <code>srcJars</code> is <code>null</code>
447      */
448     public static void wrap(String destJar,
449 			    String baseDir,
450 			    String[] srcJars,
451 			    String httpmdAlg,
452 			    boolean index)
453 	throws IOException
454     {
455 	wrap(destJar, baseDir, srcJars, httpmdAlg, index, null);
456     }
457 
458     /**
459      * Generates a wrapper JAR file based on the provided values in the same
460      * manner as described in the documentation for {@link #main}.  The only
461      * difference between this method and <code>main</code> is that it receives
462      * its values as explicit arguments instead of in a command line, and
463      * indicates failure by throwing an exception.
464      *
465      * @param destJar name of the wrapper JAR file to generate
466      * @param baseDir base directory from which to locate source JAR
467      * files to wrap
468      * @param srcJars list of top-level source JAR files to process
469      * @param httpmdAlg name of algorithm to use for generating HTTPMD URLs, or
470      * <code>null</code> if plain HTTP URLs should be used
471      * @param index if <code>true</code>, generate a JAR index; if
472      * <code>false</code>, do not generate one
473      * @param mf manifest containing values to include in the manifest file 
474      * of the generated wrapper JAR file
475      * 
476      * @throws IOException if an I/O error occurs while processing source JAR
477      * files or generating the wrapper JAR file
478      * @throws IllegalArgumentException if the provided values are invalid
479      * @throws NullPointerException if <code>destJar</code>,
480      * <code>baseDir</code>, <code>srcJars</code>, or any element of
481      * <code>srcJars</code> is <code>null</code>
482      * @since 2.1
483      */
484     public static void wrap(String destJar,
485                             String baseDir,
486                             String[] srcJars,
487                             String httpmdAlg,
488                             boolean index, 
489                             Manifest mf)
490         throws IOException
491     {
492 	wrap(destJar, baseDir, srcJars, httpmdAlg, index, mf, null);
493     }
494 
495     /**
496      * Generates a wrapper JAR file based on the provided values in the same
497      * manner as described in the documentation for {@link #main}.  The only
498      * difference between this method and <code>main</code> is that it receives
499      * its values as explicit arguments instead of in a command line, and
500      * indicates failure by throwing an exception.
501      *
502      * @param destJar name of the wrapper JAR file to generate
503      * @param baseDir base directory from which to locate source JAR
504      * files to wrap
505      * @param srcJars list of top-level source JAR files to process
506      * @param httpmdAlg name of algorithm to use for generating HTTPMD URLs, or
507      * <code>null</code> if plain HTTP URLs should be used
508      * @param index if <code>true</code>, generate a JAR index; if
509      * <code>false</code>, do not generate one
510      * @param mf manifest containing values to include in the manifest file
511      * of the generated wrapper JAR file
512      * @param apiClasses list of binary class names (type <code>String</code>)
513      * that must be considered API classes in case a preferences conflict
514      * arises during wrapping of the JAR files, or <code>null</code> in case
515      * no such list is available
516      *
517      * @throws IOException if an I/O error occurs while processing source JAR
518      * files or generating the wrapper JAR file
519      * @throws IllegalArgumentException if the provided values are invalid
520      * @throws NullPointerException if <code>destJar</code>,
521      * <code>baseDir</code>, <code>srcJars</code>, or any element of
522      * <code>srcJars</code> is <code>null</code>
523      */
524     public static void wrap(String destJar,
525                             String baseDir,
526                             String[] srcJars,
527                             String httpmdAlg,
528                             boolean index,
529                             Manifest mf,
530                             List apiClasses)
531         throws IOException
532     {
533         new JarWrapper(destJar, baseDir, srcJars, httpmdAlg, index, mf,
534 	    apiClasses).wrap();
535     }
536 
537     /**
538      * Generates a wrapper JAR file based on the provided values in the same
539      * manner as described in the documentation for {@link #main}.
540      * <p>
541      * The difference between this method and the 6 and 7-arg <code>wrap</code>
542      * method is that the source JAR files must be specified by an absolute path
543      * and that for processing the classpath will be flattened, i.e. each source
544      * JAR file will be considered as relative to its parent directory (that
545      * will serve as a virtual base directory) for the assembly of the
546      * <code>Class-Path</code> entry.
547      *
548      * @param destJar name of the wrapper JAR file to generate
549      * @param srcJars list of top-level source JAR files to process, must be
550      * absolute paths
551      * @param httpmdAlg name of algorithm to use for generating HTTPMD URLs, or
552      * <code>null</code> if plain HTTP URLs should be used
553      * @param index if <code>true</code>, generate a JAR index; if
554      * <code>false</code>, do not generate one
555      * @param mf manifest containing values to include in the manifest file
556      * of the generated wrapper JAR file
557      * @param apiClasses list of binary class names (type <code>String</code>)
558      * that must be considered API classes in case a preferences conflict
559      * arises during wrapping of the JAR files, or <code>null</code> in case
560      * no such list is available
561      *
562      * @throws IOException if an I/O error occurs while processing source JAR
563      * files or generating the wrapper JAR file
564      * @throws IllegalArgumentException if the provided values are invalid
565      * @throws NullPointerException if <code>destJar</code>,
566      * <code>srcJars</code>, or any element of <code>srcJars</code> is
567      * <code>null</code>
568      */
569     public static void wrap(String destJar,
570                             String[] srcJars,
571                             String httpmdAlg,
572                             boolean index,
573                             Manifest mf,
574                             List apiClasses)
575         throws IOException
576     {
577         new JarWrapper(destJar, null, srcJars, httpmdAlg, index, mf,
578 	    apiClasses).wrap();
579     }
580 
581     /**
582      * Processes source JAR files and outputs wrapper JAR file.
583      */
584     private void wrap() throws IOException {
585 	for (int i = 0; i < srcJars.length; i++) {
586 	    process(srcJars[i], null);
587 	}
588 	outputWrapperJar();
589     }
590 
591     /**
592      * Processes source JAR file indicated by the given URL, determining
593      * preferred resources using the provided preferred list reader.  If the
594      * preferred list reader is null, then the URL is for a top-level source
595      * JAR file, in which case the preferred list of the JAR file should be
596      * read, if the JAR file has not already been processed.
597      */
598     private void process(SourceJarURL url, PreferredListReader prefReader)
599 	throws IOException
600     {
601 	File file = baseDir == null ? url.toFile() : url.toFile(baseDir);
602 	boolean seen = seenJars.contains(file);
603 	boolean checkMainClass = mainClass == null && prefReader == null;
604 	if (seen && !checkMainClass) {
605 	    return;
606 	}
607 
608 	if (logger.isLoggable(Level.FINE)) {
609 	    logger.log(Level.FINE, "processing {0}", new Object[]{ file });
610 	}
611 	if (!file.exists()) {
612 	    throw new LocalizedIOException("jarwrapper.filenotfound", file);
613 	}
614 	JarFile jar = new JarFile(file, false);
615 
616 	if (checkMainClass) {
617 	    mainClass = getMainClass(jar);
618 	}
619 	if (!seen) {
620 	    seenJars.add(file);
621 
622 	    if (digest != null) {
623 		url = new SourceJarURL(
624 		    url.path,
625 		    digest.getAlgorithm(),
626 		    getDigestString(digest, file),
627 		    null);
628 	    }
629 	    if (classPath.length() > 0) {
630 		classPath.append(' ');
631 	    }
632 	    classPath.append(url);
633 
634 	    if (indexWriter != null) {
635 		indexWriter.addEntries(jar, url);
636 	    }
637 	    if (prefReader == null) {
638 		prefReader = new PreferredListReader(jar);
639 	    }
640 	    prefWriter.addEntries(jar, prefReader);
641 
642 	    List l = new ArrayList();
643 	    l.addAll(new JarIndexReader(jar).getJars());
644 	    l.addAll(getClassPath(jar));
645 	    for (Iterator i = l.iterator(); i.hasNext(); ) {
646 		SourceJarURL u = (SourceJarURL) i.next();
647 		u = url.resolve(new SourceJarURL(u.path, null, null, null));
648 		process(u, prefReader);
649 	    }
650 	}
651     }
652 
653     /**
654      * Returns URLs contained in the Class-Path attribute (if any) of the given
655      * JAR file, as a list of SourceJarURL instances.
656      */
657     private List getClassPath(JarFile jar) throws IOException {
658 	Manifest mf = jar.getManifest();
659 	if (mf == null) {
660 	    return Collections.EMPTY_LIST;
661 	}
662 	Attributes atts = mf.getMainAttributes();
663 	String cp = atts.getValue(Name.CLASS_PATH);
664 	if (cp == null) {
665 	    return Collections.EMPTY_LIST;
666 	}
667 	if (logger.isLoggable(Level.FINER)) {
668 	    logger.log(Level.FINER, "Class-Path: {0}", new Object[]{ cp });
669 	}
670 	List l = new ArrayList();
671 	for (StringTokenizer tok = new StringTokenizer(cp, " ");
672 	     tok.hasMoreTokens(); )
673 	{
674 	    SourceJarURL url = new SourceJarURL(tok.nextToken());
675 	    if (digest != null && url.algorithm == null) {
676 		throw new LocalizedIOException("jarwrapper.nonhttpmdurl", url);
677 	    }
678 	    l.add(url);
679 	}
680 	return l;
681     }
682 
683 
684     /**
685      * Writes wrapper JAR file based on information from processed source JARs.
686      */
687     private void outputWrapperJar() throws IOException {
688 	if (logger.isLoggable(Level.FINE)) {
689 	    logger.log(Level.FINE, "writing {0}", new Object[]{ destJar });
690 	}
691 	Attributes atts = manifest.getMainAttributes();
692         if (atts.get(Name.MANIFEST_VERSION) == null)
693 	    atts.put(Name.MANIFEST_VERSION, "1.0");
694         Name creatorName = new Name("Created-By");
695         if (atts.get(creatorName) == null )
696 	    atts.put(creatorName, JarWrapper.class.getName());
697         if (atts.get(Name.CLASS_PATH) == null)
698 	    atts.put(Name.CLASS_PATH, classPath.toString());
699         if ((atts.get(Name.MAIN_CLASS) == null) && (mainClass != null)) {
700 	    atts.put(Name.MAIN_CLASS, mainClass);
701 	}
702 
703 	boolean completed = false;
704 	try {
705 	    JarOutputStream jout =
706 		new JarOutputStream(new FileOutputStream(destJar), manifest);
707 	    if (indexWriter != null) {
708 		indexWriter.write(jout);
709 	    }
710 	    prefWriter.write(jout);
711 	    jout.close();
712 	    completed = true;
713 	} finally {
714 	    if (!completed) {
715 		deleteWrapperJar();
716 	    }
717 	}
718     }
719 
720     /**
721      * Attempts to delete wrapper JAR file.
722      */
723     private void deleteWrapperJar() {
724 	try {
725 	    if (!destJar.delete() && logger.isLoggable(Level.WARNING)) {
726 		logger.log(
727 		    Level.WARNING,
728 		    "failed to delete {0}",
729 		    new Object[]{ destJar });
730 	    }
731 	} catch (Throwable t) {
732 	    logger.log(
733 		Level.WARNING, "exception deleting wrapper JAR file", t);
734 	}
735     }
736 
737     /**
738      * Returns the value of the Main-Class attribute of the given JAR file, or
739      * null if none is present.
740      */
741     private static String getMainClass(JarFile jar) throws IOException {
742 	Manifest mf = jar.getManifest();
743 	if (mf == null) {
744 	    return null;
745 	}
746 	Attributes atts = mf.getMainAttributes();
747 	String mc = atts.getValue(Name.MAIN_CLASS);
748 	if (mc != null && logger.isLoggable(Level.FINER)) {
749 	    logger.log(Level.FINER, "Main-Class: {0}", new Object[]{ mc });
750 	}
751 	return mc;
752     }
753 
754     /**
755      * Returns a string representation of the message digest of the given file.
756      */
757     private static String getDigestString(MessageDigest digest, File file)
758 	throws IOException
759     {
760 	FileInputStream fin = new FileInputStream(file);
761 	byte[] buf = new byte[2048];
762 	int n;
763 	while ((n = fin.read(buf)) >= 0) {
764 	    digest.update(buf, 0, n);
765 	}
766 	buf = digest.digest();
767 	fin.close();
768 
769 	StringBuffer sb = new StringBuffer(buf.length * 2);
770 	for (int i = 0; i < buf.length; i++) {
771 	    byte b = buf[i];
772 	    sb.append(Character.forDigit((b >> 4) & 0xf, 16));
773 	    sb.append(Character.forDigit(b & 0xf, 16));
774 	}
775 	return sb.toString();
776     }
777 
778     /**
779      * Sets logging and console handler output level.
780      */
781     private static void setLoggingLevel(Level level) {
782 	logger.setLevel(level);
783 	for (Logger l = logger; l != null; l = l.getParent()) {
784 	    Handler[] handlers = l.getHandlers();
785 	    for (int i = 0; i < handlers.length; i++) {
786 		if (handlers[i] instanceof ConsoleHandler) {
787 		    handlers[i].setLevel(level);
788 		}
789 	    }
790 	    if (!l.getUseParentHandlers()) {
791 		break;
792 	    }
793 	}
794     }
795 
796     /**
797      * Returns localized message text corresponding to the given key string.
798      */
799     static String localize(String key) {
800 	return localize(key, new Object[0]);
801     }
802 
803     /**
804      * Returns localized message text corresponding to the given key string,
805      * passing the provided value as an argument when formatting the message.
806      */
807     static String localize(String key, Object val) {
808 	return localize(key, new Object[] { val });
809     }
810 
811     /**
812      * Returns localized message text corresponding to the given key string,
813      * passing the provided values as an argument when formatting the message.
814      */
815     static String localize(String key, Object[] vals) {
816 	String fmt = getResourceString(key);
817 	if (fmt == null) {
818 	    return "error: no text found in resource bundle for key: " + key;
819 	}
820 	return MessageFormat.format(fmt, vals);
821     }
822 
823     /**
824      * Returns localized format string, obtained from the resource bundle for
825      * JarWrapper, that corresponds to the given key, or null if the resource
826      * bundle does not contain a corresponding string.
827      */
828     private static String getResourceString(String key) {
829 	synchronized (resourcesLock) {
830 	    if (resources == null) {
831 		resources = ResourceBundle.getBundle(
832 		    "org.apache.river.tool.resources.jarwrapper");
833 	    }
834 	}
835 	try {
836 	    return resources.getString(key);
837 	} catch (MissingResourceException e) {
838 	    return null;
839 	}
840     }
841 
842     /**
843      * Returns the Manifest object derived from a manifest file as specified 
844      * on the command line. 
845      */
846     private static Manifest retrieveManifest(String fileName) 
847         throws IOException 
848     {
849         FileInputStream fis = new FileInputStream(fileName);
850         Manifest mf = new Manifest(fis);
851         fis.close();
852         return mf;
853     }
854 
855     /**
856      * IllegalArgumentException with a localized detail message.
857      */
858     private static class LocalizedIllegalArgumentException
859 	extends IllegalArgumentException
860     {
861 	private static final long serialVersionUID = 0L;
862 
863 	/**
864 	 * Creates exception with localized message text corresponding to the
865 	 * given key string, passing the provided value as an argument when
866 	 * formatting the message.
867 	 */
868 	LocalizedIllegalArgumentException(String key, Object val) {
869 	    super(localize(key, val));
870 	}
871 
872 	/**
873 	 * Creates exception with the localized message text of the given
874 	 * cause.
875 	 */
876 	LocalizedIllegalArgumentException(LocalizedIOException cause) {
877 	    super(cause.getMessage());
878 	    initCause(cause);
879 	}
880     }
881 
882     /**
883      * IOException with a localized detail message.
884      */
885     private static class LocalizedIOException extends IOException {
886 
887 	private static final long serialVersionUID = 0L;
888 
889 	/**
890 	 * Creates exception with localized message text corresponding to the
891 	 * given key string, passing the provided value as an argument when
892 	 * formatting the message.
893 	 */
894 	LocalizedIOException(String key, Object val) {
895 	    super(localize(key, val));
896 	}
897 
898 	/**
899 	 * Creates exception with localized message text corresponding to the
900 	 * given key string, passing the provided values as an argument when
901 	 * formatting the message.
902 	 */
903 	LocalizedIOException(String key, Object[] vals) {
904 	    super(localize(key, vals));
905 	}
906     }
907 
908     /**
909      * Represents URL to a source JAR file.  Source JAR URLs must be relative,
910      * and may contain HTTPMD digests.
911      */
912     private static class SourceJarURL {
913 
914 	private static final Pattern httpmdPattern =
915 	    Pattern.compile("(.*);(.+?)=(.+?)(?:,(.*))?$");
916 
917 	/** raw URL string, including HTTPMD information (if any) */
918 	final String raw;
919 	/** URL path component, excluding any HTTPMD information */
920 	final String path;
921 	/** HTTPMD digest algorithm, or null if non-HTTPMD URL */
922 	final String algorithm;
923 	/** HTTPMD digest value, or null if non-HTTPMD URL */
924 	final String digest;
925 	/** HTTPMD digest comment, or null if non-HTTPMD URL */
926 	final String comment;
927 	/**
928 	 * Base directory associated with relative path for JAR file, only
929 	 * set in case the flatten classpath option is used.
930 	 */
931 	private File baseDir;
932 
933 	/**
934 	 * Creates SourceJarURL based on given raw URL string.
935 	 */
936 	SourceJarURL(String raw) throws IOException {
937 	    try {
938 		this.raw = raw;
939 		Matcher m = httpmdPattern.matcher(raw);
940 		if (m.matches()) {
941 		    path = m.group(1);
942 		    algorithm = m.group(2);
943 		    digest = m.group(3);
944 		    comment = m.group(4);
945 		} else {
946 		    path = raw;
947 		    algorithm = null;
948 		    digest = null;
949 		    comment = null;
950 		}
951 
952 		URI uri = new URI(path);
953 		if (uri.getScheme() != null) {
954 		    throw new LocalizedIOException(
955 			"jarwrapper.urlhasscheme", raw);
956 		} else if (uri.getAuthority() != null) {
957 		    throw new LocalizedIOException(
958 			"jarwrapper.urlhasauthority", raw);
959 		}
960 		String p = uri.getPath();
961 		if (p == null || p.length() == 0) {
962 		    throw new LocalizedIOException(
963 			"jarwrapper.urlemptypath", raw);
964 		} else if (p.startsWith("/")) {
965 		    throw new LocalizedIOException(
966 			"jarwrapper.urlabsolute", raw);
967 		}
968 	    } catch (URISyntaxException e) {
969 		throw (IOException) new LocalizedIOException(
970 		    "jarwrapper.invalidurlsyntax", raw).initCause(e);
971 	    }
972 	}
973 
974 	/**
975 	 * Creates SourceJarURL based on given raw URL string that has an
976 	 * individual associated base directory.
977 	 */
978 	SourceJarURL(String raw, File baseDir) throws IOException {
979 	    this(raw);
980 
981 	    this.baseDir = baseDir;
982 	}
983 
984 	/**
985 	 * Creates SourceJarURL based on given components.
986 	 */
987 	SourceJarURL(String path, 
988 		     String algorithm, 
989 		     String digest,
990 		     String comment) 
991 	{
992 	    if (algorithm != null) {
993 		raw = path + ';' + algorithm + '=' + digest +
994 		      ((comment != null) ? ',' + comment : "");
995 	    } else {
996 		raw = path;
997 	    }
998 	    this.path = path;
999 	    this.algorithm = algorithm;
1000 	    this.digest = digest;
1001 	    this.comment = comment;
1002 	}
1003 
1004 	/**
1005 	 * Resolves given URL relative to this URL.
1006 	 */
1007 	SourceJarURL resolve(SourceJarURL other) {
1008 	    try {
1009 		// hack around URI bug 4548698 by temporarily prepending slash
1010 		URI uri = new URI('/' + path);
1011 		String p = uri.resolve(other.path).getPath().substring(1);
1012 		return new SourceJarURL(
1013 		    p, other.algorithm, other.digest, other.comment);
1014 	    } catch (URISyntaxException e) {
1015 		throw new AssertionError(e);
1016 	    }
1017 	}
1018 
1019 	/**
1020 	 * Returns file represented by this URL.
1021 	 */
1022 	File toFile() {
1023 	    return toFile(baseDir);
1024 	}
1025 
1026 	/**
1027 	 * Returns file represented by this URL.
1028 	 */
1029 	File toFile(File base) {
1030 	    try {
1031 		String p = new URI(path).getPath();	// decode path
1032 		return new File(base, p.replace('/', File.separatorChar));
1033 	    } catch (URISyntaxException e) {
1034 		throw new InternalError(e);
1035 	    }
1036 	}
1037 
1038 	public boolean equals(Object obj) {
1039 	    return obj instanceof SourceJarURL && 
1040 		   raw.equals(((SourceJarURL) obj).raw);
1041 	}
1042 
1043 	public int hashCode() {
1044 	    return raw.hashCode();
1045 	}
1046 
1047 	public String toString() {
1048 	    return raw;
1049 	}
1050     }
1051 
1052     /**
1053      * Parses JAR indexes.
1054      */
1055     private static class JarIndexReader {
1056 
1057 	private static final Pattern headerPattern = 
1058 	    Pattern.compile("^JarIndex-Version:\\s*(.*?)$");
1059 	private static final Pattern versionPattern =
1060 	    Pattern.compile("^1(\\.\\d+)*$");
1061 
1062 	private final List jars;
1063 
1064 	/**
1065 	 * Parses the given JAR file's JAR index, if any.
1066 	 */
1067 	JarIndexReader(JarFile jar) throws IOException {
1068 	    List l = new ArrayList();
1069 	    jars = Collections.unmodifiableList(l);
1070 	    JarEntry ent = jar.getJarEntry("META-INF/INDEX.LIST");
1071 	    if (ent == null) {
1072 		return;
1073 	    }
1074 	    logger.finer("reading JAR index");
1075 	    BufferedReader r = new BufferedReader(
1076 		new InputStreamReader(jar.getInputStream(ent), "UTF8"));
1077 
1078 	    String s = r.readLine();
1079 	    if (s == null) {
1080 		throw new IOException("missing JAR index header");
1081 	    }
1082 	    s = s.trim();
1083 	    Matcher m = headerPattern.matcher(s);
1084 	    if (!m.matches()) {
1085 		throw new IOException("illegal JAR index header: " + s);
1086 	    }
1087 	    s = m.group(1);
1088 	    if (!versionPattern.matcher(s).matches()) {
1089 		throw new IOException("unsupported JAR index version: " + s);
1090 	    }
1091 
1092 	    s = r.readLine();
1093 	    if (s == null) {
1094 		throw new IOException("truncated JAR index");
1095 	    }
1096 	    s = s.trim();
1097 	    if (s.length() > 0) {
1098 		throw new IOException(
1099 		    "non-empty line after JAR index header: " + s);
1100 	    }
1101 
1102 	    while ((s = r.readLine()) != null) {
1103 		SourceJarURL url = new SourceJarURL(s.trim());
1104 		if (logger.isLoggable(Level.FINEST)) {
1105 		    logger.log(
1106 			Level.FINEST,
1107 			"JAR index references {0}",
1108 			new Object[]{ url });
1109 		}
1110 		l.add(url);
1111 		do {
1112 		    s = r.readLine();
1113 		} while (s != null && s.trim().length() > 0);
1114 	    }
1115 	    if (l.isEmpty()) {
1116 		throw new IOException("empty JAR index");
1117 	    }
1118 	}
1119 
1120 	/**
1121 	 * Returns list of SourceJarURLs representing the JAR files referenced
1122 	 * in the JAR index.
1123 	 */
1124 	List getJars() {
1125 	    return jars;
1126 	}
1127     }
1128 
1129     /**
1130      * Assembles and writes JAR indexes.
1131      */
1132     private static class JarIndexWriter {
1133 
1134 	private final List urls = new ArrayList();
1135 	private final Map contentMap = new HashMap();
1136 
1137 	JarIndexWriter() {
1138 	}
1139 
1140 	/**
1141 	 * Tabulates contents of the given JAR file, associating them with the
1142 	 * provided URL for the JAR file.
1143 	 */
1144 	void addEntries(JarFile jar, SourceJarURL url) {
1145 	    Set contents = new HashSet();
1146 	    for (Enumeration e = jar.entries(); e.hasMoreElements();) {
1147 		String name = ((JarEntry) e.nextElement()).getName();
1148 		if (!(name.startsWith("META-INF") || name.endsWith("/"))) {
1149 		    int pos = name.lastIndexOf("/");
1150 		    contents.add((pos != -1) ? name.substring(0, pos) : name);
1151 		}
1152 	    }
1153 	    if (!contents.isEmpty()) {
1154 		urls.add(url);
1155 		contentMap.put(url, contents);
1156 	    }
1157 	}
1158 
1159 	/**
1160 	 * Writes JAR index to the given output stream.
1161 	 */
1162 	void write(JarOutputStream jout) throws IOException {
1163 	    if (contentMap.isEmpty()) {
1164 		logger.finer("omitting empty JAR index");
1165 		return;
1166 	    }
1167 	    logger.finer("writing JAR index");
1168 	    jout.putNextEntry(new JarEntry("META-INF/INDEX.LIST"));
1169 	    Writer w = 
1170 		new BufferedWriter(new OutputStreamWriter(jout, "UTF8"));
1171 	    w.write("JarIndex-Version: 1.0\n\n");
1172 
1173 	    // preserve original insertion order
1174 	    for (Iterator i = urls.iterator(); i.hasNext();) {
1175 		SourceJarURL url = (SourceJarURL) i.next();
1176 		Set contents = (Set) contentMap.get(url);
1177 
1178 		if (!url.raw.endsWith(".jar")) {
1179 		    if (url.algorithm != null) {
1180 			url = new SourceJarURL(
1181 			    url.path, url.algorithm, url.digest, ".jar");
1182 		    } else if (logger.isLoggable(Level.WARNING)) {
1183 			logger.log(
1184 			    Level.WARNING,
1185 			    "JAR index entry {0} does not end in .jar",
1186 			    new Object[]{ url });
1187 		    }
1188 		}
1189 		if (logger.isLoggable(Level.FINEST)) {
1190 		    logger.log(
1191 			Level.FINEST,
1192 			"writing JAR index entry {0}: {1}",
1193 			new Object[]{ url, contents });
1194 		}
1195 		w.write(url + "\n");
1196 		for (Iterator j = contents.iterator(); j.hasNext(); ) {
1197 		    w.write(j.next() + "\n");
1198 		}
1199 		w.write("\n");
1200 	    }
1201 	    w.flush();
1202 	    jout.closeEntry();
1203 	}
1204     }
1205 
1206     /**
1207      * Parses preferred lists.
1208      */
1209     private static class PreferredListReader {
1210 
1211 	private static final Pattern headerPattern = 
1212 	    Pattern.compile("^PreferredResources-Version:\\s*(.*?)$");
1213 	private static final Pattern versionPattern =
1214 	    Pattern.compile("^1\\.\\d+$");
1215 	private static final Pattern namePattern =
1216 	    Pattern.compile("^Name:\\s*(.*)$");
1217 	private static final Pattern preferredPattern =
1218 	    Pattern.compile("^Preferred:\\s*(.*)$");
1219 
1220 	private final boolean defaultPref;
1221 	private final Map namePrefs = new HashMap();
1222 	private final Map packagePrefs = new HashMap();
1223 	private final Map subtreePrefs = new HashMap();
1224 
1225 	/**
1226 	 * Parses the given JAR file's preferred list, if any.
1227 	 */
1228 	PreferredListReader(JarFile jar) throws IOException {
1229 	    JarEntry ent = jar.getJarEntry("META-INF/PREFERRED.LIST");
1230 	    if (ent == null) {
1231 		defaultPref = false;
1232 		return;
1233 	    }
1234 	    logger.finer("reading preferred list");
1235 	    BufferedReader r = new BufferedReader(
1236 		new InputStreamReader(jar.getInputStream(ent), "UTF8"));
1237 
1238 	    String s = r.readLine();
1239 	    if (s == null) {
1240 		throw new IOException("missing preferred list header");
1241 	    }
1242 	    s = s.trim();
1243 	    Matcher m = headerPattern.matcher(s);
1244 	    if (!m.matches()) {
1245 		throw new IOException("illegal preferred list header: " + s);
1246 	    }
1247 	    s = m.group(1);
1248 	    if (!versionPattern.matcher(s).matches()) {
1249 		throw new IOException(
1250 		    "unsupported preferred list version: " + s);
1251 	    }
1252 
1253 	    s = nextNonBlankLine(r);
1254 	    if (s == null) {
1255 		throw new IOException("empty preferred list");
1256 	    }
1257 	    if ((m = preferredPattern.matcher(s)).matches()) {
1258 		defaultPref = Boolean.valueOf(m.group(1)).booleanValue();
1259 		s = nextNonBlankLine(r);
1260 	    } else {
1261 		defaultPref = false;
1262 	    }
1263 
1264 	    while (s != null) {
1265 		if (!(m = namePattern.matcher(s)).matches()) {
1266 		    throw new IOException(
1267 			"expected preferred entry name: " + s);
1268 		}
1269 		String name = m.group(1);
1270 
1271 		s = nextNonBlankLine(r);
1272 		if (s == null) {
1273 		    throw new IOException("EOF before preferred entry");
1274 		}
1275 		if (!(m = preferredPattern.matcher(s)).matches()) {
1276 		    throw new IOException("expected preferred entry: " + s);
1277 		}
1278 		Boolean pref = Boolean.valueOf(m.group(1));
1279 
1280 		String key;
1281 		Map map;
1282 		if (name.endsWith("/*")) {
1283 		    key = name.substring(0, name.length() - 2);
1284 		    map = packagePrefs;
1285 		} else if (name.endsWith("/")) {
1286 		    key = name.substring(0, name.length() - 1);
1287 		    map = packagePrefs;
1288 		} else if (name.endsWith("/-")) {
1289 		    key = name.substring(0, name.length() - 2);
1290 		    map = subtreePrefs;
1291 		} else {
1292 		    key = name;
1293 		    map = namePrefs;
1294 		}
1295 		if (key.length() == 0) {
1296 		    throw new IOException(
1297 			"invalid preferred entry name: " + name);
1298 		}
1299 		map.put(key, pref);
1300 		if (logger.isLoggable(Level.FINEST)) {
1301 		    logger.log(
1302 			Level.FINEST,
1303 			"read preferred list entry {0}: {1}",
1304 			new Object[]{ name, pref });
1305 		}
1306 
1307 		s = nextNonBlankLine(r);
1308 	    }
1309 	}
1310 
1311 	/**
1312 	 * Returns true if list prefers given entry, or false otherwise.
1313 	 */
1314 	 boolean isPreferred(String entry) {
1315 	     Boolean b = (Boolean) namePrefs.get(entry);
1316 	     if (b != null) {
1317 		 return b.booleanValue();
1318 	     }
1319 
1320 	     if (entry.endsWith(".class")) {
1321 		 int i = entry.lastIndexOf('$');
1322 		 while (i >= 0) {
1323 		     String outer = entry.substring(0, i) + ".class";
1324 		     if ((b = (Boolean) namePrefs.get(outer)) != null) {
1325 			 return b.booleanValue();
1326 		     }
1327 		     i = entry.lastIndexOf('$', i - 1);
1328 		 }
1329 	     }
1330 
1331 	     int i = entry.lastIndexOf('/');
1332 	     if (i >= 0) {
1333 		 String base = entry.substring(0, i);
1334 		 if ((b = (Boolean) packagePrefs.get(base)) != null) {
1335 		     return b.booleanValue();
1336 		 }
1337 
1338 		 for (;;) {
1339 		     if ((b = (Boolean) subtreePrefs.get(base)) != null) {
1340 			 return b.booleanValue();
1341 		     }
1342 		     if ((i = base.lastIndexOf('/')) < 0) {
1343 			 break;
1344 		     }
1345 		     base = base.substring(0, i);
1346 		 }
1347 	     }
1348 
1349 	     return defaultPref;
1350 	 }
1351 
1352 	/**
1353 	 * Returns next non-blank, non-comment line, or null if end of file has
1354 	 * been reached.
1355 	 */
1356 	private static String nextNonBlankLine(BufferedReader reader)
1357 	    throws IOException
1358 	{
1359 	    String s;
1360 	    while ((s = reader.readLine()) != null) {
1361 		s = s.trim();
1362 		if (s.length() > 0 && s.charAt(0) != '#') {
1363 		    return s;
1364 		}
1365 	    }
1366 	    return null;
1367 	}
1368     }
1369 
1370     /**
1371      * Compiles and writes combined preferred lists.
1372      */
1373     private static class PreferredListWriter {
1374 
1375 	private static final int NAME_LEN      = "Name: ".length();
1376 	private static final int PREFERRED_LEN = "Preferred: ".length();
1377 	private static final int TRUE_LEN      = "true".length();
1378 	private static final int FALSE_LEN     = "false".length();
1379 	private static final int NEWLINE_LEN   = "\n".length();
1380 
1381 	private final HashMap pathMap = new HashMap();
1382 	private final DirNode rootNode = new DirNode("");
1383 	private int numPrefs = 0;
1384 	private final List apiClasses;
1385 
1386 
1387 	/**
1388 	 * Constructs a <code>PreferredListWriter</code>.
1389 	 *
1390 	 * @param apiClasses list of URI paths representing classes that must be
1391 	 * considered API classes in case a preferences conflict arrises during
1392 	 * wrapping of JAR files
1393 	 */
1394 	PreferredListWriter(List apiClasses) {
1395 	    this.apiClasses = apiClasses;
1396 	    pathMap.put("", rootNode);
1397 	}
1398 
1399 	/**
1400 	 * Records preferred status of each file entry in the given JAR file,
1401 	 * determined using the provided preferred list reader.
1402 	 */
1403 	void addEntries(JarFile jar, PreferredListReader prefReader)
1404 	    throws IOException
1405 	{
1406 	    for (Enumeration e = jar.entries(); e.hasMoreElements(); ) {
1407 		String path = ((JarEntry) e.nextElement()).getName();
1408 		if (!(path.startsWith("META-INF") || path.endsWith("/"))) {
1409 		    boolean pref = prefReader.isPreferred(path);
1410 		    if (logger.isLoggable(Level.FINEST)) {
1411 			logger.log(
1412 			    Level.FINEST,
1413 			    pref ? "preferred: {0}" : "not preferred: {0}",
1414 			    new Object[]{ path });
1415 		    }
1416 		    addFile(path, jar.getName(), pref);
1417 		}
1418 	    }
1419 	}
1420 
1421 	/**
1422 	 * Writes minimal combined preferred list to given output stream.
1423 	 */
1424 	void write(JarOutputStream jout) throws IOException {
1425 	    if (numPrefs == 0) {
1426 		logger.finer("omitting empty preferred list");
1427 		return;
1428 	    }
1429 	    logger.finer("writing preferred list");
1430 
1431 	    jout.putNextEntry(new JarEntry("META-INF/PREFERRED.LIST"));
1432 	    Writer w = 
1433 		new BufferedWriter(new OutputStreamWriter(jout, "UTF8"));
1434 	    w.write("PreferredResources-Version: 1.0\n");
1435 
1436 	    rootNode.compileList();
1437 	    rootNode.writeList(w);
1438 
1439 	    w.flush();
1440 	    jout.closeEntry();
1441 	}
1442 
1443 	/**
1444 	 * Records the preferred setting of the given file entry.
1445 	 */
1446 	private void addFile(String path, String jarFileName, boolean preferred)
1447 	    throws IOException
1448 	{
1449 	    FileNode fn = (FileNode) pathMap.get(path);
1450 	    if (fn != null) {
1451 		if (fn.preferred != preferred) {
1452 		    // in case it is part of what are considered API classes
1453 		    // we correct the preferred value if required and correct
1454 		    // the total number of preferred classes encountered
1455 		    if (apiClasses.contains(path)) {
1456 			if (fn.preferred) {
1457 			    fn.preferred = false;
1458 			    numPrefs--;
1459 			}
1460 		    }
1461 		    else {
1462 			throw new LocalizedIOException(
1463 			    "jarwrapper.prefconflict",
1464 			    new Object[] { path, jarFileName, fn.jarFileName });
1465 		    }
1466 		}
1467 		return;
1468 	    }
1469 
1470 	    fn = new FileNode(path, jarFileName, preferred);
1471 	    pathMap.put(path, fn);
1472 	    if (preferred) {
1473 		numPrefs++;
1474 	    }
1475 
1476 	    path = parentPath(path);
1477 	    DirNode dn = (DirNode) pathMap.get(path);
1478 	    if (dn != null) {
1479 		dn.files.add(fn);
1480 		return;
1481 	    }
1482 	    dn = new DirNode(path);
1483 	    pathMap.put(path, dn);
1484 	    dn.files.add(fn);
1485 
1486 	    for (path = parentPath(path); ; path = parentPath(path)) {
1487 		DirNode pn = (DirNode) pathMap.get(path);
1488 		if (pn != null) {
1489 		    pn.subdirs.add(dn);
1490 		    return;
1491 		}
1492 		pn = new DirNode(path);
1493 		pathMap.put(path, pn);
1494 		pn.subdirs.add(dn);
1495 		dn = pn;
1496 	    }
1497 	}
1498 
1499 	/**
1500 	 * Returns path of the parent directory of the indicated JAR entry.
1501 	 */
1502 	private static String parentPath(String path) {
1503 	    if (path.endsWith("/")) {
1504 		path = path.substring(0, path.length() - 1);
1505 	    }
1506 	    int i = path.lastIndexOf('/');
1507 	    return (i >= 0) ? path.substring(0, i + 1) : "";
1508 	}
1509 
1510 	static int min(int i1, int i2, int i3) {
1511 	    return Math.min(i1, Math.min(i2, i3));
1512 	}
1513 
1514 	/**
1515 	 * Returns the number of characters needed to write a preferred list
1516 	 * entry with the given name and preferred setting.  If the given name
1517 	 * is null, then the length of a "default" preferred list entry (i.e.,
1518 	 * an entry without a name) is returned.
1519 	 */
1520 	static int calcEntryLength(String name, boolean pref) {
1521 	    int len = NEWLINE_LEN;
1522 	    if (name != null) {
1523 		len += NAME_LEN + name.length() + NEWLINE_LEN;
1524 	    }
1525 	    len += PREFERRED_LEN + (pref ? TRUE_LEN : FALSE_LEN) + NEWLINE_LEN;
1526 	    return len;
1527 	}
1528 
1529 	/**
1530 	 * Writes preferred list entry with the given name and preferred
1531 	 * setting.  If the given name is null, then a "default" preferred list
1532 	 * entry is written.
1533 	 */
1534 	static void writeEntry(Writer w, String name, boolean pref)
1535 	    throws IOException
1536 	{
1537 	    if (logger.isLoggable(Level.FINEST)) {
1538 		logger.log(
1539 		    Level.FINEST,
1540 		    "writing preferred list entry {0}: {1}",
1541 		    new Object[]{
1542 			(name != null) ? name : "<default>",
1543 			Boolean.valueOf(pref) });
1544 	    }
1545 	    w.write("\n");
1546 	    if (name != null) {
1547 		w.write("Name: " + name + "\n");
1548 	    }
1549 	    w.write("Preferred: " + pref + "\n");
1550 	}
1551 
1552 	/**
1553 	 * Stores file preference state.
1554 	 */
1555 	private static class FileNode {
1556 
1557 	    /* action constants */
1558 	    static final int NONE    = 0;
1559 	    static final int SKIP    = 1;
1560 	    static final int INCLUDE = 2;
1561 
1562 	    final String path;
1563 	    final String jarFileName;
1564 	    boolean preferred;
1565 	    int action;
1566 
1567 	    FileNode(String path, String jarFileName, boolean preferred) {
1568 		this.path = path;
1569 		this.preferred = preferred;
1570 		this.jarFileName = jarFileName;
1571 	    }
1572 	}
1573 
1574 	/**
1575 	 * Represents JAR-internal directory.
1576 	 */
1577 	private class DirNode {
1578 
1579 	    final String path;
1580 	    final List subdirs = new ArrayList();
1581 	    final List files = new ArrayList();
1582 
1583 	    /*
1584 	     * The length, in characters, of the preferred list covering this
1585 	     * directory subtree if the default preferred setting for the
1586 	     * entire subtree is true.
1587 	     */
1588 	    int prefSubtreeLen;
1589 	    /*
1590 	     * The length, in characters, of the preferred list covering this
1591 	     * directory subtree if the default preferred setting for the
1592 	     * immediate directory is true, but the default preferred setting
1593 	     * for the subtree as a whole is false.
1594 	     */
1595 	    int prefPackageLen;
1596 	    /*
1597 	     * The length, in characters, of the preferred list covering this
1598 	     * directory subtree if the default preferred setting for the
1599 	     * entire subtree is false.
1600 	     */
1601 	    int unprefSubtreeLen;
1602 	    /*
1603 	     * The length, in characters, of the preferred list covering this
1604 	     * directory subtree if the default preferred setting for the
1605 	     * immediate directory is false, but the default preferred setting
1606 	     * for the subtree as a whole is true.
1607 	     */
1608 	    int unprefPackageLen;
1609 
1610 	    DirNode(String path) {
1611 		this.path = path;
1612 	    }
1613 
1614 	    /**
1615 	     * Computes minimal list length using dynamic programming.
1616 	     */
1617 	    void compileList() {
1618 		int prefLen = 0, unprefLen = 0;
1619 		for (Iterator i = files.iterator(); i.hasNext(); ) {
1620 		    FileNode fn = (FileNode) i.next();
1621 
1622 		    for (int j = fn.path.lastIndexOf('$'); 
1623 			 j != -1;
1624 			 j = fn.path.lastIndexOf('$', j - 1))
1625 		    {
1626 			FileNode fn2 = (FileNode) pathMap.get(
1627 			    fn.path.substring(0, j) + ".class");
1628 			if (fn2 != null) {
1629 			    fn.action = (fn.preferred == fn2.preferred) ?
1630 				FileNode.SKIP : FileNode.INCLUDE;
1631 			    break;
1632 			}
1633 		    }
1634 
1635 		    int entryLen = calcEntryLength(fn.path, fn.preferred);
1636 		    if (fn.action == FileNode.SKIP) {
1637 			// won't list, so don't increment length counts
1638 		    } else if (fn.action == FileNode.INCLUDE) {
1639 			prefLen += entryLen;
1640 			unprefLen += entryLen;
1641 		    } else if (fn.preferred) {
1642 			unprefLen += entryLen;
1643 		    } else {
1644 			prefLen += entryLen;
1645 		    }
1646 		}
1647 		prefSubtreeLen = prefLen;
1648 		prefPackageLen = prefLen;
1649 		unprefSubtreeLen = unprefLen;
1650 		unprefPackageLen = unprefLen;
1651 
1652 		for (Iterator i = subdirs.iterator(); i.hasNext();) {
1653 		    DirNode dn = (DirNode) i.next();
1654 		    dn.compileList();
1655 		    String subtreePath = dn.path + "-";
1656 
1657 		    prefSubtreeLen += min(
1658 			dn.prefSubtreeLen,
1659 			dn.unprefSubtreeLen +
1660 			    calcEntryLength(subtreePath, false),
1661 			dn.unprefPackageLen + calcEntryLength(dn.path, false));
1662 		    prefPackageLen += min(
1663 			dn.prefSubtreeLen + calcEntryLength(subtreePath, true),
1664 			dn.prefPackageLen + calcEntryLength(dn.path, true),
1665 			dn.unprefSubtreeLen);
1666 		    unprefSubtreeLen += min(
1667 			dn.prefSubtreeLen + calcEntryLength(subtreePath, true),
1668 			dn.prefPackageLen + calcEntryLength(dn.path, true),
1669 			dn.unprefSubtreeLen);
1670 		    unprefPackageLen += min(
1671 			dn.prefSubtreeLen,
1672 			dn.unprefSubtreeLen +
1673 			    calcEntryLength(subtreePath, false),
1674 			dn.unprefPackageLen + calcEntryLength(dn.path, false));
1675 		}
1676 	    }
1677 
1678 	    /**
1679 	     * Writes preferred list.  This method is only called on the 
1680 	     * root node.
1681 	     */
1682 	    void writeList(Writer w) throws IOException {
1683 		int totalPrefSubtreeLen =
1684 		    prefSubtreeLen + calcEntryLength(null, true);
1685 		boolean defaultPref = totalPrefSubtreeLen < unprefSubtreeLen;
1686 		if (defaultPref) {
1687 		    writeEntry(w, null, true);
1688 		}
1689 		writeFiles(w, defaultPref);
1690 		for (Iterator i = subdirs.iterator(); i.hasNext();) {
1691 		    ((DirNode) i.next()).writeDir(w, defaultPref);
1692 		}
1693 	    }
1694 
1695 	    /**
1696 	     * Writes preferred list entries (if any) for this directory, which
1697 	     * inherits the given preferred value as its default.
1698 	     */
1699 	    void writeDir(Writer w, boolean contextPref) throws IOException {
1700 		boolean dirPref;
1701 		boolean subdirPref;
1702 		String subtreePath = path + "-";
1703 		if (contextPref) {
1704 		    int totalUnprefPackageLen =
1705 			unprefPackageLen + calcEntryLength(path, false);
1706 		    int totalUnprefSubtreeLen =
1707 			unprefSubtreeLen + calcEntryLength(subtreePath, false);
1708 		    int best = min(
1709 			prefSubtreeLen,
1710 			totalUnprefPackageLen,
1711 			totalUnprefSubtreeLen);
1712 		    if (best == prefSubtreeLen) {
1713 			dirPref = true;
1714 			subdirPref = true;
1715 		    } else if (best == totalUnprefPackageLen) {
1716 			writeEntry(w, path, false);
1717 			dirPref = false;
1718 			subdirPref = true;
1719 		    } else {
1720 			writeEntry(w, subtreePath, false);
1721 			dirPref = false;
1722 			subdirPref = false;
1723 		    }
1724 		} else {
1725 		    int totalPrefPackageLen =
1726 			prefPackageLen + calcEntryLength(path, true);
1727 		    int totalPrefSubtreeLen =
1728 			prefSubtreeLen + calcEntryLength(subtreePath, true);
1729 		    int best = min(
1730 			unprefSubtreeLen,
1731 			totalPrefPackageLen,
1732 			totalPrefSubtreeLen);
1733 		    if (best == unprefSubtreeLen) {
1734 			dirPref = false;
1735 			subdirPref = false;
1736 		    } else if (best == totalPrefPackageLen) {
1737 			writeEntry(w, path, true);
1738 			dirPref = true;
1739 			subdirPref = false;
1740 		    } else {
1741 			writeEntry(w, subtreePath, true);
1742 			dirPref = true;
1743 			subdirPref = true;
1744 		    }
1745 		}
1746 		writeFiles(w, dirPref);
1747 		for (Iterator i = subdirs.iterator(); i.hasNext(); ) {
1748 		    ((DirNode) i.next()).writeDir(w, subdirPref);
1749 		}
1750 	    }
1751 
1752 	    /**
1753 	     * Writes preferred list entries (if any) for files in this
1754 	     * directory, which has the given preferred value as its default.
1755 	     */
1756 	    void writeFiles(Writer w, boolean contextPref) throws IOException {
1757 		for (Iterator i = files.iterator(); i.hasNext(); ) {
1758 		    FileNode fn = (FileNode) i.next();
1759 		    if (fn.action != FileNode.SKIP &&
1760 			(fn.action == FileNode.INCLUDE ||
1761 			 fn.preferred != contextPref))
1762 		    {
1763 			writeEntry(w, fn.path, fn.preferred);
1764 		    }
1765 		}
1766 	    }
1767 	}
1768     }
1769 }