/*******************************************************************************
* Copyright (c) 1998, 2016 Oracle and/or its affiliates. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
* which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
* Oracle - initial API and implementation from Oracle TopLink
******************************************************************************/
package org.eclipse.persistence.tools.workbench.utility;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.Serializable;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.eclipse.persistence.tools.workbench.utility.filters.Filter;
import org.eclipse.persistence.tools.workbench.utility.io.FileTools;
import org.eclipse.persistence.tools.workbench.utility.iterators.ArrayIterator;
import org.eclipse.persistence.tools.workbench.utility.iterators.CompositeIterator;
import org.eclipse.persistence.tools.workbench.utility.iterators.FilteringIterator;
import org.eclipse.persistence.tools.workbench.utility.iterators.NullIterator;
import org.eclipse.persistence.tools.workbench.utility.iterators.TransformationIterator;
/**
* TODO
*/
public class Classpath
implements Serializable
{
/** The entries in the classpath */
private final Entry[] entries;
private static final long serialVersionUID = 1L;
// ********** static methods **********
// ***** factory methods for "standard" classpaths *****
/**
* Return the Java "boot" classpath. This includes rt.jar.
*/
public static Classpath bootClasspath() {
return new Classpath(System.getProperty("sun.boot.class.path"));
}
/**
* Return a "virtual classpath" that contains all the jars
* that would be used by the Java Extension Mechanism.
*/
public static Classpath javaExtensionClasspath() {
File[] dirs = javaExtensionDirectories();
int len = dirs.length;
List jarFileNames = new ArrayList();
for (int i = 0; i < len; i++) {
File dir = dirs[i];
if (dir.isDirectory()) {
addJarFileNamesTo(dir, jarFileNames);
}
}
return new Classpath(jarFileNames);
}
/**
* Return the Java "system" classpath.
*/
public static Classpath javaClasspath() {
return new Classpath(System.getProperty("java.class.path"));
}
/**
* Return the unretouched "complete" classpath.
* This includes the boot classpath, the Java Extension
* Mechanism classpath, and the normal "system" classpath.
*/
public static Classpath completeClasspath() {
return new Classpath(new Classpath[] {
bootClasspath(),
javaExtensionClasspath(),
javaClasspath()
});
}
/**
* Return a classpath that contains the location of the specified class.
*/
public static Classpath classpathFor(Class javaClass) {
return new Classpath(locationFor(javaClass));
}
// ***** file => class *****
/**
* Convert a relative file name to a class name; this will work for
* any file that has a single extension beyond the base
* class name.
* e.g. "java/lang/String.class" is converted to "java.lang.String"
* e.g. "java/lang/String.java" is converted to "java.lang.String"
*/
public static String convertToClassName(String classFileName) {
String className = FileTools.stripExtension(classFileName);
// do this for archive entry names
className = className.replace('/', '.');
// do this for O/S-specific file names
if (File.separatorChar != '/') {
className = className.replace(File.separatorChar, '.');
}
return className;
}
/**
* Convert a file to a class name;
* e.g. File(java/lang/String.class) is converted to "java.lang.String"
*/
public static String convertToClassName(File classFile) {
return convertToClassName(classFile.getPath());
}
/**
* Convert a relative file name to a class;
* e.g. "java/lang/String.class" is converted to java.lang.String.class
*/
public static Class convertToClass(String classFileName) throws ClassNotFoundException {
return Class.forName(convertToClassName(classFileName));
}
/**
* Convert a relative file to a class;
* e.g. File(java/lang/String.class) is converted to java.lang.String.class
*/
public static Class convertToClass(File classFile) throws ClassNotFoundException {
return convertToClass(classFile.getPath());
}
// ***** class => JAR entry *****
/**
* Convert a class name to an archive entry name base;
* e.g. "java.lang.String" is converted to "java/lang/String"
*/
public static String convertToArchiveEntryNameBase(String className) {
return className.replace('.', '/');
}
/**
* Convert a class to an archive entry name base;
* e.g. java.lang.String.class is converted to "java/lang/String"
*/
public static String convertToArchiveEntryNameBase(Class javaClass) {
return convertToArchiveEntryNameBase(javaClass.getName());
}
/**
* Convert a class name to an archive class file entry name;
* e.g. "java.lang.String" is converted to "java/lang/String.class"
*/
public static String convertToArchiveClassFileEntryName(String className) {
return convertToArchiveEntryNameBase(className) + ".class";
}
/**
* Convert a class to an archive class file entry name;
* e.g. java.lang.String.class is converted to "java/lang/String.class"
*/
public static String convertToArchiveClassFileEntryName(Class javaClass) {
return convertToArchiveClassFileEntryName(javaClass.getName());
}
// ***** class => file (.class or .java) *****
/**
* Convert a class name to a file name base for the current O/S;
* e.g. "java.lang.String" is converted to "java/lang/String" on Unix
* and "java\\lang\\String" on Windows
*/
public static String convertToFileNameBase(String className) {
return className.replace('.', File.separatorChar);
}
/**
* Convert a class to a file name base for the current O/S;
* e.g. java.lang.String.class is converted to "java/lang/String" on Unix
* and "java\\lang\\String" on Windows
*/
public static String convertToFileNameBase(Class javaClass) {
return convertToFileNameBase(javaClass.getName());
}
/**
* Convert a class name to a class file name for the current O/S;
* e.g. "java.lang.String" is converted to "java/lang/String.class" on Unix
* and "java\\lang\\String.class" on Windows
*/
public static String convertToClassFileName(String className) {
return convertToFileNameBase(className) + ".class";
}
/**
* Convert a class to a class file name for the current O/S;
* e.g. java.lang.String.class is converted to "java/lang/String.class" on Unix
* and "java\\lang\\String.class" on Windows
*/
public static String convertToClassFileName(Class javaClass) {
return convertToClassFileName(javaClass.getName());
}
/**
* Convert a class name to a class file for the current O/S;
* e.g. "java.lang.String" is converted to File(java/lang/String.class)
*/
public static File convertToClassFile(String className) {
return new File(convertToClassFileName(className));
}
/**
* Convert a class to a class file for the current O/S;
* e.g. java.lang.String.class is converted to File(java/lang/String.class)
*/
public static File convertToClassFile(Class javaClass) {
return convertToClassFile(javaClass.getName());
}
/**
* Convert a class name to a java file name for the current O/S;
* e.g. "java.lang.String" is converted to "java/lang/String.java" on Unix
* and "java\\lang\\String.java" on Windows
*/
public static String convertToJavaFileName(String className) {
return convertToFileNameBase(className) + ".java";
}
/**
* Convert a class to a java file name for the current O/S;
* e.g. java.lang.String.class is converted to "java/lang/String.java" on Unix
* and "java\\lang\\String.java" on Windows
*/
public static String convertToJavaFileName(Class javaClass) {
return convertToJavaFileName(javaClass.getName());
}
/**
* Convert a class name to a java file for the current O/S;
* e.g. "java.lang.String" is converted to File(java/lang/String.java)
*/
public static File convertToJavaFile(String className) {
return new File(convertToJavaFileName(className));
}
/**
* Convert a class to a java file for the current O/S;
* e.g. java.lang.String.class is converted to File(java/lang/String.java)
*/
public static File convertToJavaFile(Class javaClass) {
return convertToJavaFile(javaClass.getName());
}
// ***** class => resource *****
/**
* Convert a class to a resource name;
* e.g. java.lang.String.class is converted to "/java/lang/String.class".
*/
public static String convertToResourceName(Class javaClass) {
return '/' + convertToArchiveClassFileEntryName(javaClass);
}
/**
* Convert a class to a resource;
* e.g. java.lang.String.class is converted to
* URL(jar:file:/C:/jdk/1.4.2_04/jre/lib/rt.jar!/java/lang/String.class).
*/
public static URL convertToResource(Class javaClass) {
return javaClass.getResource(convertToResourceName(javaClass));
}
// ***** utilities *****
/**
* Return whether the specified file is an archive file;
* i.e. its name ends with ".zip" or ".jar"
*/
public static boolean fileNameIsArchive(String fileName) {
String ext = FileTools.extension(fileName).toLowerCase();
return ext.equals(".jar") || ext.equals(".zip");
}
/**
* Return whether the specified file is an archive file;
* i.e. its name ends with ".zip" or ".jar"
*/
public static boolean fileIsArchive(File file) {
return fileNameIsArchive(file.getName());
}
/**
* Return what should be the fully-qualified file name
* for the JRE runtime JAR;
* e.g. "C:\jdk1.4.2_04\jre\lib\rt.jar".
*/
public static String rtJarName() {
return locationFor(java.lang.Object.class);
}
/**
* Return the location from where the specified class was loaded.
*/
public static String locationFor(Class javaClass) {
URL url = convertToResource(javaClass);
String path;
try {
path = FileTools.buildFile(url).getPath();
} catch (URISyntaxException ex) {
throw new RuntimeException(ex);
}
String protocol = url.getProtocol().toLowerCase();
if (protocol.equals("jar")) {
// if the class is in a JAR, the URL will look something like this:
// jar:file:/C:/jdk/1.4.2_04/jre/lib/rt.jar!/java/lang/String.class
return path.substring(0, path.indexOf('!'));
} else if (protocol.equals("file")) {
// if the class is in a directory, the URL will look something like this:
// file:/C:/dev/main/mwdev/class/oracle/toplink/workbench/utility/ClasspathTools.class
return path.substring(0, path.length() - convertToClassFileName(javaClass).length() - 1);
} else if ("jrt".equals(protocol)) {
// jdk9 - jrt:/java.base/java/lang/Object.class
return path.substring(0, path.indexOf('/', 5));
}
throw new IllegalStateException(url.toString());
}
/**
* Return the directories used by the Java Extension Mechanism.
*/
public static File[] javaExtensionDirectories() {
return convertToFiles(javaExtensionDirectoryNames());
}
/**
* Return the directory names used by the Java Extension Mechanism.
*/
public static String[] javaExtensionDirectoryNames() {
//jdk 9, see http://openjdk.java.net/jeps/220
String extDirs = System.getProperty("java.ext.dirs");
return extDirs != null ? extDirs.split(File.pathSeparator) : new String[0];
}
// ***** internal *****
private static File[] convertToFiles(String[] fileNames) {
File[] files = new File[fileNames.length];
for (int i = fileNames.length; i-- > 0; ) {
files[i] = new File(fileNames[i]);
}
return files;
}
private static void addJarFileNamesTo(File dir, List jarFileNames) {
File[] jarFiles = jarFilesIn(dir);
int len = jarFiles.length;
for (int i = 0; i < len; i++) {
jarFileNames.add(FileTools.canonicalFile(jarFiles[i]).getPath());
}
}
private static File[] jarFilesIn(File directory) {
return directory.listFiles(jarFileFilter());
}
private static FileFilter jarFileFilter() {
return new FileFilter() {
@Override
public boolean accept(File file) {
return FileTools.extension(file.getName()).toLowerCase().equals(".jar");
}
};
}
// ********** constructors **********
/**
* Construct a classpath with the specified entries.
*/
private Classpath(Entry[] entries) {
super();
this.entries = entries;
}
/**
* Construct a classpath with the specified entries.
*/
public Classpath(String[] fileNames) {
this(buildEntries(fileNames));
}
/**
* Skip empty file names because they will end up expanding to the current
* working directory, which is not what we want. Empty file names actually
* occur with some frequency; such as when the classpath has been built up
* dynamically with too many separators. For example:
* "C:\dev\foo.jar;;C:\dev\bar.jar"
* will be parsed into three file names:
* { "C:\dev\foo.jar", "", "C:\dev\bar.jar" }
*/
private static Entry[] buildEntries(String[] fileNames) {
int len = fileNames.length;
List entries = new ArrayList();
for (int i = 0; i < len; i++) {
String fileName = fileNames[i];
if ((fileName != null) && (fileName.length() != 0)) {
entries.add(new Entry(fileName));
}
}
return (Entry[]) entries.toArray(new Entry[entries.size()]);
}
/**
* Construct a classpath with the specified path.
*/
public Classpath(String path) {
this(path.split(File.pathSeparator));
}
/**
* Construct a classpath with the specified entries.
*/
public Classpath(List fileNames) {
this((String[]) fileNames.toArray(new String[fileNames.size()]));
}
/**
* Consolidate the specified classpaths into a single classpath.
*/
public Classpath(Classpath[] classpaths) {
this(consolidateEntries(classpaths));
}
private static Entry[] consolidateEntries(Classpath[] classpaths) {
int len = classpaths.length;
List entries = new ArrayList();
for (int i = 0; i < len; i++) {
CollectionTools.addAll(entries, classpaths[i].getEntries());
}
return (Entry[]) entries.toArray(new Entry[entries.size()]);
}
// ********** public API **********
/**
* Return the classpath's entries.
*/
public Entry[] getEntries() {
return this.entries;
}
/**
* Return the classpath's path.
*/
public String path() {
Entry[] localEntries = this.entries;
int max = localEntries.length - 1;
if (max == -1) {
return "";
}
StringBuffer sb = new StringBuffer(2000);
for (int i = 0; i < max; i++) {
sb.append(localEntries[i].fileName());
sb.append(File.pathSeparatorChar);
}
sb.append(localEntries[max].fileName());
return sb.toString();
}
/**
* Search the classpath for the specified (unqualified) file
* and return its entry. Return null if an entry is not found.
* For example, you could use this method to find the entry
* for "rt.jar" or "toplink.jar".
*/
public Entry entryForFileNamed(String shortFileName) {
Entry[] localEntries = this.entries;
int len = localEntries.length;
for (int i = 0; i < len; i++) {
if (localEntries[i].file().getName().equals(shortFileName)) {
return localEntries[i];
}
}
return null;
}
/**
* Return the first entry file in the classpath
* that contains the specified class.
* Return null if an entry is not found.
*/
public Entry entryForClassNamed(String className) {
String relativeClassFileName = convertToClassFileName(className);
String archiveEntryName = convertToArchiveClassFileEntryName(className);
Entry[] localEntries = this.entries;
int len = localEntries.length;
for (int i = 0; i < len; i++) {
if (localEntries[i].contains(relativeClassFileName, archiveEntryName)) {
return localEntries[i];
}
}
return null;
}
/**
* Return the names of all the classes discovered on the classpath,
* with duplicates removed.
*/
public String[] classNames() {
return this.classNames(Filter.NULL_INSTANCE);
}
/**
* Return the names of all the classes discovered on the classpath
* and accepted by the specified filter, with duplicates removed.
*/
public String[] classNames(Filter filter) {
Collection classNames = new HashSet(10000);
this.addClassNamesTo(classNames, filter);
return (String[]) classNames.toArray(new String[classNames.size()]);
}
/**
* Add the names of all the classes discovered on the classpath
* to the specified collection.
*/
public void addClassNamesTo(Collection classNames) {
this.addClassNamesTo(classNames, Filter.NULL_INSTANCE);
}
/**
* Add the names of all the classes discovered on the classpath
* and accepted by the specified filter to the specified collection.
*/
public void addClassNamesTo(Collection classNames, Filter filter) {
Entry[] localEntries = this.entries;
int len = localEntries.length;
for (int i = 0; i < len; i++) {
localEntries[i].addClassNamesTo(classNames, filter);
}
}
/**
* Return the names of all the classes discovered on the classpath.
* Just a bit more performant than #classNames().
*/
public Iterator classNamesStream() {
return this.classNamesStream(Filter.NULL_INSTANCE);
}
/**
* Return the names of all the classes discovered on the classpath
* that are accepted by the specified filter.
* Just a bit more performant than #classNames(Filter).
*/
public Iterator classNamesStream(Filter filter) {
return new CompositeIterator(this.entryClassNamesStreams(filter));
}
private Iterator entryClassNamesStreams(final Filter filter) {
return new TransformationIterator(new ArrayIterator(this.entries)) {
@Override
protected Object transform(Object next) {
return ((Entry) next).classNamesStream(filter);
}
};
}
/**
* Return a "compressed" version of the classpath with its
* duplicate entries eliminated.
*/
public Classpath compressed() {
return new Classpath((Entry[]) CollectionTools.removeDuplicateElements(this.entries));
}
/**
* Convert the classpath to an array of URLs
* (that can be used to instantiate a URLClassLoader).
*/
public URL[] urls() {
Entry[] localEntries = this.entries;
int len = localEntries.length;
URL[] urls = new URL[len];
for (int i = 0; i < len; i++) {
urls[i] = localEntries[i].url();
}
return urls;
}
// ********** inner class **********
/**
* TODO
*/
public static class Entry implements Serializable {
private final String fileName;
private final File file;
private final File canonicalFile;
private static final long serialVersionUID = 1L;
Entry(String fileName) {
super();
if ((fileName == null) || (fileName.length() == 0)) {
throw new IllegalArgumentException("'fileName' must be non-empty");
}
this.fileName = fileName;
this.file = new File(fileName);
this.canonicalFile = FileTools.canonicalFile(this.file);
}
public String fileName() {
return this.fileName;
}
public File file() {
return this.file;
}
public File canonicalFile() {
return this.canonicalFile;
}
public String canonicalFileName() {
return this.canonicalFile.getAbsolutePath();
}
@Override
public boolean equals(Object o) {
if ( ! (o instanceof Entry)) {
return false;
}
return ((Entry) o).canonicalFile.equals(this.canonicalFile);
}
@Override
public int hashCode() {
return this.canonicalFile.hashCode();
}
/**
* Return the entry's "canonical" URL.
*/
public URL url() {
try {
return this.canonicalFile.toURL();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
/**
* Return whether the entry contains the specified class.
*/
public boolean contains(Class javaClass) {
return this.contains(javaClass.getName());
}
/**
* Return whether the entry contains the specified class.
*/
public boolean contains(String className) {
return this.contains(convertToClassFileName(className), convertToArchiveClassFileEntryName(className));
}
/**
* Return whether the entry contains either the specified relative
* class file or the specified archive entry.
* Not the prettiest signature, but it's internal....
*/
boolean contains(String relativeClassFileName, String archiveEntryName) {
if ( ! this.canonicalFile.exists()) {
return false;
}
if (this.canonicalFile.isDirectory() && (new File(this.canonicalFile, relativeClassFileName)).exists()) {
return true;
}
return (fileIsArchive(this.canonicalFile) && this.archiveContainsEntry(archiveEntryName));
}
/**
* Return whether the entry's archive contains the specified entry.
*/
private boolean archiveContainsEntry(String zipEntryName) {
ZipFile zipFile = null;
ZipEntry zipEntry = null;
try {
zipFile = new ZipFile(this.canonicalFile);
zipEntry = zipFile.getEntry(zipEntryName);
} catch (IOException ex) {
zipEntry = null; // something is wrong, clear out the entry
} finally {
try {
if (zipFile != null) {
zipFile.close();
}
} catch (IOException ex) {
zipEntry = null; // something is wrong, clear out the entry
}
}
return zipEntry != null;
}
/**
* Return the names of all the classes discovered in the entry.
*/
public String[] classNames() {
return this.classNames(Filter.NULL_INSTANCE);
}
/**
* Return the names of all the classes discovered in the entry
* and accepted by the specified filter.
*/
public String[] classNames(Filter filter) {
Collection classNames = new ArrayList(2000);
this.addClassNamesTo(classNames, filter);
return (String[]) classNames.toArray(new String[classNames.size()]);
}
/**
* Add the names of all the classes discovered in the entry
* to the specified collection.
*/
public void addClassNamesTo(Collection classNames) {
this.addClassNamesTo(classNames, Filter.NULL_INSTANCE);
}
/**
* Add the names of all the classes discovered in the entry
* and accepted by the specified filter to the specified collection.
*/
public void addClassNamesTo(Collection classNames, Filter filter) {
if (this.canonicalFile.exists()) {
if (this.canonicalFile.isDirectory()) {
this.addClassNamesForDirectoryTo(classNames, filter);
} else if (fileIsArchive(this.canonicalFile)) {
this.addClassNamesForArchiveTo(classNames, filter);
}
}
}
/**
* Add the names of all the classes discovered
* under the entry's directory and accepted by
* the specified filter to the specified collection.
*/
private void addClassNamesForDirectoryTo(Collection classNames, Filter filter) {
int start = this.canonicalFile.getAbsolutePath().length() + 1;
for (Iterator stream = this.classFilesForDirectory(); stream.hasNext(); ) {
String className = convertToClassName(((File) stream.next()).getAbsolutePath().substring(start));
if (filter.accept(className)) {
classNames.add(className);
}
}
}
/**
* Return an iterator on all the class files discovered
* under the entry's directory.
*/
private Iterator classFilesForDirectory() {
return new FilteringIterator(FileTools.filesInTree(this.canonicalFile)) {
@Override
protected boolean accept(Object next) {
return Entry.this.fileNameMightBeForClassFile(((File) next).getName());
}
};
}
/**
* Add the names of all the classes discovered
* in the entry's archive file and accepted by the
* specified filter to the specified collection.
*/
private void addClassNamesForArchiveTo(Collection classNames, Filter filter) {
ZipFile zipFile = null;
try {
zipFile = new ZipFile(this.canonicalFile);
} catch (IOException ex) {
return;
}
for (Enumeration stream = zipFile.entries(); stream.hasMoreElements(); ) {
ZipEntry zipEntry = (ZipEntry) stream.nextElement();
String zipEntryName = zipEntry.getName();
if (this.fileNameMightBeForClassFile(zipEntryName)) {
String className = convertToClassName(zipEntryName);
if (filter.accept(className)) {
classNames.add(className);
}
}
}
try {
zipFile.close();
} catch (IOException ex) {
return;
}
}
/**
* Return whether the specified file might be a Java class file.
* The file name must at least end with ".class" and contain no spaces.
* (Neither class names nor package names may contain spaces.)
* Whether it actually is a class file will need to be determined by
* a class loader.
*/
boolean fileNameMightBeForClassFile(String name) {
return FileTools.extension(name).toLowerCase().equals(".class")
&& (name.indexOf(' ') == -1);
}
/**
* Return the names of all the classes discovered on the classpath.
* Just a bit more performant than #classNames().
*/
public Iterator classNamesStream() {
return this.classNamesStream(Filter.NULL_INSTANCE);
}
/**
* Return the names of all the classes discovered on the classpath
* that are accepted by the specified filter.
* Just a bit more performant than #classNames(Filter).
*/
public Iterator classNamesStream(Filter filter) {
if (this.canonicalFile.exists()) {
if (this.canonicalFile.isDirectory()) {
return this.classNamesForDirectory(filter);
}
if (fileIsArchive(this.canonicalFile)) {
return this.classNamesForArchive(filter);
}
}
return NullIterator.instance();
}
/**
* Return the names of all the classes discovered
* under the entry's directory and accepted by
* the specified filter.
*/
private Iterator classNamesForDirectory(Filter filter) {
return new FilteringIterator(this.classNamesForDirectory(), filter);
}
/**
* Transform the class files to class names.
*/
private Iterator classNamesForDirectory() {
final int start = this.canonicalFile.getAbsolutePath().length() + 1;
return new TransformationIterator(this.classFilesForDirectory()) {
@Override
protected Object transform(Object next) {
return convertToClassName(((File) next).getAbsolutePath().substring(start));
}
};
}
/**
* Return the names of all the classes discovered
* in the entry's archive file and accepted by the
* specified filter.
*/
private Iterator classNamesForArchive(Filter filter) {
// we can't simply wrap iterators here because we need to close the archive file...
ZipFile zipFile = null;
try {
zipFile = new ZipFile(this.canonicalFile);
} catch (IOException ex) {
return NullIterator.instance();
}
Collection classNames = new HashSet(zipFile.size());
for (Enumeration stream = zipFile.entries(); stream.hasMoreElements(); ) {
ZipEntry zipEntry = (ZipEntry) stream.nextElement();
String zipEntryName = zipEntry.getName();
if (this.fileNameMightBeForClassFile(zipEntryName)) {
String className = convertToClassName(zipEntryName);
if (filter.accept(className)) {
classNames.add(className);
}
}
}
try {
zipFile.close();
} catch (IOException ex) {
return NullIterator.instance();
}
return classNames.iterator();
}
}
}