/*
* 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;
}
}
}