///////////////////////////////////////////////////////////////////////////////
//                                                                             
// JTOpen (IBM Toolbox for Java - OSS version)                              
//                                                                             
// Filename: JarMaker.java
//                                                                             
// The source code contained herein is licensed under the IBM Public License   
// Version 1.0, which has been approved by the Open Source Initiative.         
// Copyright (C) 1997-2001 International Business Machines Corporation and     
// others. All rights reserved.                                                
//                                                                             
///////////////////////////////////////////////////////////////////////////////

package utilities;

import com.ibm.as400.access.CommandLineArguments;
import com.ibm.as400.util.BASE64Encoder;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
// import sun.misc.BASE64Encoder;


/**
The JarMaker class is used to generate a smaller (and therefore
faster loading) JAR or ZIP file from a larger one,
based on the user's requirements.
<p>
In addition, JarMaker can also be used to:
<ul compact>
<li> <em>extract</em> desired files from a JAR or ZIP file; or
<li> <em>split</em> a JAR or ZIP file into smaller JAR or ZIP files.
</ul>
<p>
A JarMaker object can be included in the user's program,
or JarMaker can be run as a command line program, as follows:
<blockquote>
<pre>
<strong>java utilities.JarMaker</strong> [ options ]
</pre>
</blockquote>

<p>
<b><a name="jmoptions">Options</a></b>

<p>
<dl>
<dt><b><code>-source </b></code><var>sourceJarFile</var>
<dd>
Specifies the source JAR or ZIP file from which to derive the
destination JAR or ZIP file.
If a relative path is specified, the path is assumed to be relative to
the current directory.
If this option is specified as the first positional argument,
the tag (<code>-source</code>) is optional. 
The -source option may be abbreviated to -s.

<p>
<dt><b><code>-destination </b></code><var>destinationJarFile</var>
<dd>
Specifies the destination JAR or ZIP file, which will contain the desired subset
of the files in the source JAR or ZIP file.
If a pathname is not specified, the file is created in the current directory.
The -destination option may be abbreviated to -d.
The default name is generated by appending <code>"Small"</code> to the
source file name.  For example, if the source file is <code>myfile.jar</code>,
then the default destination file would be <code>myfileSmall.jar</code>.

<p>
<dt><b><code>-fileRequired </b></code><var>jarEntry1[,jarEntry2[...] ] </var>
<dd>
The files in the source JAR or ZIP file that are to be copied to the destination.
Entries are separated by commas (no spaces).
The specified files, along with all of their dependencies,
will be considered required.
Files are specified in JAR entry name syntax, such as <code>com/ibm/as400/access/DataQueue.class</code>.
The -fileRequired option may be abbreviated to -f.

<p>
<dt><b><code>-fileExcluded </b></code><var>jarEntry1[,jarEntry2[...] ] </var>
<dd>
The files in the source JAR or ZIP file that are to be excluded from the destination,
and from dependency analysis.
Entries are separated by commas (no spaces).
Files are specified in JAR entry name syntax, such as <code>com/ibm/as400/access/DataQueue.class</code>.
The -fileExcluded option may be abbreviated to -fx.

<p>
<dt><b><code>-additionalFile </b></code><var>file1[,file2[...] ] </var>
<dd>
Specifies additional files (not included in the source JAR or ZIP file)
which are to be copied to the destination.
Entries are separated by commas (no spaces).
Files are specified by either their
absolute path, or their path relative to the current directory.
<br>The specified files will be included, regardless of the settings of
other options.
The -additionalFile option may be abbreviated to -af.

<p>
<dt><b><code>-additionalFilesDirectory </b></code><var>baseDirectory</var>
<dd>
Specifies the base directory for additional files.
This should be the parent directory of the
directory where the package path starts.  For example, if file <code>foo.class</code>
in package <code>com.ibm.mypackage</code> is located in directory
<code>C:\dir1\subdir2\com\ibm\mypackage\</code>, then specify base directory <code>C:\dir1\subdir2</code>.
<br>The -additionalFilesDirectory option may be abbreviated to -afd.
The default is the current directory.

<p>
<dt><b><code>-package </b></code><var>package1[,package2[...] ] </var>
<dd>
The packages that are required.
Entries are separated by commas (no spaces).
The -package option may be abbreviated to -p.
Package names are specified in standard syntax, such as <code>com.ibm.component</code>.
<br>Note: The specified packages are simply included in the output.
No additional dependency analysis is done on the files in a package,
unless they are explicitly specified as required files.

<p>
<dt><b><code>-packageExcluded </b></code><var>package1[,package2[...] ] </var>
<dd>
The packages that are to be excluded.
Entries are separated by commas (no spaces).
The -packageExcluded option may be abbreviated to -px.
Package names are specified in standard syntax, such as <code>com.ibm.component</code>.

<p>
<dt><b><code>-extract </b></code><var>[baseDirectory]</var>
<dd>
Extracts the desired entries of the source JAR or ZIP file
into the specified base directory, without generating a new JAR or ZIP file.
This option enables the user to build up a customized JAR or ZIP file
empirically, based on the requirements of their particular application.
When this option is specified, <code>-additionalFile</code>,
<code>-additionalFilesDirectory</code>, and <code>-destination</code>
are ignored.
The -extract option may be abbreviated to -x.
By default, no extraction is done.
The default base directory is the current directory.

<p>
<dt><b><code>-split </b></code><var>[splitSize]</var>
<dd>
Splits the source JAR or ZIP file into smaller JAR or ZIP files.
No ZIP entries are added or removed;
the entries in the source JAR or ZIP file are simply distributed
among the destination JAR or ZIP files.
The split size is in units of kilobytes (1024 bytes),
and specifies the maximum size for the destination files.
The destination files are created in the current directory,
and are named by appending integers to the source file name;
any existing files by the same name are overwritten.
For example, if the source JAR file is <code>myfile.jar</code>,
then the destination JAR files would be
<code>myfile0.jar</code>, <code>myfile1.jar</code>, and so on.
When this option is specified, all other options except
<code>-source</code> and <code>-verbose</code> are ignored.
The -split option may be abbreviated to -sp.
The default split size is 2 megabytes (2048 kilobytes).

<p>
<dt><b><code>-verbose </b></code>
<dd>
Causes progress messages to be displayed.
The -verbose option may be abbreviated to -v.
The default is non-verbose.

<p>
<dt><b><code>-help </b></code>
<dd>
Displays the help text.
The -help option may be abbreviated to -h.
The default is no help text.

</dl>

<p>
At least one of the following options must be specified:
<ul compact>
<li>-fileRequired
<li>-fileExcluded
<li>-additionalFile
<li>-package
<li>-packageExcluded
<li>-extract
<li>-split
</ul>

<p>
If the following options are specified multiple times in a single command
string, only the final specification applies:
<ul compact>
<li>-source
<li>-destination
<li>-additionalFilesDirectory
<li>-extract
<li>-split
</ul>
Other options have a cumulative effect when specified multiple times
in a single command string.

<p>
<b><a name="exam">Example usage</a></b>
<p>
<p>
Suppose the source JAR file is named <code>myJar.jar</code>,
and is in the current directory.
To create a JAR file that contains only the classes
<code>mypackage.MyClass1</code> and
<code>mypackage.MyClass2</code>, along with their dependencies,
do the following:

<pre>
import utilities.JarMaker;
<p>
// Set up the list of required files.
Vector classList = new Vector();
classList.addElement ("mypackage/MyClass1.class");
classList.addElement ("mypackage/MyClass2.class");
JarMaker jm = new JarMaker();
jm.setFilesRequired (classList);
<p>
// Make a new JAR file, that contains only MyClass1, MyClass2,
// and their dependencies.
File sourceJar = new File ("myJar.jar");
File newJar = jm.makeJar (sourceJar);  // smaller JAR file
</pre>
<p>
Alternatively, the above action can be performed directly
from the command line as follows:
<pre>
java utilities.JarMaker -source myJar.jar
        -fileRequired mypackage/MyClass1.class,mypackage/MyClass2.class
</pre>

</em>
**/

//
// * Implementation note:  We can use the java.util.jar support
//                         when we start supporting JDK 1.2.
//

public class JarMaker
{
  // Constants.
  static final boolean DEBUG                        = false;
  private static final boolean DEBUG_MANIFEST       = false;
  private static final boolean DEBUG_CP             = false;
  private static final boolean DEBUG_ZIP            = false;
  private static final boolean DEBUG_REF            = false;
  static final String          CLASS_SUFFIX         = ".class";
  private static final String MANIFEST_DIR_NAME     = "META-INF/";
  private static final String MANIFEST_ENTRY_NAME   = "META-INF/MANIFEST.MF";
  private static final String MANIFEST_NAME_KEYWORD = "Name:";
  private static final String MANIFEST_VERSION_KEYWORD = "Manifest-Version:";
  private static final String MANIFEST_REQVERS_KEYWORD = "Required-Version:";
  private static final int    BUFFER_SIZE              = 2*1024; // bytes
  private static final int    SPLIT_SIZE_KBYTES        = 2*1024; // kilobytes
  private static final char   FILE_SEPARATOR =
    System.getProperty("file.separator").charAt(0);
  static final File CURRENT_DIR = new File(System.getProperty("user.dir"));
  // Constants for copyVector() method:
  static final boolean        CHECK_DUPS               = true;           // @A3a
  static final boolean        NO_CHECK_DUPS            = false;          // @A3a
  // Constants for removeElements() method:
  static final int            STARTS_WITH   = 1;
  static final int            ENDS_WITH     = 2;
  static final int            CONTAINS      = 3;

  private static boolean noSHA_ = false;  // turns on if we don't find SHA algorithm
  private static boolean noMD5_ = false;  // turns on if we don't find MD5 algorithm


  // Variables.

  boolean verbose_;      // Verbose output mode.
  boolean requestedUsageInfo_;  // Indicates whether -help option was detected.
  private File sourceJarFile_;       // Source JAR or ZIP file.
  private File destinationJarFile_;  // Destination JAR or ZIP file.

  // Required files (ZIP entries), as specified by user. (String objects)
  private Vector filesRequired_      = new Vector(); // Never null.
  // Files (ZIP entries) to be excluded, as specified by user. (String objects)
  private Vector filesExcluded_      = new Vector(); // Never null.

  // Packages specified by user (String's).
  private Vector packages_                = new Vector(); // Never null.
  // Packages to exclude (String's).
  private Vector packagesExcluded_        = new Vector(); // Never null.
 // Additional files specified by user (File's).
  private Hashtable additionalFiles_      = new Hashtable();
                         // Key=File, value=base directory (File).
                         // Never null.
  boolean excludeSomeDependencies_; // Indicates whether -excludeSomeDependencies was specified.    @A4a
  Vector dependenciesToExclude_      = new Vector(); // Never null.
  // Dependendencies which should be ignored.   @A4a

  private boolean extract_ = false;  // Whether or not to do an extract.
  // Base directory for extracting.
  private File baseDirectoryForExtract_ = new File(System.getProperty("user.dir"));

  private boolean split_ = false;  // Whether or not to do a split.
  private int splitSize_ = SPLIT_SIZE_KBYTES;  // kilobytes

  // The following literals specify the context package prefix,
  // for example, "com/ibm/myPackage".
  // These are used when analyzing class dependencies,
  // to test whether a reference might be to a properties file
  // or to a class loaded by Class.forName().
  private String jarEntryDefaultPrefix_;
  private String jarEntryDefaultPrefixDotted_;
                             // Same as jarEntryDefaultPrefix_,
                             // but with slashes replaced by dots.
  private transient Vector eventListeners_    = new Vector();

  // Invocation arguments.  This is used only when JarMaker is invoked
  // from the command line.
  private Arguments arguments_ = new Arguments();


  /**
   Constructs a JarMaker object.
   **/
  public JarMaker()
  {
  }


  /**
   Constructs a JarMaker object.  This constructor is provided for use
   by subclasses.

   @param jarEntryDefaultPrefix Default prefix for use when parsing class files.
   **/
  JarMaker(String jarEntryDefaultPrefix)
  {
    jarEntryDefaultPrefix_ = jarEntryDefaultPrefix;
    jarEntryDefaultPrefixDotted_ = jarEntryDefaultPrefix.replace('/', '.');
  }


  //@A1a
  // Adds an element to a Vector, only if not already a member.
  // @return false if object was already a member of the vector. 
  static boolean addElement(Vector vector, Object object)
  {
    if (vector.contains(object))
      return false;

    vector.addElement(object);
    return true;
  }


  // Removes elements (matching a pattern) from a Vector of Strings.
  // If patternLocation==STARTS_WITH, namePattern is a "startsWith" pattern.
  // If patternLocation==ENDS_WITH, namePattern is an "endsWith" pattern.
  // Otherwise, namePattern is a "contains String" pattern.
  static void removeElements(Vector vector, String namePattern, int patternLocation)
  {
    Vector entriesToRemove = new Vector();
    Enumeration e = vector.elements();
    while (e.hasMoreElements())
    {
      String entry = (String)e.nextElement();
      boolean removeEntry = false;
      switch (patternLocation) {
        case STARTS_WITH:
          if (entry.startsWith(namePattern)) removeEntry = true;
          break;
        case ENDS_WITH:
          if (entry.endsWith(namePattern)) removeEntry = true;
          break;
        default:  // CONTAINS
          if (entry.indexOf(namePattern) != -1) removeEntry = true;
      }
      if (removeEntry) {
        // Note: Must not remove elements from underneath the current Enumeration.
        entriesToRemove.addElement(entry);
        if (/*verbose_ ||*/ DEBUG) {
          System.out.println("Excluding entry: " + entry);
        }
      }
    }
    Enumeration e2 = entriesToRemove.elements();
    while (e2.hasMoreElements())
      vector.removeElement((String)e2.nextElement());
  }


  /**
   Adds a listener to the listener list.

   @param listener The listener.
   **/
  public synchronized void addJarMakerListener(JarMakerListener listener)
  {
    if (listener == null) throw new NullPointerException("listener");
    eventListeners_.addElement(listener);
  }


  // @A3a
  /**
   Adds files for any specified packages to the additional files list.

   @param neededJarEntries The current list of required files
   and their dependencies.
   The list should contain only <code>String</code> objects.
   @param jarMap A map of the source JAR or ZIP file.
   **/
  static void addPackageFiles(Vector neededJarEntries, JarMap jarMap, Vector packages)
    throws IOException
  {
    // Load the entry names associated with any required packages.
    // Note that these files will not be explicitly analyzed.
    Enumeration pkgs = packages.elements();
    while (pkgs.hasMoreElements())
    {
      String packageName = (String)pkgs.nextElement();
      String packagePrefix = packageName.replace('.', '/');
      Vector entriesInPackage =
        getEntryNamesForPackage(packagePrefix, jarMap);
      if (entriesInPackage.size() == 0) {
        System.err.println("Error: Specified package not found in source file:");
        System.err.println("       " + packageName);
        throw new ZipException(packageName);
      }
      copyVector(entriesInPackage, neededJarEntries, CHECK_DUPS);  // @A3c
    }
  }


  /**
   Removes files for any specified packages from the additional files list.

   @param neededJarEntries The current list of required files
   and their dependencies.
   The list should contain only <code>String</code> objects.
   @param jarMap A map of the source JAR or ZIP file.
   **/
  static void removePackageFiles(Vector neededJarEntries, JarMap jarMap, Vector packages)
    throws IOException
  {
    // Load the entry names associated with any required packages.
    Enumeration pkgs = packages.elements();
    while (pkgs.hasMoreElements())
    {
      String packageName = (String)pkgs.nextElement();
      String packagePrefix = packageName.replace('.', '/');
      Vector entriesInPackage =
        getEntryNamesForPackage(packagePrefix, jarMap);
      if (entriesInPackage.size() == 0) {
        System.err.println("Error: Specified package not found in source file:");
        System.err.println("       " + packageName);
        throw new ZipException(packageName);
      }
      removeElements(neededJarEntries, entriesInPackage);
    }
  }


  /**
   Adds or removes ZIP entry names from the "required files" list,
   prior to initial generation of the dependencies list.
   This method is provided so that subclasses of JarMaker can override it.
   <br><em>This method is meant to be called by the JarMaker class only.</em>

   @param neededJarEntries An unsorted list of names of ZIP entries.
   @param jarMap A map of the source JAR or ZIP file.
   that should be included in the output.
   @return The modified list of ZIP entry names
   (must never be <code>null</code>.
   This should be a Vector of Strings.
   @exception IOException If an I/O error occurs when reading the JAR file.
   **/
  Vector adjustDependencies1(Vector neededJarEntries, JarMap jarMap)
    throws IOException
  {
    return neededJarEntries;
  }


  /**
   Adds or removes ZIP entry names from the dependencies list,
   prior to final presentation of the dependencies list.
   This method is provided so that subclasses of JarMaker can override it.
   <br><em>This method is meant to be called by the JarMaker class only.</em>

   @param neededJarEntries An unsorted list of names of ZIP entries.
   @param jarMap A map of the source JAR or ZIP file.
   that should be included in the output.
   @return The modified list of ZIP entry names
   (must never be <code>null</code>.
   This should be a Vector of Strings.
   @exception IOException If an I/O error occurs when reading the JAR file.
   **/
  Vector adjustDependencies2(Vector neededJarEntries, JarMap jarMap)
    throws IOException
  {
    // Remove excluded packages.
    removePackageFiles(neededJarEntries, jarMap, packagesExcluded_);

    // Load the entry names associated with any required packages.
    // Note that these files will not be explicitly analyzed.
    addPackageFiles(neededJarEntries, jarMap, packages_);            // @A3a

    return neededJarEntries;
  }


  /**
   Determines the dependencies of a single file in the source JAR or ZIP file.

   @param jarEntryName The entry name for the file.
   @param unanalyzedEntries List of as-yet unanalyzed entry names (Strings).
   @param referencedJarEntries Cumulative list of referenced ZIP entries.
   @param jarMap A map of the source JAR or ZIP file.
   @exception IOException If an I/O error occurs when reading the source file.
   **/
  void analyzeJarEntry( String jarEntryName,
                         Vector unanalyzedEntries,
                         Vector referencedJarEntries,
                         JarMap jarMap )
    throws IOException
  {
    fireAnalysisEvent(true, jarEntryName);
    if (jarEntryName.endsWith(CLASS_SUFFIX))
    {
          if (excludeSomeDependencies_ &&
              dependenciesToExclude_.contains(jarEntryName))    // @A4a
          {
            if (verbose_ || DEBUG)
              System.out.println("\nExcluding entry from dependency analysis: " +
                                  jarEntryName + "\n");      // @A4a
            addElement(referencedJarEntries, jarEntryName); // keep this one.
          }
          else
          {
            // Start with list of directly referenced entries.
            Vector referencedEntries =
              getReferencedEntries(jarEntryName, jarMap);
            if (DEBUG_REF) {
              System.out.println(jarEntryName + " references: ");
              Enumeration e1 = referencedEntries.elements(); // entry names
              while (e1.hasMoreElements())
                System.out.println("   " + (String)e1.nextElement());
            }
            // Now recurse through the list and add indirectly referenced entries.
            Enumeration e = referencedEntries.elements(); // entry names
            while (e.hasMoreElements())
            {
              String entryName = (String)e.nextElement();
              if (unanalyzedEntries.contains(entryName))
              {
                unanalyzedEntries.removeElement(entryName);
                analyzeJarEntry(entryName, unanalyzedEntries,
                                   referencedJarEntries, jarMap);
                addElement(referencedJarEntries, entryName);
              }
            }
          }
    }
    else {}  // Not a class file, so no analysis to do
    fireAnalysisEvent(false, jarEntryName);
  }


  /**
   Constructs a manifest entry for the specified additional file,
   and adds the entry to the buffer.

   @param buffer The buffer to which to append the new manifest entry.
   @param file The file to create a manifest entry for.
   @param entryName Name for the ZIP entry.
   @exception IOException If an I/O error occurs when reading the JAR file.
   **/
   private static void constructManifestEntry(StringBuffer buffer, File file,
                                               String entryName)
     throws IOException
   {
     String lineSHA = null;  // SHA-Digest: <value>
     String lineMD5 = null;  // MD5-Digest: <value>

     byte[] fileContents = getBytes(file); // read file into byte array

     if (!noSHA_)
     {
       try
       {
         MessageDigest shaMD = MessageDigest.getInstance("SHA");
         byte[] shaDigest = shaMD.digest(fileContents);
         String encodedStr = (new BASE64Encoder()).encodeBuffer(shaDigest);
         lineSHA = "SHA-Digest: " + encodedStr;
       }
       catch (NoSuchAlgorithmException e) {
         noSHA_ = true;
         if (DEBUG) {
           System.err.println("Debug: Warning: Manifest entry " +
                              "will contain no SHA digest:");
           System.err.println("       " + file.getAbsolutePath());
         }
       }
     }

     if (!noMD5_)
     {
       try
       {
         MessageDigest md5MD = MessageDigest.getInstance("MD5");
         byte[] md5Digest = md5MD.digest(fileContents);
         String encodedStr = (new BASE64Encoder()).encodeBuffer(md5Digest);
         lineMD5 = "MD5-Digest: " + encodedStr;
       }
       catch (NoSuchAlgorithmException e) {
         noMD5_ = true;
         if (DEBUG) {
           System.err.println("Debug: Warning: Manifest entry " +
                              "will contain no MD5 digest:");
           System.err.println("       " + file.getAbsolutePath());
         }
       }
     }

     if (lineSHA != null || lineMD5 != null)
     {
       buffer.append("Name: " + entryName + "\n");
       buffer.append("Digest-Algorithms:");
       if (lineSHA != null) buffer.append(" SHA");
       if (lineMD5 != null) buffer.append(" MD5");
       buffer.append('\n');
       if (lineSHA != null)
       {
         buffer.append(lineSHA);
         // Note: BASE64Encoder.encodeBuffer() likes to append a newline.
         if (!lineSHA.endsWith("\n")) { buffer.append('\n'); }
       }
       if (lineMD5 != null)
       {
         buffer.append(lineMD5);
         if (!lineMD5.endsWith("\n")) { buffer.append('\n'); }
       }
       buffer.append('\n'); // terminate the section with a zero-length line
     }
     else {
       System.err.println("Error: Failed to construct manifest entry for file");
       System.err.println("       " + file.getAbsolutePath());
     }
   }


   // Utility method to determine if an int array contains a specific value.
   // Assumes that the array is sorted in ascending order.
  static final boolean contains(int[] list, int element)
  {
    if (Arrays.binarySearch(list, element) >= 0) return true;
    else return false;
  }


  /**
   Copies specified number of bytes from input stream to output stream.
   @param inStream     The input stream from which the entry is being read.
   @param outStream    The output stream being copied to.
   @param bytesToCopy  The number of bytes to copy.

   @exception IOException If an I/O error occurs when reading the input stream
                          or writing to the output stream.
   **/
  private static void copyBytes(InputStream inStream, OutputStream outStream,
                             long bytesToCopy)
    throws IOException
  {
    byte[] buffer = new byte[BUFFER_SIZE];
    int bytesRead = 0;
    int totalBytes = 0;
    boolean done = false;
    while (!done && totalBytes < bytesToCopy)
    {
      bytesRead = inStream.read(buffer, 0, buffer.length);
      if (bytesRead == -1) done = true;
      else {
        outStream.write(buffer, 0, bytesRead);
        totalBytes += bytesRead;
      }
    }
  }


  /**
   Copies a file onto another file (replacing it if it exists).
   **/
    static void copyFile(File sourceFile, File destinationFile)
        throws IOException
    {
      if (sourceFile == null)
        throw new NullPointerException("sourceFile");
      if (destinationFile == null)
        throw new NullPointerException("destinationFile");
      BufferedInputStream source = null;
      BufferedOutputStream destination = null;
      String parentDirName = destinationFile.getParent();
      if (parentDirName == null)
        throw new NullPointerException("parentDirectory");
      File outFileDir = new File(parentDirName);
      if (!outFileDir.exists() && !outFileDir.mkdirs()) {
        throw new IOException(outFileDir.getAbsolutePath() +
                              ": Cannot create directory.");
      }
      byte[] buffer = new byte[BUFFER_SIZE];
      try
      {
        source =
          new BufferedInputStream(new FileInputStream(sourceFile),
                                   BUFFER_SIZE);
        destination =
          new BufferedOutputStream(new FileOutputStream(destinationFile),
                                    BUFFER_SIZE);
        boolean done = false;
        while (!done)
        {
          int bytesRead = source.read(buffer);
          if (bytesRead == -1) done = true;
          else destination.write(buffer, 0, bytesRead);
        }
        destination.flush();
      }
      catch (IOException e) {
        System.err.println("Error: IOException when copying file");
        System.err.println("       " + destinationFile.getAbsolutePath() + ":");
        System.err.println(e.toString());
        if (DEBUG) e.printStackTrace(System.err);
        throw e;
      }
      finally
      {
        if (source != null) { try { source.close(); } catch (Throwable t) {} }
        if (destination != null) { destination.close(); }
      }
    }


  /**
   Copies (appends) a vector into another vector.
   **/
  static void copyVector(Vector fromList, Vector toList, boolean checkDups)
  {
    Enumeration e = fromList.elements();
    while (e.hasMoreElements()) {
      Object element = e.nextElement();
      if (!checkDups || !toList.contains(element))
        toList.addElement(element);
    }
  }


  /**
   Removes the entries listed in one vector from another vector.
   **/
  static void removeElements(Vector list, Vector entriesToRemove)
  {
    Enumeration e = entriesToRemove.elements();
    while (e.hasMoreElements()) {
      list.remove(e.nextElement());
    }
  }


  /**
   Copies a ZIP entry from an input stream to an output stream.
   **/
  static void copyZipEntry(ZipEntry inZipEntry,
                            InputStream inStream,
                            ZipOutputStream zipOutStream)
    throws IOException
  {
    if (inZipEntry == null)   throw new NullPointerException("inZipEntry");
    if (inStream == null)  throw new NullPointerException("inStream");
    if (zipOutStream == null) throw new NullPointerException("zipOutStream");

    // Create a new ZipEntry for the output ZipFile
    ZipEntry outZipEntry = new ZipEntry(inZipEntry.getName());
    outZipEntry.setComment(inZipEntry.getComment());

    long crc = inZipEntry.getCrc();
    if ( crc != -1 ) outZipEntry.setCrc(crc);

    outZipEntry.setExtra(inZipEntry.getExtra());

    int method = inZipEntry.getMethod();
    if ( method != -1 ) outZipEntry.setMethod(method);

    long size = inZipEntry.getSize();
    if ( size != -1 ) outZipEntry.setSize(size);

    long time = inZipEntry.getTime();
    if (time != -1) outZipEntry.setTime(time);

    // Put the entry into the output ZIP file.
    zipOutStream.putNextEntry(outZipEntry);

    // If the entry is not a directory, then there is also data to write.
    byte[] buffer = new byte [BUFFER_SIZE];
    if ( !inZipEntry.isDirectory() ) {
      // Loop until all output is read and written.
      int bytesRead = 0;
      int totalBytes = 0;
      boolean done = false;
      while (!done && totalBytes < size) {
        bytesRead = inStream.read(buffer);
        if (bytesRead == -1) done = true;
        else {
          zipOutStream.write(buffer,0,bytesRead);
          totalBytes += bytesRead;
        }
      }
    }
  }


  /**
   Calculates the size of the zip metadata that will be associated
   with a given list of directory names.
   **/
  private static int determineDirMetadataSize(Vector dirNames,
                                               int baseMetadataPerZipEntry)
  {
    int result = 0;
    Enumeration e = dirNames.elements();
    while (e.hasMoreElements()) {
      String dirName = (String)e.nextElement();
      result += dirName.length();
      result += baseMetadataPerZipEntry;
    }
    return result;
  }


  /**
   Extracts the desired entries and their dependencies from
   the specified JAR or ZIP file.
   The extracted files are placed under the current directory.
   <br>Note: No "additional files" are copied.

   @param sourceJarFile The source JAR or ZIP file.
   @return The base directory under which the extracted files were written.
   @exception FileNotFoundException If the source file does not exist.
   @exception IOException If an I/O error occurs when reading the source file
   or writing the extracted files.
   @exception ZipException If a ZIP error occurs when reading the source file.
   **/
  public File extract(File sourceJarFile)
    throws FileNotFoundException, IOException, ZipException
  {
    if (sourceJarFile == null) throw new NullPointerException("sourceJarFile");
    // Default: Extract into current directory.
    File outputDirectory = new File(System.getProperty("user.dir"));
    extract(sourceJarFile, outputDirectory);
    return outputDirectory;
  }


  /**
   Extracts the desired entries and their dependencies from
   the specified JAR or ZIP file.
   <br>Note: No "additional files" are copied.

   @param sourceJarFile The source JAR or ZIP file.
   @param outputDirectory The directory under which to put the extracted files.
   @exception FileNotFoundException If the source file does not exist.
   @exception IOException If an I/O error occurs when reading the source file
   or writing the extracted files.
   @exception ZipException If a ZIP error occurs when reading the source file.
   **/
  public void extract(File sourceJarFile, File outputDirectory)
    throws FileNotFoundException, IOException, ZipException
  {
    if (sourceJarFile == null) throw new NullPointerException("sourceJarFile");
    if (outputDirectory == null)
      throw new NullPointerException("outputDirectory");
    if (!sourceJarFile.exists())
      throw new FileNotFoundException(sourceJarFile.getAbsolutePath());
    if (verbose_ || DEBUG) {
      System.out.println("Source file is " + sourceJarFile.getAbsolutePath());
      System.out.println("Extraction output directory is " + outputDirectory.getAbsolutePath());
    }

    BufferedOutputStream destinationFile = null;
    InputStream inStream = null;
    String basePath = outputDirectory.getAbsolutePath();
    JarMap jarMap = null;

    // Warn the user if additional files were specified.
    if (additionalFiles_.size() != 0)
      System.err.println("Warning: Additional files were specified.  " +
                          "They are ignored by extract().");
    try
    {
      // Make a map of the source JAR or ZIP file.
      jarMap = new JarMap(sourceJarFile, verbose_);

      // Get list of the names of the ZIP entries that will be extracted.
      Vector referencedJarEntries = identifyDependencies(jarMap);

      // Make sure to copy the Manifest also.
      if (jarMap.hasManifest() &&
          !referencedJarEntries.contains(MANIFEST_ENTRY_NAME))
      {
        int dirIndex = referencedJarEntries.indexOf(MANIFEST_DIR_NAME);
        if (dirIndex == -1) {
          referencedJarEntries.insertElementAt(MANIFEST_DIR_NAME, 0);
          dirIndex = 0;
        }
        referencedJarEntries.insertElementAt(MANIFEST_ENTRY_NAME, dirIndex+1);
      }

      // Copy the referenced files to the destination.
      if (verbose_ || DEBUG) System.out.println("Extracting files");
      Enumeration e = referencedJarEntries.elements();
      while (e.hasMoreElements())
      {
        String entryName = (String)e.nextElement();

        // If the entry represents a directory, simply create the
        // directory.  Otherwise extract the file.
        String filePath = generateFilePath(basePath, entryName);
        if (DEBUG) System.out.println(filePath);
        else if (verbose_) System.out.print(".");
        if (entryName.endsWith("/"))
        { // Simply create the directory.  Remove the final separator.
          File outFileDir = new File(filePath.substring(0, filePath.length()-1));
          if (!outFileDir.exists() && !outFileDir.mkdirs()) {
            throw new IOException(outFileDir.getAbsolutePath() +
                                  ": Cannot create directory.");
          }
        }
        else
        {
          // We assume that the referencedJarEntries list is sorted,
          // and already contains all needed directory entries.

          // Open the destination file for writing.
          destinationFile =
            new BufferedOutputStream(new FileOutputStream(filePath),
                                      BUFFER_SIZE);

          // Gather information from the source file.
          ZipEntry entry = jarMap.getEntry(entryName);
          if (entry == null)
            throw new FileNotFoundException(entryName);
          inStream = jarMap.getInputStream(entry);

          // Design note: Experimentation reveals that ZipFile.getInputStream()
          // actually returns a java.util.zip.InflaterInputStream object,
          // which does automatic decompression.

          if (DEBUG_ZIP)
          {
            int avail = inStream.available();
            long entrySize = entry.getSize();
            long compressedSize = entry.getCompressedSize();
            if (avail == entrySize) System.out.println("Debug: Sizes match");
            else
            {
              System.out.println("Debug: File sizes mismatch for " +
                                  entry.getName());
              System.out.println("   available   = " + avail);
              System.out.println("   compressed  = " + compressedSize);
              System.out.println("   uncompressed= " + entrySize);
            }
          }
          // Copy the referenced file to the destination.
          copyBytes(inStream, destinationFile, entry.getSize());
          destinationFile.close();
          destinationFile = null;
          inStream.close();
          inStream = null;
        }
      }  // ... while
    }
    catch (ZipException e) {
      System.err.println("Error: ZipException when extracting source file");
      System.err.println("       " + sourceJarFile.getAbsolutePath() + ":");
      System.err.println(e.toString());
      if (DEBUG) e.printStackTrace(System.err);
      throw e;
    }
    catch (IOException e) {
      System.err.println("Error: IOException when extracting source file");
      System.err.println("       " + sourceJarFile.getAbsolutePath() + ":");
      System.err.println(e.toString());
      if (DEBUG) e.printStackTrace(System.err);
      throw e;
    }
    finally
    {
      if (DEBUG) System.out.println();
      if (jarMap != null) {
        try { jarMap.close(); } catch (Throwable t) {}
      }

      if (destinationFile != null) {
        try { destinationFile.close(); } catch (Throwable t) {}
      }

      if (inStream != null) {
        try { inStream.close(); } catch (Throwable t) {}
      }
    }

  }


  /**
   Fires the appropriate Event.

   @param start If <code>true</code>, fires a
   <code>dependencyAnalysisStarted</code> event;
   otherwise fires a <code>dependencyAnalysisCompleted</code> event.
   @param entryName The name of the ZIP entry.
   **/
  private void fireAnalysisEvent(boolean start, String entryName)
  {
    Vector targets;
    synchronized (this)
    {
      targets = (Vector) eventListeners_.clone();
    }
    JarMakerEvent event = new JarMakerEvent(this, entryName);
    for (int i = 0; i < targets.size(); i++)
    {
      JarMakerListener target = (JarMakerListener)targets.elementAt(i);
      if (start)
      {
        target.dependencyAnalysisStarted(event);
      }
      else
      {
        target.dependencyAnalysisCompleted(event);
      }
    }
  }


  /**
   Generates any needed ZIP directory entries to add to the destination file,
   that are prerequisite to the specifed ZIP entry.
   For example, if entryName is "com/ibm/comp1/someFile",
   and listSoFar contains "com/",
   then the returned list will contain "com/ibm/" and "com/ibm/comp1/".
   If the specified entry is a directory entry, it does not get included
   in the returned list.

   @param entryName The name of the ZIP entry about to be added.
   @param listSoFar The ZIP directory entries that have already been
   added to the destination file.
   @return The list of needed additional ZIP directory entries.
   **/
  private static Vector generateDirEntries(String entryName,
                                            Vector listSoFar)
  {
    Vector outList = new Vector();
    String pathPrefix = entryName.substring(0, entryName.lastIndexOf("/")+1);
    if (pathPrefix.length() != 0)
    {
      int slashPos = pathPrefix.indexOf("/");
      while (slashPos != -1 &&
             slashPos < entryName.length()-1)
      {
        String subPrefix = pathPrefix.substring(0, slashPos+1);
        if (!listSoFar.contains(subPrefix))
          outList.addElement(subPrefix);
        slashPos = pathPrefix.indexOf("/", slashPos+1);
      }
    }
    return outList;
  }


  /**
   Converts a ZIP entry name to a file pathname.
   For example, in a Windows environment, converts
   "com/ibm/as400/access/CommandCall.class" to
   "com\ibm\as400\access\CommandCall.class".

   @param baseDirectory The base directory for the file.
   @param entryName The ZIP entry name.
   **/
  static String generateFilePath(File baseDirectory,
                                  String entryName)
  {
    if (baseDirectory == null) throw new NullPointerException("baseDirectory");
    return generateFilePath(baseDirectory.getAbsolutePath(), entryName);
  }


  /**
   Converts a ZIP entry name to a file pathname.

   @param basePath The absolute path of the base directory for the file.
   @param entryName The ZIP entry name.
   **/
  static String generateFilePath(String basePath, String entryName)
  {
    if (basePath == null) throw new NullPointerException("basePath");
    if (entryName == null) throw new NullPointerException("entryName");
    StringBuffer pathBuf = new StringBuffer(basePath.trim());
    if (pathBuf.charAt(pathBuf.length()-1) != FILE_SEPARATOR)
      pathBuf.append(FILE_SEPARATOR);

    // Replace all occurrences of "/" with fileSeparator.
    StringBuffer convertedName =
      new StringBuffer(entryName.trim().replace('/',FILE_SEPARATOR));

    pathBuf.append(convertedName);
    String path = pathBuf.toString();

    return path;
  }


  /**
   Generates the ZIP entry names for a list of files.

   @param fileList A Hashtable of (File, File) pairs, where the
                   second arg is the "base path" for the first arg.
   @return         The derived ZIP entry names - a map of (File, String).
   **/
  private static Hashtable generateJarEntryMap(Hashtable fileList)
  {
    Hashtable entryNames;
    if (fileList.size() != 0) entryNames = new Hashtable(fileList.size());
    else entryNames = new Hashtable();
    Enumeration e = fileList.keys();
    while (e.hasMoreElements())
    {
      File file = (File)e.nextElement();
      File baseDir = (File)fileList.get(file);
      String entryName = generateJarEntryName(file, baseDir);
      entryNames.put(file, entryName);
    }
    return entryNames;
  }


  /**
   Generates the ZIP entry name for a given file.

   @param file     The file.
   @param baseDir  The base directory for the file.
   @return         The derived ZIP entry name.
   **/
  private static String generateJarEntryName(File file, File baseDir)
  {
    if (DEBUG) System.out.println("Debug: File = " + file.getAbsolutePath() +
                                   ", baseDir = " + baseDir.getAbsolutePath());
    // Strip off the base path, if it matches the beginning of the file path.
    String filePath = file.getAbsolutePath();
    String basePath = baseDir.getAbsolutePath();
    if (filePath.startsWith(basePath))
      filePath = filePath.substring(basePath.length());
    else
    {
      System.err.println("Warning: File path does not begin with " +
                          "base path for additional files.");
      System.err.println("   File path: " + filePath);
      System.err.println("   Base path: " + basePath);
    }

    // Replace all filepath separators with "/".
    String entryName = filePath.replace(FILE_SEPARATOR, '/');

    // Remove leading filepath separator if present.
    if (entryName.startsWith("/"))
      entryName = entryName.substring(1);

    if (DEBUG) System.out.println("Debug: Generated entry name: "+entryName);

    return entryName;
  }


  /**
   Returns the additional files that are to be included in
   the destination JAR or ZIP file.

   @return The additional files specified by the user.
   The list will be empty if none has been specified.
   The list will contain only <code>java.io.File</code> objects.
   **/
  public Vector getAdditionalFiles()
  {
    Vector files = new Vector();
    Enumeration e = additionalFiles_.keys();
    while (e.hasMoreElements())
      files.addElement((File)e.nextElement());
    return files;
  }


   /**
    Returns the contents of the specified file as a byte array.

    @return The contents of the specified file as a byte array.
    @exception IOException If an I/O error occurs when reading the file.
    **/
   static byte[] getBytes(File file)
     throws IOException
   {
     long fileSize = file.length();
     byte[] buffer = new byte[(int)fileSize];
     InputStream inStream = new FileInputStream(file);
     try
     {
       int bytesRead = inStream.read(buffer);
       if (bytesRead < fileSize) {
         throw new IOException(file.getAbsolutePath() +
                                ": Failed to read entire file.");
       }
     }
     catch (IOException e) {
       System.err.println("Error: IOException when reading file");
       System.err.println("       " + file.getAbsolutePath() + ":");
       System.err.println(e.toString());
       if (DEBUG) e.printStackTrace(System.err);
       throw e;
     }
     finally { if (inStream != null) inStream.close(); }
     return buffer;
   }


  /**
   Returns the destination JAR or ZIP file.
   This method is provided for internal use.

   @return The destination JAR or ZIP file.
   **/
  File getDestinationJar() { return destinationJarFile_; }


  /**
   Returns the names of ZIP entries associated with a specific package.

   @param packagePrefix The ZIP entry prefix for the package of interest.
                        Does not include the final '/'.
                        May be zero-length, indicating the default package.
   @param jarMap A map of the JAR or ZIP file.
   @return The names of the ZIP entries for the specified package (Strings).
   **/
  private static Vector getEntryNamesForPackage(String packagePrefix,
                                         JarMap jarMap)
  {
    Vector entriesInPackage = new Vector();
    int prefixLength = packagePrefix.length();
    Enumeration e = jarMap.elements();
    while (e.hasMoreElements())
    {
      String entryName = (String)e.nextElement();
      switch (prefixLength)
      {
        case 0:  // No package specifier, so assume the default package.
          if (entryName.indexOf('/') == -1)
            entriesInPackage.addElement(entryName);
          if (DEBUG)
            System.out.println("getEntryNamesForPackage: " +
                                "Zero-length packagePrefix encountered.");
          break;
        default:
          if (entryName.startsWith(packagePrefix) &&
              entryName.lastIndexOf('/') == prefixLength)
            entriesInPackage.addElement(entryName);
      }
    }
    return entriesInPackage;
  }


  /**
   Returns the base directory for the directory tree into which the
   source JAR or ZIP file is to be extracted.
   This method is provided for internal use.

   @return The directory at the base of the tree into which the
   source JAR or ZIP file will be extracted.
   **/
  File getExtractionDirectory() { return baseDirectoryForExtract_; }


  /**
   Returns the ZIP entries directly referenced by a specified file
   in a JAR or ZIP file.

   @param jarEntryName  The name of the ZIP entry.
   @param jarMap A map of the JAR or ZIP file.
   @return  The names of referenced ZIP entries (String's).

   @exception IOException If an I/O error occurs when reading the JAR or ZIP file.
   **/
  private Vector getReferencedEntries(String jarEntryName, JarMap jarMap)
    throws IOException
  {
    if (DEBUG && false)
      System.out.println("Debug: getReferencedEntries(" + jarEntryName + ")");

    // Open up the class file and process its bytecodes, looking
    // for referenced files.
    if (verbose_) System.out.print(".");
    ZipEntry entry = jarMap.getEntry(jarEntryName);
    String entryName = entry.getName();
    String contextPackageName = "";
    int finalSlashPos = entryName.lastIndexOf('/');
    if (finalSlashPos != -1)
      contextPackageName =
        entryName.substring(0, finalSlashPos).replace('/','.');
    InputStream inStream = null;
    try
    {
      inStream = jarMap.getInputStream(entry);
      Vector classIndexes = prescanForClassIndexes(inStream,
                                                   contextPackageName,
                                                   jarMap);
      inStream.close();
      inStream = jarMap.getInputStream(entry);
      Vector referenced = processBytecodeStream(inStream,
                                                contextPackageName,
                                                jarMap,
                                                classIndexes);
      inStream.close();
      inStream = null;
      return referenced;
    }
    finally { if (inStream != null) inStream.close(); }

  }


  /**
   @deprecated Use getFilesRequired() instead.
   **/
  public Vector getRequiredFiles() { return filesRequired_; }


  /**
   Returns the names of the required files specified by the user.

   @return The names of required files specified by the user.
   The list will be empty if none has been specified.
   The list will contain only <code>String</code> objects.
   **/
  public Vector getFilesRequired() { return filesRequired_; }


  /**
   Returns the names of the required files specified by the user.

   @return The names of required files specified by the user.
   The list will be empty if none has been specified.
   The list will contain only <code>String</code> objects.
   **/
  public Vector getFilesExcluded() { return filesExcluded_; }


  /**
   Returns the names of the packages that are to be included in the output.

   @return The names of the required packages specified by the user.
   The list will be empty if none has been specified.
   The list will contain only <code>String</code> objects.
   **/
  public Vector getPackages() { return packages_; }


  /**
   Returns the names of the packages that are to be excluded from the output.

   @return The names of the excluded packages specified by the user.
   The list will be empty if none has been specified.
   The list will contain only <code>String</code> objects.
   **/
  public Vector getPackagesExcluded() { return packagesExcluded_; }


  /**
   Returns the source JAR or ZIP file.
   This method is provided for internal use.

   @return The source JAR or ZIP file.
   **/
  File getSourceJar() { return sourceJarFile_; }


  /**
   Returns the maximum file size for the destination JAR or ZIP files that
   are produced by the <code>split()</code> method.

   @return The maximum file size for the destination JAR or ZIP files;
   in units of kilobytes (1024 bytes).
   **/
  int getSplitSize() { return splitSize_; }


  /**
   Returns the command line arguments that were not recognized.
   This method is provided for use by subclasses.

   @return The command line arguments that were not recognized.
   **/
  String [] getUnrecognizedArgs() { return arguments_.getUnrecognized(); }


  /**
   Determines which entries in the source file should be included in the output.

   @param jarMap A map of the JAR or ZIP file.
   @return The source entry names that should be included in the output.
   (empty list if no dependencies are found).
   This will be a Strings, sorted alphabetically.
   @exception IOException If an I/O error occurs when reading the JAR file.
   **/
  private Vector identifyDependencies(JarMap jarMap)
    throws IOException
  {
    if (verbose_ || DEBUG)
      System.out.println("Analyzing source file");

    // Set up lists.
    Vector referencedJarEntries = new Vector();  // referenced entry names
    Vector unanalyzedEntries = new Vector(); // entry names not yet looked at
    Enumeration e = jarMap.entries(); // all entries in the jar
    while (e.hasMoreElements())
    {
      unanalyzedEntries.addElement(((ZipEntry)e.nextElement()).getName());
    }
    // We don't need to do dependency analysis on the manifest entry.
    unanalyzedEntries.removeElement(MANIFEST_ENTRY_NAME);

    // Give the subclass an opportunity to modify the list.
    Vector filesToInclude = new Vector();
    copyVector(filesRequired_, filesToInclude, NO_CHECK_DUPS);
    copyVector(filesExcluded_, dependenciesToExclude_, CHECK_DUPS);
    filesToInclude = adjustDependencies1(filesToInclude, jarMap);

    // Start marking referenced files, starting with the required files
    // that were specified.
    if (filesToInclude.size() == 0)
    {
      if (verbose_ || DEBUG)
        System.out.println("No required JAR or ZIP entries were specified");
    }
    else
    {
      if (verbose_ || DEBUG)
        System.out.println("Analyzing " + filesToInclude.size() +
                            " required entries, starting with " +
                            filesToInclude.elementAt(0) + ".");
      Enumeration reqEntries = filesToInclude.elements();
      while (reqEntries.hasMoreElements())
      {
        String entryName = (String)reqEntries.nextElement();
        if (DEBUG) System.out.print(":");
        // Verify that the source JAR or ZIP actually contains the required file.
        if (!jarMap.contains(entryName)) {
          System.err.println("Warning: The source file does not contain " +
                              "the specified required file: " + entryName);
          filesToInclude.removeElement(entryName);
        }
        else if (unanalyzedEntries.contains(entryName))
        {
          unanalyzedEntries.removeElement(entryName);
          analyzeJarEntry(entryName, unanalyzedEntries,
                           referencedJarEntries, jarMap);
          addElement(referencedJarEntries, entryName);
        }
      }
      if (verbose_ || DEBUG) System.out.println();
    }

    if (referencedJarEntries.size() == 0 && packages_.size() == 0)  // @A3c
    { // Assume user wants all the files copied.
      if (DEBUG)
        System.out.println("Debug: identifyDependencies(): " +
                            "Adding all files to list");
      copyVector(jarMap.getEntryNames(), referencedJarEntries, NO_CHECK_DUPS);  // @A3c
    }

    // Give the subclass an opportunity to modify the list.
    referencedJarEntries = adjustDependencies2(referencedJarEntries, jarMap);

    // Make sure the "excluded files" don't end up in the jar.
    removeElements(referencedJarEntries, filesExcluded_);

    // Sort the list alphabetically.
    referencedJarEntries = sortStrings(referencedJarEntries);

    // Add directory entries, e.g. com/, com/ibm/, com/ibm/myproduct/
    referencedJarEntries = insertDirectoryEntries(referencedJarEntries);

    return referencedJarEntries;
  }


  /**
   Inserts directory entries into a list of ZIP entry names.
   For example, if the list contains
     com/ibm/product/foo.class,
   this method will insert the following entries (if they aren't
   already in the list):
     com/
     com/ibm/
     com/ibm/product/
   @param oldList The list of ZIP entry names (String objects).
   Assumed to be sorted alphabetically.
   @return The list with directory entries added.
   **/
  private static Vector insertDirectoryEntries(Vector oldList)
  {
    String priorPrefix = "";
    Vector newList = new Vector(oldList.size());
    Enumeration e = oldList.elements();
    while (e.hasMoreElements())
    {
      String listEntry = (String)e.nextElement();
      String prefix = listEntry.substring(0, listEntry.lastIndexOf("/")+1);
      if (!prefix.equals(priorPrefix))
      {
        priorPrefix = prefix;
        // Add any parent directories if not already in list.
        int slashPos = prefix.indexOf("/");
        while (slashPos != -1)
        {
          String subPrefix = prefix.substring(0, slashPos+1);
          if (!(newList.contains(subPrefix)) &&
              !(subPrefix.equals(listEntry)))
          {
            newList.addElement(subPrefix);
          }
          slashPos = prefix.indexOf("/",slashPos+1);
        }
      }
      newList.addElement(listEntry);
    }
    return newList;
  }


  /**
   Indicates if extract was specified.
   This method is provided for internal use.
   @return <code>true</code> if extract was specified,
           <code>false</code> otherwise.
   **/
  boolean isExtract() { return extract_; }


  /**
   Indicates if sufficient option information was specified.
   This method is provided for use by subclasses.
   @return <code>true</code> if sufficient info was specified,
           <code>false</code> otherwise.
   **/
  boolean isOptionInfoSufficient() { return arguments_.isOptionInfoSufficient(); }


  /**
   Indicates if split was specified.
   This method is provided for internal use.
   @return <code>true</code> if split was specified,
           <code>false</code> otherwise.
   **/
  boolean isSplit() { return split_; }


  /**
   Indicates whether <code>verbose</code> mode is in effect.
   This method is provided for internal use.
   @return <code>true</code> if <code>verbose</code> mode has been set;
   <code>false</code> otherwise.
   **/
  boolean isVerbose() { return verbose_; }


  /** Internal utility that prints out the command-line options, when in verbose mode. **/
  static String listCommandOptions(CommandLineArguments arguments, boolean listAll)
  {
    StringBuffer sb = new StringBuffer();
    Enumeration opts;
    if (listAll) opts = arguments.getOptionNames();
    else         opts = arguments.getExtraOptions();

    while (opts.hasMoreElements()) {
      String name = (String)opts.nextElement();
      sb.append(" [" + name);
      String val = arguments.getOptionValue(name);
      if (val != null && val.length() != 0) sb.append(" " + val);
      sb.append("]");
    }
    return sb.toString().trim();
  }


  /**
   Generates a smaller JAR or ZIP file, containing only the desired entries
   and their dependencies.

   @param sourceJarFile The source JAR or ZIP file.
   @return The destination JAR or ZIP file.
   @exception FileNotFoundException If the source file does not exist.
   @exception IOException If an I/O error occurs when reading the source file
   or writing the destination file.
   @exception ZipException If a ZIP error occurs when reading the source file
   or writing the destination file.
   **/
  public File makeJar(File sourceJarFile)
    throws FileNotFoundException, IOException, ZipException
  {
    if (sourceJarFile == null) throw new NullPointerException("sourceJarFile");
    File destinationJarFile =
      setupDefaultDestinationJarFile(sourceJarFile);
    makeJar(sourceJarFile, destinationJarFile);
    return destinationJarFile;
  }


  /**
   Generates a smaller JAR or ZIP file, containing only the desired entries
   and their dependencies.

   @param sourceJarFile The source JAR or ZIP file.
   @param destinationJarFile The destination JAR or ZIP file.
   @exception FileNotFoundException If the source file does not exist.
   @exception IOException If an I/O error occurs when reading the source file
   or writing the destination file.
   @exception ZipException If a ZIP error occurs when reading the source file
   or writing the destination file.
   **/
  public void makeJar(File sourceJarFile, File destinationJarFile)
    throws FileNotFoundException, IOException, ZipException
  {
    if (sourceJarFile == null)
      throw new NullPointerException("sourceJarFile");
    if (destinationJarFile == null)
      throw new NullPointerException("destinationJarFile");
    if (!sourceJarFile.exists())
      throw new FileNotFoundException(sourceJarFile.getAbsolutePath());
    if (destinationJarFile.exists() && !destinationJarFile.canWrite()) {
      System.err.println("Error: Cannot write file");
      System.err.println("       " + destinationJarFile.getAbsolutePath());
      throw new IOException(destinationJarFile.getAbsolutePath());
    }

    if (verbose_ || DEBUG) {
      System.out.println("Source file is " + sourceJarFile.getAbsolutePath());
      System.out.println("Destination file is " +
                          destinationJarFile.getAbsolutePath());
    }

    // Check that the destination files isn't the source file.
    if (destinationJarFile.getAbsolutePath().equals(sourceJarFile.getAbsolutePath()))
    {
      System.err.println("Error: Destination file is same as source file.");
      throw new IllegalArgumentException("destinationJarFile(" +
                                destinationJarFile.getAbsolutePath() + ")");
    }

    JarMap jarMap = null;    // Map of entries in the source JAR or ZIP.
    ManifestMap manifestMap = null;  // Map of the source JAR's manifest.
                                     // (If ZIP file, the map will be empty.)
    ZipOutputStream zipOutStream = null;  // Stream for writing destination file.
    InputStream inStream = null;
    BufferedOutputStream bufferedOutStream = null;

    try
    {
      // Make a map of the source file.
      jarMap = new JarMap(sourceJarFile, verbose_);

      // Get sorted list of the names of the entries that will be required
      // in the destination file.
      Vector referencedJarEntries = identifyDependencies(jarMap);

      // Verify the existence of any "additional files" that were specified.
      Enumeration e1 = additionalFiles_.keys();
      while (e1.hasMoreElements())
      {
        File file = (File)e1.nextElement();
        if (!file.exists() || !file.isFile())
        {
          System.err.println("Error: A specified additional file was not found:");
          System.err.println("       " + file.getAbsolutePath());
        }
      }

      // Prepare a list of ZIP entry names for any additional files that
      // were specified.  If any of these match an existing ZIP entry,
      // withhold the existing ZIP entry from the destination file.

      // Make a map of (File) to (ZIP entry name)
      Hashtable additionalFilesMap = generateJarEntryMap(additionalFiles_);

      // Open a stream for writing to the destination file.
      if (verbose_ || DEBUG)
      {
        System.out.println();
        System.out.println("Opening destination file " +
                            destinationJarFile.getAbsolutePath());
      }
      bufferedOutStream =
        new BufferedOutputStream(new FileOutputStream(destinationJarFile),
                                  BUFFER_SIZE);
      zipOutStream = new ZipOutputStream(bufferedOutStream);

      // Regenerate the manifest file.  We need to read it completely before
      // writing, so we can set up the ZIP entry with the correct size.

      if (jarMap.hasManifest())
      {
        manifestMap = new ManifestMap(jarMap);
        StringBuffer buffer = new StringBuffer();

        // Copy the manifest header entries if present.
        String manifestHeader = manifestMap.getHeader();
        if (manifestHeader.length() != 0)
          buffer.append(manifestHeader);

        // Copy manifest entries for all referenced files.
        Enumeration e3 = manifestMap.elements();
        while (e3.hasMoreElements())
        {
          String fileName = (String)e3.nextElement();
          if (referencedJarEntries.contains(fileName) &&
              !additionalFilesMap.contains(fileName))
          {
            String manifestEntryText = (String)(manifestMap.get(fileName));
            buffer.append(manifestEntryText);
          }
        }

        // Create manifest entries for any additional files.
        if (additionalFiles_.size() != 0)
        {
          if (verbose_ || DEBUG) System.out.println("Creating manifest entries " +
                                            "for additional files");
          Enumeration af = additionalFiles_.keys();
          while (af.hasMoreElements())
          {
            File file = (File)af.nextElement();
            String entryName = (String)(additionalFilesMap.get(file));
            constructManifestEntry(buffer, file, entryName);
          }
        }

        // Now that we know how big the new manifest file is,
        // write it to the destination JAR.
        byte[] bufferBytes = buffer.toString().getBytes();
        ZipEntry newManifestEntry = new ZipEntry(MANIFEST_ENTRY_NAME);
        newManifestEntry.setSize(bufferBytes.length);
        zipOutStream.putNextEntry(newManifestEntry);
        zipOutStream.write(bufferBytes);
        zipOutStream.flush();
        bufferedOutStream.flush();  // java bug 4077821
        zipOutStream.closeEntry();
        bufferedOutStream.flush();  // this might be overkill
      }

      // Copy the referenced files to the destination.
      if (DEBUG)
        System.out.println("Debug: Copying referenced ZIP entries(" +
                            referencedJarEntries.size() + ")");
      Vector directoriesSoFar = new Vector(); // directory entries created
      Enumeration refEntries = referencedJarEntries.elements();
      while (refEntries.hasMoreElements())
      {
        String referencedEntry = (String)refEntries.nextElement();
        if (verbose_) System.out.print(".");
        if (DEBUG) System.out.println(referencedEntry);

        // Copy this entry if it's not also in the "additional files" list.
        if (!additionalFilesMap.contains(referencedEntry))
        {
          if (referencedEntry.endsWith("/"))
            directoriesSoFar.addElement(referencedEntry);

          // Gather information from the source file.
          ZipEntry entry = jarMap.getEntry(referencedEntry);
          if (entry == null)
          {
            // Tolerate missing directory entries in source jar.
            if (referencedEntry.endsWith("/")) {
              if (DEBUG) System.err.println(referencedEntry +
                           ": Missing directory entry in source file.");
            }
            else
              throw new IOException(referencedEntry +
                                     ": No such entry in source file.");
          }
          else
          {
            inStream = jarMap.getInputStream(entry);
            // Design note - ZipFile.getInputStream() returns a 
            // java.util.zip.InflaterInputStream object.  Therefore
            // the current implementation causes each ZIP entry to be
            // decompressed and recompressed as it is copied from the
            // source file to the destination file.
            // If we need to improve performance, try to
            // find a way to copy entries without decompressing.
            // (See Java bug 4085767)

            // Copy the ZIP entry to the destination file.
            copyZipEntry(entry, inStream, zipOutStream);
            zipOutStream.flush();
            bufferedOutStream.flush(); // java bug 4077821
            zipOutStream.closeEntry();
            bufferedOutStream.flush(); // this might be overkill
            inStream.close();
            inStream = null;
          }
        }
      }  // ... while
      if (verbose_ || DEBUG) System.out.println();


      // Copy any additional files to destination file.

      if (DEBUG) System.out.println("Debug: Copying " +
                                      additionalFiles_.size() +
                                      " additional files");
      Enumeration addlFiles = additionalFiles_.keys();
      while (addlFiles.hasMoreElements())
      {
        File file = (File)addlFiles.nextElement();

        // Prepare to copy the file to the destination file.
        String entryName = (String)(additionalFilesMap.get(file));

        // Make sure any needed directory entries get added first.
        Vector dirsToAdd =
          generateDirEntries(entryName, directoriesSoFar);
        Enumeration de = dirsToAdd.elements();
        while (de.hasMoreElements()) {
          String dirName = (String)de.nextElement();
          ZipEntry dirEntry = new ZipEntry(dirName);
          dirEntry.setSize(0);
          zipOutStream.putNextEntry(dirEntry);
          zipOutStream.flush();
          bufferedOutStream.flush();  // java bug 4077821
          zipOutStream.closeEntry();
          bufferedOutStream.flush();  // this might be overkill
          directoriesSoFar.addElement(dirName);
        }

        ZipEntry entry = new ZipEntry(entryName);
        entry.setSize(file.length());

        // Note for future enhancement: Verify timestamps on the various platforms,
        // since lastModified() is system dependent.  Javadoc says:
        // "The return value is system dependent and should only be
        // used to compare with other values returned by last modified.
        // It should not be interpreted as an absolute time."
        entry.setTime(file.lastModified());

        inStream =
          new BufferedInputStream(new FileInputStream(file), BUFFER_SIZE);
        // Copy the entry to the destination file.
        zipOutStream.putNextEntry(entry);
        copyBytes(inStream, zipOutStream, entry.getSize());
        zipOutStream.flush();
        bufferedOutStream.flush(); // java bug 4077821
        zipOutStream.closeEntry();
        bufferedOutStream.flush(); // this might be overkill
        inStream.close();
        inStream = null;
      }
    }
    catch (ZipException e) {
      System.err.println("Error: ZipException when writing file");
      System.err.println("       " + destinationJarFile.getAbsolutePath() + ":");
      System.err.println(e.toString());
      if (DEBUG) e.printStackTrace(System.err);
      throw e;
    }
    catch (IOException e) {
      System.err.println("Error: IOException when writing file");
      System.err.println("       " + destinationJarFile.getAbsolutePath() + ":");
      System.err.println(e.toString());
      if (DEBUG) e.printStackTrace(System.err);
      throw e;
    }
    finally
    {
      if (manifestMap != null) {
        try { manifestMap.close(); } catch (Throwable e) {}
      }
      if (jarMap != null) {
        try { jarMap.close(); } catch (Throwable e) {}
      }
      if (zipOutStream != null) {
        try { zipOutStream.close(); } catch (Throwable e) {}  // @A1c
      }
      if (bufferedOutStream != null) {
        try { bufferedOutStream.close(); } catch (Throwable e) {} // @A1c
      }
      if (inStream != null) {
        try { inStream.close(); } catch (Throwable e) {}
      }

    }

  }


  /**
   Parses and validates the arguments specified on the command line.

   @param args The command line arguments.
   @param tolerateUnrecognizedArgs Whether to tolerate unrecognized args.
   @return An indication of whether the parse succeeded.
   **/
  boolean parseArgs(String[] args, boolean tolerateUnrecognizedArgs)
  {
    boolean succeeded = false;
    // Wipe the slate clean, in case this JarMaker object is being recycled.
    reset();
    if (arguments_.parse(args, this, tolerateUnrecognizedArgs))
      succeeded = true;
    return succeeded;
  }


  static final boolean OPTIONS_ALL = true;  // list all options
  static final boolean OPTIONS_UNRECOGNIZED = false;  // list only the 'extra' options


  /**
   Prescan the Constant Pool from a bytecode stream (from a JAR or ZIP file),
   looking for class names.

   @param  inStream           The input stream.
   @param  contextPackageName The package name for the referencing entry.
   @param  jarMap             A map of the JAR or ZIP file.
   @returns The list of constant_pool indexes for class names, as Integer's.

   @exception IOException If an I/O error occurs when reading the JAR or ZIP file.
   **/
  // Design note:
  // The .class file format is documented at:
  //       http://java.sun.com/docs/books/vmspec/html/ClassFile.doc.html
  //
  private Vector prescanForClassIndexes(InputStream inStream,
                                      String contextPackageName,
                                      JarMap jarMap)
    throws IOException
  {
    // Initialization.
    Vector classIndexes = new Vector();  // List of constant_pool indexes
                                          // for class names.
    DataInputStream dataInput = new DataInputStream(inStream);

    // Read the prefix information.
    skipBytes(dataInput, 4);     // Magic.
    skipBytes(dataInput, 2);     // Minor version.
    skipBytes(dataInput, 2);     // Major version.

    // Read the constant pool.  Constant pool indices are
    // numbered from 1 to constantPoolCount - 1.
    int cpCount = dataInput.readUnsignedShort();
    if (DEBUG_CP)
      System.out.println("Constant pool count = " + cpCount);

    for (int cpIndex = 1; cpIndex < cpCount; ++cpIndex)
    {
      // Read the tag.
      byte tag = dataInput.readByte();
      if (DEBUG_CP)
        System.out.println("Tag number " + cpIndex + " = " + tag);

      // If a "class" entry in the constant_pool pointed to this index
      // for its class name, verify that this entry is a utf8 entry.
      if (DEBUG_CP &&
          (tag != 1) &&
          (classIndexes.contains(new Integer(cpIndex))))
        System.err.println("Error: Class file format");

      // Decide what to do based on the tag.
      switch (tag)
      {
        case 1: // CONSTANT_Utf8
          int length = dataInput.readUnsignedShort();
          skipBytes(dataInput, length);
          break;

        case 3: // CONSTANT_Integer
        case 4: // CONSTANT_Float
          skipBytes(dataInput, 4);    // Bytes.
          break;

        case 5: // CONSTANT_Long
        case 6: // CONSTANT_Double
          skipBytes(dataInput, 8);    // Bytes.
          ++cpIndex;                  // These take up 2 slots!
          break;

        case 7: // CONSTANT_Class
          int nameIndex = dataInput.readUnsignedShort();
          if (DEBUG_CP)
            System.out.println("Found class constant pointing to index "
                                + nameIndex);
          classIndexes.addElement(new Integer(nameIndex));
          break;

        case 8: // CONSTANT_String
          skipBytes(dataInput, 2);    // String index.
          break;

        case 9: // CONSTANT_Fieldref
        case 10: // CONSTANT_Methodref
        case 11: // CONSTANT_InterfaceMethodref
          skipBytes(dataInput, 2);    // Class index.
          skipBytes(dataInput, 2);    // Name and type index.
          break;

        case 12: // CONSTANT_NameAndType
          skipBytes(dataInput, 2);    // Name index.
          skipBytes(dataInput, 2);    // Descriptor index.
          break;

        default:
          if (DEBUG_CP)
            System.err.println("Ignoring unrecognized tag during prescan: " + tag);
          break;
      }
    }
    return classIndexes;
  }


  /**
   Processes a bytecode stream (from a JAR or ZIP file),
   looking for referenced files.

   @param  inStream      The input stream.
   @param  packagePrefix The package prefix for the referencing entry.
   @param  jarMap        A map of the JAR or ZIP file.
   @param  classIndexes  The list of indices that contain references
                         to classes.  List elements are Integer objects.
   @returns The output list of referenced classes (ZIP entry names), as String's.

   @exception IOException If an I/O error occurs when reading the JAR or ZIP file.
   **/
  // Design note:
  // The .class file format is documented at:
  //       http://java.sun.com/docs/books/vmspec/html/ClassFile.doc.html
  //
  private Vector processBytecodeStream(InputStream inStream,
                                        String packagePrefix,
                                        JarMap jarMap,
                                        Vector classIndexes)
    throws IOException
  {
    // Initialization.
    Vector allReferences = new Vector(); // Resulting list of references.
    DataInputStream dataInput = new DataInputStream(inStream);

    // Read the prefix information.
    skipBytes(dataInput, 4);     // Magic.
    skipBytes(dataInput, 2);     // Minor version.
    skipBytes(dataInput, 2);     // Major version.

    // Read the constant pool.  Constant pool indices are
    // numbered from 1 to constantPoolCount - 1.
    int cpCount = dataInput.readUnsignedShort();
    if (DEBUG_CP)
      System.out.println("Constant pool count = " + cpCount);
    for (int cpIndex = 1; cpIndex < cpCount; ++cpIndex)
    {
      // Read the tag.
      byte tag = dataInput.readByte();
      if (DEBUG_CP)
        System.out.println("Tag number " + cpIndex + " = " + tag);

      // If a "class" entry in the constant_pool pointed to this index
      // for its class name, verify that this entry is a utf8 entry.
      if (DEBUG_CP)
        if ((tag != 1) && (classIndexes.contains(new Integer(cpIndex))))
          System.err.println("Error: Class file format");

      // Decide what to do based on the tag.
      switch (tag)
      {
        case 1: // CONSTANT_Utf8
          int length = dataInput.readUnsignedShort();
          if (length <= 0)
          {
            if (DEBUG_CP)
              System.err.println("Bad length in apparent CONSTANT_Utf8: " + length);
          }
          else
          {
            byte[] bytes = new byte[length];
            dataInput.readFully(bytes);
            String referencedJarEntry =
              processUtf8(cpIndex, new String(bytes,"UTF8"),
                           classIndexes, packagePrefix, jarMap);
            if (referencedJarEntry != null)
              allReferences.addElement(referencedJarEntry);
          }
          break;

        case 3: // CONSTANT_Integer
        case 4: // CONSTANT_Float
          skipBytes(dataInput, 4);    // Bytes.
          break;

        case 5: // CONSTANT_Long
        case 6: // CONSTANT_Double
          skipBytes(dataInput, 8);    // Bytes.
          ++cpIndex;                  // These take up 2 slots!
          break;

        case 7: // CONSTANT_Class
          int nameIndex = dataInput.readUnsignedShort();
          if (DEBUG_CP)
            System.out.println("Found class constant pointing to index "
                                + nameIndex);
          break;

        case 8: // CONSTANT_String
          skipBytes(dataInput, 2);    // String index.
          break;

        case 9: // CONSTANT_Fieldref
        case 10: // CONSTANT_Methodref
        case 11: // CONSTANT_InterfaceMethodref
          skipBytes(dataInput, 2);    // Class index.
          skipBytes(dataInput, 2);    // Name and type index.
          break;

        case 12: // CONSTANT_NameAndType
          skipBytes(dataInput, 2);    // Name index.
          skipBytes(dataInput, 2);    // Descriptor index.
          break;

        default:
          if (DEBUG_CP)
            System.err.println("Ignoring unrecognized tag: " + tag);
          break;
      }
    }
    return allReferences;
  }


  /**
   Processes a Utf8 literal.

   @param  cpIndex            The constant pool index.
   @param  literal            The literal.
   @param  classIndexes       The list of indices that contain references
                              to classes.
   @param  contextPackageName The package name to prepend to gif files.
   @param  jarMap             A map of the source JAR or ZIP file.
   @returns The name of the referenced ZIP entry name.
   <code>null</code> if a corresponding ZIP entry was not found in source file.
   **/
  private String processUtf8(int cpIndex,
                            String literal,
                            Vector classIndexes,
                            String contextPackageName,
                            JarMap jarMap)
  {
    if (DEBUG_CP) System.out.println("processUtf8(" + literal + ")");
    String result = null;

    // If this index was flagged as one with a CONSTANT_class
    // structure, then it is a referenced class.
    if (classIndexes.contains(new Integer(cpIndex)))
    {
      // Also verify that the class is in the source file, and
      // not the JDK or some primitive class like [[B (byte array).
      String classFileName = literal + CLASS_SUFFIX;
      if (jarMap.contains(classFileName))
        result = classFileName;
    }

    // Check if this could be a reference to a properties
    // file or a class loaded with Class.forName().
    else if ((contextPackageName.length() != 0 &&
              literal.startsWith(contextPackageName)) ||
             ((jarEntryDefaultPrefix_ != null) &&
              ((literal.startsWith(jarEntryDefaultPrefix_)) ||
               (literal.startsWith(jarEntryDefaultPrefixDotted_)))))
    {
      // Check if it might be a properties file.
      String propertiesFileName = literal.replace('.','/') + ".properties";
      if (jarMap.contains(propertiesFileName))
        result = propertiesFileName;

      // Check if it is a class loaded with Class.forName().
      else
        // Design note: Might want to make both checks regardless.
        // The mystery literal might refer to either a properties file,
        // a class file, both, or neither.  For example, there might be
        // both "foo.class" and "foo.properties" in the source JAR or ZIP.
      {
        String classFileName = literal.replace('.','/') + CLASS_SUFFIX;
        if (jarMap.contains(classFileName))
          result = classFileName;
      }
    }

    // Check if this could be a reference to a gif or jpg file.
    // The Unity JAR may also contain files of type html, pdml,
    // pcml, and ser.

    // These references do not necessarily include the package name.
    // Thus we use the contextPackageName to make our best guess.
    // Note for future enhancement - May need to add logic to look for other occurrences
    // of the file in other packages within the source JAR or ZIP.

    else
    {
      String suffix = null;
      int dotPos = literal.lastIndexOf('.');
      if (dotPos != -1) suffix = literal.substring(dotPos).toLowerCase();
      if ((suffix != null) &&
          (suffix.equals(".gif")  ||
           suffix.equals(".jpg")  ||
           suffix.equals(".html") ||
           suffix.equals(".pdml") ||
           suffix.equals(".pcml") ||
           suffix.equals(".ser")))
      {
        String gifFileName = null;
        if (contextPackageName.length() != 0)
        {
          gifFileName = contextPackageName.replace('.','/') +
                         "/" + literal;
          if (jarMap.contains(gifFileName))  // try with package prefix
            result = gifFileName;
          else if (jarMap.contains(literal)) // try with no package prefix
            result = literal;
          else if (DEBUG)
            System.err.println("Debug: Referenced file not found in jar: " +
                                literal);
        }
        else if (jarMap.contains(literal)) // try with no package prefix
          result = literal;
        else if (DEBUG)
          System.err.println("Debug: JAR does not contain referenced file: " +
                              literal);
      }
      else if (DEBUG_CP)
      {
        int endIndex = 80;  // just print out the first 80 chars
        if (literal.length() < 80) endIndex = literal.length();
        System.err.println("processUtf8: Ignoring reference: " +
                            literal.substring(0,endIndex));
      }
    }
    return result;
  }


  /**
   Removes a listener from the listener list.

   @param listener The listener.
   **/
  public synchronized void removeJarMakerListener(JarMakerListener listener)
  {
    if (listener == null) throw new NullPointerException("listener");
    eventListeners_.removeElement(listener);
  }


  /**
   Resets the JarMaker object to a clean, default state,
   to facilitate object reuse.
   **/
  public void reset()
  {
    sourceJarFile_ = null;
    destinationJarFile_ = null;
    filesRequired_.removeAllElements();
    filesExcluded_.removeAllElements();
    packages_.removeAllElements();
    packagesExcluded_.removeAllElements();
    additionalFiles_.clear();
    verbose_ = false;
    extract_ = false;
    baseDirectoryForExtract_ = new File(System.getProperty("user.dir"));
    split_ = false;
    splitSize_ = SPLIT_SIZE_KBYTES;
    eventListeners_.removeAllElements();
    arguments_ = new Arguments();

    // Note: jarEntryDefaultPrefix_ and jarEntryDefaultPrefixDotted_
    // are set in the constructor only, so leave them unchanged.
  }


  /**
   Specifies additional files to include in the destination JAR or ZIP file.
   These are files that reside outside of the source JAR or ZIP file.
   If an additional file resolves to the same entry name as an existing
   entry in the source file, the additional file will replace the
   existing entry in the generated JAR or ZIP file.
   When deriving ZIP entry names,
   the base directory for the files is the current directory.
   <br>Note: This augments any previously specified additional files.
   This method does not verify the existence of the specified files.

   @param fileList The additional files to include in the
   destination JAR or ZIP file.
   The list should contain only <code>java.io.File</code> objects.
   **/
  public void setAdditionalFiles(Vector fileList)
  {
    // Default: Base directory is current directory.
    File baseDirectory = new File(System.getProperty("user.dir"));
    setAdditionalFiles(fileList, baseDirectory);
  }


  /**
   Specifies additional files to include in the destination JAR or ZIP file.
   These are files that reside outside of the source JAR or ZIP file.
   If an additional file resolves to the same entry name as an existing
   entry in the source file, the additional file will replace the
   existing entry in the generated JAR or ZIP file.
   <br>Note: This augments any previously specified additional files.
   This method does not verify the existence of the specified files or directory.

   @param fileList The additional files to include in the
   destination JAR or ZIP file.
   The list should contain only <code>java.io.File</code> objects.
   @param baseDirectory The base directory for the specifed files.  This path
   is used when assigning a ZIP entry name for the additional file.
   <br>The path below this directory
   should match the package name sequence for the files.
   For example, if the additional file is
   <code>C:\dir1\subdir2\com\ibm\myproduct\MyClass.class</code>,
   and class <code>MyClass</code> is in package <code>com.ibm.myproduct</code>,
   then the base directory should be set to <code>C:\dir1\subdir2</code>.
   **/
  public void setAdditionalFiles(Vector fileList, File baseDirectory)
  {
    if (fileList == null)
      throw new NullPointerException("fileList");
    if (baseDirectory == null)
      throw new NullPointerException("baseDirectory");
    // Check for nulls and for correct element type.
    fileList = validateList(fileList, "additionalFile",
                             "java.io.File", verbose_);
    Enumeration e = fileList.elements();
    while (e.hasMoreElements())
    {
      File file = (File)e.nextElement();
      if (!additionalFiles_.containsKey(file)) {
        additionalFiles_.put(file, baseDirectory);
      }
    }
    // Postpone verification of files' existence until we need to read them,
    // since we don't want setter throwing "file not found" exceptions.
  }


  /**
   Sets the destination JAR or ZIP file.
   This method is provided for internal use.

   @param destinationJarFile The destination JAR or ZIP file.
   **/
  void setDestinationJar(File destinationJarFile) {
    if (destinationJarFile == null)
      throw new NullPointerException("destinationJarFile");
    destinationJarFile_ = destinationJarFile;
  }


  /**
   Sets the <code>extract</code> mode on or off.
   This method is provided for internal use when parsing command-line args.

   @param extract If <code>true</code>, turn extract mode on;
   otherwise turn extract mode off.
   **/
  void setExtract(boolean extract) { extract_ = extract; }


  /**
   Sets the base directory for the directory tree into which the
   source JAR or ZIP file is to be extracted.
   This method is provided for internal use.

   @param baseDirectory The base directory for the extraction.
   **/
  void setExtractionDirectory(File baseDirectory)
  { baseDirectoryForExtract_ = baseDirectory; }


  /**
   Specifies the names of packages that are to be included in the output.
   Packages are specified in standard syntax, such as <code>com.ibm.component</code>.
   The specified packages are simply included in the output.
   No additional dependency analysis is done on the files in a package,
   unless those files are also specified as required files.
   <br>Note: This augments any previously specified packages.
   This method does not verify the existence of the specified packages
   in the source JAR or ZIP file.

   @param packages The required packages.
   The list should contain only <code>String</code> objects.
  **/
  public void setPackages(Vector packages)
  {
    if (packages == null)
      throw new NullPointerException("packages");
    // Check for nulls and for correct element type.
    packages = validateList(packages, "packageName",
                             "java.lang.String", verbose_);
    copyVector(packages, packages_, CHECK_DUPS);  // @A3c
  }


  /**
   Specifies the names of packages that are to be excluded from the output.
   Packages are specified in standard syntax, such as <code>com.ibm.component</code>.
   <br>Note: This augments any previously specified packages.

   @param packages The packages to be excluded.
   The list should contain only <code>String</code> objects.
  **/
  public void setPackagesExcluded(Vector packages)
  {
    if (packages == null)
      throw new NullPointerException("packages");
    // Check for nulls and for correct element type.
    packages = validateList(packages, "packageName",
                             "java.lang.String", verbose_);
    copyVector(packages, packagesExcluded_, CHECK_DUPS);
  }


  /**
   @deprecated Use setFilesRequired() instead.
   **/
  public void setRequiredFiles(Vector entryList)
  {
    setFilesRequired(entryList);
  }


  /**
   Specifies the names of required entries in the source JAR or ZIP file.
   The names are specified in JAR entry name syntax, such as
   <code>com/ibm/component/className.class</code>.
   <br>Note: This augments any previously specified required entries.
   This method does not verify the existence of the specified entries
   in the source file.

   @param entryList The names of required JAR or ZIP entries.
   The list should contain only <code>String</code> objects.
   **/
  public void setFilesRequired(Vector entryList)
  {
    if (entryList == null)
      throw new NullPointerException("entryList");
    // Check for nulls and for correct element type.
    entryList = validateList(entryList, "entryName",
                              "java.lang.String", verbose_);
    copyVector(entryList, filesRequired_, CHECK_DUPS);  // @A3c
  }


  /**
   Specifies the names of entries in the source JAR or ZIP file that are to be excluded from the target.
   The names are specified in JAR entry name syntax, such as
   <code>com/ibm/component/className.class</code>.
   <br>Note: This augments any previously specified excluded entries.
   This method does not verify the existence of the specified entries
   in the source file.

   @param entryList The names of JAR or ZIP entries to be excluded.
   The list should contain only <code>String</code> objects.
   **/
  public void setFilesExcluded(Vector entryList)
  {
    if (entryList == null)
      throw new NullPointerException("entryList");
    // Check for nulls and for correct element type.
    entryList = validateList(entryList, "entryName",
                              "java.lang.String", verbose_);
    copyVector(entryList, filesExcluded_, CHECK_DUPS);
  }


  /**
   Sets the source JAR or ZIP file.
   This method is provided for internal use.

   @param sourceJarFile The source JAR or ZIP file.
   **/
  void setSourceJar(File sourceJarFile)
  {
    if (sourceJarFile == null)
      throw new NullPointerException("sourceJarFile");
    sourceJarFile_ = sourceJarFile;
  }


  /**
   Sets the <code>split</code> mode on or off.
   This method is provided for internal use.

   @param split If <code>true</code>, turn split mode on;
   otherwise turn split mode off.
   **/
  void setSplit(boolean split) { split_ = split; }


  /**
   Sets the maximum file size for the destination JAR or ZIP files that
   are produced by the <code>split()</code> method.
   This method is provided for internal use.

   @param splitSize The maximum file size for the destination JAR or ZIP files;
   in units of kilobytes (1024 bytes).
   **/
  void setSplitSize(int splitSize) { splitSize_ = splitSize; }


  /**
   Generates a default destination JAR or ZIP file name,
   based on the name of the source file.

   @param sourceJarFile The source JAR or ZIP file.
   @return              A default destination JAR or ZIP file.
   **/
  static File setupDefaultDestinationJarFile(File sourceJarFile)
  {
    String sourceJarName = sourceJarFile.getName();
    int index = sourceJarName.lastIndexOf('.');
    String destinationJarName;
    String suffix = "Small";
    if (index == -1)
      destinationJarName = sourceJarName + suffix;
    else
      destinationJarName = sourceJarName.substring(0, index)
        + suffix + sourceJarName.substring(index);
    // Put it in the current directory.
    return new File(CURRENT_DIR, destinationJarName);
  }


  /**
   Generates a destination JAR or ZIP file name,
   based on the name of the source file.
   Inserts the specified suffix, before the final ".".

   @param sourceJarFile The source JAR or ZIP file.
   @param suffix The suffix to append to the name.
   @return A destination JAR or ZIP file.
   **/
  private static File setupSplitJarFile(File sourceJarFile, int suffix)
  {
    String sourceJarName = sourceJarFile.getName();
    String destinationJarName;
    int index = sourceJarName.lastIndexOf('.');
    if (index == -1)
      destinationJarName = sourceJarName + suffix;
    else
      destinationJarName = sourceJarName.substring(0, index)
        + Integer.toString(suffix) + sourceJarName.substring(index);
    // Put it in the current directory.
    return new File(CURRENT_DIR, destinationJarName);
  }


  /**
   Sets <code>verbose</code> mode 'on'.
  **/
  public void setVerbose() { setVerbose(true); }


  /**
   Sets <code>verbose</code> mode on or off.

   @param verbose If <code>true</code>, turn verbose mode on;
   otherwise turn verbose mode off.
   **/
  public void setVerbose(boolean verbose) { verbose_ = verbose; }


  private static final void skipBytes(DataInputStream dataInput, int length)
    throws IOException
  {
    int bytesSkipped = dataInput.skipBytes(length);
    if (bytesSkipped != length) {
      throw new IOException("Fewer bytes skipped ("+bytesSkipped+") than specified ("+length+").");
    }
  }


  /**
   Sorts a list of Strings lexicographically.
   List entries are compared using <code>String.compareTo(String)</code>.
   @param originalList The list of Strings to sort.
   @return The sorted list.
   **/
  static Vector sortStrings(Vector originalList)
  {
    Vector sortedList = new Vector(originalList.size());
    Enumeration e = originalList.elements();
    while (e.hasMoreElements())
    {
      String oldEntry = (String)e.nextElement();
      boolean done = false;
      int insertionPosition = sortedList.size();  // default: add at end
      Enumeration sorted = sortedList.elements();
      for (int i=0; !done && sorted.hasMoreElements(); ++i)
      {
        String sortedEntry = (String)sorted.nextElement();
        if ((oldEntry.compareTo(sortedEntry)) < 0)
        {
          insertionPosition = i;
          done = true;
        }
      }
      sortedList.insertElementAt(oldEntry, insertionPosition);
    }
    return sortedList;
  }


  /**
   Splits the specified JAR or ZIP file into smaller JAR or ZIP files
   whose size does not exceed 2 megabytes.
   No files are added or removed; the entries in the source file
   are simply distributed among the generated JAR or ZIP files.
   If any single file within the source JAR or ZIP file exceeds
   2 megabytes, a warning is printed to <code>System.err</code>.

   @param sourceJarFile The source JAR or ZIP file.
   @return The generated files.
   This is a list of <code>java.io.File</code> objects.
   @exception FileNotFoundException If the source file does not exist.
   @exception IOException If an I/O error occurs when reading the source file
   or writing a generated file.
   @exception ZipException If a ZIP error occurs when reading the source file
   or writing a generated file.
   **/
  public Vector split(File sourceJarFile)
    throws FileNotFoundException, IOException, ZipException
  {
    return split(sourceJarFile, SPLIT_SIZE_KBYTES);
  }


  /**
   Splits the specified source JAR or ZIP file into smaller JAR or ZIP files
   whose size does not exceed the specified number of kilobytes.
   No files are added or removed; the entries in the source file
   are simply distributed among the destination files.
   If any single file within the source file exceeds
   the specified size, a warning is printed to <code>System.err</code>.

   @param sourceJarFile The source JAR or ZIP file.
   @param splitSizeKbytes The maximum size for the generated JAR or ZIP files
   (in kilobytes).  Must be greater than zero.
   @return The generated files.
   This is a list of <code>java.io.File</code> objects.
   @exception FileNotFoundException If the source file does not exist.
   @exception IOException If an I/O error occurs when reading the source file
   or writing a generated file.
   @exception ZipException If a ZIP error occurs when reading the source file
   or writing a generated file.
   **/
  public Vector split(File sourceJarFile, int splitSizeKbytes)
    throws FileNotFoundException, IOException, ZipException
  {
    if (sourceJarFile == null)
      throw new NullPointerException("sourceJarFile");
    if (splitSizeKbytes < 1)
      throw new IllegalArgumentException("splitSizeKbytes (" +
                                          splitSizeKbytes + ")");
    if (!sourceJarFile.exists())
      throw new FileNotFoundException(sourceJarFile.getAbsolutePath());
    if (verbose_ || DEBUG) {
      System.out.println("Source file is " +
                          sourceJarFile.getAbsolutePath());
      System.out.println("Split size is " + splitSizeKbytes + " kilobytes");
    }

    Vector destJarList = new Vector();
    JarMap jarMap = null;
    ManifestMap manifestMap = null;
    Long splitSizeLong = new Long(splitSizeKbytes);
    long splitSize = splitSizeLong.longValue() * 1024; // Kbytes -> bytes
    File currentOutputFile = null;

    try
    {
      // Design note: Refrain from first deleting any existing JAR files,
      // on the chance that the user might actually want to keep them.

      int outFileIndex = 0;
      currentOutputFile = setupSplitJarFile(sourceJarFile, outFileIndex++);

      // Simplest case: If the source file is smaller than splitSize,
      // just copy it to a single destination file.
      if (sourceJarFile.length() <= splitSize)
      {
        copyFile(sourceJarFile, currentOutputFile);
        destJarList.addElement(currentOutputFile);
        return destJarList;
      }

      // Make a map of the source file.
      if (DEBUG) System.out.println("Debug: Making JarMap");
      jarMap = new JarMap(sourceJarFile, verbose_);
      manifestMap = new ManifestMap(jarMap);
      boolean manifestExists = jarMap.hasManifest();
      int baseMetadataPerZipEntry = jarMap.getSizeOfZipMetadataPerEntry();

      // Get size of ZIP "end of central directory" record.
      int baseMetadataPerZip = jarMap.getSizeOfZipMetadataPerZip();
      // Allow for ZIP metadata for the Manifest entry.
      if (manifestExists)
        baseMetadataPerZip +=
          baseMetadataPerZipEntry + 2*(MANIFEST_ENTRY_NAME.length());
      // Design note: For a complete description of the ZIP file
      // format, including detailed descriptions of the metadata fields,
      // refer to the ZIP specification.  It may be downloaded from:
      //     http://www.pkware.com/download.html

      // Gather names of entries to write to the next output file.
      Vector entriesToWriteNext = new Vector(); // ZIP entry names (String's)
      // Running total (compressed) size of entries:
      long cumulativeSize = baseMetadataPerZip;

      Vector directoriesSoFar = new Vector();
      Vector dirsToAddForThisEntry = null;

      Enumeration e = jarMap.elements();
      while (e.hasMoreElements())
      {
        String entryName = (String)e.nextElement();
        ZipEntry entry = jarMap.getEntry(entryName);
        if (entry == null)
          throw new RuntimeException("Programming error: No JarMap entry for " +
                                      entryName);
        if (entry.isDirectory()) directoriesSoFar.addElement(entryName);

        // Get the uncompressed size of this ZIP entry.
        // Design note: Since we're not sure we'll be able
        // to recompress it to the original compressed size, err on the
        // conservative side, rather than risk generating an oversize file.
        long entrySize = entry.getSize();
        long entryMetadataSize = baseMetadataPerZipEntry + 2*(entryName.length());

        // Add size of comment.
        String comment = entry.getComment();
        if (comment != null) entryMetadataSize += comment.length();

        // Add size of extra field data (twice, since it will appear
        // in both the Local Field Header and in the Central Directory).
        byte[] extraFieldData = entry.getExtra();
        if (extraFieldData != null) {
          entryMetadataSize += 2*extraFieldData.length;
          if (false && DEBUG_ZIP)
            System.out.println("Debug: extraFieldData.length = " +
                                extraFieldData.length);
        }

        // Add size of manifest entry for this file.
        // This is the uncompressed size,
        // so we will err on the conservative side.
        if (manifestExists)
          entryMetadataSize += manifestMap.getEntrySize(entryName);

        // Add the size of metadata for any additional directory entries
        // that will be needed for this entry.
        dirsToAddForThisEntry =
          generateDirEntries(entryName, directoriesSoFar);
        int directoriesMetadataSize =
          determineDirMetadataSize(dirsToAddForThisEntry,
                                    baseMetadataPerZipEntry); 

        // If the size of this single entry equals or exceeds the split size,
        // write it to its own separate JAR file.
        if ((entrySize + entryMetadataSize + baseMetadataPerZip +
             directoriesMetadataSize)
            >= splitSize)
        { 
          Vector entryToWriteNow = new Vector();
          entryToWriteNow.addElement(entryName);
          writeJarEntries(entryToWriteNow, currentOutputFile, jarMap, manifestMap, splitSize, verbose_);
          if (currentOutputFile.length() > splitSize)
          {
            System.err.println("Warning: Oversize ZIP entry " + entryName);
            System.err.println("         was written to file " +
                                currentOutputFile.getAbsolutePath()+".");
          }
          destJarList.addElement(currentOutputFile);
          currentOutputFile = setupSplitJarFile(sourceJarFile, outFileIndex++);
        }
        else
        {
          if ((cumulativeSize + entrySize + entryMetadataSize +
               directoriesMetadataSize) > splitSize)
          { // Flush the buffer before adding this entry.
            writeJarEntries(entriesToWriteNext, currentOutputFile,
                             jarMap, manifestMap, splitSize, verbose_);
            entriesToWriteNext.removeAllElements();
            destJarList.addElement(currentOutputFile);
            currentOutputFile = setupSplitJarFile(sourceJarFile, outFileIndex++);
            cumulativeSize = baseMetadataPerZip;

            // Recalculate the size of metadata for needed directories.
            directoriesSoFar.removeAllElements();
            dirsToAddForThisEntry =
              generateDirEntries(entryName, directoriesSoFar);
            directoriesMetadataSize =
              determineDirMetadataSize(dirsToAddForThisEntry,
                                        baseMetadataPerZipEntry); 
          }
          entriesToWriteNext.addElement(entryName);
          if (dirsToAddForThisEntry != null) {
            copyVector(dirsToAddForThisEntry, directoriesSoFar, NO_CHECK_DUPS);  // @A3c
          }
          cumulativeSize += (entrySize + entryMetadataSize +
                             directoriesMetadataSize);
        }
      }  // end of 'while'

      // Write any pending entries.
      if (entriesToWriteNext.size() != 0) {
        writeJarEntries(entriesToWriteNext, currentOutputFile,
                         jarMap, manifestMap, splitSize, verbose_);
        entriesToWriteNext.removeAllElements();
        destJarList.addElement(currentOutputFile);
      }

      // Design note - ZipFile.getInputStream() returns a 
      // java.util.zip.InflaterInputStream object.  Therefore
      // the current implementation causes each ZIP entry to be
      // decompressed and recompressed as it is copied from the
      // source file to the destination file.
      // If we need to improve performance, try to
      // find a way to copy entries without decompressing.
    }
    catch (ZipException e) {
      System.err.println("Error: ZipException when splitting file");
      System.err.println("       " + sourceJarFile.getAbsolutePath() + ":");
      System.err.println(e.toString());
      if (DEBUG) e.printStackTrace(System.err);
      throw e;
    }
    catch (IOException e) {
      System.err.println("Error: IOException when splitting file");
      System.err.println("       " + sourceJarFile.getAbsolutePath() + ":");
      System.err.println(e.toString());
      if (DEBUG) e.printStackTrace(System.err);
      throw e;
    }
    finally
    {
      if (manifestMap != null) {
        try { manifestMap.close(); } catch (Throwable t) {}
      }
      if (jarMap != null) {
        try { jarMap.close(); } catch (Throwable t) {}
      }

    }

    return destJarList;
  }

  /**
   Checks for null pointers, verifies that list entries are of correct
   type, and removes duplicate list entries.  Checks that Integers have
   non-negative values.  Trims leading and trailing whitespace from Strings
   and checks that they have nonzero length.
   Only references to the same object are considered duplicates.
   The element sequence is preserved in the returned list.
   Does not verify existence of referenced Files.
   @param oldList The list to remove duplicate entries from.
   @param className The name of the parameter.
   @param className The name of the class that the list members are
   supposed to be instances of.
   @param verbose The verbose output mode.
   @return The list without duplicates.
   @exception IllegalArgumentException If any element is not of correct
   class, or is out of range.
   **/
  static Vector validateList(Vector oldList, String parmName,
                              String className, boolean verbose)
  {
    if (oldList == null) throw new NullPointerException("list");
    if (parmName == null) throw new NullPointerException("parmName");
    if (className == null) throw new NullPointerException("className");
    // Note: The above exceptions would be due to internal programming errors.
    Class expectedClass = null;
    try { expectedClass = Class.forName(className); }
    catch (ClassNotFoundException e) { // This can only happen if JarMaker has defect
      throw new RuntimeException("Programming error: Class not found: " +
                                  className);
    }

    Vector newList = new Vector(oldList.size());
    Enumeration e = oldList.elements();
    for (int i=0; e.hasMoreElements(); i++)
    {
      Object element = e.nextElement();
      if (element == null)
        throw new NullPointerException(parmName);
      if (!(expectedClass.isInstance(element))) {
        String actualClass = element.getClass().getName();
        throw new IllegalArgumentException(parmName +
                                   " (object of class " + actualClass + ")");
      }
      if (element instanceof Integer) {
        int value = ((Integer)element).intValue();
        if (value < 0)
          throw new IllegalArgumentException(parmName + " (" + value + ")");
      }
      else if (element instanceof String) {
        String elem = ((String)element).trim();  // strip leading/trailing blanks
        if (elem.length() == 0)
          throw new IllegalArgumentException(parmName + "()");
        element = elem;
      }
      if (!newList.contains(element))
        newList.addElement(element);
      else if (DEBUG) {
        System.err.print("Debug: Ignoring duplicate list entry: ");
        if (element instanceof String)
          System.err.println((String)element);
        else if (element instanceof File)
          System.err.println(((File)element).getPath());
        else if (element instanceof Integer)
          System.err.println(((Integer)element).toString());
        else System.err.println(parmName + " at offset " + i);
      }
    }
    return newList;
  }


  /**
   Writes the specified ZIP entries (along with their manifest entries,
   in the case of a JAR file) to the specified destination file.

   @param entryNames The names of the entries to copy.  This is a list of Strings.
   @param outFile The destination file.
   @param jarMap A map of the source file.
   @param manifestMap A map of the manifest of the source JAR or ZIP file.
                      In the case of a ZIP file, this is an empty map.
   @param splitSize The desired maximum size (bytes) for generated files.
   @param verbose The verbose output mode.
   @exception FileNotFoundException If the source file does not exist.
   @exception IOException If an I/O error occurs when reading the source file
   or writing the destination file.
   @exception ZipException If a ZIP error occurs when reading the source file
   or writing the destination file.
   **/
  private static void writeJarEntries(Vector entryNames, File outFile,
                                       JarMap jarMap, ManifestMap manifestMap,
                                       long splitSize, boolean verbose)
    throws FileNotFoundException, IOException, ZipException
  {
    if (entryNames == null) throw new NullPointerException("entryNames");
    if (outFile == null) throw new NullPointerException("outFile");
    if (jarMap == null) throw new NullPointerException("jarMap");
    if (manifestMap == null) throw new NullPointerException("manifestMap");

    if (verbose) System.out.println("writeJarEntries( " +
                                   outFile.getName() + " )");
    ZipOutputStream zipOutStream = null;
    BufferedOutputStream bufferedOutStream = null;
    InputStream inStream = null;

    try
    {
      // Set up a stream through which to write the file.
      bufferedOutStream = new BufferedOutputStream(new FileOutputStream(outFile), BUFFER_SIZE);
      zipOutStream = new ZipOutputStream(bufferedOutStream);

      // Build the manifest (if needed).
      StringBuffer manifestBuffer = new StringBuffer();
      if (jarMap.hasManifest())
      {
        String manifestHeader = manifestMap.getHeader();
        if (manifestHeader.length() != 0)
          manifestBuffer.append(manifestHeader);

        Enumeration e = entryNames.elements();
        while (e.hasMoreElements())
        {
          String entryName = (String)e.nextElement();
          String manifestText = manifestMap.get(entryName);
          if (manifestText != null)
            manifestBuffer.append(manifestText);
        }
      }

      // Write the manifest to the file.
      if (manifestBuffer.length() != 0)
      {
        byte[] bufferBytes = manifestBuffer.toString().getBytes();
        ZipEntry newManifestEntry = new ZipEntry(MANIFEST_ENTRY_NAME);
        newManifestEntry.setSize(bufferBytes.length);

        try {
          zipOutStream.putNextEntry(newManifestEntry);
          zipOutStream.write(bufferBytes);
          zipOutStream.flush();
          bufferedOutStream.flush(); // java bug 4077821
          zipOutStream.closeEntry();
          bufferedOutStream.flush(); // this might be overkill
        }
        catch (ZipException e) {
          System.err.println("Error: ZipException for manifest entry");
          System.err.println("       " + newManifestEntry.getName() + ":");
          System.err.println(e.toString());
          if (DEBUG) e.printStackTrace(System.err);
          throw e;
        }
        catch (IOException e) {
          System.err.println("Error: IOException for manifest entry");
          System.err.println("       " + newManifestEntry.getName() + ":");
          System.err.println(e.toString());
          if (DEBUG) e.printStackTrace(System.err);
          throw e;
        }
      }

      // Write the entries to the file.
      Vector directoriesSoFar = new Vector(); // directory entries created
      Enumeration e1 = entryNames.elements();
      while (e1.hasMoreElements())
      {
        String entryName = (String)e1.nextElement();
        ZipEntry entry = jarMap.getEntry(entryName);
        if (entry == null)
          System.err.println("Error: Entry not found in source file: " +
                              entryName);
        else
        {
          // Make sure any needed 'directory' entries get added first.
          Vector dirsToAdd =
            generateDirEntries(entryName, directoriesSoFar);
          Enumeration e2 = dirsToAdd.elements();
          while (e2.hasMoreElements()) {
            String dirName = (String)e2.nextElement();
            ZipEntry dirEntry = new ZipEntry(dirName);
            dirEntry.setSize(0);
            zipOutStream.putNextEntry(dirEntry);
            zipOutStream.flush();
            bufferedOutStream.flush(); // java bug 4077821
            zipOutStream.closeEntry();
            bufferedOutStream.flush(); // this might be overkill
            directoriesSoFar.addElement(dirName);
          }

          // Get an input stream from which to read the ZIP entry.
          inStream = jarMap.getInputStream(entry);
          // Design note - ZipFile.getInputStream() returns a 
          // java.util.zip.InflaterInputStream object.  Therefore
          // the current implementation causes each ZIP entry to be
          // decompressed and recompressed as it is copied from the
          // source file to the destination file.
          // If we need to improve performance, try to
          // find a way to copy the entry without decompressing.

          // Copy the ZIP entry to the destination file.
          copyZipEntry(entry, inStream, zipOutStream);
          zipOutStream.flush();
          bufferedOutStream.flush(); // java bug 4077821
          zipOutStream.closeEntry();
          bufferedOutStream.flush(); // this might be overkill
          inStream.close();
          inStream = null;

          // If this is a directory entry, remember that we've seen it.
          if (entry.isDirectory())
            directoriesSoFar.addElement(entryName);
        }
      }  // end of 'while' loop

      if (outFile.length() > splitSize) {
        System.err.println("Error: Generated file exceeds specified size:");
        System.err.println("       " + outFile.getAbsolutePath());
      }
    }
    catch (ZipException e) {
      System.err.println("Error: ZipException when writing to file");
      System.err.println("       " + outFile.getAbsolutePath() + ":");
      System.err.println(e.toString());
      if (DEBUG) e.printStackTrace(System.err);
      throw e;
    }
    catch (IOException e) {
      System.err.println("Error: IOException when writing to file");
      System.err.println("       " + outFile.getAbsolutePath() + ":");
      System.err.println(e.toString());
      if (DEBUG) e.printStackTrace(System.err);
      throw e;
    }
    finally
    {
      if (inStream != null) {
        try { inStream.close(); } catch (Throwable t) {}
      }
      if (zipOutStream != null) {
        try { zipOutStream.close(); } catch (Throwable t) {}
      }
      if (bufferedOutStream != null) {
        try { bufferedOutStream.close(); } catch (Throwable t) {}
      }
    }
  }


  /**
   Performs the actions specified in the invocation arguments.

   @param args The command line arguments.
   **/
  public static void main(String[] args)
  {
    try
    {
      JarMaker jm = new JarMaker();

      if (jm.parseArgs(args, false))
      {
        if (jm.isSplit())  // -split overrides all other options
        {
          File srcJar = jm.getSourceJar();
          int splitSize = jm.getSplitSize();
          jm.split(srcJar, splitSize); // unpack required files
        }
        else if (jm.isExtract())
        {
          File srcJar = jm.getSourceJar();
          File outputDir = jm.getExtractionDirectory();
          jm.extract(srcJar, outputDir); // unpack required files
        }
        else
        {
          File srcJar = jm.getSourceJar();
          File destJar = jm.getDestinationJar();
          jm.makeJar(srcJar, destJar); // create a new JAR file
        }
      }
      else System.exit(1);
    }
    catch (Throwable e) {
      System.err.println(e.toString());
      e.printStackTrace(System.err);
      System.exit(1);
    }

    System.exit(0);
  }



  class Arguments
  {
    private Vector unrecognizedArgs_ = new Vector();
    private boolean optionsAreSufficient_;

    String [] getUnrecognized()
    {
      String [] stringArray = new String [unrecognizedArgs_.size()];
      unrecognizedArgs_.copyInto(stringArray);
      return stringArray;
    }


    /**
     Indicates if sufficient option information was specified.
     @return <code>true</code> if sufficient info was specified,
     <code>false</code> otherwise.
     **/
    boolean isOptionInfoSufficient() { return optionsAreSufficient_; }

    /**
     Parses and validates the arguments specified on the command line.

     @param args The command line arguments.
     @param jmaker The object to apply the arguments to.
     @param tolerateUnrecognizedArgs Whether to tolerate unrecognized args.
     @return An indication of whether the parse succeeded.
     **/
    boolean parse(String[] args, JarMaker jmaker,
                   boolean tolerateUnrecognizedArgs)
    {
      if (args.length == 0)
      {
        System.err.println("Error: No options were specified.");
        // Don't print usage info yet if a subclass will complete the parse.
        if (!tolerateUnrecognizedArgs) printUsage(System.err);
        return false;
      }

      Vector options = new Vector();
      options.addElement("-source");
      options.addElement("-destination");
      options.addElement("-fileRequired");
      options.addElement("-fileExcluded");
      options.addElement("-additionalFile");
      options.addElement("-additionalFilesDirectory");
      options.addElement("-package");
      options.addElement("-packageExcluded");
      options.addElement("-extract");
      options.addElement("-split");
      options.addElement("-verbose");
      options.addElement("-help");

      Hashtable shortcuts = new Hashtable();
      shortcuts.put("-s",                  "-source");
      shortcuts.put("-src",                "-source");
      shortcuts.put("-so",                 "-source");
      shortcuts.put("-d",                  "-destination");
      shortcuts.put("-dest",               "-destination");
      shortcuts.put("-fr",                 "-fileRequired");
      shortcuts.put("-file",               "-fileRequired");
      shortcuts.put("-rf",                 "-fileRequired");
      shortcuts.put("-req",                "-fileRequired");
      shortcuts.put("-required",           "-fileRequired");
      shortcuts.put("-requiredfile",       "-fileRequired");
      shortcuts.put("-fx",                 "-fileExcluded");
      shortcuts.put("-fileex",             "-fileExcluded");
      shortcuts.put("-filesex",            "-fileExcluded");
      shortcuts.put("-af",                 "-additionalFile");
      shortcuts.put("-additional",         "-additionalFile");
      shortcuts.put("-afd",                "-additionalFilesDirectory");
      shortcuts.put("-additionalfilesdir", "-additionalFilesDirectory");
      shortcuts.put("-p",                  "-package");
      shortcuts.put("-px",                 "-packageExcluded");
      shortcuts.put("-packageex",          "-packageExcluded");
      shortcuts.put("-packagesex",         "-packageExcluded");
      shortcuts.put("-x",                  "-extract");
      shortcuts.put("-sp",                 "-split");
      shortcuts.put("-v",                  "-verbose");
      shortcuts.put("-h",                  "-help");

      CommandLineArguments arguments = new CommandLineArguments(args, options, shortcuts);

      boolean destinationWasSpecified = false;
      Vector filesRequired = null; // Strings
      Vector filesExcluded = null; // Strings
      Vector additionalFiles = null; // Files
      File additionalFilesDir = null;
      Vector packages = null; // Strings
      Vector packagesExcluded = null; // Strings
      String extractionDirName = null;
      boolean succeeded = true;

      unrecognizedArgs_ = new Vector();

      // Examine the arguments.

      String val;

      if (arguments.isOptionSpecified("-verbose")) {

        jmaker.setVerbose(true);

        System.out.print("Arguments parsed by JarMaker:");
        String opts = JarMaker.listCommandOptions(arguments, JarMaker.OPTIONS_ALL);
        System.out.println(opts);

        opts = JarMaker.listCommandOptions(arguments, JarMaker.OPTIONS_UNRECOGNIZED);
        if (opts.length() != 0) {
          System.out.print("Arguments unrecognized by JarMaker: ");
          System.out.println(opts);
        }
      }

      requestedUsageInfo_ = arguments.isOptionSpecified("-help");

      // See if any args were specified preceding the options.
      val = arguments.getOptionValue("");
      if (val != null && val.length() != 0) {
        // See if more than 1 token was specified.
        val = val.trim();
        StringTokenizer st = new StringTokenizer(val, " ");
        if (st.countTokens() > 1)
        {
          val = st.nextToken();
          StringBuffer sb = new StringBuffer();
          while (st.hasMoreTokens()) {
            sb.append(st.nextToken() + " ");
          }
          System.err.println("Warning: Ignoring extra arguments: " + sb.toString());
        }
        setSourceJar(new File(val));
      }

      val = arguments.getOptionValue("-source");
      if (val != null) {
        if (val.length() != 0) {
          setSourceJar(new File(val));
        }
        else {
          System.err.println("Warning: No file specified after -source.");
        }
      }

      val = arguments.getOptionValue("-destination");
      if (val != null) {
        if (val.length() != 0) {
          setDestinationJar(new File(val));
          destinationWasSpecified = true; // remember not to take the default
        }
        else {
          System.err.println("Warning: No file specified after -destination.");
        }
      }

      val = arguments.getOptionValue("-fileRequired");
      if (val != null) {
        if (val.length() != 0) {
          // Parse the list of ZIP entries, separated by commas.
          StringTokenizer st = new StringTokenizer(val, ",");
          if (st.countTokens() != 0)
          {
            if (filesRequired == null)
              filesRequired = new Vector(st.countTokens());
            while (st.hasMoreTokens())
              filesRequired.addElement(st.nextToken());
          }
        }
        else {
          System.err.println("Warning: No file specified after -fileRequired.");
        }
      }

      val = arguments.getOptionValue("-fileExcluded");
      if (val != null) {
        if (val.length() != 0) {
          // Parse the list of ZIP entries, separated by commas.
          StringTokenizer st = new StringTokenizer(val, ",");
          if (st.countTokens() != 0)
          {
            if (filesExcluded == null)
              filesExcluded = new Vector(st.countTokens());
            while (st.hasMoreTokens())
              filesExcluded.addElement(st.nextToken());
          }
        }
        else {
          System.err.println("Warning: No package specified after -fileExcluded.");
        }
      }

      val = arguments.getOptionValue("-package");
      if (val != null) {
        if (val.length() != 0) {
          // Parse the list of packages, separated by commas.
          StringTokenizer st = new StringTokenizer(val, ",");
          if (st.countTokens() != 0)
          {
            if (packages == null) {
              packages = new Vector(st.countTokens());
            }
            while (st.hasMoreTokens())
              packages.addElement(st.nextToken());
          }
        }
        else {
          System.err.println("Warning: No package specified after -package.");
        }
      }

      val = arguments.getOptionValue("-packageExcluded");
      if (val != null) {
        if (val.length() != 0) {
          // Parse the list of packages, separated by commas.
          StringTokenizer st = new StringTokenizer(val, ",");
          if (st.countTokens() != 0)
          {
            if (packagesExcluded == null)
              packagesExcluded = new Vector(st.countTokens());
            while (st.hasMoreTokens())
              packagesExcluded.addElement(st.nextToken());
          }
        }
        else {
          System.err.println("Warning: No package specified after -packageExcluded.");
        }
      }

      val = arguments.getOptionValue("-additionalFile");
      if (val != null) {
        if (val.length() != 0) {
          // Parse the list of files, separated by commas.
          StringTokenizer st = new StringTokenizer(val, ",");
          if (st.countTokens() != 0)
          {
            if (additionalFiles == null)
              additionalFiles = new Vector(st.countTokens());
            while (st.hasMoreTokens()) {
              additionalFiles.addElement(new File(st.nextToken()));
            }
          }
        }
        else {
          System.err.println("Warning: No file specified after -additionalFile.");
        }
      }

      val = arguments.getOptionValue("-additionalFilesDirectory");
      if (val != null) {
        if (val.length() != 0) {
          additionalFilesDir = new File(val);
        }
        else {
          System.err.println("Warning: No directory specified after -additionalFilesDirectory.");
        }
      }

      val = arguments.getOptionValue("-extract");
      if (val != null) {
        jmaker.setExtract(true);
        if (val.length() != 0) {
          jmaker.setExtractionDirectory(new File(val));
        }
      }

      val = arguments.getOptionValue("-split");
      if (val != null) {
        jmaker.setSplit(true);
        if (val.length() != 0) {
          int size = 0;
          boolean badValue = false;
          try { size = Integer.parseInt(val); }
          catch (NumberFormatException e) {
            System.err.println("Error: Non-integer split size: " + val);
            succeeded = false;
            badValue = true;
          }
          if (size < 0) {
            System.err.println("Error: Negative split size: " + val);
            succeeded = false;
            badValue = true;
          }
          if (!badValue)
            jmaker.setSplitSize(size);
        }
      }


      // Check for any extra arguments.
      Enumeration enum1 = arguments.getExtraOptions();
      while (enum1.hasMoreElements()) {
        String optionName = (String)enum1.nextElement();
        String optionVal = arguments.getOptionValue(optionName);
        unrecognizedArgs_.addElement(optionName);
        if (optionVal != null) unrecognizedArgs_.addElement(optionVal);
        if (!tolerateUnrecognizedArgs) {
          String optionWithVal = ( optionVal == null ? optionName : optionName + " " + optionVal );
          System.err.println("Error: Unrecognized option: " + optionWithVal);
          succeeded = false;
        }
      }

      if (requestedUsageInfo_)  // the -help option was specified
      {
        if (tolerateUnrecognizedArgs)
          return succeeded;  // let the subclass print the usage info
        else
        {
          printUsage(System.out);
          return false;
        }
      }

      if (jmaker.getSourceJar() == null)
      {
        System.err.println("Error: Source JAR or ZIP file was not specified.");
        succeeded = false;
      }

      else if (jmaker.isSplit())
      {
        optionsAreSufficient_ = true;
        jmaker.setExtract(false);  // '-split' overrides '-extract'
        if ((destinationWasSpecified == true) ||
            (filesRequired != null) ||
            (filesExcluded != null) ||
            (packages != null) ||
            (packagesExcluded != null) ||
            (additionalFiles != null))
        {
          System.err.println("Warning: When -split is specified, " +
                              "all other options are ignored, except " +
                              "-source, and -verbose.");
        }
      }

      else  // not a split
      {
        // Check for sufficient options.
        if ((filesRequired == null) &&
            (filesExcluded == null) &&
            (additionalFiles == null) &&
            (packages == null) &&
            (packagesExcluded == null) &&
            (!jmaker.isExtract()))
        {
          if (unrecognizedArgs_.size() == 0 || !tolerateUnrecognizedArgs) {
            System.err.println("Error: Need to specify more options.");
            succeeded = false;
          }
        }
        else optionsAreSufficient_ = true;

        if (jmaker.isExtract())
        {
          if ((destinationWasSpecified == true) ||
              (additionalFiles != null))
          {
            System.err.println ("Warning: When -extract is specified, " +
                                "the following options are ignored: " +
                                "-destination, -additionalFile, and " +
                                "-additionalFilesDirectory.");
          }
        }

        // Wrap up.
        if (!destinationWasSpecified)
        {
          File destJar = JarMaker.setupDefaultDestinationJarFile(jmaker.getSourceJar());
          jmaker.setDestinationJar(destJar);
        }

        // Set any additional files.
        if (additionalFilesDir == null)
          additionalFilesDir = new File(System.getProperty("user.dir"));
        if (additionalFiles != null)
          setAdditionalFiles(additionalFiles, additionalFilesDir);

        // Set other lists, now that they've been accumulated.

        if (filesRequired != null)
          setFilesRequired(filesRequired);

        if (filesExcluded != null)
          setFilesExcluded(filesExcluded);

        if (packages != null)
        {
          if (packages.size() > 0)
            setPackages(packages);
          else succeeded = false;  // bogus packages
        }

        if (packagesExcluded != null)
        {
          if (packagesExcluded.size() > 0)
            setPackagesExcluded(packagesExcluded);
          else succeeded = false;  // bogus packages
        }

      }

      if (!tolerateUnrecognizedArgs && !succeeded)
        printUsage(System.err);

      return succeeded;
    }

    /**
     Prints the usage information.

     @param output   The output stream.
     **/
    private void printUsage(PrintStream output)
    {
      output.println("");
      output.println("Usage: ");
      output.println("");
      output.println("  JarMaker -source sourceJarFile");
      output.println("           [-destination jarFile]");
      output.println("           [-fileRequired entry1[,entry2[...]]]");
      output.println("           [-fileExcluded entry1[,entry2[...]]]");
      output.println("           [-additionalFile file1[,file2[...]]]");
      output.println("           [-additionalFilesDirectory directory");
      output.println("           [-package pkg1[,pkg2[...]]]");
      output.println("           [-packageExcluded pkg1[,pkg2[...]]]");
      output.println("           [-extract [directory]]");
      output.println("           [-split [kilobytes]]");
      output.println("           [-verbose]");
      output.println("           [-help]");
      output.println("");
      output.println("At least one of the following options must be specified: ");
      output.println("-fileRequired, -fileExcluded, -additionalFile, -package, -packageExcluded, -extract, -split");
    }

  }




  static class JarMap
  {
    private ZipFile zipFile_;  // ZipFile view of the JAR file.
    private ZipEntry manifest_;  // The Manifest entry.
                                 // Null if JAR has no manifest.

    // Names of all entries (except for the Manifest) in the JAR file.
    // Entries are arranged in alphabetical order.
    // Never null.
    private Vector entryList_ = new Vector();

    //private File jarFile_;
    private boolean verbose_;

    // Length of the zipfile comment in the Central Directory Record.
    private int zipfileCommentLength_;

    /**
     Constructs a JarMap object for the JAR file.
     @param jarFile The JAR file.
     @exception IOException If an I/O error occurs when reading the JAR file.
     @exception ZipException If a ZIP error occurs when reading the JAR file.
     **/
    JarMap(File jarFile, boolean verbose)
      throws IOException, ZipException
    {
      if (jarFile == null)
        throw new NullPointerException("jarFile");

      if (!jarFile.isFile())
        throw new FileNotFoundException(jarFile.getAbsolutePath());

      if (DEBUG) System.out.println("Debug: Creating ZipFile");
      zipFile_ = new ZipFile(jarFile);
      //jarFile_ = jarFile;
      verbose_ = verbose;

      if (DEBUG) System.out.println("Debug: Getting manifest");
      manifest_ = zipFile_.getEntry(MANIFEST_ENTRY_NAME);

      // Avoid having to sort the entry name list,
      // since this can take a very long time if there are many
      // entries in the source zip or JAR file.
      if (DEBUG) System.out.println("Debug: Gathering entry names");
      InputStream inStream = null;
      ZipInputStream zipInStream = null;
      try
      {
        inStream =
          new BufferedInputStream(new FileInputStream(jarFile), BUFFER_SIZE);
        zipInStream = new ZipInputStream(inStream);
        ZipEntry entry = zipInStream.getNextEntry();
        while (entry != null)
        {
          entryList_.addElement(entry.getName());
          entry = zipInStream.getNextEntry();
        }
      }
      finally
      {
        if (zipInStream != null) {
          try { zipInStream.close(); inStream = null; } catch (Throwable t) {}
        }
        if (inStream != null) {
          try { inStream.close(); } catch (Throwable t) {}
        }
      }

      // Design note: ZipFile.entries() does not preserve the
      // original sequencing of the ZIP entries.
      // We could theoretically build the entryList using
      // ZipInputStream.getNextEntry() instead of ZipFile.entries(),
      // and thereby preserve the entry sequencing,
      // if not for Java bug 4079029, documented at:
      //   http://developer.java.sun.com/developer/bugParade/bugs/4079029.html

      // Leave the Manifest out of the ZIP entry list.
      entryList_.removeElement(MANIFEST_ENTRY_NAME);
    }

    // Closes this JarMap.
    void close()
    {
      if (verbose_ || DEBUG) System.out.println("Closing source file");
      entryList_.removeAllElements();
      manifest_ = null;
      if (zipFile_ != null)
      {
        try { zipFile_.close(); }
        catch (Exception e) {
          System.err.println("Error: While closing source file:");
          System.err.println(e.toString());
          if (DEBUG) e.printStackTrace(System.err);
        }
        zipFile_ = null;
      }
    }

    // Indicates whether the JAR contains the specified entry.
    boolean contains(String entryName)
    { return entryList_.contains(entryName); }

    // Returns the names of all entries in the JAR (except for the Manifest).
    // These are String objects.
    Enumeration elements() { return entryList_.elements(); }

    // Returns an enumeration of all the entries in the JAR file
    // (including the Manifest).
    // These are ZipEntry objects.
    Enumeration entries() { return zipFile_.entries(); }

    // Returns the names of all entries in the JAR (except for the Manifest).
    // These are String objects.
    Vector getEntryNames()
    { return entryList_; }


    // Returns the entries (String objects).
    ZipEntry getEntry(String entryName)
    { return zipFile_.getEntry(entryName); }

    // Returns an input stream for reading the specified entry in the jar.
    InputStream getInputStream(ZipEntry entry)
      throws IOException, ZipException
    { return zipFile_.getInputStream(entry); }

    // Returns the Manifest as a ZIP entry.
    // Returns null if the JAR contains no manifest.
    ZipEntry getManifest() { return manifest_; }

    int getSizeOfZipMetadataPerZip()
    {
      int result = 0;
      // Add size of fixed fields in the End of Central Directory Record:
      result += 22;
      // Add size of the "zipfile comment":
      result += zipfileCommentLength_;
      return result;
    }

    int getSizeOfZipMetadataPerEntry()
      throws IOException, UnsupportedEncodingException
    {
      // Total length of all fixed-length zip fields per ZIP entry.
      return 88;
    }

    // Indicates whether the JAR has a manifest entry.
    boolean hasManifest() { return(manifest_ != null); }

  }




  static class ManifestMap
  {

    // Names of all entries in the JAR file's manifest.
    // Entries are in the order in which they appear in the manifest.
    // Never null.
    private Vector entryList_ = new Vector();

    // Map of all entries in the JAR file's manifest.
    // Key=entry name, value=String (uncompressed text of the manifest entry).
    // Never null.
    private Hashtable entryMap_ = new Hashtable();

    // A map of the JAR which contains this Manifest.
    private JarMap jarMap_;

    /**
     Constructs a ManifestMap object for a JAR or ZIP file.
     In the case of a ZIP file, the ManifestMap will represent an empty list.
     @param jarMap A map of the JAR file.
     @exception IOException If an I/O error occurs when reading the JAR file.
     @exception ZipException If a ZIP error occurs when reading the JAR file.
     **/
    ManifestMap(JarMap jarMap)
      throws IOException, ZipException
    {
      if (jarMap == null)
        throw new NullPointerException("jarMap");

      jarMap_ = jarMap;

      ZipEntry manifestEntry = jarMap_.getEntry(MANIFEST_ENTRY_NAME);
      if (manifestEntry == null)
        System.err.println("Warning: Source file has no manifest." +
                            "  No manifest will be created.");
      else
      {
        BufferedReader reader = null;
        try
        {
          // Set up for reading.  We must use a reader because we
          // are dealing with text.
          reader = new BufferedReader(new InputStreamReader(jarMap.getInputStream(manifestEntry)));

          // Read the manifest file.
          if (DEBUG) System.out.println("Debug: Analyzing manifest");
          boolean alreadySawVersion = false;
          boolean alreadySawRequiredVersion = false;
          String line;
          while (reader.ready())
          {
            line = reader.readLine();
            if (line != null)         // @A1a
            {
              if (line.startsWith(MANIFEST_NAME_KEYWORD)) // is this a "name" line
              {
                StringBuffer buffer = new StringBuffer();
                String entryName = line.substring(MANIFEST_NAME_KEYWORD.length()).trim();
                if (DEBUG_MANIFEST) System.out.println("Manifest entry: ");
                if (DEBUG_MANIFEST) System.out.println(line);
                buffer.append(line);
                buffer.append('\n');
                // Copy the rest of this manifest section.
                // (sections are terminated by zero-length line)
                while (reader.ready() && (line.length() != 0))
                {
                  line = reader.readLine();
                  if (DEBUG_MANIFEST) System.out.println(line);
                  if (line != null)  // @A1a
                  {
                    buffer.append(line);
                    buffer.append('\n');
                  }
                }
                String string = buffer.toString();
                entryMap_.put(entryName, string);
                entryList_.addElement(entryName); // this will preserve the sequence
              }
              else if (!alreadySawVersion &&
                       line.startsWith(MANIFEST_VERSION_KEYWORD))
              {                                       // "Manifest-Version:"
                alreadySawVersion = true;
                if (DEBUG_MANIFEST) {
                  String version = line.substring(MANIFEST_VERSION_KEYWORD.length()).trim();
                  System.out.println("Manifest version: " + version);
                }
                entryMap_.put(MANIFEST_VERSION_KEYWORD, line + '\n');
              }
              else if (!alreadySawRequiredVersion &&
                       line.startsWith(MANIFEST_REQVERS_KEYWORD))
              {                                       // "Required-Version:"
                alreadySawRequiredVersion = true;
                if (DEBUG_MANIFEST) {
                  String version = line.substring(MANIFEST_REQVERS_KEYWORD.length()).trim();
                  System.out.println("Required version: " + version);
                }
                entryMap_.put(MANIFEST_REQVERS_KEYWORD, line + '\n');
              }
            }
          }  // ... while
        }
        finally
        {
          if (reader != null) { reader.close(); }
        }
      }
    }

    // Closes this ManifestMap.
    void close()
    {
      if (DEBUG) System.out.println("Closing manifest");
      entryList_.removeAllElements();
      entryMap_.clear();
      jarMap_ = null;
    }

    // Indicates whether there is a manifest entry for the specified ZIP entry.
    boolean contains(String entryName)
    { return entryList_.contains(entryName); }

    // Returns names of all entries in the manifest.  These are String objects.
    Enumeration elements() { return entryList_.elements(); }

    // Returns the manifest text for the specified entry.
    String get(String entryName)
    { return (String)(entryMap_.get(entryName)); }

    // Returns the uncompressed size of the specified entry in the manifest.
    // If there is no such entry in the manifest, returns zero.
    // Design note: For better precision, figure out how to get the
    // compressed size.
    int getEntrySize(String entryName)
    {
      int size = 0;
      String entryText = (String)(entryMap_.get(entryName));
      if (entryText != null) size = entryText.length();
      return size;
    }

    // Returns the manifest header information.
    // If no header info, returns zero-length string.
    String getHeader()
    {
      String version = get(MANIFEST_VERSION_KEYWORD);
      String reqVersion = get(MANIFEST_REQVERS_KEYWORD);
      StringBuffer buffer = new StringBuffer();
      if (version != null) buffer.append(version);
      if (reqVersion != null) buffer.append(reqVersion);
      return buffer.toString();
    }

  }

}
