Package com.sun.jini.tool

Source Code of com.sun.jini.tool.JarWrapper$PreferredListWriter$FileNode

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.sun.jini.tool;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* A tool for generating "wrapper" JAR files.  A wrapper JAR file contains a
* <code>Class-Path</code> manifest attribute listing a group of JAR files to
* be loaded from a common codebase.  It may also, depending on applicability
* and selected options, contain a JAR index file, a preferred class list
* and/or a <code>Main-Class</code> manifest entry for the grouped JAR files.
* <p>
* The following items are discussed below:
* <ul>
*   <li> <a href="#applicability">Applicability</a>
*   <li> <a href="#running">Using the Tool</a>
*   <li> <a href="#logging">Logging</a>
*   <li> {@linkplain #main Processing Options}
* </ul>
* <p>
* <a name="applicability"><h3>Applicability</h3></a>
* <p>
* The <code>JarWrapper</code> tool is applicable in the following deployment
* situations, which may overlap:
* <ul>
*   <li> If a codebase contains multiple JAR files which declare preferred
*    resources, <code>JarWrapper</code> can be used to produce a wrapper
*    JAR file with a combined preferred list.  Preferred resources are
*    described in the documentation for the {@link net.jini.loader.pref}
*    package.
*        <p>
*   <li> If a codebase contains multiple JAR files and requires integrity
*    protection, <code>JarWrapper</code> can be used to produce a wrapper
*    JAR file with a <code>Class-Path</code> attribute that uses HTTPMD
*    URLs.  HTTPMD URLs are described in the documentation for the
*    {@link net.jini.url.httpmd} package.
*        <p>
*   <li> If an application or service packaged as an executable JAR file
*    refers to classes specified at deployment time (e.g., via a
*    {@link net.jini.config.Configuration Configuration}) which are not
*    present in the JAR file or its <code>Class-Path</code>,
*    <code>JarWrapper</code> can be used to produce a wrapper JAR file
*    which includes the extra classes in its <code>Class-Path</code> while
*    retaining the original <code>Main-Class</code> declaration; the
*    wrapper JAR file can then be executed in place of the original JAR
*    file.
* </ul>
* <p>
* <a name="running"><h3>Using the Tool</h3></a>
* <code>JarWrapper</code> can be run directly from the
* {@linkplain #main command line} or can be invoked programmatically using the
* {@link #wrap wrap} method.
* <p>
* To run the tool on UNIX platforms:
* <blockquote><pre>
* java -jar <var><b>install_dir</b></var>/lib/jarwrapper.jar <var><b>processing_options</b></var>
* </pre></blockquote>
* To run the tool on Microsoft Windows platforms:
* <blockquote><pre>
* java -jar <var><b>install_dir</b></var>\lib\jarwrapper.jar <var><b>processing_options</b></var>
* </pre></blockquote>
* <p>
* A more specific example with options for running directly from a Unix command
* line might be:
* <blockquote><pre>
* % java -jar <var><b>install_dir</b></var>/lib/jarwrapper.jar \
*        -httpmd=SHA-1 wrapper.jar base_dir src1.jar src2.jar
* </pre></blockquote>
* where <var><b>install_dir</b></var> is the directory where the Apache
* River release is installed. This command line would result in the creation
* of a wrapper JAR file, <code>wrapper.jar</code>, in the current working
* directory, whose contents would be based on the source JAR files
* <code>src1.jar</code> and <code>src2.jar</code> (as well as any other JAR
* files referenced transitively through their <code>Class-Path</code>
* attributes or JAR indexes).  The paths for <code>src1.jar</code> and
* <code>src2.jar</code>, as well as any transitively referenced JAR files,
* would be resolved relative to the <code>base_dir</code> directory.  The
* <code>Class-Path</code> attribute of <code>wrapper.jar</code> would use
* HTTPMD URLs with SHA-1 digests.  If any of the HTTPMD URLs encountered is
* found to be invalid and can not be resolved, the <code>JarWrapper</code>
* operation will fail.
* <p>
* The equivalent programmatic invocation of <code>JarWrapper</code> would be:
* <blockquote><pre>
* JarWrapper.wrap("wrapper.jar", "base_dir", new String[]{ "src1.jar", "src2.jar" }, "SHA-1", true, "manifest.mf" );
* </pre></blockquote>
*
* <p>
* <a name="logging"><h3>Logging</h3></a>
* <p>
* <code>JarWrapper</code> uses the {@link Logger} named
* <code>com.sun.jini.tool.JarWrapper</code> to log information at the
* following logging levels:
* <p>
* <table border="1" cellpadding="5"
*     summary="Describes logging performed by JarWrapper at different
*       logging levels">
* <caption halign="center" valign="top"><b><code>
*    com.sun.jini.tool.JarWrapper</code></b></caption>
*
*   <tr> <th scope="col"> Level   <th scope="col"> Description </tr>
*   <tr>
*     <td> {@link Level#WARNING WARNING}
*     <td> Generated JAR index entries that do not end in <code>".jar"</code>
*   </tr>
*   <tr>
*     <td> {@link Level#FINE FINE}
*     <td> Names of processed source JAR files and output wrapper JAR file
*   </tr>
*   <tr>
*     <td> {@link Level#FINER FINER}
*     <td> Processing of <code>Main-Class</code> and <code>Class-Path</code>
*          attributes, and presence of preferred lists and JAR indexes
*   </tr>
*   <tr>
*     <td> {@link Level#FINEST FINEST}
*     <td> Processing and compilation of preferred lists and JAR indexes
*   </tr>
* </table>
*
* @author Sun Microsystems, Inc.
* @since 2.0
*/
public class JarWrapper {

    private static ResourceBundle resources;
    private static final Object resourcesLock = new Object();
    private static final Logger logger =
  Logger.getLogger(JarWrapper.class.getName());

    private final File destJar;
    /**
     * Base directory, <code>null</code> in case the JAR files are specified as
     * absolute files, the so called flatten classpath option.
     */
    private final File baseDir;
    private final SourceJarURL[] srcJars;
    private final Manifest manifest;
    private final MessageDigest digest;
    private final JarIndexWriter indexWriter;
    private final PreferredListWriter prefWriter;
    private final StringBuffer classPath = new StringBuffer();
    private String mainClass = null;
    private final Set seenJars = new HashSet();

    static private final String DEFAULT_HTTPMD_ALGORITHM = "SHA-1";

    /**
     * Initializes JarWrapper based on the given values.
     */
    private JarWrapper(String destJar,
           String baseDir,
           String[] srcJars,
           String httpmdAlg,
           boolean index,
           Manifest mf,
           List apiClasses)
    {
  this.destJar = new File(destJar);
  if (this.destJar.exists()) {
      throw new LocalizedIllegalArgumentException(
    "jarwrapper.fileexists", destJar);
  }
  if (baseDir != null) {
  this.baseDir = new File(baseDir);
  if (!this.baseDir.isDirectory()) {
      throw new LocalizedIllegalArgumentException(
    "jarwrapper.invalidbasedir", baseDir);
  }
  }
  else {
      this.baseDir = null;
  }
  this.srcJars = new SourceJarURL[srcJars.length];
  for (int i = 0; i < srcJars.length; i++) {
      try {
    SourceJarURL url;
    if (baseDir == null) {
        File file = new File(srcJars[i]);
        url = new SourceJarURL(file.getName(),
      file.getParentFile());
    }
    else {
        url = new SourceJarURL(srcJars[i]);
    }
    if (url.algorithm != null) {
        throw new LocalizedIllegalArgumentException(
      "jarwrapper.urlhasdigest", url);
    }
    this.srcJars[i] = url;
      } catch (LocalizedIOException e) {
    throw new LocalizedIllegalArgumentException(e);
      } catch (IOException e) {
    throw (IllegalArgumentException)
        new IllegalArgumentException(e.getMessage()).initCause(e);
      }
  }
  if (httpmdAlg != null) {
      try {
    digest = MessageDigest.getInstance(httpmdAlg);
      } catch (NoSuchAlgorithmException e) {
    throw (IllegalArgumentException)
        new LocalizedIllegalArgumentException(
      "jarwrapper.invalidhttpmdalg", httpmdAlg).initCause(e);
      }
  } else {
      digest = null;
  }
        manifest = mf != null ? new Manifest(mf) : new Manifest();
  indexWriter = index ? new JarIndexWriter() : null;
  List classes = new ArrayList();
  if (apiClasses != null) {
      for (Iterator classNames = apiClasses.iterator();
        classNames.hasNext();) {
    String className = (String) classNames.next();
    if (className != null) {
        classes.add(className.replace('.', '/') + ".class");
    }
      }
  }
  prefWriter = new PreferredListWriter(classes);
    }

    /**
     * Generates a wrapper JAR file for the specified JAR files.  The command
     * line arguments are:
     * <pre>
     * [ <var>options</var> ] <var>dest-jar</var> <var>base-dir</var> <var>src-jar</var> [ <var>src-jar</var> ...]
     * </pre>
     * The <var>dest-jar</var> argument specifies the name of the wrapper JAR
     * file to generate.  The <var>base-dir</var> argument specifies the base
     * directory from which to locate source JAR files to wrap.  The
     * <var>src-jar</var> arguments are non-absolute URLs to "top-level" source
     * JAR files relative to <var>base-dir</var>; they also constitute the
     * basis of the <code>Class-Path</code> attribute included in the generated
     * wrapper JAR file.  JAR files not present in the command line but
     * indirectly referenced via JAR index or <code>Class-Path</code> entries
     * in source JAR files will themselves be used as source JAR files, and
     * will appear alongside the top-level source JAR files in the
     * <code>Class-Path</code> attribute of the wrapper JAR file in depth-first
     * order, with JAR index references appearing before
     * <code>Class-Path</code> references.  This utility does not modify any
     * source JAR files.
     * <p>
     * If any of the top-level source JAR files contain preferred resources (as
     * indicated by a preferred list in the JAR file), then a preferred list
     * describing resource preferences across all source JAR files will be
     * included in the wrapper JAR file.  The preferred list of a top-level
     * source JAR file is interpreted as applying to that JAR file along with
     * all JAR files transitively referenced by it through JAR index or
     * <code>Class-Path</code> entries, excluding JAR files that have already
     * been encountered in the processing of preceding top-level JAR files.  If
     * a given top-level source JAR file does not contain a preferred list,
     * then all resources contained in it and its transitively referenced JAR
     * files (again, excluding those previously encountered) are considered not
     * preferred.  Preferred lists are described further in the documentation
     * for {@link net.jini.loader.pref.PreferredClassLoader
     * PreferredClassLoader}.
     * <p>
     * If any of the top-level source JAR files declare a
     * <code>Main-Class</code> manifest entry, then the wrapper JAR file will
     * include a <code>Main-Class</code> manifest entry whose value is that of
     * the first top-level source JAR file listed on the command line which
     * defines a <code>Main-Class</code> entry.
     * <p>
     * Note that attribute values generated by this utility, such as those for
     * the <code>Class-Path</code> and <code>Main-Class</code> attributes
     * described above, do not take precedence over values for the same
     * attributes contained in a manifest file explicitly specified using the
     * <code>-manifest</code> option (described below).
     * <p>
     * Supported options for this tool include:
     * <p>
     * <dl>
     *   <dt> <code>-verbose</code>
     *   <dd> Sets the level of the <code>com.sun.jini.tool.JarWrapper</code>
     *         logger to <code>Level.FINER</code>.
     *        <p>
     *   <dt> <code>-httpmd[=algorithm]</code>
     *   <dd> Use (relative) HTTPMD URLs in the <code>Class-Path</code>
     *        attribute of the generated wrapper JAR file.  The default is to
     *        use HTTP URLs.  Digests for HTTPMD URLs are calculated using the
     *        given algorithm, or SHA-1 if none is specified.
     *        <p>
     *   <dt> <code>-noindex</code>
     *   <dd> Do not include a JAR index in the generated wrapper JAR file.  The
     *        default is to compile an index based on the contents of the
     *        source JAR files.
     *        <p>
     *   <dt> <code>-manifest=<I>file</I></code>
     *   <dd> Specifies a manifest file containing attribute values to include
     *        in the manifest file inside the generated wrapper JAR file.
     *         This allows enables users to  add additional metadata or
     *        override JarWrapper's generated  values to customize the resulting
     *        manifest.  The values contained in this optional file take
     *        precedence over the generated content. This flag is conceptually
     *        similar to the jar utilities <code>m</code> flag. In the current
     *        version there are four possible attributes that can be overridden
     *        in the target Manifest.  These are
     *        <code>Name.MANIFEST_VERSION</code>,
     *        <code>Name("Created-By")</code>, <code>Name.CLASS_PATH</code> and
     *        <code>Name.MAIN_CLASS</code>.  Any additonal attributes beyond
     *        these four will be appended to the manifest attribute list and
     *        will appear in the resultant <code>MANIFEST.MF</code> file.
     * </dl>
     */
    public static void main(String[] args) {
  String destJar;
  String baseDir;
  String[] srcJars;
  String httpmdAlg = null;
  boolean index = true;
        Manifest mf = null;

  int i = 0;
  while (i < args.length && args[i].startsWith("-")) {
      String s = args[i++];
      if (s.equals("-help")) {
    System.err.println(localize("jarwrapper.usage"));
    System.exit(0);
      } else if (s.equals("-verbose")) {
    setLoggingLevel(Level.FINER);
      } else if (s.equals("-debug")) {
    setLoggingLevel(Level.ALL);
      } else if (s.equals("-httpmd") || s.startsWith("-httpmd=")) {
    if (httpmdAlg != null) {
        System.err.println(localize("jarwrapper.multiplehttpmd"));
        System.err.println(localize("jarwrapper.usage"));
        System.exit(1);
    }
    int split = s.indexOf('=');
    httpmdAlg = (split != -1) ?
                    s.substring(split + 1) : DEFAULT_HTTPMD_ALGORITHM;
            } else if (s.startsWith("-manifest=")) {
                int split = s.indexOf('=');
                String fileName = s.substring(split + 1);
                try {
                    mf = retrieveManifest(fileName);
                } catch (IOException ioe) {
        System.err.println(localize("jarwrapper.badmanifest", s));
              System.exit(1);
                }
      } else if (s.equals("-noindex")) {
    index = false;
      } else {
    System.err.println(localize("jarwrapper.badoption", s));
    System.err.println(localize("jarwrapper.usage"));
    System.exit(1);
      }
  }
  if (args.length - i < 3) {
      System.err.println(localize("jarwrapper.insufficientargs"));
      System.err.println(localize("jarwrapper.usage"));
      System.exit(1);
  }
  destJar = args[i++];
  baseDir = args[i++];
  srcJars = new String[args.length - i];
  System.arraycopy(args, i, srcJars, 0, srcJars.length);

  try {
      wrap(destJar, baseDir, srcJars, httpmdAlg, index, mf);
  } catch (Throwable t) {
      if (t instanceof LocalizedIllegalArgumentException ||
    t instanceof LocalizedIOException)
      {
    System.err.println(t.getMessage());
      } else {
    System.err.println(localize("jarwrapper.fatalexception"));
    t.printStackTrace();
      }
      System.exit(1);
  }
    }

    /**
     * Invokes {@link #wrap(String, String, String[], String, boolean, Manifest)
     * wrap} with the provided values and a <code>null</code> manifest.
     *
     * @param destJar name of the wrapper JAR file to generate
     * @param baseDir base directory from which to locate source JAR
     * files to wrap
     * @param srcJars list of top-level source JAR files to process
     * @param httpmdAlg name of algorithm to use for generating HTTPMD URLs, or
     * <code>null</code> if plain HTTP URLs should be used
     * @param index if <code>true</code>, generate a JAR index; if
     * <code>false</code>, do not generate one
     * @throws IOException if an I/O error occurs while processing source JAR
     * files or generating the wrapper JAR file
     * @throws IllegalArgumentException if the provided values are invalid
     * @throws NullPointerException if <code>destJar</code>,
     * <code>baseDir</code>, <code>srcJars</code>, or any element of
     * <code>srcJars</code> is <code>null</code>
     */
    public static void wrap(String destJar,
          String baseDir,
          String[] srcJars,
          String httpmdAlg,
          boolean index)
  throws IOException
    {
  wrap(destJar, baseDir, srcJars, httpmdAlg, index, null);
    }

    /**
     * Generates a wrapper JAR file based on the provided values in the same
     * manner as described in the documentation for {@link #main}.  The only
     * difference between this method and <code>main</code> is that it receives
     * its values as explicit arguments instead of in a command line, and
     * indicates failure by throwing an exception.
     *
     * @param destJar name of the wrapper JAR file to generate
     * @param baseDir base directory from which to locate source JAR
     * files to wrap
     * @param srcJars list of top-level source JAR files to process
     * @param httpmdAlg name of algorithm to use for generating HTTPMD URLs, or
     * <code>null</code> if plain HTTP URLs should be used
     * @param index if <code>true</code>, generate a JAR index; if
     * <code>false</code>, do not generate one
     * @param mf manifest containing values to include in the manifest file
     * of the generated wrapper JAR file
     *
     * @throws IOException if an I/O error occurs while processing source JAR
     * files or generating the wrapper JAR file
     * @throws IllegalArgumentException if the provided values are invalid
     * @throws NullPointerException if <code>destJar</code>,
     * <code>baseDir</code>, <code>srcJars</code>, or any element of
     * <code>srcJars</code> is <code>null</code>
     * @since 2.1
     */
    public static void wrap(String destJar,
                            String baseDir,
                            String[] srcJars,
                            String httpmdAlg,
                            boolean index,
                            Manifest mf)
        throws IOException
    {
  wrap(destJar, baseDir, srcJars, httpmdAlg, index, mf, null);
    }

    /**
     * Generates a wrapper JAR file based on the provided values in the same
     * manner as described in the documentation for {@link #main}.  The only
     * difference between this method and <code>main</code> is that it receives
     * its values as explicit arguments instead of in a command line, and
     * indicates failure by throwing an exception.
     *
     * @param destJar name of the wrapper JAR file to generate
     * @param baseDir base directory from which to locate source JAR
     * files to wrap
     * @param srcJars list of top-level source JAR files to process
     * @param httpmdAlg name of algorithm to use for generating HTTPMD URLs, or
     * <code>null</code> if plain HTTP URLs should be used
     * @param index if <code>true</code>, generate a JAR index; if
     * <code>false</code>, do not generate one
     * @param mf manifest containing values to include in the manifest file
     * of the generated wrapper JAR file
     * @param apiClasses list of binary class names (type <code>String</code>)
     * that must be considered API classes in case a preferences conflict
     * arises during wrapping of the JAR files, or <code>null</code> in case
     * no such list is available
     *
     * @throws IOException if an I/O error occurs while processing source JAR
     * files or generating the wrapper JAR file
     * @throws IllegalArgumentException if the provided values are invalid
     * @throws NullPointerException if <code>destJar</code>,
     * <code>baseDir</code>, <code>srcJars</code>, or any element of
     * <code>srcJars</code> is <code>null</code>
     */
    public static void wrap(String destJar,
                            String baseDir,
                            String[] srcJars,
                            String httpmdAlg,
                            boolean index,
                            Manifest mf,
                            List apiClasses)
        throws IOException
    {
        new JarWrapper(destJar, baseDir, srcJars, httpmdAlg, index, mf,
      apiClasses).wrap();
    }

    /**
     * Generates a wrapper JAR file based on the provided values in the same
     * manner as described in the documentation for {@link #main}.
     * <p>
     * The difference between this method and the 6 and 7-arg <code>wrap</code>
     * method is that the source JAR files must be specified by an absolute path
     * and that for processing the classpath will be flattened, i.e. each source
     * JAR file will be considered as relative to its parent directory (that
     * will serve as a virtual base directory) for the assembly of the
     * <code>Class-Path</code> entry.
     *
     * @param destJar name of the wrapper JAR file to generate
     * @param srcJars list of top-level source JAR files to process, must be
     * absolute paths
     * @param httpmdAlg name of algorithm to use for generating HTTPMD URLs, or
     * <code>null</code> if plain HTTP URLs should be used
     * @param index if <code>true</code>, generate a JAR index; if
     * <code>false</code>, do not generate one
     * @param mf manifest containing values to include in the manifest file
     * of the generated wrapper JAR file
     * @param apiClasses list of binary class names (type <code>String</code>)
     * that must be considered API classes in case a preferences conflict
     * arises during wrapping of the JAR files, or <code>null</code> in case
     * no such list is available
     *
     * @throws IOException if an I/O error occurs while processing source JAR
     * files or generating the wrapper JAR file
     * @throws IllegalArgumentException if the provided values are invalid
     * @throws NullPointerException if <code>destJar</code>,
     * <code>srcJars</code>, or any element of <code>srcJars</code> is
     * <code>null</code>
     */
    public static void wrap(String destJar,
                            String[] srcJars,
                            String httpmdAlg,
                            boolean index,
                            Manifest mf,
                            List apiClasses)
        throws IOException
    {
        new JarWrapper(destJar, null, srcJars, httpmdAlg, index, mf,
      apiClasses).wrap();
    }

    /**
     * Processes source JAR files and outputs wrapper JAR file.
     */
    private void wrap() throws IOException {
  for (int i = 0; i < srcJars.length; i++) {
      process(srcJars[i], null);
  }
  outputWrapperJar();
    }

    /**
     * Processes source JAR file indicated by the given URL, determining
     * preferred resources using the provided preferred list reader.  If the
     * preferred list reader is null, then the URL is for a top-level source
     * JAR file, in which case the preferred list of the JAR file should be
     * read, if the JAR file has not already been processed.
     */
    private void process(SourceJarURL url, PreferredListReader prefReader)
  throws IOException
    {
  File file = baseDir == null ? url.toFile() : url.toFile(baseDir);
  boolean seen = seenJars.contains(file);
  boolean checkMainClass = mainClass == null && prefReader == null;
  if (seen && !checkMainClass) {
      return;
  }

  if (logger.isLoggable(Level.FINE)) {
      logger.log(Level.FINE, "processing {0}", new Object[]{ file });
  }
  if (!file.exists()) {
      throw new LocalizedIOException("jarwrapper.filenotfound", file);
  }
  JarFile jar = new JarFile(file, false);

  if (checkMainClass) {
      mainClass = getMainClass(jar);
  }
  if (!seen) {
      seenJars.add(file);

      if (digest != null) {
    url = new SourceJarURL(
        url.path,
        digest.getAlgorithm(),
        getDigestString(digest, file),
        null);
      }
      if (classPath.length() > 0) {
    classPath.append(' ');
      }
      classPath.append(url);

      if (indexWriter != null) {
    indexWriter.addEntries(jar, url);
      }
      if (prefReader == null) {
    prefReader = new PreferredListReader(jar);
      }
      prefWriter.addEntries(jar, prefReader);

      List l = new ArrayList();
      l.addAll(new JarIndexReader(jar).getJars());
      l.addAll(getClassPath(jar));
      for (Iterator i = l.iterator(); i.hasNext(); ) {
    SourceJarURL u = (SourceJarURL) i.next();
    u = url.resolve(new SourceJarURL(u.path, null, null, null));
    process(u, prefReader);
      }
  }
    }

    /**
     * Returns URLs contained in the Class-Path attribute (if any) of the given
     * JAR file, as a list of SourceJarURL instances.
     */
    private List getClassPath(JarFile jar) throws IOException {
  Manifest mf = jar.getManifest();
  if (mf == null) {
      return Collections.EMPTY_LIST;
  }
  Attributes atts = mf.getMainAttributes();
  String cp = atts.getValue(Name.CLASS_PATH);
  if (cp == null) {
      return Collections.EMPTY_LIST;
  }
  if (logger.isLoggable(Level.FINER)) {
      logger.log(Level.FINER, "Class-Path: {0}", new Object[]{ cp });
  }
  List l = new ArrayList();
  for (StringTokenizer tok = new StringTokenizer(cp, " ");
       tok.hasMoreTokens(); )
  {
      SourceJarURL url = new SourceJarURL(tok.nextToken());
      if (digest != null && url.algorithm == null) {
    throw new LocalizedIOException("jarwrapper.nonhttpmdurl", url);
      }
      l.add(url);
  }
  return l;
    }


    /**
     * Writes wrapper JAR file based on information from processed source JARs.
     */
    private void outputWrapperJar() throws IOException {
  if (logger.isLoggable(Level.FINE)) {
      logger.log(Level.FINE, "writing {0}", new Object[]{ destJar });
  }
  Attributes atts = manifest.getMainAttributes();
        if (atts.get(Name.MANIFEST_VERSION) == null)
      atts.put(Name.MANIFEST_VERSION, "1.0");
        Name creatorName = new Name("Created-By");
        if (atts.get(creatorName) == null )
      atts.put(creatorName, JarWrapper.class.getName());
        if (atts.get(Name.CLASS_PATH) == null)
      atts.put(Name.CLASS_PATH, classPath.toString());
        if ((atts.get(Name.MAIN_CLASS) == null) && (mainClass != null)) {
      atts.put(Name.MAIN_CLASS, mainClass);
  }

  boolean completed = false;
  try {
      JarOutputStream jout =
    new JarOutputStream(new FileOutputStream(destJar), manifest);
      if (indexWriter != null) {
    indexWriter.write(jout);
      }
      prefWriter.write(jout);
      jout.close();
      completed = true;
  } finally {
      if (!completed) {
    deleteWrapperJar();
      }
  }
    }

    /**
     * Attempts to delete wrapper JAR file.
     */
    private void deleteWrapperJar() {
  try {
      if (!destJar.delete() && logger.isLoggable(Level.WARNING)) {
    logger.log(
        Level.WARNING,
        "failed to delete {0}",
        new Object[]{ destJar });
      }
  } catch (Throwable t) {
      logger.log(
    Level.WARNING, "exception deleting wrapper JAR file", t);
  }
    }

    /**
     * Returns the value of the Main-Class attribute of the given JAR file, or
     * null if none is present.
     */
    private static String getMainClass(JarFile jar) throws IOException {
  Manifest mf = jar.getManifest();
  if (mf == null) {
      return null;
  }
  Attributes atts = mf.getMainAttributes();
  String mc = atts.getValue(Name.MAIN_CLASS);
  if (mc != null && logger.isLoggable(Level.FINER)) {
      logger.log(Level.FINER, "Main-Class: {0}", new Object[]{ mc });
  }
  return mc;
    }

    /**
     * Returns a string representation of the message digest of the given file.
     */
    private static String getDigestString(MessageDigest digest, File file)
  throws IOException
    {
  FileInputStream fin = new FileInputStream(file);
  byte[] buf = new byte[2048];
  int n;
  while ((n = fin.read(buf)) >= 0) {
      digest.update(buf, 0, n);
  }
  buf = digest.digest();
  fin.close();

  StringBuffer sb = new StringBuffer(buf.length * 2);
  for (int i = 0; i < buf.length; i++) {
      byte b = buf[i];
      sb.append(Character.forDigit((b >> 4) & 0xf, 16));
      sb.append(Character.forDigit(b & 0xf, 16));
  }
  return sb.toString();
    }

    /**
     * Sets logging and console handler output level.
     */
    private static void setLoggingLevel(Level level) {
  logger.setLevel(level);
  for (Logger l = logger; l != null; l = l.getParent()) {
      Handler[] handlers = l.getHandlers();
      for (int i = 0; i < handlers.length; i++) {
    if (handlers[i] instanceof ConsoleHandler) {
        handlers[i].setLevel(level);
    }
      }
      if (!l.getUseParentHandlers()) {
    break;
      }
  }
    }

    /**
     * Returns localized message text corresponding to the given key string.
     */
    static String localize(String key) {
  return localize(key, new Object[0]);
    }

    /**
     * Returns localized message text corresponding to the given key string,
     * passing the provided value as an argument when formatting the message.
     */
    static String localize(String key, Object val) {
  return localize(key, new Object[] { val });
    }

    /**
     * Returns localized message text corresponding to the given key string,
     * passing the provided values as an argument when formatting the message.
     */
    static String localize(String key, Object[] vals) {
  String fmt = getResourceString(key);
  if (fmt == null) {
      return "error: no text found in resource bundle for key: " + key;
  }
  return MessageFormat.format(fmt, vals);
    }

    /**
     * Returns localized format string, obtained from the resource bundle for
     * JarWrapper, that corresponds to the given key, or null if the resource
     * bundle does not contain a corresponding string.
     */
    private static String getResourceString(String key) {
  synchronized (resourcesLock) {
      if (resources == null) {
    resources = ResourceBundle.getBundle(
        "com.sun.jini.tool.resources.jarwrapper");
      }
  }
  try {
      return resources.getString(key);
  } catch (MissingResourceException e) {
      return null;
  }
    }

    /**
     * Returns the Manifest object derived from a manifest file as specified
     * on the command line.
     */
    private static Manifest retrieveManifest(String fileName)
        throws IOException
    {
        FileInputStream fis = new FileInputStream(fileName);
        Manifest mf = new Manifest(fis);
        fis.close();
        return mf;
    }

    /**
     * IllegalArgumentException with a localized detail message.
     */
    private static class LocalizedIllegalArgumentException
  extends IllegalArgumentException
    {
  private static final long serialVersionUID = 0L;

  /**
   * Creates exception with localized message text corresponding to the
   * given key string, passing the provided value as an argument when
   * formatting the message.
   */
  LocalizedIllegalArgumentException(String key, Object val) {
      super(localize(key, val));
  }

  /**
   * Creates exception with the localized message text of the given
   * cause.
   */
  LocalizedIllegalArgumentException(LocalizedIOException cause) {
      super(cause.getMessage());
      initCause(cause);
  }
    }

    /**
     * IOException with a localized detail message.
     */
    private static class LocalizedIOException extends IOException {

  private static final long serialVersionUID = 0L;

  /**
   * Creates exception with localized message text corresponding to the
   * given key string, passing the provided value as an argument when
   * formatting the message.
   */
  LocalizedIOException(String key, Object val) {
      super(localize(key, val));
  }

  /**
   * Creates exception with localized message text corresponding to the
   * given key string, passing the provided values as an argument when
   * formatting the message.
   */
  LocalizedIOException(String key, Object[] vals) {
      super(localize(key, vals));
  }
    }

    /**
     * Represents URL to a source JAR file.  Source JAR URLs must be relative,
     * and may contain HTTPMD digests.
     */
    private static class SourceJarURL {

  private static final Pattern httpmdPattern =
      Pattern.compile("(.*);(.+?)=(.+?)(?:,(.*))?$");

  /** raw URL string, including HTTPMD information (if any) */
  final String raw;
  /** URL path component, excluding any HTTPMD information */
  final String path;
  /** HTTPMD digest algorithm, or null if non-HTTPMD URL */
  final String algorithm;
  /** HTTPMD digest value, or null if non-HTTPMD URL */
  final String digest;
  /** HTTPMD digest comment, or null if non-HTTPMD URL */
  final String comment;
  /**
   * Base directory associated with relative path for JAR file, only
   * set in case the flatten classpath option is used.
   */
  private File baseDir;

  /**
   * Creates SourceJarURL based on given raw URL string.
   */
  SourceJarURL(String raw) throws IOException {
      try {
    this.raw = raw;
    Matcher m = httpmdPattern.matcher(raw);
    if (m.matches()) {
        path = m.group(1);
        algorithm = m.group(2);
        digest = m.group(3);
        comment = m.group(4);
    } else {
        path = raw;
        algorithm = null;
        digest = null;
        comment = null;
    }

    URI uri = new URI(path);
    if (uri.getScheme() != null) {
        throw new LocalizedIOException(
      "jarwrapper.urlhasscheme", raw);
    } else if (uri.getAuthority() != null) {
        throw new LocalizedIOException(
      "jarwrapper.urlhasauthority", raw);
    }
    String p = uri.getPath();
    if (p == null || p.length() == 0) {
        throw new LocalizedIOException(
      "jarwrapper.urlemptypath", raw);
    } else if (p.startsWith("/")) {
        throw new LocalizedIOException(
      "jarwrapper.urlabsolute", raw);
    }
      } catch (URISyntaxException e) {
    throw (IOException) new LocalizedIOException(
        "jarwrapper.invalidurlsyntax", raw).initCause(e);
      }
  }

  /**
   * Creates SourceJarURL based on given raw URL string that has an
   * individual associated base directory.
   */
  SourceJarURL(String raw, File baseDir) throws IOException {
      this(raw);

      this.baseDir = baseDir;
  }

  /**
   * Creates SourceJarURL based on given components.
   */
  SourceJarURL(String path,
         String algorithm,
         String digest,
         String comment)
  {
      if (algorithm != null) {
    raw = path + ';' + algorithm + '=' + digest +
          ((comment != null) ? ',' + comment : "");
      } else {
    raw = path;
      }
      this.path = path;
      this.algorithm = algorithm;
      this.digest = digest;
      this.comment = comment;
  }

  /**
   * Resolves given URL relative to this URL.
   */
  SourceJarURL resolve(SourceJarURL other) {
      try {
    // hack around URI bug 4548698 by temporarily prepending slash
    URI uri = new URI('/' + path);
    String p = uri.resolve(other.path).getPath().substring(1);
    return new SourceJarURL(
        p, other.algorithm, other.digest, other.comment);
      } catch (URISyntaxException e) {
    throw new AssertionError(e);
      }
  }

  /**
   * Returns file represented by this URL.
   */
  File toFile() {
      return toFile(baseDir);
  }

  /**
   * Returns file represented by this URL.
   */
  File toFile(File base) {
      try {
    String p = new URI(path).getPath()// decode path
    return new File(base, p.replace('/', File.separatorChar));
      } catch (URISyntaxException e) {
    throw (Error) new InternalError().initCause(e);
      }
  }

  public boolean equals(Object obj) {
      return obj instanceof SourceJarURL &&
       raw.equals(((SourceJarURL) obj).raw);
  }

  public int hashCode() {
      return raw.hashCode();
  }

  public String toString() {
      return raw;
  }
    }

    /**
     * Parses JAR indexes.
     */
    private static class JarIndexReader {

  private static final Pattern headerPattern =
      Pattern.compile("^JarIndex-Version:\\s*(.*?)$");
  private static final Pattern versionPattern =
      Pattern.compile("^1(\\.\\d+)*$");

  private final List jars;

  /**
   * Parses the given JAR file's JAR index, if any.
   */
  JarIndexReader(JarFile jar) throws IOException {
      List l = new ArrayList();
      jars = Collections.unmodifiableList(l);
      JarEntry ent = jar.getJarEntry("META-INF/INDEX.LIST");
      if (ent == null) {
    return;
      }
      logger.finer("reading JAR index");
      BufferedReader r = new BufferedReader(
    new InputStreamReader(jar.getInputStream(ent), "UTF8"));

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

      s = r.readLine();
      if (s == null) {
    throw new IOException("truncated JAR index");
      }
      s = s.trim();
      if (s.length() > 0) {
    throw new IOException(
        "non-empty line after JAR index header: " + s);
      }

      while ((s = r.readLine()) != null) {
    SourceJarURL url = new SourceJarURL(s.trim());
    if (logger.isLoggable(Level.FINEST)) {
        logger.log(
      Level.FINEST,
      "JAR index references {0}",
      new Object[]{ url });
    }
    l.add(url);
    do {
        s = r.readLine();
    } while (s != null && s.trim().length() > 0);
      }
      if (l.isEmpty()) {
    throw new IOException("empty JAR index");
      }
  }

  /**
   * Returns list of SourceJarURLs representing the JAR files referenced
   * in the JAR index.
   */
  List getJars() {
      return jars;
  }
    }

    /**
     * Assembles and writes JAR indexes.
     */
    private static class JarIndexWriter {

  private final List urls = new ArrayList();
  private final Map contentMap = new HashMap();

  JarIndexWriter() {
  }

  /**
   * Tabulates contents of the given JAR file, associating them with the
   * provided URL for the JAR file.
   */
  void addEntries(JarFile jar, SourceJarURL url) {
      Set contents = new HashSet();
      for (Enumeration e = jar.entries(); e.hasMoreElements();) {
    String name = ((JarEntry) e.nextElement()).getName();
    if (!(name.startsWith("META-INF") || name.endsWith("/"))) {
        int pos = name.lastIndexOf("/");
        contents.add((pos != -1) ? name.substring(0, pos) : name);
    }
      }
      if (!contents.isEmpty()) {
    urls.add(url);
    contentMap.put(url, contents);
      }
  }

  /**
   * Writes JAR index to the given output stream.
   */
  void write(JarOutputStream jout) throws IOException {
      if (contentMap.isEmpty()) {
    logger.finer("omitting empty JAR index");
    return;
      }
      logger.finer("writing JAR index");
      jout.putNextEntry(new JarEntry("META-INF/INDEX.LIST"));
      Writer w =
    new BufferedWriter(new OutputStreamWriter(jout, "UTF8"));
      w.write("JarIndex-Version: 1.0\n\n");

      // preserve original insertion order
      for (Iterator i = urls.iterator(); i.hasNext();) {
    SourceJarURL url = (SourceJarURL) i.next();
    Set contents = (Set) contentMap.get(url);

    if (!url.raw.endsWith(".jar")) {
        if (url.algorithm != null) {
      url = new SourceJarURL(
          url.path, url.algorithm, url.digest, ".jar");
        } else if (logger.isLoggable(Level.WARNING)) {
      logger.log(
          Level.WARNING,
          "JAR index entry {0} does not end in .jar",
          new Object[]{ url });
        }
    }
    if (logger.isLoggable(Level.FINEST)) {
        logger.log(
      Level.FINEST,
      "writing JAR index entry {0}: {1}",
      new Object[]{ url, contents });
    }
    w.write(url + "\n");
    for (Iterator j = contents.iterator(); j.hasNext(); ) {
        w.write(j.next() + "\n");
    }
    w.write("\n");
      }
      w.flush();
      jout.closeEntry();
  }
    }

    /**
     * Parses preferred lists.
     */
    private static class PreferredListReader {

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

  private final boolean defaultPref;
  private final Map namePrefs = new HashMap();
  private final Map packagePrefs = new HashMap();
  private final Map subtreePrefs = new HashMap();

  /**
   * Parses the given JAR file's preferred list, if any.
   */
  PreferredListReader(JarFile jar) throws IOException {
      JarEntry ent = jar.getJarEntry("META-INF/PREFERRED.LIST");
      if (ent == null) {
    defaultPref = false;
    return;
      }
      logger.finer("reading preferred list");
      BufferedReader r = new BufferedReader(
    new InputStreamReader(jar.getInputStream(ent), "UTF8"));

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

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

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

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

    String key;
    Map map;
    if (name.endsWith("/*")) {
        key = name.substring(0, name.length() - 2);
        map = packagePrefs;
    } else if (name.endsWith("/")) {
        key = name.substring(0, name.length() - 1);
        map = packagePrefs;
    } else if (name.endsWith("/-")) {
        key = name.substring(0, name.length() - 2);
        map = subtreePrefs;
    } else {
        key = name;
        map = namePrefs;
    }
    if (key.length() == 0) {
        throw new IOException(
      "invalid preferred entry name: " + name);
    }
    map.put(key, pref);
    if (logger.isLoggable(Level.FINEST)) {
        logger.log(
      Level.FINEST,
      "read preferred list entry {0}: {1}",
      new Object[]{ name, pref });
    }

    s = nextNonBlankLine(r);
      }
  }

  /**
   * Returns true if list prefers given entry, or false otherwise.
   */
   boolean isPreferred(String entry) {
       Boolean b = (Boolean) namePrefs.get(entry);
       if (b != null) {
     return b.booleanValue();
       }

       if (entry.endsWith(".class")) {
     int i = entry.lastIndexOf('$');
     while (i >= 0) {
         String outer = entry.substring(0, i) + ".class";
         if ((b = (Boolean) namePrefs.get(outer)) != null) {
       return b.booleanValue();
         }
         i = entry.lastIndexOf('$', i - 1);
     }
       }

       int i = entry.lastIndexOf('/');
       if (i >= 0) {
     String base = entry.substring(0, i);
     if ((b = (Boolean) packagePrefs.get(base)) != null) {
         return b.booleanValue();
     }

     for (;;) {
         if ((b = (Boolean) subtreePrefs.get(base)) != null) {
       return b.booleanValue();
         }
         if ((i = base.lastIndexOf('/')) < 0) {
       break;
         }
         base = base.substring(0, i);
     }
       }

       return defaultPref;
   }

  /**
   * Returns next non-blank, non-comment line, or null if end of file has
   * been reached.
   */
  private static String nextNonBlankLine(BufferedReader reader)
      throws IOException
  {
      String s;
      while ((s = reader.readLine()) != null) {
    s = s.trim();
    if (s.length() > 0 && s.charAt(0) != '#') {
        return s;
    }
      }
      return null;
  }
    }

    /**
     * Compiles and writes combined preferred lists.
     */
    private static class PreferredListWriter {

  private static final int NAME_LEN      = "Name: ".length();
  private static final int PREFERRED_LEN = "Preferred: ".length();
  private static final int TRUE_LEN      = "true".length();
  private static final int FALSE_LEN     = "false".length();
  private static final int NEWLINE_LEN   = "\n".length();

  private final HashMap pathMap = new HashMap();
  private final DirNode rootNode = new DirNode("");
  private int numPrefs = 0;
  private final List apiClasses;


  /**
   * Constructs a <code>PreferredListWriter</code>.
   *
   * @param apiClasses list of URI paths representing classes that must be
   * considered API classes in case a preferences conflict arrises during
   * wrapping of JAR files
   */
  PreferredListWriter(List apiClasses) {
      this.apiClasses = apiClasses;
      pathMap.put("", rootNode);
  }

  /**
   * Records preferred status of each file entry in the given JAR file,
   * determined using the provided preferred list reader.
   */
  void addEntries(JarFile jar, PreferredListReader prefReader)
      throws IOException
  {
      for (Enumeration e = jar.entries(); e.hasMoreElements(); ) {
    String path = ((JarEntry) e.nextElement()).getName();
    if (!(path.startsWith("META-INF") || path.endsWith("/"))) {
        boolean pref = prefReader.isPreferred(path);
        if (logger.isLoggable(Level.FINEST)) {
      logger.log(
          Level.FINEST,
          pref ? "preferred: {0}" : "not preferred: {0}",
          new Object[]{ path });
        }
        addFile(path, jar.getName(), pref);
    }
      }
  }

  /**
   * Writes minimal combined preferred list to given output stream.
   */
  void write(JarOutputStream jout) throws IOException {
      if (numPrefs == 0) {
    logger.finer("omitting empty preferred list");
    return;
      }
      logger.finer("writing preferred list");

      jout.putNextEntry(new JarEntry("META-INF/PREFERRED.LIST"));
      Writer w =
    new BufferedWriter(new OutputStreamWriter(jout, "UTF8"));
      w.write("PreferredResources-Version: 1.0\n");

      rootNode.compileList();
      rootNode.writeList(w);

      w.flush();
      jout.closeEntry();
  }

  /**
   * Records the preferred setting of the given file entry.
   */
  private void addFile(String path, String jarFileName, boolean preferred)
      throws IOException
  {
      FileNode fn = (FileNode) pathMap.get(path);
      if (fn != null) {
    if (fn.preferred != preferred) {
        // in case it is part of what are considered API classes
        // we correct the preferred value if required and correct
        // the total number of preferred classes encountered
        if (apiClasses.contains(path)) {
      if (fn.preferred) {
          fn.preferred = false;
          numPrefs--;
      }
        }
        else {
      throw new LocalizedIOException(
          "jarwrapper.prefconflict",
          new Object[] { path, jarFileName, fn.jarFileName });
        }
    }
    return;
      }

      fn = new FileNode(path, jarFileName, preferred);
      pathMap.put(path, fn);
      if (preferred) {
    numPrefs++;
      }

      path = parentPath(path);
      DirNode dn = (DirNode) pathMap.get(path);
      if (dn != null) {
    dn.files.add(fn);
    return;
      }
      dn = new DirNode(path);
      pathMap.put(path, dn);
      dn.files.add(fn);

      for (path = parentPath(path); ; path = parentPath(path)) {
    DirNode pn = (DirNode) pathMap.get(path);
    if (pn != null) {
        pn.subdirs.add(dn);
        return;
    }
    pn = new DirNode(path);
    pathMap.put(path, pn);
    pn.subdirs.add(dn);
    dn = pn;
      }
  }

  /**
   * Returns path of the parent directory of the indicated JAR entry.
   */
  private static String parentPath(String path) {
      if (path.endsWith("/")) {
    path = path.substring(0, path.length() - 1);
      }
      int i = path.lastIndexOf('/');
      return (i >= 0) ? path.substring(0, i + 1) : "";
  }

  static int min(int i1, int i2, int i3) {
      return Math.min(i1, Math.min(i2, i3));
  }

  /**
   * Returns the number of characters needed to write a preferred list
   * entry with the given name and preferred setting.  If the given name
   * is null, then the length of a "default" preferred list entry (i.e.,
   * an entry without a name) is returned.
   */
  static int calcEntryLength(String name, boolean pref) {
      int len = NEWLINE_LEN;
      if (name != null) {
    len += NAME_LEN + name.length() + NEWLINE_LEN;
      }
      len += PREFERRED_LEN + (pref ? TRUE_LEN : FALSE_LEN) + NEWLINE_LEN;
      return len;
  }

  /**
   * Writes preferred list entry with the given name and preferred
   * setting.  If the given name is null, then a "default" preferred list
   * entry is written.
   */
  static void writeEntry(Writer w, String name, boolean pref)
      throws IOException
  {
      if (logger.isLoggable(Level.FINEST)) {
    logger.log(
        Level.FINEST,
        "writing preferred list entry {0}: {1}",
        new Object[]{
      (name != null) ? name : "<default>",
      Boolean.valueOf(pref) });
      }
      w.write("\n");
      if (name != null) {
    w.write("Name: " + name + "\n");
      }
      w.write("Preferred: " + pref + "\n");
  }

  /**
   * Stores file preference state.
   */
  private static class FileNode {

      /* action constants */
      static final int NONE    = 0;
      static final int SKIP    = 1;
      static final int INCLUDE = 2;

      final String path;
      final String jarFileName;
      boolean preferred;
      int action;

      FileNode(String path, String jarFileName, boolean preferred) {
    this.path = path;
    this.preferred = preferred;
    this.jarFileName = jarFileName;
      }
  }

  /**
   * Represents JAR-internal directory.
   */
  private class DirNode {

      final String path;
      final List subdirs = new ArrayList();
      final List files = new ArrayList();

      /*
       * The length, in characters, of the preferred list covering this
       * directory subtree if the default preferred setting for the
       * entire subtree is true.
       */
      int prefSubtreeLen;
      /*
       * The length, in characters, of the preferred list covering this
       * directory subtree if the default preferred setting for the
       * immediate directory is true, but the default preferred setting
       * for the subtree as a whole is false.
       */
      int prefPackageLen;
      /*
       * The length, in characters, of the preferred list covering this
       * directory subtree if the default preferred setting for the
       * entire subtree is false.
       */
      int unprefSubtreeLen;
      /*
       * The length, in characters, of the preferred list covering this
       * directory subtree if the default preferred setting for the
       * immediate directory is false, but the default preferred setting
       * for the subtree as a whole is true.
       */
      int unprefPackageLen;

      DirNode(String path) {
    this.path = path;
      }

      /**
       * Computes minimal list length using dynamic programming.
       */
      void compileList() {
    int prefLen = 0, unprefLen = 0;
    for (Iterator i = files.iterator(); i.hasNext(); ) {
        FileNode fn = (FileNode) i.next();

        for (int j = fn.path.lastIndexOf('$');
       j != -1;
       j = fn.path.lastIndexOf('$', j - 1))
        {
      FileNode fn2 = (FileNode) pathMap.get(
          fn.path.substring(0, j) + ".class");
      if (fn2 != null) {
          fn.action = (fn.preferred == fn2.preferred) ?
        FileNode.SKIP : FileNode.INCLUDE;
          break;
      }
        }

        int entryLen = calcEntryLength(fn.path, fn.preferred);
        if (fn.action == FileNode.SKIP) {
      // won't list, so don't increment length counts
        } else if (fn.action == FileNode.INCLUDE) {
      prefLen += entryLen;
      unprefLen += entryLen;
        } else if (fn.preferred) {
      unprefLen += entryLen;
        } else {
      prefLen += entryLen;
        }
    }
    prefSubtreeLen = prefLen;
    prefPackageLen = prefLen;
    unprefSubtreeLen = unprefLen;
    unprefPackageLen = unprefLen;

    for (Iterator i = subdirs.iterator(); i.hasNext();) {
        DirNode dn = (DirNode) i.next();
        dn.compileList();
        String subtreePath = dn.path + "-";

        prefSubtreeLen += min(
      dn.prefSubtreeLen,
      dn.unprefSubtreeLen +
          calcEntryLength(subtreePath, false),
      dn.unprefPackageLen + calcEntryLength(dn.path, false));
        prefPackageLen += min(
      dn.prefSubtreeLen + calcEntryLength(subtreePath, true),
      dn.prefPackageLen + calcEntryLength(dn.path, true),
      dn.unprefSubtreeLen);
        unprefSubtreeLen += min(
      dn.prefSubtreeLen + calcEntryLength(subtreePath, true),
      dn.prefPackageLen + calcEntryLength(dn.path, true),
      dn.unprefSubtreeLen);
        unprefPackageLen += min(
      dn.prefSubtreeLen,
      dn.unprefSubtreeLen +
          calcEntryLength(subtreePath, false),
      dn.unprefPackageLen + calcEntryLength(dn.path, false));
    }
      }

      /**
       * Writes preferred list.  This method is only called on the
       * root node.
       */
      void writeList(Writer w) throws IOException {
    int totalPrefSubtreeLen =
        prefSubtreeLen + calcEntryLength(null, true);
    boolean defaultPref = totalPrefSubtreeLen < unprefSubtreeLen;
    if (defaultPref) {
        writeEntry(w, null, true);
    }
    writeFiles(w, defaultPref);
    for (Iterator i = subdirs.iterator(); i.hasNext();) {
        ((DirNode) i.next()).writeDir(w, defaultPref);
    }
      }

      /**
       * Writes preferred list entries (if any) for this directory, which
       * inherits the given preferred value as its default.
       */
      void writeDir(Writer w, boolean contextPref) throws IOException {
    boolean dirPref;
    boolean subdirPref;
    String subtreePath = path + "-";
    if (contextPref) {
        int totalUnprefPackageLen =
      unprefPackageLen + calcEntryLength(path, false);
        int totalUnprefSubtreeLen =
      unprefSubtreeLen + calcEntryLength(subtreePath, false);
        int best = min(
      prefSubtreeLen,
      totalUnprefPackageLen,
      totalUnprefSubtreeLen);
        if (best == prefSubtreeLen) {
      dirPref = true;
      subdirPref = true;
        } else if (best == totalUnprefPackageLen) {
      writeEntry(w, path, false);
      dirPref = false;
      subdirPref = true;
        } else {
      writeEntry(w, subtreePath, false);
      dirPref = false;
      subdirPref = false;
        }
    } else {
        int totalPrefPackageLen =
      prefPackageLen + calcEntryLength(path, true);
        int totalPrefSubtreeLen =
      prefSubtreeLen + calcEntryLength(subtreePath, true);
        int best = min(
      unprefSubtreeLen,
      totalPrefPackageLen,
      totalPrefSubtreeLen);
        if (best == unprefSubtreeLen) {
      dirPref = false;
      subdirPref = false;
        } else if (best == totalPrefPackageLen) {
      writeEntry(w, path, true);
      dirPref = true;
      subdirPref = false;
        } else {
      writeEntry(w, subtreePath, true);
      dirPref = true;
      subdirPref = true;
        }
    }
    writeFiles(w, dirPref);
    for (Iterator i = subdirs.iterator(); i.hasNext(); ) {
        ((DirNode) i.next()).writeDir(w, subdirPref);
    }
      }

      /**
       * Writes preferred list entries (if any) for files in this
       * directory, which has the given preferred value as its default.
       */
      void writeFiles(Writer w, boolean contextPref) throws IOException {
    for (Iterator i = files.iterator(); i.hasNext(); ) {
        FileNode fn = (FileNode) i.next();
        if (fn.action != FileNode.SKIP &&
      (fn.action == FileNode.INCLUDE ||
       fn.preferred != contextPref))
        {
      writeEntry(w, fn.path, fn.preferred);
        }
    }
      }
  }
    }
}
TOP

Related Classes of com.sun.jini.tool.JarWrapper$PreferredListWriter$FileNode

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.