package ca.ucalgary.minnow;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.jar.*;
import java.net.URL;
import java.util.regex.Pattern;

/**
 * This class should be called from the command line using a "clean" JVM
 * (i.e. no extra libraries installed) to act  as a recorder of what classes 
 * particular to a program are accessed during its running.  These classes
 * are then jarred up to provide the minimal non-standard JVM codeset 
 * required to execute the program.  This class is based on (but is much more 
 * comprehensive than) an example at
 *
 * http://www.devx.com/Java/Article/17674/1954?pf=true
 *
 * This application is called "Minnow".
 *
 * @author Paul Gordon gordonp@ucalgary.ca
 */

public class MinJarMaker extends ClassLoader implements Runnable{
    public static String VERBOSE = "verbose.class";

    // The JAR file we want to create
    protected String jarfile;
    
    // The target program we want to run
    protected String program;
    
    // The arguments to the program
    protected String programArgs[];

    protected Map<String,String> savedClasses;  //<className,sentinelValue>
    protected Map<String,String> corePackages;  //One from which we've loaded a class in running the code
    protected Map<String,String> secondaryPackageWritten;  //Keep track of whole package import statements in JNLP
    private boolean createSecondaryJars;
    private String webStartIndexFileName = null;
    private FileWriter webStartIndexWriter = null;
    private int noPackageCounter;

    private Class manifestMainClass;
    public final static String MANIFEST_PATH = "META-INF/MANIFEST.MF";
    public final static String MANIFEST_MAIN_CLASS_PROPERTY = "minnow.manifest.main.class";

    // For the class loading interface, keep track of what we've loaded
    private Set<String> loadedClasses =
      Collections.synchronizedSet( new LinkedHashSet<String>() );

    static public void main( String args[] ) throws Exception {
	// Check arguments
	if (args.length < 2) {
	    System.err.println("Usage:\n\n");
	    System.err.println("java -cp /dir/containing/only/the/MinJarMaker/class/file -Djarmaker.class.extras=\"extra/classes/to/include.class\" -Djarmaker.class.path=$CLASSPATH MinJarMaker main_output.jar [-s | -sw indexoutfile.xml] Program [args...]");
	    System.err.println("Where -s causes the creation of secondary jar files for each package that doesn't");
	    System.err.println("have all of its classes loaded (i.e. may be loaded in further program execution)");
	    System.err.println("Where -sw also creates a Web Start document fragment describing the location of");
	    System.err.println("classes in the secondary jars (an index of sorts)");
	    System.exit( 1 );
	}

	// Grab arguments
	String jarfile = args[0];
	String program = args[1];
	int progArgOffset = 2;
	boolean secondaryJars = false;
	String webStartIndex = null;
	if(args.length > 2 && (args[1].equals("-s") || args[1].equals("-sw"))){
	    secondaryJars = true;
	    if(args[1].equals("-sw")){
		webStartIndex = args[2];
		program = args[3];
		progArgOffset = 4;
	    }
	    else{
		program = args[2];
		progArgOffset = 3;
	    }
	}
	
	// Copy arguments to target program into
	// separate array
	String programArgs[] = new String[args.length-progArgOffset];
	System.arraycopy( args, progArgOffset, programArgs, 0, programArgs.length );
	
	// Start the target program
	new MinJarMaker( jarfile, program, programArgs, secondaryJars, webStartIndex);
    }
    
    public MinJarMaker( String jarfile, String program, String programArgs[], boolean secondaryJars, String webStartIndex )
	throws ClassNotFoundException, InvocationTargetException,
	       NoSuchMethodException, IllegalAccessException,
	       IOException {
	// Safe off program info
	this.jarfile = jarfile;
	this.program = program;
	this.programArgs = programArgs;
	createSecondaryJars = secondaryJars;
	webStartIndexFileName = webStartIndex;
	noPackageCounter = 1;  // var to increment to create unique prefixes for unqualified class files' jars

	// Install the shutdown hook that writes
	// the JAR file
	createShutdownHook();
	
	// Start the target program
	startProgram( jarfile, program, programArgs );
    }
    
    protected void startProgram( String jarfile, String program,
				 String programArgs[] )
	throws ClassNotFoundException, InvocationTargetException,
	       NoSuchMethodException, IllegalAccessException,
	       IOException {
	
        // Some classes will not call getClass().getClassLoader(), but getClass().getContextClassLoader()
	// in order to get a ClassLoader.  Set the Context so we capture these cases too.
        // e.g. javax.xml.parsers.DocumentBuilderFactory will not find Xerces with out this line
        // We get all the threads, so that AWT events, etc. that cause classes to be loaded also use us as
        // the class loader!

        Thread[] threads = new Thread[Thread.currentThread().activeCount()];
        Thread.currentThread().enumerate(threads);
        for(int i = 0; i < threads.length; i++){
          Thread thread = threads[i];
          if(thread != null){
            thread.setContextClassLoader(this);
          }
        }

	// Get the main class of the target program
	Class mainClass = Class.forName( program, true, this );
	
	// Start the target program, passing the
	// command-line arguments
	callMain( mainClass, programArgs );
    }
    
    protected void callMain( Class mainClass, String programArgs[] )
	throws NoSuchMethodException, IllegalAccessException,
	       ClassNotFoundException, InvocationTargetException {
	// Get the main method
	Class mainArgs[] = {String[].class};
	Method main = mainClass.getMethod( "main", mainArgs );
	
	// Keep track of the class we called main for:
	// we'll use this as the Main-class for the jar manifest
	// Unless the user specifically overrides the main class with
	// an environment variable setting
	String mainClassName = System.getProperty(MANIFEST_MAIN_CLASS_PROPERTY);
	if(mainClassName != null){
	    manifestMainClass = loadClass(mainClassName);
	    if(manifestMainClass == null){
		throw new ClassNotFoundException("Cannot find class specified as main class for JAR manifest ("+
						  "environment variable " + MANIFEST_MAIN_CLASS_PROPERTY +
						  " was " + mainClassName + ")");
	    }
	}
	else{
	    manifestMainClass = mainClass;
	}

        // Make sure its a valid public static main, returning void 
        main.setAccessible(true);
        int mods = main.getModifiers();
        if(main.getReturnType() != void.class || !Modifier.isStatic(mods) || !Modifier.isPublic(mods)) {
           throw new NoSuchMethodException("public void static main(String[])");
        }

	// Create an argument array containing the array
	// of command-line arguments
	String foo[][] = { programArgs }; 
	
	// Call the main method (first arg [target object] is null, because the method is static)
	main.invoke((Object) null, (Object[]) foo);

        // Note that in the future, we could check for an init() method, in case the class in an applet, and no
        // application hook is available.
    }

    // ClassLoader override methods to get classes not in the standard path
    public URL findResource(String name){
	URL resourceURL = null;
	resourceURL = getResourceURL(name); 
	// Record the class, so we can put it into
	// a JAR file.
	if(resourceURL != null){
	    loadedClasses.add(name);
	}
	return resourceURL;
    }
    
    protected Enumeration<URL> findResources(String name)
	throws IOException{
	Vector<URL> v = new Vector<URL>(1);
        URL url = findResource(name);
	if(url != null){v.add(url);}
	return v.elements();
    }

    public Class findClass(String name)
	throws ClassNotFoundException {
	Class clas = null;
	
	// Try to load the class directly using the utilities class
	try {
	    // Get the raw bytes, if they are there.
	    byte classBytes[] = getClassBytes( name );
	    
	    // We got them!  Turn the bytes into a class.
	    if(classBytes != null){
		clas = defineClass( name, classBytes, 0, classBytes.length );
	    }
	} catch( IOException ie ) {
	} catch(ClassFormatError cfe){ 
	    // If the data did not contain a valid class 
	    cfe.printStackTrace(System.err);
	} catch(NoClassDefFoundError ncdfe){ 
	    // If name is not equal to the binary name of the class specified by classBytes	
	    ncdfe.printStackTrace(System.err);
	}

	// We weren't able to get the class, so
	// use the default Classloader.
	if (clas == null) {
	    clas = Class.forName(name);
	}
	if (clas == null) {
	    throw new ClassNotFoundException( name );
	}
	
	// Record the class, so we can put it into
	// a JAR file.
	loadedClasses.add( clas.getName() );
	
	// Return the class to the runtime system.
	return clas;
    }
    
    public Set<String> getLoadedClasses() {
	return loadedClasses;
    }
    // End class loader overrides


    /**
     * This method can be used to truncate names only to their class hierachy location
     * (i.e. cuts off the run-time specific part of the file path name that we got from
     * the classpath)
     */
    static protected String truncatePath(String fullPath){
	StringTokenizer classPathTokens = new StringTokenizer(System.getProperty("jarmaker.class.path"), File.pathSeparator);
	while(classPathTokens.hasMoreElements()){
	    String pathPrefix = classPathTokens.nextToken();
	    if(fullPath.startsWith(pathPrefix+"/")){
		return fullPath.substring(pathPrefix.length()+1);
	    }
	}
	return null;
    }
    
    static protected String truncateURL(URL fullURL){
	int jarSpecIndex = fullURL.toString().indexOf("!/");
	if(jarSpecIndex != -1){
	    return fullURL.toString().substring(jarSpecIndex+2);
	}

	StringTokenizer classPathTokens = new StringTokenizer(System.getProperty("jarmaker.class.path"), File.pathSeparator);
	while(classPathTokens.hasMoreElements()){
	    String classPathElement = classPathTokens.nextToken();
	    String urlPrefix = null;
	    try{
		urlPrefix = (new File(classPathElement)).toURI().toURL().toString();
	    }
	    catch(Exception e){
		System.err.println("Warning: can't generate URL from classpath element " + classPathElement + ", " +e);
		continue;
	    }
	    String fullURLString = fullURL.toString();
	    if(fullURLString.startsWith(urlPrefix)){
		return fullURLString.substring(urlPrefix.length());
	    }
	}
	return null;
    }
    
    protected byte[] getClassBytes( String name ) throws IOException {
	// Check all of the run-time specified class path dirs for the 
	// class file in question
	StringTokenizer classPathTokens = new StringTokenizer(System.getProperty("jarmaker.class.path"), 
							      File.pathSeparator);
	while(classPathTokens.hasMoreElements()){
	    String pathElement = classPathTokens.nextToken();
	    // Turn package.class name into a relative path of a class resource
	    String pathSuffix = name.replace('.', File.separatorChar) + ".class";
	    String path = pathElement + File.separator + pathSuffix;
	    File classFile = new File(path);
	    if(classFile.isFile()){
		return getClassBytesFromFile(classFile, name);
	    }
	    else{
		// If it's not a file, maybe it's in a JAR
		File f = new File(pathElement);
		if(f.isFile()){
		    byte[] bytes = getClassBytesFromJar(f, pathSuffix);
		    if(bytes != null){
			return bytes;
		    }
		}
	    }
	}
	return null;
    }

    protected byte[] getClassBytesFromFile(File classFile, String name) throws IOException{
	long len = classFile.length();
	byte[] data = new byte[(int)len];  // 2G limit
	FileInputStream fin = new FileInputStream(classFile);
	int r = fin.read(data);
	if (r != len){
	    throw new IOException( "Could only read "+r+" of "+len+" bytes from "+classFile );
	}
	fin.close();
	if(Boolean.getBoolean(VERBOSE)){
	    System.err.println("[Loaded " + name + " from " + classFile.getCanonicalPath() + "]");
	}
	return data;
    }
    
    protected byte[] getClassBytesFromJar(File f, String entryName) throws IOException{
	try{
	    return getClassBytesFromJar(new JarFile(f), entryName);
	}
	catch(Exception e){
	    System.err.println("Class path element " + f.getCanonicalPath() + 
			       " was not a directory, or a valid JAR file");
	    return null;
	}
    }

    protected byte[] getClassBytesFromJar(JarFile jarFile, String entryName) throws IOException{
	JarEntry je = jarFile.getJarEntry(entryName);
	if(je == null){
	    return null;
	}
	long classSize = je.getSize();

	InputStream classStream = jarFile.getInputStream(je);
	byte[] classBytes = null;
	// NOTE: This if case is commented out because false file sizes are sometimes
	// reported, which leads to invalid class definitions
	// This has to do with different versions of compression algorithms being used by different JAR makers.
	//
	// 		    if(classSize != -1){ // We know the size of the class already
	
	// 			classBytes = new byte[(int) classSize]; //classes better not be bigger than 2 GB!
	// 			// Slurp it up in one shot
	// 			classStream.read(classBytes, 0, (int) classSize); 
	//                         if(Boolean.getBoolean(VERBOSE)){
	//                           System.err.println("[Loaded " + name + " from " + f.getCanonicalPath() + "]");
	//                         }
	// 			return classBytes;
	// 		    }
	// 		    else{

	byte[] byteBufferChunk = new byte[1024];
	ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
	for(int r = classStream.read(byteBufferChunk, 0, 1024);
	    r != -1; 
	    r = classStream.read(byteBufferChunk, 0, 1024)){
	    byteBuffer.write(byteBufferChunk, 0, r);
	}
	if(Boolean.getBoolean(VERBOSE)){
	    System.err.println("[Loaded " + entryName + " from " + jarFile.getName() + "]");
	}
	return byteBuffer.toByteArray();
    }
    
    protected URL getResourceURL(String name){
	// Check all of the run-time specified class path dirs for the 
	// properties file in question
	StringTokenizer classPathTokens = new StringTokenizer(System.getProperty("jarmaker.class.path"), 
							      File.pathSeparator);
	while(classPathTokens.hasMoreElements()){
	    String pathElement = classPathTokens.nextToken();
	    String path = pathElement + File.separator + name;
	    File resourceFile = new File(path);
	    if(resourceFile.isFile()){
		try{
		    return resourceFile.toURI().toURL();
		}
		catch(Exception e){
		    System.err.println("Warning: tried to return properties file " + 
				       path + " as URL, but could not: " + e);
		}
	    }
	    else{
		// If it's not a file, maybe it's in a JAR
		File f = new File(pathElement);
		if(f.isFile()){
		    JarFile jarFile = null;
		    try{
			jarFile = new JarFile(f);
		    }
		    catch(Exception e){
			System.err.println("Class path element " + pathElement + 
					   " was not a directory, or a valid JAR file");
			continue;
		    }

		    JarEntry je = jarFile.getJarEntry(name);
		    try{
			jarFile.close();
		    }
		    catch(IOException ioe){
			System.err.println("Couldn't close JAR file: " + pathElement);
		    }
		    if(je == null){
			continue;
		    }
		    else{
			try{
			    return new URL("jar:"+f.toURI().toURL().toString()+"!/"+name);
			} catch(java.net.MalformedURLException murle){
			    try{
				System.err.println("URL jar:"+f.toURI().toURL().toString()+"!"+name + 
						   " was invalid: " +murle);}catch(Exception e){}
			    return null;
			}
		    }
		}
				
	    }
	}
	return null;
    }
    
    protected void copyFile( OutputStream out, InputStream in )
	throws IOException {
	byte buffer[] = new byte[4096];
	
	while (true) {
	    int r = in.read( buffer );
	    if (r<=0) {
		break;
	    }
	    out.write( buffer, 0, r );
	}
    }
    
    protected void copyFile( OutputStream out, byte[] buffer )
	throws IOException {	
	out.write(buffer);
    }
    
    protected void copyFile( OutputStream out, String infile )
	throws IOException {
	FileInputStream fin = new FileInputStream( infile );
	copyFile( out, fin );
	fin.close();
    }
    
    protected void createManifest(JarOutputStream jout) throws IOException{
	JarEntry je = new JarEntry(MANIFEST_PATH);
	jout.putNextEntry( je );
	copyFile( jout, ("Main-Class: "+manifestMainClass.getName()+"\n").getBytes());
	jout.closeEntry();
    }

    protected void addExtraClasses(Set<String> classes){
	if(System.getProperty("jarmaker.class.extras") == null){
	    return;
	}

	StringTokenizer extraClassesTokens = new StringTokenizer(System.getProperty("jarmaker.class.extras"), 
								 File.pathSeparator);
	while(extraClassesTokens.hasMoreElements()){
	    String classPathElement = extraClassesTokens.nextToken();
	    // A literal class to include, not a pattern
	    if(classPathElement.indexOf("*") == -1){
		byte[] classBytes = null;
		try{
		    classBytes = getClassBytes(classPathElement);
		}
		catch(Exception e){
		    System.err.println("While trying to get extra class " + classPathElement);
		    e.printStackTrace();
		}
		if(classBytes == null){
		    URL u = findResource(classPathElement);
		    if(u == null){
			System.err.println("Could not find the specified extra class: " + 
					   classPathElement);			
			continue;
		    }
		}
		classes.add(classPathElement);
	    }
	    // A class pattern that we must find all matches for
	    else{
		Set<String> matchedClasses = getMatchingClasses(classPathElement);
		if(matchedClasses == null || matchedClasses.size() == 0){
		    System.err.println("Could not find any classes matching the pattern specified for extra class: " + 
				       classPathElement);
		    continue;
		}
		classes.addAll(matchedClasses);
	    }
	}
    }

    protected Set<String> findDirMatches(String pathPrefix, File dir, Pattern p, boolean recursive){
	Set<String> matches = new HashSet<String>();

	String[] files = dir.list();
	for(int i = 0; i < files.length; i++){
	    if(files[i].indexOf(".") == 0){
		// skip hidden UNIX files, current dir, parent dir
		continue;
	    }
	    File file = new File(dir, files[i]);
	    if(recursive && file.isDirectory()){
		matches.addAll(findDirMatches(pathPrefix, file, p, recursive));
	    }
	    else if(p.matcher(files[i]).matches()){
		// This will mess up on windows, because the file separator \ will be an escape...
		matches.add(file.getPath().replace(pathPrefix, ""));
	    }
	}

	return matches;
    }

    /**
     * The pattern may have the form of an Ant path specification, i.e. "*" matches anything in a directory, 
     * and "**" matches anything in any subdirectory.
     */
    protected Set<String> getMatchingClasses(String pattern){
	Set<String> results = new HashSet<String>();

	// This code largely copied from the getResourceURL method, with mods for asterisk handling
	StringTokenizer classPathTokens = new StringTokenizer(System.getProperty("jarmaker.class.path"), 
							      File.pathSeparator);
	while(classPathTokens.hasMoreElements()){
	    String pathElement = classPathTokens.nextToken();
	    String path = pathElement + File.separator + pattern;
	    String parentPattern = "";
	    if(pattern.indexOf(File.separator) != -1 &&
	       pattern.indexOf(File.separator) != pattern.length()-1){
		parentPattern = pattern.substring(0, pattern.lastIndexOf(File.separator)+1);
	    }

	    boolean recursive = false;
	    if(path.indexOf("**"+File.separator) != -1){
		recursive = true;
		path = path.replaceAll("\\*\\*"+File.separator, "");
		parentPattern = parentPattern.replaceAll("\\*\\*"+File.separator, "(.*/)?");
	    }

	    File pathPattern = new File(path);
	    File pathParent = pathPattern.getParentFile();

	    // For now, we only take * or ** in the last part of the pattern 
	    // (i.e. specific subdirectories cannot be specified after an asterisk, unless it's in a jar)
	    String patternWildcard = pathPattern.getName();
	    // Turn the asterisked patterns into into regexs
	    if(patternWildcard.indexOf("**") != -1 && !recursive){
		recursive = true;
	    }
	    patternWildcard = patternWildcard.replaceAll("\\.", "\\\\."); // turn all periods into literal periods
	    patternWildcard = patternWildcard.replaceFirst("\\Q$\\E", "\\\\\\$"); // turn any $ into a literal one
	    patternWildcard = patternWildcard.replaceAll("\\*{1,2}", ".*");
	    // Must match the entirety of the name (anchor start and end of regex 
	    // to start and end of the file name to be compared to)
	    Pattern filep = Pattern.compile("^"+patternWildcard+"$");
	    Pattern jarp = Pattern.compile("^"+parentPattern+ 
					   (recursive ? "(.*/)?" : "") + 
					   patternWildcard.replaceAll(File.separator, "/")+"$");

	    // The path exists in the file system
	    if(pathParent == null || pathParent.isDirectory()){
		results.addAll(findDirMatches(pathElement + File.separator, pathParent, filep, recursive));
	    }
	    else{

		// If it's not a valid file system path, maybe it's in a JAR
		File f = new File(pathElement);
		if(f.isFile()){
		    JarFile jarFile = null;
		    try{
			jarFile = new JarFile(f);
		    }
		    catch(Exception e){
			System.err.println("Class path element " + pathElement + 
					   " was not a directory, or a valid JAR file");
			continue;
		    }
		    
		    for(Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
			JarEntry entry = entries.nextElement();
			if(jarp.matcher(entry.getName()).matches()){
			    results.add(entry.getName());
			}
		    }
		    
		    try{
			jarFile.close();
		    }
		    catch(IOException ioe){
			System.err.println("Couldn't close JAR file: " + pathElement);
		    }
		} //end if file (jar)
	    }  //end else if not dir
	}  //end for extra class path elements
	return results;
    }

    protected void dumpJar( String jarfile )
	throws IOException {
	// Get the list of classes
	Set<String> loadedClasses = getLoadedClasses();
        savedClasses = new Hashtable<String,String>();
        corePackages = new Hashtable<String,String>();
	int partId = 1;  //ID for part names in JNLP file (shorter than full package name to save space)
	
	addExtraClasses(loadedClasses);

	synchronized( loadedClasses ) {
	    // Open up a new JAR file
	    FileOutputStream fout = new FileOutputStream( jarfile );
	    JarOutputStream jout = new JarOutputStream( fout );
	    jout.setLevel(9);// most compression

	    // Create the manifest so user can do java -jar foo.jar and get same app
	    createManifest(jout);

	    // Stuff the classes in
	    for (Iterator it=loadedClasses.iterator(); it.hasNext();) {
		String classname = (String)it.next(); 
		
		// Don't write the system classes (i.e. those in the java.* packages)
		if (classname.startsWith( "java." )) {
		    continue;
		}
		if(classname.startsWith("META-INF")){ //this one causes problems for signed Web start apps 
                    continue;
                }
		if(classname.startsWith("junit")){  //for testing only, we assume
		    continue;
		}
		//if(classname.startsWith("javax.servlet")){  //servlet container will have a version, we assume
		//    continue;
		//}
		
		// Haven't noted the inclusion of classes from this package yet for future reference by
		// secondary jar dumper
		if(webStartIndexFileName != null){
		    String packageName = "";
		    int packageIndex = classname.lastIndexOf('.');
		    if(packageIndex > 0){  // Non-null package name
			packageName = classname.substring(0, classname.lastIndexOf('.'));
		    }
		    if(!corePackages.containsKey(packageName)){
			corePackages.put(packageName, ""+(partId++));
		    }
		}

		// Store the class, TODO: be updated since JAR class origins allowed
		byte[] classBytes = getClassBytes(classname);
                String relativePath = null;
                if(classBytes != null){ // Found it
                  relativePath = classname.replaceAll("\\.", File.separator)+".class";
                  if(savedClasses.containsKey(relativePath)){
                    continue;  //already saved
                  }

		  //System.out.println( "Adding class "+path );
		  JarEntry je = new JarEntry(relativePath);
		  jout.putNextEntry( je );
		  copyFile(jout, classBytes);
		  jout.closeEntry();
                }
                else {  // A resource, not a class?
                  java.net.URL resourceURL = getResourceURL(classname);
                  if(resourceURL != null){
                    relativePath = truncateURL(resourceURL);
		    if(relativePath == null){
			System.err.println("Could not create relative path for "+ resourceURL + ", excluding from JAR");
			continue;
		    }
                    else if(savedClasses.containsKey(relativePath)){
                      continue;  //already saved
                    }

		    //System.out.println( "Adding resource "+resourceURL);
                    JarEntry je = new JarEntry(relativePath);
		    jout.putNextEntry( je );
		    copyFile( jout, resourceURL.openStream());
		    jout.closeEntry();
                  }
                  else{
                    System.err.println("Warning: Could not find class or resource file for " + classname + ", excluding from JAR" );
                    continue;
                  }
                }
                // Only get to here if the save was successful
		//System.err.println("Saved "+relativePath);
                savedClasses.put(relativePath, "saved");
	    }
	    jout.close();
	}

	// If we asked for the ancillary jars, create them here
	if(createSecondaryJars){
	    
	    if(webStartIndexFileName != null){
		secondaryPackageWritten = new Hashtable<String,String>();
		try{
		    webStartIndexWriter = new FileWriter(webStartIndexFileName);
		}
		catch(IOException ioe){
		    System.err.println("WARNING: Could not write Web Start index file: " + ioe);
		}
	    }

	    // For each top level class path element, write out its jard recursively on a per-directory basis
	    StringTokenizer classPathTokens = new StringTokenizer(System.getProperty("jarmaker.class.path"), File.pathSeparator);
	    while(classPathTokens.hasMoreElements()){
		String classPathElement = classPathTokens.nextToken();
		System.err.println("Dumping secondary JAR for "+classPathElement);
		try{
		    dumpSecondaryJar(new File(classPathElement), classPathElement, (new File(jarfile)).getParent());
		}
		catch(Exception e){
		    System.err.println("Warning: Could not use class path element " + 
				       classPathElement + " to find files: " + e);
		    e.printStackTrace();
		}
	    }

	    if(webStartIndexFileName != null){
		try{
		    webStartIndexWriter.close();
		}
		catch(IOException ioe2){
		    System.err.println("WARNING: Could not close Web Start index file: " + ioe2);
		}
	    }
	}
    }

    /**
     * This method recursively jars up files in each subdirectory that haven't 
     * already benn included in the main dumpJar call.
     */
    private void dumpSecondaryJar(File classPath, String classPrefix, String jarFilePrefix) throws Exception{
	boolean isDir = classPath.isDirectory();
	JarOutputStream jarFile = null;
	String packageName = null;
	List entries = new Vector(); // files or jar entries
	JarFile srcJarFile = null;
	if(isDir){
	    if(classPrefix.equals(classPath.toString()) || (classPrefix+File.separator).equals(classPath.toString())){
		packageName = "no-package";
	    }
	    else{
		packageName = classPath.toString().substring(classPrefix.length()+1).replace('/', '.');
	    }
	    File[] contents = classPath.listFiles();
	    for(int i = 0; i < contents.length; i++){
		//System.err.println("Adding "+contents[i]);
		entries.add(contents[i]);
	    }
	}
	else{  // Get the classes from another JAR
	    //System.err.println(classPath+" is a JAR");
	    packageName = classPath.getName().replaceFirst(".jar$","");  // JAR file name without path or suffix
	    JarInputStream srcJarStream = null;
	    try{	      
		srcJarFile = new JarFile(classPath);
		srcJarStream = new JarInputStream(new FileInputStream(classPath));
	    } catch(Exception e){
		System.err.println("Skipping class path element that is neither a directory " +
				   "nor a valid JAR file " + e);
		return;
	    }
	    try{
		for(JarEntry je = srcJarStream.getNextJarEntry(); je != null; je = srcJarStream.getNextJarEntry()){
		    entries.add(je);
		}
	    } catch(Exception e){
		System.err.println("Error while reading from JAR file "+classPath);
		e.printStackTrace();
		return;
	    }
	}

	for(Object entry: entries){	    
	    String relativePath = entry instanceof JarEntry ? ((JarEntry) entry).getName() : 
		entry.toString().substring(classPrefix.length()+1);
	    //System.err.println("Checking status of "+relativePath);

	    if(savedClasses.containsKey(relativePath) || // already saved
	       relativePath.startsWith(".") ||  // or not interesting
	       relativePath.startsWith("TEST-") ||  // JUnit output files
	       relativePath.endsWith("~") ||    //emacs scratch file
	       relativePath.endsWith(".java") || 
	       relativePath.endsWith(".jar") || 
	       relativePath.equals("META-INF") || //this one causes null pointers in jarsigner 
	       relativePath.equals("testdata") || 
	       relativePath.equals("deployment") || 
	       relativePath.endsWith("CVS")){
		//System.err.println("Skipping " + relativePath);
		continue;
	    }
	    else if(entry instanceof File && ((File) entry).isDirectory()){ //recurse for subdirs
		//System.err.println("Recursing for " + relativePath);
		dumpSecondaryJar((File) entry, classPrefix, jarFilePrefix);		
		continue;
	    }
	    else if(jarFile == null){ // Need to create the jar file
		String jarFileName = jarFilePrefix + File.separator;  //in same dir as main jar file dump
		jarFileName += packageName;
		jarFileName += ".jar";
		File destinationFile = new File(jarFileName);
		if(destinationFile.exists()){
		    //System.err.println("Updating jar file " + jarFileName);
		    
		    // Byte-for-byte copy the existing JAR to a temp file
		    File tmpJarFile = File.createTempFile("MinJarMaker", ".jar");
		    InputStream origInStream = new FileInputStream(destinationFile);
		    OutputStream tmpOutStream = new FileOutputStream(tmpJarFile);
		    copyFile(tmpOutStream, origInStream);
		    tmpOutStream.close();
		    origInStream.close();
		    
		    // Then start writing the data out to the destination file, to be appended to later on too
		    JarInputStream existingJar = new JarInputStream(new FileInputStream(tmpJarFile));
		    jarFile = new JarOutputStream(new FileOutputStream( destinationFile ));
		    jarFile.setLevel(9); //best compression
		    for(JarEntry je = existingJar.getNextJarEntry(); je != null; je = existingJar.getNextJarEntry()){
			try{
			    // By the way, there is no good way to check if a JAR already contains a 
			    jarFile.putNextEntry( je );
			}catch(java.util.zip.ZipException ze){
			    System.err.println("Skipping existing entry " + je);
			    continue;
			}
			/*jar i/o works on a per entry basis, so copy File copies one entry*/
			copyFile(jarFile, existingJar);  
			jarFile.closeEntry();
		    }
		    tmpJarFile.delete();
		}
		else{
		    System.err.println("Creating jar file " + jarFileName);
		    jarFile = new JarOutputStream(new FileOutputStream( destinationFile ));
		    jarFile.setLevel(9); //best compression
		    if(webStartIndexWriter != null){
			// If it's a package that's going to have each class enumerated, 
			// use the short id as the part name 
			if(corePackages.containsKey(packageName)){
			    webStartIndexWriter.write("<jar download=\"lazy\" part=\"" + corePackages.get(packageName) +
						      "\" href=\"" + packageName + ".jar\"/>\n"); 
			}
			// Otherwise it's only going to be used once, so use the full name
			// for clarity's sake
			else{
			    webStartIndexWriter.write("<jar download=\"lazy\" part=\"" + packageName + "\" href=\"" +
						      packageName + ".jar\"/>\n"); 
			}
		    }
		}
	    }
	    // A file to add to the jar, not necessarily a class (could be a resource), 
	    // so it's a bit of a misnomer.
	    String className = relativePath;
	    if(relativePath.endsWith(".class")){
		className = relativePath.substring(0, relativePath.length()-6).replace('/', '.'); //chops off .class
	    }
	    //System.err.println("Adding " + relativePath);
	    if(webStartIndexWriter != null){
		// The package is split between the core jar and this jar, so we need to 
		// enumerate each class explicitly that isn't part of the core
		if(packageName.equals("no-package")){
		    webStartIndexWriter.write("<package name=\"" + className + "\" part=\"no-package\"/>\n");
		}
		else if(corePackages.containsKey(packageName)){
		    webStartIndexWriter.write("<package name=\"" + className + "\" part=\"" + 
					      corePackages.get(packageName) + "\"/>\n");
		}
		// Is this a purely secondary package that we haven't made an index for yet?
		else if(!secondaryPackageWritten.containsKey(packageName)){
		    webStartIndexWriter.write("<package name=\"" + packageName + ".*\" part=\"" + packageName + "\"/>\n");
		    secondaryPackageWritten.put(packageName, "written");
		}
	    }
	    try{
		jarFile.putNextEntry(new JarEntry(relativePath));
		if(entry instanceof JarEntry){
		    //copyFile(jarFile, srcJarFile.getInputStream((JarEntry) entry));
		    // Use the line below, not above, because different JARs are zipped differently, so we need to 
		    // get the bytes then rewrite them rather than copy zip entries to ensure entry sizes are correct,
		    // reflecting the output JAR file's compression scheme, etc.
		    copyFile(jarFile, getClassBytesFromJar(srcJarFile, relativePath));
		}
		else{
		    copyFile(jarFile, entry.toString());
		}
		jarFile.closeEntry();
	    }
	    catch(java.util.zip.ZipException ze2){
		System.err.println("Skipping duplicate entry: " + ze2);
		ze2.printStackTrace();
	    }
	    savedClasses.put(relativePath, "saved2");
	}
	if(jarFile != null){  //We saved something
	    jarFile.close();
	}
    }

    // Create the shutdown hook
    protected void createShutdownHook() {
	Thread thread = new Thread( this );
	Runtime.getRuntime().addShutdownHook( thread );
    }
    
    // Create the JAR file.  Called at shutdown.
    public void run() {
	try {
	    System.out.println( "Starting dump." );
	    dumpJar( jarfile );
	    System.out.println( "Dump done." );
	} catch( Exception e ) {
	    System.err.println( "Can't dump jarfile "+jarfile );
	    e.printStackTrace();
	}
    }
}
