Package org.openquark.gems.client.jfit

Source Code of org.openquark.gems.client.jfit.JFit$Pattern

/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
*     * Redistributions of source code must retain the above copyright notice,
*       this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of Business Objects nor the names of its contributors
*       may be used to endorse or promote products derived from this software
*       without specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/


/*
* JFit.java
* Creation date: Sep 14, 2005.
* By: Edward Lam
*/
package org.openquark.gems.client.jfit;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.StreamHandler;

import org.openquark.cal.compiler.CompilerMessage;
import org.openquark.cal.compiler.CompilerMessageLogger;
import org.openquark.cal.compiler.MessageLogger;
import org.openquark.cal.compiler.ModuleName;
import org.openquark.cal.compiler.SourceModel;
import org.openquark.cal.compiler.UnableToResolveForeignEntityException;
import org.openquark.cal.compiler.SourceModel.ModuleDefn;
import org.openquark.cal.compiler.SourceModel.TopLevelSourceElement;
import org.openquark.cal.machine.StatusListener;
import org.openquark.cal.services.CALSourcePathMapper;
import org.openquark.cal.services.CALWorkspace;
import org.openquark.cal.services.DefaultWorkspaceDeclarationProvider;
import org.openquark.cal.services.Status;
import org.openquark.cal.services.WorkspaceConfiguration;
import org.openquark.cal.services.WorkspaceDeclaration;
import org.openquark.cal.services.WorkspaceManager;
import org.openquark.util.FileSystemHelper;
import org.openquark.util.SimpleConsoleHandler;
import org.openquark.util.WildcardPatternMatcher;



/**
* Java Foreign Import Tool (JFit).
* A command-line tool for generating foreign imports for Java.
*
* To use:
* Invoke main() with the appropriate command-line options.
* Alternatively, create an instance of a JFit tool, with JFit options and a cal workspace, and call autoFit().
*
* @author Edward Lam
*/
public class JFit {

    /** The boilerplate string which always gets displayed. */
    private static final String toolBoilerPlate = "Java Foreign Import Tool(JFit)     Version 0.0.1     (c) 2005 Business Objects.";

    /** All class files end with this string. */
    public static final String CLASS_EXTENSION = ".class";
   
    /** Whether or not to use a nullary workspace for the command-line tool. */
    private static final boolean USE_NULLARY_WORKSPACE = true;
   
    /** The default workspace client id for the command-line tool. */
    private static final String DEFAULT_WORKSPACE_CLIENT_ID = USE_NULLARY_WORKSPACE ? null : "ice";

    /** The default workspace file to use in the absence of a specified workspace file.*/
    private static final String DEFAULT_WORKSPACE_NAME = "gemcutter.default.cws";
   
    /** The provider for the input stream on the workspace declaration.  Null if the workspace is provided. */
    private final WorkspaceDeclaration.StreamProvider streamProvider;
   
    /** The cal workspace.   If not provided in the constructor, this will be set on a call to compileWorkspace() using the stream provider. */
    private CALWorkspace calWorkspace;
   
    /** An instance of a Logger for messages. */
    private final Logger jfitLogger;

    /** The tool options. */
    private final Options options;
   
    /**
     * A class to encapsulate the generation options to the JFit tool.
     *
     * @author Edward Lam
     */
    public static class Options {
        private final ModuleName moduleName;
        private final File[] inputFiles;
        private final File[] inputDirectories;
        private final String[] classPath;
        private final JFit.Pattern[] patterns;
        private final ForeignImportGenerator.GenerationScope generationScope;
        private final String[] excludeMethods;
       
        /**
         * Constructor for an Options object.
         * Private -- to create, call makeOptions().
         *
         * @param moduleName
         * @param inputFiles
         * @param classPath
         * @param patterns
         * @param generationScope
         * @param excludeMethods
         */
        private Options(ModuleName moduleName, File[] inputFiles, File[] inputDirectories, String[] classPath, JFit.Pattern[] patterns,
                ForeignImportGenerator.GenerationScope generationScope, String[] excludeMethods) {
            this.moduleName = moduleName;
            this.inputFiles = inputFiles;               // may not be null.
            this.inputDirectories = inputDirectories;   // may not be null.
            this.classPath = classPath;                 // null == empty array.
            this.patterns = patterns;
            this.generationScope = generationScope;
            this.excludeMethods = excludeMethods;
           
            if (inputFiles == null) {
                throw new NullPointerException("Argument 'inputFiles' ,may not be null.");
            }
            if (inputDirectories == null) {
                throw new NullPointerException("Argument 'inputDirectories' ,may not be null.");
            }
            if (classPath == null) {
                throw new NullPointerException("Argument 'classPath' ,may not be null.");
            }
            if (generationScope == null) {
                throw new NullPointerException("Argument 'generationScope Path' ,may not be null.");
            }
            if (excludeMethods == null) {
                throw new NullPointerException("Argument 'excludeMethods' ,may not be null.");
            }
        }
       
        /**
         * Create an options object given the arguments.
         *
         * @param moduleNameString
         *              the module name.  May not be null.
         * @param inputFiles
         *              the input .jar files.  May be null.
         * @param inputDirectories
         *              the input source folders.  Maybe be null.
         * @param classPath
         *              the extra class path entries to use.  May be null.
         * @param patterns
         *              the class name patterns to match.  If null, all classes are matched.
         * @param generationScope
         *              if null, default to all private.
         * @param excludeMethods
         *              the methods to exclude.  If null, none are excluded.
         * @param logger
         *              the logger to which to log any errors.
         *
         * @return the options object, or null if there were errors in the provided option arguments.
         * If null, errors will have been logged to the logger.
         */
        public static Options makeOptions(String moduleNameString, File[] inputFiles, File[] inputDirectories, String[] classPath, JFit.Pattern[] patterns,
                ForeignImportGenerator.GenerationScope generationScope, String[] excludeMethods, Logger logger) {
           
            if (moduleNameString == null) {
                logger.severe("No module name provided.");
                return null;
            }
           
            ModuleName moduleName = ModuleName.maybeMake(moduleNameString);
            if (moduleName == null) {
                logger.severe("Invalid module name: " + moduleName);
                return null;
            }
           
            // If no patterns are provided, we assume everything on the classpath.
            if (patterns == null) {
                patterns = new JFit.Pattern[] {JFit.Pattern.MATCH_ALL};
            }
           
            if (inputFiles == null) {
                inputFiles = new File[] {};
               
            } else {
                for (final File inputFile : inputFiles) {
                    if (inputFile == null) {
                        logger.severe("Null input file encountered.");
                        return null;
                    }
                    if (!FileSystemHelper.fileExists(inputFile)) {
                        logger.severe("Input file \'" + inputFile.getPath() + "\' does not exist.");
                        return null;
                    }
                }
            }
           
            if (inputDirectories == null) {
                inputDirectories = new File[] {};
               
            } else {
                for (final File inputDirectory : inputDirectories) {
                    if (inputDirectory == null) {
                        logger.severe("Null input directory encountered.");
                        return null;
                    }
                    if (!inputDirectory.isDirectory()) {
                        logger.severe("Input directory \'" + inputDirectory.getPath() + "\' does not exist.");
                        return null;
                    }
                }
            }
           
            if (classPath == null) {
                classPath = new String[] {};
            }
           
            if (generationScope == null) {
                generationScope = ForeignImportGenerator.GenerationScope.ALL_PRIVATE;
            }
           
            if (excludeMethods == null) {
                excludeMethods = new String[] {};
               
            } else {
                // Check for nulls.
                for (final String excludeMethod : excludeMethods) {
                    if (excludeMethod == null) {
                        logger.severe("Null exclude method encountered.");
                    }
                }
            }
           
            return new Options(moduleName, inputFiles, inputDirectories, classPath, patterns, generationScope, excludeMethods);
        }
       
        /**
         * @return the module name.  Always valid.
         */
        public ModuleName getModuleName() {
            return this.moduleName;
        }
       
        /**
         * @return the input directories.  Existence of files in the array has been checked.
         */
        public File[] getInputFiles() {
            return this.inputFiles;
        }
       
        /**
         * @return the input directories.  Existence of directories in the array has been checked.
         */
        public File[] getInputDirectories() {
            return this.inputDirectories;
        }
       
        /**
         * @return the classpath elements.  Never null.  May be empty.
         */
        public String[] getClassPath() {
            return this.classPath;
        }
       
        /**
         * @return the patterns.  Never null.  May be empty.
         */
        public JFit.Pattern[] getPatterns() {
            return this.patterns;
        }

        /**
         * @return the generation scope.  Never null.
         */
        public ForeignImportGenerator.GenerationScope getGenerationScope() {
            return this.generationScope;
        }

        /**
         * @return the methods to exclude.  Never null.  May be empty.
         * These should be of the form (fully-qualified class name).(method name).  eg. java.lang.Object.equals
         */
        public String[] getExcludeMethods() {
            return this.excludeMethods;
        }
    }
   
    /**
     * A class to encapsulate the command-line options passed to the JFit tool.
     *
     * When this class is instantiated, the following are true:
     *   moduleName is a valid module name.
     *   workspaceName is provided
     *   patterns are specified.
     *   inputFile is non-null (for now) and exists.
     *   outputFolder exists if non-null.
     *
     * @author Edward Lam
     */
    private static class CommandLineOptions {
       
        private static final String pathSeparator = System.getProperty("path.separator");
       
        private static final String[] usageString = {
            toolBoilerPlate,
            "Usage: java " + JFit.class.getName() + " [options] cal_moduleName",
            "   -cp classPath      additional classpath entries",
            "   -cpf classPathFile specify a file with additional classpath entries, one per line",
            "   -p classPatterns   specify class name patterns",
            "                      eg. \"my.desired.Clazz" + pathSeparator + "my.desired.classes.*_Test" + pathSeparator + "-my.desired.ExcludeClass\"",
            "   -pf patFile        file specifying desired class name patterns, one per line",
            "   -xm excludeMethods specify methods to exclude, qualified by class name",
            "                      eg. java.lang.Object.wait" + pathSeparator + "java.lang.Object.notify",
            "   -xmf exMethodFile  file specifying desired methods to exclude, one per line",
            "   -o outputDir       specify folder in which to generate output",
            "   -f jarfile         specify one or more jar files which contains desired classes",
            "                      eg. \"lib\\classes1.jar" + pathSeparator + "lib\\classes2.jar\"",
            "   -d directory       specify one or more directories as the classpath root of desired classes",
            "                      eg. \".." + pathSeparator + "lib\"",
            "   -ws workspaceName  specify the name of the workspace",
            "   -public            specify public scope for generated types and functions",
            "   -private           specify private scope for generated types and functions (default)",
            "   -privateTypeImpl   specify public scope for generated types and functions, but private scope type implementations",
            "   -quiet, -q         be extra quiet",
            "   -verbose, -v       be extra verbose",
            "   -h                 this help message"
        };

        private final String workspaceName;
        private final File outputFolder;
        private final Level verbosity;
        private final Options options;

        /**
         * An internal exception which is thrown if there was a problem parsing the command line arguments.
         * @author Edward Lam
         */
        private static class ParseException extends Exception {
            private static final long serialVersionUID = -8214168472347843070L;

            ParseException() {
            }
        }
       
        /**
         * Private constructor for an Options object.
         * To instantiate, call parseOptions() or makeOptions().
         *
         * @param workspaceName
         * @param outputFolder
         * @param verbosity the specified verbosity of the logger
         * @param options
         */
        private CommandLineOptions(String workspaceName, File outputFolder, Level verbosity, Options options) {
            this.workspaceName = workspaceName;
            this.outputFolder = outputFolder;  // may be null
            this.verbosity = verbosity;
            this.options = options;
        }

        /**
         * Dump the usage string to the logger.
         */
        private static void logUsageString(Logger logger) {
            for (final String usageLine : usageString) {
                logger.info(usageLine);
            }
        }
       
        /**
         * Parse command line arguments into options for this tool.
         * If there are problems parsing the options, null is returned and the usage string is logged.
         *
         * @param args the command line arguments for this tool.
         * @param logger the logger to which to log any error messages.
         * @return the corresponding options object.  Null if there are problems with the command line options.
         */
        static CommandLineOptions parseOptions(String[] args, Logger logger) {
           
            if (args.length < 1) {
                logUsageString(logger);
                return null;
            }
            List<String> argList = new ArrayList<String>(Arrays.asList(args));
           
            // flag to indicate whether the usage string has already been logged via -h option.
            boolean loggedUsageStringForOption = false;
           
            String moduleName = null;
            String workspaceName = null;
            List<String> inputFileStrings = new ArrayList<String>();            // (List of String)
            List<String> inputDirectoryStrings = new ArrayList<String>();       // (List of String);
            File outputFolder = null;
            String[] classPath = null;
            String[] patterns = null;
            Level verbosity = null;
            ForeignImportGenerator.GenerationScope generationScope = null;
            String[] excludeMethods = null;
           
            try {
                while (!argList.isEmpty()) {
                    String nextArg = argList.get(0);
                   
                    // Handle non-dash argument.
                    // This should give the module name and the workspace name.
                    if (!nextArg.startsWith("-")) {
                        // Check that there is one arg remaining.
                        if (argList.size() != 1) {
                            throw new ParseException();
                        }
                       
                        moduleName = nextArg;
                        argList = Collections.emptyList();
                       
                    } else {
                       
                        // Save the option text.
                        String option = nextArg;
                       
                        // Shorten argList by one arg.
                        argList = argList.subList(1, argList.size());
                       
                        //
                        // Zero-argument options.
                        //
                        if (option.equals("-h")) {
                            if (!loggedUsageStringForOption) {
                                loggedUsageStringForOption = true;
                                logUsageString(logger);
                            }
                           
                        } else if (option.equals("-v") || option.equals("-verbose")) {
                            if (verbosity != null) {
                                logger.severe("Multiple verbosity options provided.");
                                throw new ParseException();
                            }
                            verbosity = Level.FINEST;
                           
                        } else if (option.equals("-q") || option.equals("-quiet")) {
                            if (verbosity != null) {
                                logger.severe("Multiple verbosity options provided.");
                                throw new ParseException();
                            }
                            verbosity = Level.SEVERE;
                           
                        } else if (option.equals("-public")) {
                            if (generationScope != null) {
                                logger.severe("Multiple generation scope options provided.");
                                throw new ParseException();
                            }
                            generationScope = ForeignImportGenerator.GenerationScope.ALL_PUBLIC;
                           
                        } else if (option.equals("-private")) {
                            if (generationScope != null) {
                                logger.severe("Multiple generation scope options provided.");
                                throw new ParseException();
                            }
                            generationScope = ForeignImportGenerator.GenerationScope.ALL_PRIVATE;
                           
                        } else if (option.equals("-privateTypeImpl")) {
                            if (generationScope != null) {
                                logger.severe("Multiple generation scope options provided.");
                                throw new ParseException();
                            }
                            generationScope = ForeignImportGenerator.GenerationScope.PARTIAL_PUBLIC;

                            //
                            // One-argument options.
                            //
                        } else {
                            // Make sure there's another argument
                            if (argList.size() < 1) {
                                throw new ParseException();
                            }
                           
                            // Get the argument to the option.
                            String optionArg = argList.get(0);
                           
                            // Shorten argList by another arg.
                            argList = argList.subList(1, argList.size());
                           
                            if (option.equals("-ws")) {
                                // The name of the workspace.
                                if (workspaceName != null) {
                                    logger.severe("Multiple workspace names provided.");
                                    throw new ParseException();
                                }
                                workspaceName = optionArg;
                               
                            } else if (option.equals("-cp")) {
                                // Classpath provided on the command line.
                                if (classPath != null) {
                                    logger.severe("Multiple classpath options provided.");
                                    throw new ParseException();
                                }
                               
                                classPath = pathsToStrings(optionArg);
                               
                            } else if (option.equals("-cpf")) {
                                // Read the classpath from a file.
                                if (classPath != null) {
                                    logger.severe("Multiple classpath options provided.");
                                    throw new ParseException();
                                }
                               
                                classPath = fileToStrings(optionArg, logger);
                                if (classPath == null) {
                                    // There was a problem reading the file.
                                    return null;
                                }
                               
                            } else if (option.equals("-p")) {
                                // Patterns provided on the command line.
                                if (patterns != null) {
                                    logger.severe("Multiple pattern options provided.");
                                    throw new ParseException();
                                }
                               
                                patterns = pathsToStrings(optionArg);
                               
                            } else if (option.equals("-pf")) {
                                // Patterns provided from a file.
                                if (patterns != null) {
                                    logger.severe("Multiple pattern options provided.");
                                    throw new ParseException();
                                }
                               
                                patterns = fileToStrings(optionArg, logger);
                                if (patterns == null) {
                                    // There was a problem reading the file.
                                    return null;
                                }
                               
                            } else if (option.equals("-xm")) {
                                // Exclude methods provided on the command line.
                                if (excludeMethods != null) {
                                    logger.severe("Multiple exclude method options provided.");
                                    throw new ParseException();
                                }
                                excludeMethods = pathsToStrings(optionArg);
                               
                            } else if (option.equals("-xmf")) {
                                // Exclude methods provided from a file.
                                if (excludeMethods != null) {
                                    logger.severe("Multiple exclude method options provided.");
                                    throw new ParseException();
                                }
                                excludeMethods = fileToStrings(optionArg, logger);
                                if (excludeMethods == null) {
                                    // There was a problem reading the file.
                                    return null;
                                }
                               
                            } else if (option.equals("-f")) {
                                // Input jar file.
                                inputFileStrings.addAll(Arrays.asList(pathsToStrings(optionArg)));
                               
                            } else if (option.equals("-d")) {
                                // Input directory.
                                inputDirectoryStrings.addAll(Arrays.asList(pathsToStrings(optionArg)));
                               
                            } else if (option.equals("-o")) {
                                // output folder
                                if (outputFolder != null) {
                                    logger.severe("Multiple output folders specified.");
                                    throw new ParseException();
                                }
                                outputFolder = new File(optionArg);
                               
                            } else {
                                logger.severe("Unknown option: " + option);
                                throw new ParseException();
                            }
                        }
                    }
                }
           
            } catch (ParseException pe) {
                // Some problem occurred parsing the arguments.
                // Log the usage string and return null.
                if (!loggedUsageStringForOption) {
                    logUsageString(logger);
                }
                return null;
            }
           
            File[] inputFiles = new File[inputFileStrings.size()];
            {
                int index = 0;
                for (final String inputFileString : inputFileStrings) {
                    inputFiles[index] = new File(inputFileString);
                    index++;
                }
            }

            File[] inputDirectories = new File[inputDirectoryStrings.size()];
            {
                int index = 0;
                for (final String inputDirString : inputDirectoryStrings) {
                    inputDirectories[index] = new File(inputDirString);
                    index++;
                }
            }

            return makeOptions(moduleName, workspaceName, inputFiles, inputDirectories, outputFolder,
                    classPath, patterns, generationScope, excludeMethods, verbosity, logger);
        }
       
        /**
         * Create a command-line options object given the arguments.
         *
         * @param moduleName
         * @param workspaceName the name of the workspace, or null for the default workspace.
         * @param inputFiles
         * @param inputDirectories
         * @param outputFolder
         * @param classPath
         * @param patternStrings
         * @param generationScope
         * @param excludeMethods
         * @param verbosity
         * @param logger the logger to which to log any errors.
         *
         * @return the options object, or null if there were errors in the provided option arguments.
         * If null, errors will have been logged to the logger.
         */
        public static CommandLineOptions makeOptions(String moduleName, String workspaceName, File[] inputFiles, File[] inputDirectories, File outputFolder, String[] classPath, String[] patternStrings, ForeignImportGenerator.GenerationScope generationScope, String[] excludeMethods, Level verbosity, Logger logger) {
          
            JFit.Pattern[] patterns = getPatterns(patternStrings);
           
            Options jfitOptions = Options.makeOptions(moduleName, inputFiles, inputDirectories, classPath, patterns, generationScope, excludeMethods, logger);
           
            if (jfitOptions == null) {
                return null;
            }
           
            return new CommandLineOptions(workspaceName, outputFolder, verbosity, jfitOptions);
        }
       
        /**
         * Convert the command line patterns to Pattern objects.
         * @param commandLinePats the strings specified on the command line for patterns.
         * @return the corresponding Pattern objects.
         */
        private static JFit.Pattern[] getPatterns(String[] commandLinePats) {
           
            if (commandLinePats == null) {
                return new JFit.Pattern[] {};
            }
           
            int nPats = commandLinePats.length;
            JFit.Pattern[] patterns = new JFit.Pattern[nPats];
           
            for (int i = 0; i < nPats; i++) {
                String commandLinePat = commandLinePats[i];
               
                boolean isInclude;
                String patternString;
                if (commandLinePat.startsWith("-")) {
                    isInclude = false;
                    patternString = commandLinePat.substring(1);
                } else {
                    isInclude = true;
                    patternString = commandLinePat;
                }
               
                patterns[i] = new Pattern(patternString, isInclude);
            }
           
            return patterns;
        }
       
        /**
         * Split up a string delimited with path separators into its component paths.
         * eg. if ";" is the path separator,
         *     "foo;bar;baz" -> {"foo", "bar", "baz"}
         *
         * @param stringWithPaths the string with path separators.
         * @return the component paths.
         */
        private static String[] pathsToStrings(String stringWithPaths) {
            // Read the classpath
            List<String> classPathList = new ArrayList<String>();

            while (true) {
                int pathSeparatorIndex = stringWithPaths.indexOf(pathSeparator);
               
                if (pathSeparatorIndex < 0) {
                    // The last entry -- no more separators.
                    stringWithPaths = stringWithPaths.trim();
                    if (stringWithPaths.length() > 0) {
                        classPathList.add(stringWithPaths);
                    }
                    break;
                }
               
                // The next class path entry is everything up to before the next separator.
                String classPathEntry = stringWithPaths.substring(0, pathSeparatorIndex).trim();
                if (classPathEntry.length() > 0) {
                    classPathList.add(classPathEntry);
                }
                stringWithPaths = stringWithPaths.substring(pathSeparatorIndex + 1);

            }
           
            return classPathList.toArray(new String[classPathList.size()]);
        }
       
        /**
         * Read a file, and return the lines as an array of strings.
         * Leading and trailing whitespace are ignored.
         *
         * TODOEL: accept comments.
         *
         * @param fileName the name of the file to read.
         * @param logger the logger to which to log any error messages.
         * @return The lines from the file. If there was a problem reading the file, null (messages will be logged).
         */
        private static String[] fileToStrings(String fileName, Logger logger) {
            BufferedReader classPathReader = null;
            try {
                classPathReader = new BufferedReader(new FileReader(fileName));
               
                List<String> classPathList = new ArrayList<String>();
               
                for (String lineText = classPathReader.readLine(); lineText != null; lineText = classPathReader.readLine()) {
                    lineText = lineText.trim();
                   
                    if (lineText.length() != 0) {
                        classPathList.add(lineText);
                    }
                }
               
                return classPathList.toArray(new String[classPathList.size()]);
               
            } catch (IOException e) {
                logger.log(Level.SEVERE, "Can't read file: " + fileName, e);
                return null;
               
            } finally {
                if (classPathReader != null) {
                    try {
                        classPathReader.close();
                    } catch (IOException e) {
                        // can't do much about this..
                    }
                }
            }
        }
       
        public String getWorkspaceName() {
            return this.workspaceName;
        }
       
        public File getOutputFolder() {
            return this.outputFolder;
        }
       
        /**
         * @return the specified verbosity (Logging) level.  May be null if not specified.
         */
        public Level getVerbosity() {
            return verbosity;
        }
       
        public Options getJFitOptions() {
            return options;
        }
    }

    /**
     * A simple container class to encapsulate a pattern to match and whether it is an include or an exclude pattern.
     * @author Edward Lam
     */
    public static class Pattern {
        /** The pattern to match everything. */
        public static final Pattern MATCH_ALL = new JFit.Pattern("*", true);
       
        private final String pattern;
        private final boolean include;
       
        /**
         * Constructor for a JFit.Pattern.
         * @param pattern the pattern.
         * @param include true for an include pattern, false for an exclude pattern.
         */
        public Pattern(String pattern, boolean include) {
            this.pattern = pattern;
            this.include = include;
        }
       
        /**
         * @return true for an include pattern, false for an exclude pattern.
         */
        public boolean isInclude() {
            return this.include;
        }
       
        /**
         * @return the pattern.
         */
        public String getPattern() {
            return this.pattern;
        }
       
        /**
         * {@inheritDoc}
         */
        public boolean equals(Object obj) {
            if (obj instanceof Pattern) {
                Pattern otherPattern = (Pattern)obj;
                return include == otherPattern.include && pattern.equals(otherPattern.pattern);
            }
            return false;
        }
       
        /**
         * {@inheritDoc}
         */
        public int hashCode() {
            return pattern.hashCode() + (include ? 17 : 37);
        }
       
        /**
         * {@inheritDoc}
         */
        public String toString() {
            return "(" + (include ? "include: " : "exclude: ") + pattern + ")";
        }
    }

    /**
     * An exception to signal that a class found to be required by JFit is invalid or not present on the classpath.
     * @author Edward Lam
     */
    public static class MissingClassException extends Exception {
        private static final long serialVersionUID = 6846885687084625419L;

        /** The name of the missing class. */
        private final String missingClassName;
       
        /** The name of the class which requires the class which is missing. */
        private final String requiringClassName;
       
        /**
         * Constructor for a MissingClassException.
         * @param requiringClassName the name of the class requiring the missing class.
         * @param e the exception signalling the missing class.
         */
        MissingClassException(String requiringClassName, ClassNotFoundException e) {
            this.requiringClassName = requiringClassName;
            this.missingClassName = e.getMessage().replace('/','.');
        }
       
        /**
         * Constructor for a MissingClassException.
         * @param requiringClassName the name of the class requiring the missing class.
         * @param e the exception signalling the missing class.
         */
        MissingClassException(String requiringClassName, NoClassDefFoundError e) {
            this.requiringClassName = requiringClassName;
            String message = e.getMessage();
            this.missingClassName = message == null ? null : message.replace('/','.');
        }
       
        /**
         * Constructor for a MissingClassException.
         * @param requiringClassName the name of the class requiring the missing class.
         * @param e the exception signalling the missing class.
         */
        MissingClassException(String requiringClassName, LinkageError e) {
            this.requiringClassName = requiringClassName;
            this.missingClassName = null;
        }
       
        /**
         * @return the name of the class requiring the missing class.
         */
        String getRequiringClassName() {
            return requiringClassName;
        }
       
        /**
         * @return the name of the missing class.
         * This may return null if this could not be determined.
         */
        String getMissingClassName() {
            return missingClassName;
        }
       
        /**
         * {@inheritDoc}
         */
        public String toString() {
            if (missingClassName == null) {
                return "MissingClassException - could not load definition of class: " + requiringClassName;
            } else {
                return "MissingClassException - missing: " + missingClassName + ", required by: " + requiringClassName;
            }
        }
    }
   
    /**
     * The command-line entry point.
     * @param args the command-line args.
     */
    public static void main(String[] args) {
       
        Logger commandLineLogger = Logger.getLogger(JFit.class.getPackage().getName());
        commandLineLogger.setLevel(Level.FINEST);
        commandLineLogger.setUseParentHandlers(false);
       
        StreamHandler consoleHandler = new SimpleConsoleHandler();

        consoleHandler.setLevel(Level.INFO);
        commandLineLogger.addHandler(consoleHandler);
       
        CommandLineOptions commandLineOptions = CommandLineOptions.parseOptions(args, commandLineLogger);

        if (commandLineOptions == null) {
            // There was an error in the provided command-line args.
            return;
        }
        Level verbosity = commandLineOptions.getVerbosity();
        if (verbosity != null) {
            consoleHandler.setLevel(verbosity);
        }
       
        // Log the boilerplate message.
        commandLineLogger.info(toolBoilerPlate);
        commandLineLogger.info("  '-h' for help.");

       
        // Log the specified options.
        String notSpecifiedString = "(not specified)";
        String noneSpecifiedString = "(none specified)";
       
        Options jFitOptions = commandLineOptions.getJFitOptions();
       
        commandLineLogger.config("Specified options: ");
       
        ModuleName moduleName = jFitOptions.getModuleName();
        commandLineLogger.config("  Module name:    " + (moduleName == null ? notSpecifiedString : moduleName.toSourceText()));
       
        String workspaceName = commandLineOptions.getWorkspaceName();
        commandLineLogger.config("  Workspace name: " + (workspaceName == null ? notSpecifiedString : workspaceName));
       
        File[] inputFiles = jFitOptions.getInputFiles();
        if (inputFiles.length == 0) {
            commandLineLogger.config("  Input file:     " + noneSpecifiedString);
        } else {
            for (final File inputFile : inputFiles) {
                commandLineLogger.config("  Input file:     " + inputFile.getPath());
            }
        }
       
        File[] inputDirectories = jFitOptions.getInputDirectories();
        if (inputDirectories.length == 0) {
            commandLineLogger.config("  Input dir:      " + noneSpecifiedString);
        } else {
            for (final File inputDirectory : inputDirectories) {
                commandLineLogger.config("  Input dir:      " + inputDirectory.getPath());
            }
        }
       
        File outputFolder = commandLineOptions.getOutputFolder();
        commandLineLogger.config("  Output folder:  " + (outputFolder == null ? notSpecifiedString : outputFolder.getPath()));
       
        String[] classPath = jFitOptions.getClassPath();
        if (classPath.length == 0) {
            commandLineLogger.config("  Class path:     " + noneSpecifiedString);

        } else {
            for (final String classPathEntry : classPath) {
                commandLineLogger.config("  Class path entry: " + classPathEntry);
            }
        }
        Pattern[] patterns = jFitOptions.getPatterns();
        if (patterns.length == 0) {
            commandLineLogger.config("  Patterns:       " + noneSpecifiedString);

        } else {
            commandLineLogger.config("  Patterns: ");
            for (final Pattern nthPattern : patterns) {
                String patternDescription = (nthPattern.isInclude() ? "include" : "exclude") + ": \"" + nthPattern.getPattern() + "\"";
                commandLineLogger.config("    " + patternDescription);
            }
        }

        // If there is an output folder, ensure it exists.
        if (outputFolder != null && !outputFolder.isDirectory()) {
            if (!FileSystemHelper.ensureDirectoryExists(outputFolder)) {
                commandLineLogger.severe("Output folder \'" + outputFolder.getPath() + "\' does not exist, and could not be created.");
                return;
            }
        }
       
        JFit jfit = new JFit(commandLineOptions, commandLineLogger);
        jfit.fitToFile(outputFolder);
    }
   
    /**
     * Constructor for a JFit.
     * @param options
     * @param workspace
     * @param logger
     */
    public JFit(Options options, CALWorkspace workspace, Logger logger) {
       
        if (logger == null) {
            throw new NullPointerException("Argument 'logger' cannot be null.");
        }
        this.jfitLogger = logger;
       
        if (options == null) {
            // No provided options.
            throw new NullPointerException("Argument 'options' cannot be null.");
        }
        this.options = options;
       
        this.streamProvider = null;
        this.calWorkspace = workspace;
    }
   
    /**
     * Constructor for a JFit.
     * @param commandLineOptions
     * @param logger
     */
    private JFit(CommandLineOptions commandLineOptions, Logger logger) {
       
        if (logger == null) {
            throw new NullPointerException("Argument 'logger' cannot be null.");
        }
        this.jfitLogger = logger;
       
        if (commandLineOptions == null) {
            // No provided options.
            throw new NullPointerException("Argument 'commandLineOptions' cannot be null.");
        }
        this.options = commandLineOptions.getJFitOptions();
       
        String workspaceName = commandLineOptions.getWorkspaceName();
        if (workspaceName == null) {
            workspaceName = DEFAULT_WORKSPACE_NAME;
        }
       
        streamProvider = DefaultWorkspaceDeclarationProvider.getDefaultWorkspaceDeclarationProvider(workspaceName);
        if (streamProvider == null) {
            logger.severe("Workspace not found: " + commandLineOptions.getWorkspaceName());
        }
       
        // the workspace will be set on a call to compileWorkspace().
        this.calWorkspace = null;
    }
   
    /**
     * Automatically figure out what the required classes and functions are, generate them, output to a file.
     * Does nothing if there was a problem with the command-line options.
     */
    private void fitToFile(File outputFolder) {
       
        ModuleDefn defn = autoFit();
        if (defn == null) {
            jfitLogger.info("");
            jfitLogger.info("Generation failed.");
            return;
        }
       
        String sourceText = defn.toSourceText();
       
        String[] moduleNameQualifier = defn.getModuleName().getQualifier().getComponents();
        String unqualifiedModuleName = defn.getModuleName().getUnqualifiedModuleName();
       
        File fileFolder = outputFolder;
        for (final String component : moduleNameQualifier) {
            if (fileFolder == null) {
                fileFolder = new File(component);
            } else {
                fileFolder = new File(fileFolder, component);
            }
        }
       
        if (fileFolder != null) {
            if (!FileSystemHelper.ensureDirectoryExists(fileFolder)) {
                jfitLogger.severe("The folder \'" + fileFolder.getPath() + "\' does not exist, and could not be created.");
                return;
            }
        }
       
        String fileName = unqualifiedModuleName + "." + CALSourcePathMapper.INSTANCE.getFileExtension();
        File outputFile = fileFolder == null ? new File(fileName) : new File(fileFolder, fileName);
       
        jfitLogger.info("Writing file: " + outputFile.getAbsolutePath() + "..");
       
        boolean wroteFile;
       
        Writer writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(outputFile));
            writer.write(sourceText);
            wroteFile = true;
           
        } catch (IOException e) {
            jfitLogger.log(Level.SEVERE, "Error writing file: " + outputFile.getPath(), e);
            wroteFile = false;

        } finally {
            if (writer != null) {
                try {
                    writer.flush();
                    writer.close();
                } catch (IOException e) {
                    // Not much we can do about this.
                }
            }
        }
       
        if (wroteFile) {
            int nGeneratedFunctions = 0;
            int nGeneratedTypes = 0;
           
            int nTopLevelDefns = defn.getNTopLevelDefns();
            for (int i = 0; i < nTopLevelDefns; i++) {
                TopLevelSourceElement nthTopLevelDefn = defn.getNthTopLevelDefn(i);
               
                if (nthTopLevelDefn instanceof SourceModel.FunctionDefn.Foreign) {
                    nGeneratedFunctions++;
               
                } else if (nthTopLevelDefn instanceof SourceModel.TypeConstructorDefn.ForeignType) {
                    nGeneratedTypes++;
                }
            }
            jfitLogger.info("");
            jfitLogger.info("Generation succeeded.");
            jfitLogger.info(nGeneratedTypes + " types, " + nGeneratedFunctions + " functions generated.");
           
        } else {
            jfitLogger.info("");
            jfitLogger.info("Generation failed.");
        }
    }
   
    /**
     * The function which automatically does all the work.
     * Figure out what the required classes and functions are, and generate them.
     * Does nothing if there was a problem with the options.
     *
     * Note: the outputFolder option is ignored..
     */
    public ModuleDefn autoFit() {
        if (options == null) {
            return null;
        }
        ModuleName calModuleName = options.getModuleName();
        JFit.Pattern[] patterns = options.getPatterns();
        File[] inputFiles = options.getInputFiles();
        File[] inputDirectories = options.getInputDirectories();
        String[] classPath = options.getClassPath();
        ForeignImportGenerator.GenerationScope generationScope = options.getGenerationScope();
        String[] excludeMethods = options.getExcludeMethods();
       
        JarClassAnalyzer jarClassAnalyzer = JarClassAnalyzer.getAnalyzer(inputFiles, inputDirectories, classPath, jfitLogger);
        if (jarClassAnalyzer == null) {
            return null;
        }
       
        // (List of String) fully-qualified matching class names.
        Set<String> matchingClassNames = jarClassAnalyzer.getMatchingClassNames(patterns, jfitLogger);
        if (matchingClassNames == null) {
            return null;
        }
       
        if (matchingClassNames.isEmpty()) {
            jfitLogger.severe("No classes match the provided patterns.");
            return null;
        }
       
        Set<Class<?>> requiredClassSet;
        try {
            requiredClassSet = jarClassAnalyzer.calculateRequiredClasses(matchingClassNames, jfitLogger);
           
        } catch (MissingClassException e) {
            String missingClassName = e.getMissingClassName();
            if (missingClassName == null) {
                jfitLogger.log(Level.SEVERE, "Could not load definition of class " + e.getRequiringClassName(), e);
            } else {
                jfitLogger.log(Level.SEVERE, "Missing class " + e.getMissingClassName() + " required by class " + e.getRequiringClassName(), e);
            }
            return null;
        }
       
        if (requiredClassSet.isEmpty()) {
            jfitLogger.severe("No required classes to generate.");
            return null;
        }
       
        // Generate map (Class->Set of String) from class to method names for methods to exclude.
        Map<Class<?>, Set<String>> classToMethodExcludeNamesMap = new HashMap<Class<?>, Set<String>>();
        for (final String excludeMethodString : excludeMethods) {
           
            int lastDotIndex = excludeMethodString.lastIndexOf('.');

            if (lastDotIndex < 0) {
                jfitLogger.warning("Unqualified exclude method: \"" + excludeMethodString + "\"");
                jfitLogger.warning("Exclude patterns must be of the form: (fully-qualified-class name).(method name).  eg. java.lang.Object.equals");
                continue;
            }
           
            String qualifiedClassName = excludeMethodString.substring(0, lastDotIndex);
            String methodName = excludeMethodString.substring(lastDotIndex + 1);
           
            Class<?> excludeClass;
            try {
                excludeClass = jarClassAnalyzer.getClass(qualifiedClassName);
           
            } catch (MissingClassException e) {
                jfitLogger.warning("Exclude method: \"" + excludeMethodString + "\" - cannot find class \"" + qualifiedClassName + "\".");
                continue;
            }
           
            boolean hasMethod = false;
            Method[] methods = excludeClass.getMethods();
            for (final Method method : methods) {
                if (method.getName().equals(methodName)) {
                    hasMethod = true;
                    break;
                }
            }
           
            if (!hasMethod) {
                jfitLogger.warning("Exclude class \"" + qualifiedClassName + "\" does not have any method named \"" + methodName + "\".");
                continue;
            }
           
            // Add the mapping.
            Set<String> classExcludesSet = classToMethodExcludeNamesMap.get(excludeClass);
            if (classExcludesSet == null) {
                classExcludesSet = new HashSet<String>();
                classToMethodExcludeNamesMap.put(excludeClass, classExcludesSet);
            }
            classExcludesSet.add(methodName);
        }
       
        if (streamProvider != null) {
            // Compile the workspace.
            if (!compileWorkspace(true, false)) {
                // Compilation failed.
                return null;
            }
        }
       
        jfitLogger.info("Generating module definition..");
       
        try {
            return ForeignImportGenerator.makeDefaultModuleDefn(
                    calModuleName, matchingClassNames, requiredClassSet, generationScope, classToMethodExcludeNamesMap, calWorkspace);
           
        } catch (UnableToResolveForeignEntityException e) {
            jfitLogger.severe(e.getCompilerMessage().toString());
            return null;
        }
    }
   
    /**
     * Builds the program object using the CAL file at the current location.
     * Create a new workspace if one does not already exist.  Does nothing if this fails.
     * @param dirtyOnly true
     * @param forceCodeRegen false
     * @return whether compilation succeeded.
     */   
    private boolean compileWorkspace(boolean dirtyOnly, boolean forceCodeRegen) {

        jfitLogger.info("Compiling CAL workspace..");
       
        String clientID = WorkspaceConfiguration.getDiscreteWorkspaceID(DEFAULT_WORKSPACE_CLIENT_ID);
        WorkspaceManager workspaceManager = WorkspaceManager.getWorkspaceManager(clientID);
       
        // Add a status listener to log when modules are loaded.
        workspaceManager.addStatusListener(new StatusListener.StatusListenerAdapter() {
            public void setModuleStatus(StatusListener.Status.Module moduleStatus, ModuleName moduleName) {
                if (moduleStatus == StatusListener.SM_LOADED) {
                    jfitLogger.fine("  Module loaded: " + moduleName);
                }
            }
        });

       
        CompilerMessageLogger ml = new MessageLogger();
       
        // Init and compile the workspace.
        Status initStatus = new Status("Init status.");
        workspaceManager.initWorkspace(streamProvider, initStatus);
       
        if (initStatus.getSeverity() != Status.Severity.OK) {
            ml.logMessage(initStatus.asCompilerMessage());
        }
       
        long startCompile = System.currentTimeMillis();
       
        // If there are no errors go ahead and compile the workspace.
        if (ml.getMaxSeverity().compareTo(CompilerMessage.Severity.ERROR) < 0) {
            WorkspaceManager.CompilationOptions options = new WorkspaceManager.CompilationOptions();
            options.setForceCodeRegeneration(forceCodeRegen);
            workspaceManager.compile(ml, dirtyOnly, null, options);
        }
       
        long compileTime = System.currentTimeMillis() - startCompile;
       
        boolean compilationSucceeded;
        if (ml.getMaxSeverity().compareTo(CompilerMessage.Severity.ERROR) >= 0) {
            // Errors
            jfitLogger.severe("Compilation unsuccessful because of errors:");
            compilationSucceeded = false;
        } else {
            // Compilation successful
            jfitLogger.fine("Compilation successful");
            compilationSucceeded = true;
        }
       
        // Write out compiler messages
        java.util.List<CompilerMessage> errs = ml.getCompilerMessages();
        int errsSize = errs.size();
        for (int i = 0; i < errsSize; i++) {
            CompilerMessage err = errs.get(i);
            jfitLogger.info("  " + err.toString());
        }
       
        jfitLogger.fine("CAL: Finished compiling in " + compileTime + "ms");
       
        this.calWorkspace = workspaceManager.getWorkspace();
       
        return compilationSucceeded;
    }
   
    /**
     * Analyzes a Jar file to calculate the classes required to import desired Java functions and type from the classes in the jar.
     * @author Edward Lam
     */
    private static class JarClassAnalyzer {
       
        /** The classloader with access to the classpath and the .jar file. */
        private final ClassLoader classLoader;
       
        private final File[] jars;
        private final File[] directoryRoots;
       
        /**
         * Constructor for a JarClassAnalyzer.
         * Not intended to be called by other classes.  To instantiate, call getAnalyzer().
         * @param classLoader the classLoader with access to the classpath and the .jar file.
         */
        private JarClassAnalyzer(ClassLoader classLoader, File[] jars, File[] directoryRoots) {
            this.classLoader = classLoader;
            this.jars = jars;
            this.directoryRoots = directoryRoots;
        }
       
        /**
         * Factory method for this class.
         *
         * @param inputFiles the jar files to analyze.
         * @param inputDirectories the directory roots to analyze.
         * @param classPath additional class path entries.
         * @param logger the logger to which to log any messages.
         * @return an instance of this class.
         */
        public static JarClassAnalyzer getAnalyzer(File[] inputFiles, File[] inputDirectories, String[] classPath, Logger logger) {
           
            // If there are no analysis roots, add the current folder as the default
            int nAnalysisRoots = inputFiles.length + inputDirectories.length;
            if (nAnalysisRoots == 0) {
                File currentDir = new File(".");
                inputFiles = new File[] {currentDir};
                logger.fine("Implicit input dir: " + currentDir);
                nAnalysisRoots++;
            }

            final URL[] classPathURLs = new URL[classPath.length + nAnalysisRoots];
            for (int i = 0; i < classPath.length; i++) {
                File classPathFile = new File(classPath[i]);
                try {
                    if (classPathFile.isDirectory()) {
                        classPathURLs[i] = classPathFile.toURL();
                    } else {
                        classPathURLs[i] = new URL("jar", "", "file:/" + classPath[i].replace('\\', '/') + "!/");
                    }

                } catch (MalformedURLException e) {
                    logger.log(Level.SEVERE, "Invalid path entry: " + classPath[i], e);
                    return null;
                }
            }
           
            for (int i = 0; i < inputFiles.length; i++) {
                File inputFile = inputFiles[i];
                try {
                    if (FileSystemHelper.fileExists(inputFile)) {
                        // It's a file.
                        // TODOEL: Could we use File.toURL() here?
                        classPathURLs[classPath.length + i] = new URL("jar", "", "file:/" + inputFile.getPath().replace('\\', '/') + "!/");
                       
                    } else {
                        logger.severe("Invalid path entry: " + inputFile.getPath());
                        return null;
                    }
                   
                } catch (MalformedURLException e) {
                    logger.log(Level.SEVERE, "Invalid path entry: " + inputFile.getPath(), e);
                    return null;
                }
            }
            for (int i = 0; i < inputDirectories.length; i++) {
                File inputDirectory = inputDirectories[i];
               
                // Convert to a URI.
                URI uri;
                {
                    // Slashify.  See File.slashify() and File.toURI().
                    String slashifiedPath = inputDirectory.getAbsolutePath();
                    if (File.separatorChar != '/') {
                        slashifiedPath = slashifiedPath.replace(File.separatorChar, '/');
                    }
                    if (!slashifiedPath.startsWith("/")) {
                        slashifiedPath = "/" + slashifiedPath;
                    }
                    if (!slashifiedPath.endsWith("/")) {            // directories must end with a slash.
                        slashifiedPath = slashifiedPath + "/";
                    }
                    if (slashifiedPath.startsWith("//")) {
                        slashifiedPath = "//" + slashifiedPath;
                    }
                   
                    // Create the uri.
                    try {
                        uri = new URI("file", null, slashifiedPath, null);
                    } catch (URISyntaxException e) {
                        // The comment to File.toURI() says this can't happen.
                        logger.log(Level.SEVERE, "Error converting directory to uri.", e);
                        return null;
                    }
                }
               
                try {
                    if (inputDirectory.isDirectory()) {
                        // It's a folder.
                        classPathURLs[classPath.length + inputFiles.length + i] = uri.toURL();
                       
                    } else {
                        logger.severe("Invalid path entry: " + inputDirectory.getPath());
                        return null;
                    }
                   
                } catch (MalformedURLException e) {
                    logger.log(Level.SEVERE, "Invalid path entry: " + inputDirectory.getPath(), e);
                    return null;
                }
            }
           
            // Need a privileged block to create the class loader
            URLClassLoader ucl =
                AccessController.doPrivileged(new PrivilegedAction<URLClassLoader>() {
                    public URLClassLoader run() {
                        return new URLClassLoader(classPathURLs);
                    }
                });
            return new JarClassAnalyzer(ucl, inputFiles, inputDirectories);
        }
       
        /**
         * Get a class by name, using this analyzer's class path.
         * @param className the fully-qualified name of the class to retrieve.
         * @return the corresponding class.
         * @throws MissingClassException if the class does not exist.
         */
        Class<?> getClass(String className) throws MissingClassException {
            try {
                return classLoader.loadClass(className);
           
            } catch (ClassNotFoundException e) {
                // Wrap the exception.
                MissingClassException throwMe = new MissingClassException(className, e);
                throw throwMe;
           
            } catch (NoClassDefFoundError e) {
                // Wrap the exception.
                MissingClassException throwMe = new MissingClassException(className, e);
                throw throwMe;
            }
        }
       
        /**
         * Calculate the classes required to define all constructors, methods, and fields in the named classes.
         * @param desiredClassNames (Set of String) the fully-qualified names of the desired classes.
         * @return (Set of Class) the classes required.  Never null.
         * @throws MissingClassException
         */
        Set<Class<?>> calculateRequiredClasses(Set<String> desiredClassNames, Logger logger) throws MissingClassException {
           
            Set<Class<?>> requiredClassesSet = new HashSet<Class<?>>();

            for (final String matchingClassName : desiredClassNames) {
                Class<?> matchingClass = null;
                try {
                    matchingClass = getClass(matchingClassName);
               
                } catch (MissingClassException e) {
                    // Log and rethrow.
                    logger.throwing(getClass().getName(), "calculateRequiredClasses", e);
                    throw e;
                }
               
                // These calls currently require referenced classes to be loaded.
                // This would cause a NoClassDefFoundError if the class is not on the classpath.
                // However, we can do something smarter if we don't have to load classes in order to analyze them.
                //   eg. Use asm instead of the ClassLoader, to inspect the bytecodes.
                // For instance, if we decide not to create a foreign function for a method, it won't be
                //   necessary to load the classes it refers to.
                try {
                    // Classes required by this particular matching class.
                    // We add this at the end, since in the meantime if we run across a NoClassDefFoundError, we will be
                    //   skipping adding any classes required by this class.
                    Set<Class<?>> newRequiredClassesSet = new HashSet<Class<?>>();
                   
                    newRequiredClassesSet.add(matchingClass);
                   
                    Constructor<?>[] constructors = matchingClass.getConstructors();
                    for (final Constructor<?> constructor : constructors) {
                        Class<?>[] parameterTypes = constructor.getParameterTypes();
                        newRequiredClassesSet.addAll(Arrays.asList(parameterTypes));
                       
                        // Don't add exceptions for now.
                    }
                   
                    Field[] fields = matchingClass.getFields();
                    for (final Field field : fields) {
                        // If necessary, we can add fields declared only in this class (ie. not in a superclass)
                        //  by checking whether fields[i].getDeclaringClass() == matchingClass.
                       
                        newRequiredClassesSet.add(field.getType());
                    }
                   
                    Method[] methods = matchingClass.getMethods();
                    for (final Method method : methods) {
                        newRequiredClassesSet.addAll(Arrays.asList(method.getParameterTypes()));
                        newRequiredClassesSet.add(method.getReturnType());
                       
                        // Don't add exceptions for now.
                    }
                   
                    requiredClassesSet.addAll(newRequiredClassesSet);
                   
                } catch (NoClassDefFoundError e) {
                    MissingClassException throwMe = new MissingClassException(matchingClassName, e);
                    logger.throwing(getClass().getName(), "calculateRequiredClasses", throwMe);
                    throw throwMe;
                } catch (LinkageError e) {
                    MissingClassException throwMe = new MissingClassException(matchingClassName, e);
                    logger.throwing(getClass().getName(), "calculateRequiredClasses", throwMe);
                    throw throwMe;
                }
                /*
                 Class[] interfaces = matchingClass.getInterfaces();
                 Package classPackage = matchingClass.getPackage();
                 Class superClass = matchingClass.getSuperclass();
                 */
            }
           
            // Log all required classes.
            logger.fine("Required classes:");
           
            if (requiredClassesSet.isEmpty()) {
                logger.fine("  (none)");
               
            } else {
                SortedSet<String> requiredClassNameSet = new TreeSet<String>();
                for (final Class<?> requiredClass : requiredClassesSet) {
                    requiredClassNameSet.add(requiredClass.getName());
                }
                for (final String requiredClassName : requiredClassNameSet) {
                    logger.fine("  " + requiredClassName);
                }      
            }
           
            return requiredClassesSet;
        }
       
        /**
         * Get the class names in a jar matching a given pattern.
         * @param patterns the patterns against which to match.
         * @param logger the logger to which to log any error messages.
         * @return (Set of String) fully-qualified matching class names.
         */
        Set<String> getMatchingClassNames(JFit.Pattern[] patterns, Logger logger) {
           
            Set<String> matchingClassNames = new HashSet<String>();
           
            // Split into includes and excludes.
            List<String> includePatternList = new ArrayList<String>();
            List<String> excludePatternList = new ArrayList<String>();
            for (final Pattern pattern : patterns) {
                if (pattern.isInclude()) {
                    // an include pattern
                    String patternString = pattern.getPattern();
                    includePatternList.add(patternString);
                    logger.fine("Include pattern: " + patternString);
                   
                } else {
                    // an exclude pattern
                    String patternString = pattern.getPattern();
                    excludePatternList.add(patternString);
                    logger.fine("Exclude pattern: " + patternString);
                }
            }
           
            // If there are no include patterns, add a wildcard "*" pattern.
            if (includePatternList.isEmpty()) {
                String patternString = JFit.Pattern.MATCH_ALL.getPattern();
                includePatternList.add(patternString);
                logger.fine("Implicit include pattern: " + patternString);
            }

            // Calculate matching classes from the jar files.
            for (final File jar : jars) {
                logger.info("Calculating matching classes from jar file " + jar.getName() + "..");
               
                // Create the jar file.
                JarFile jarFile;
                try {
                    jarFile = new JarFile(jar);
                   
                } catch (IOException e) {
                    // TODOEL: Does this get thrown if the path name is too long?
                    logger.log(Level.SEVERE, "Error reading jar file: " + jar.getPath(), e);
                    return null;
                }
               
                getMatchingClassNamesHelper(jarFile, includePatternList, excludePatternList, matchingClassNames, logger);
            }
           
            // Calculate matching classes from the directory roots.
            for (final File directoryRoot : directoryRoots) {
                logger.info("Calculating matching classes from directory \"" + directoryRoot.getName() + "\"..");

                getMatchingClassNamesHelper(directoryRoot, includePatternList, excludePatternList, matchingClassNames, null, new HashSet<File>(), logger);
            }
           
            return matchingClassNames;
        }

        /**
         * Helper method for getMatchingClassNames().
         * Get the class names starting from a directory root matching a given pattern.
         *
         * @param directoryRoot the root of the directory to analyze.
         * @param includePatternList (List of String) the include patterns
         * @param excludePatternList (List of String) the exclude patterns
         * @param matchingClassNames (Set of String) the matching class names.  This collection will be populated by this method.
         * @param parentPackagePart A string indicating the current partial package name.
         * ie. if starting from the root, we have entered a directory named "com", and further entered a subdirectory of "com" named
         *   "businessobjects", this will be "com.businessobjects.".
         * On the first call to this method, this should be null.
         * @param visitedDirSet (Set of File) the files visited so far.  This collection will be populated by this method.
         * @param logger the logger to which to log any error messages.
         */
        private static void getMatchingClassNamesHelper(File directoryRoot, List<String> includePatternList, List<String> excludePatternList, Set<String> matchingClassNames,
                String parentPackagePart, HashSet<File> visitedDirSet, Logger logger) {
           
            // Check if we've already visited this directory.
            // This guards against infinite loops in the presence of symlinks linking to an ancestor folder.
            if (!visitedDirSet.add(directoryRoot)) {
                return;
            }
           
            // Create the string representing the current package part.
            String currentPackagePart = (parentPackagePart == null) ? "" : parentPackagePart + directoryRoot.getName() + ".";
           
            // Iterate over the files and directories in the directory root.
            File[] files = directoryRoot.listFiles();
            for (final File ithFile : files) {
                if (ithFile.isDirectory()) {
                    // A directory.
                   
                    // Check whether it is possible for the classes in descendant folders to ever be included and not excluded.
                    if (!prefixMatch(currentPackagePart, includePatternList, excludePatternList)) {
                        return;
                    }
                   
                    // Recursive call.
                    getMatchingClassNamesHelper(ithFile, includePatternList, excludePatternList, matchingClassNames, currentPackagePart, visitedDirSet, logger);
               
                } else {
                    // Probably a file (unless it's a directory but the path name is too long).
                    // We'll probably be ok if we match against this file if it ends with ".class".
                    String ithFileName = ithFile.getName();
                   
                    if (ithFileName.endsWith(CLASS_EXTENSION)) {
                        String qualifiedClassName = currentPackagePart + ithFileName.substring(0, ithFileName.length() - CLASS_EXTENSION.length());

                        if (matchClassName(qualifiedClassName, includePatternList, excludePatternList, logger)) {
                            matchingClassNames.add(qualifiedClassName);
                        }
                    }
                }
            }
        }
       
        /**
         * Determine whether strings starting with a given string can possibly be matched given include and exclude patterns.
         * ie. if anything is appended to the string, it is possible for the composite string to match an
         *   include pattern and not an exclude pattern.
         *  
         * Examples:
         * If prefixString is "com.businessobjects",
         * - this method can return true if includePatternList contains "com.*" or "com.*.something" or "com.bu?i?essobjects*",
         *   unless an exclude pattern matches.  If includePatternList only contains "org.*", returns false.
         * - this method returns false if excludePatternList contains "com.*" or "com.bu?i?essobjects*", but not for "com.*.something"
         *
         * @param prefixString the prefix string
         * @param includePatternList (List of String) the include patterns
         * @param excludePatternList (List of String) the exclude patterns
         * @return if anything is appended to the string, whether it is possible for the string to match an
         *   include pattern and not an exclude pattern.
         */
        private static boolean prefixMatch(String prefixString, List<String> includePatternList, List<String> excludePatternList) {
           
            int partStringLen = prefixString.length();
           
            boolean canMatchInclude = false;
           
            // Iterate over the include patterns.
            for (final String includePattern : includePatternList) {
                String truncatedPattern;
               
                // The algorithm: truncate the pattern at partString.length or the first *, and match the truncated pattern.
                int starIndex = includePattern.indexOf('*');
               
                if (partStringLen < starIndex) {
                    // truncate at partString.length.  Add a "*" to match the rest.
                    truncatedPattern = includePattern.substring(0, partStringLen) + "*";
                   
                } else if (starIndex > -1) {
                    // truncate at starIndex
                    truncatedPattern = includePattern.substring(0, starIndex + 1);
                   
                } else {
                    // don't truncate
                    truncatedPattern = includePattern;
                }
               
                // Check for a match..
                if (WildcardPatternMatcher.match(prefixString, truncatedPattern)) {
                    canMatchInclude = true;
                    break;
                }
            }
           
            // If none of the include patterns can match, return false.
            if (!canMatchInclude) {
                return false;
            }
           
           
            boolean alwaysMatchExclude = false;
           
            // Iterate over the exclude patterns.
            for (final String excludePattern : excludePatternList) {
               
                // The algorithm:
                // If pattern does not end with *, ignore.
                // If the pattern length is longer than the partString + 1 (for the trailing *), ignore.
                // Otherwise, match against the exclude pattern
               
                if (!excludePattern.endsWith("*")) {
                    continue;
                }
               
                if (excludePattern.length() > (partStringLen + 1)) {
                    continue;
                }
               
                if (WildcardPatternMatcher.match(prefixString, excludePattern)) {
                    alwaysMatchExclude = true;
                    break;
                }
            }
           
            return !alwaysMatchExclude;
        }
       
        /**
         * Helper method for getMatchingClassNames().
         * Get the class names starting from a jar matching a given pattern.
         *
         * @param jarFile the jar file to analyze.
         * @param includePatternList (List of String) the include pattern strings.
         * @param excludePatternList (List of String) the exclude pattern strings.
         * @param matchingClassNames (Set of String) the matching class names.  This collection will be populated by this method.
         * @param logger the logger to which to log messages.
         */
        private static void getMatchingClassNamesHelper(JarFile jarFile, List<String> includePatternList, List<String> excludePatternList, Set<String> matchingClassNames, Logger logger) {
            // Iterate over the entries in the jar, looking for matches.
            for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements(); ) {
                JarEntry nextEntry = entries.nextElement();
               
                String entryName = nextEntry.getName();
                if (!entryName.endsWith(CLASS_EXTENSION)) {
                    continue;
                }
               
                // replace slashes in the entry name with dots.
                String qualifiedClassName = entryName.substring(0, entryName.length() - CLASS_EXTENSION.length()).replaceAll("[\\/]", ".");
               
                // Check for a match.
                if (matchClassName(qualifiedClassName, includePatternList, excludePatternList, logger)) {
                    matchingClassNames.add(qualifiedClassName);
                }
            }
        }

        /**
         * Calculate whether the name of a class is accepted with the given patterns.
         *
         * @param qualifiedClassName the fully-qualified class name.  eg. "java.lang.String".
         * @param includePatternList (List of String) the include pattern strings.
         * @param excludePatternList (List of String) the exclude pattern strings.
         * @param logger the logger to which to log messages.
         * @return whether the given class name is accepted given the patterns.
         *   ie. if it matches an include pattern, but not an exclude pattern
         */
        private static boolean matchClassName(String qualifiedClassName, List<String> includePatternList, List<String> excludePatternList, Logger logger) {
           
            logger.finest("Matching class: " + qualifiedClassName);
            boolean match = false;
           
            for (final String includePattern : includePatternList) {
                // Check for a match with the include pattern
                if (WildcardPatternMatcher.match(qualifiedClassName, includePattern)) {
                   
                    logger.finest("  Matched include pattern: " + includePattern);
                    match = true;
                   
                    // Check that it doesn't match an exclude pattern
                    for (final String excludePattern : excludePatternList) {
                        if (WildcardPatternMatcher.match(qualifiedClassName, excludePattern)) {
                            logger.finest("  Matched exclude pattern: " + excludePattern);
                            match = false;
                        }                       
                    }
                   
                    if (match) {
                        break;
                    }
                }
            }
           
            logger.finest(match ? "  Matched." : "  Not matched.");
           
            return match;
        }
    }
}

TOP

Related Classes of org.openquark.gems.client.jfit.JFit$Pattern

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.
UA-20639858-1', 'auto'); ga('send', 'pageview');