/*
* FindBugs - Find bugs in Java programs
* Copyright (C) 2004, University of Maryland
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package edu.umd.cs.findbugs.ba;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URL;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import edu.umd.cs.findbugs.FindBugs;
import edu.umd.cs.findbugs.util.Archive;
/**
* A work-alike class to use instead of BCEL's ClassPath class. The main
* difference is that URLClassPath can load classfiles from URLs.
*
* @author David Hovemeyer
*/
public class URLClassPath implements Serializable {
private static final long serialVersionUID = 1L;
/**
* Interface describing a single classpath entry.
*/
private interface Entry {
/**
* Open an input stream to read a resource in the codebase described by
* this classpath entry.
*
* @param resourceName
* name of resource to load: e.g., "java/lang/Object.class"
* @return an InputStream, or null if the resource wasn't found
* @throws IOException
* if an I/O error occurs
*/
public InputStream openStream(String resourceName) throws IOException;
/**
* Get filename or URL as string.
*/
public String getURL();
/**
* Close the underlying resource.
*/
public void close();
}
/**
* Classpath entry class to load files from a zip/jar file in the local
* filesystem.
*/
private static class LocalArchiveEntry implements Entry {
private ZipFile zipFile;
public LocalArchiveEntry(String fileName) throws IOException {
try {
zipFile = new ZipFile(fileName);
} catch (IOException e) {
IOException ioe = new IOException("Could not open archive file " + fileName);
ioe.initCause(e);
throw ioe;
}
}
/*
* (non-Javadoc)
*
* @see
* edu.umd.cs.findbugs.URLClassPath.Entry#openStream(java.lang.String)
*/
public InputStream openStream(String resourceName) throws IOException {
ZipEntry zipEntry = zipFile.getEntry(resourceName);
if (zipEntry == null)
return null;
return zipFile.getInputStream(zipEntry);
}
/*
* (non-Javadoc)
*
* @see edu.umd.cs.findbugs.URLClassPath.Entry#getURL()
*/
public String getURL() {
return zipFile.getName();
}
public void close() {
try {
zipFile.close();
} catch (IOException e) {
// Ignore
}
}
}
/**
* Classpath entry class to load files from a directory in the local
* filesystem.
*/
private static class LocalDirectoryEntry implements Entry {
private String dirName;
/**
* Constructor.
*
* @param dirName
* name of the local directory
* @throws IOException
* if dirName is not a directory
*/
public LocalDirectoryEntry(String dirName) throws IOException {
this.dirName = dirName;
if (!(new File(dirName).isDirectory()))
throw new IOException(dirName + " is not a directory");
}
/*
* (non-Javadoc)
*
* @see
* edu.umd.cs.findbugs.URLClassPath.Entry#openStream(java.lang.String)
*/
public InputStream openStream(String resourceName) throws IOException {
File file = new File(dirName, resourceName);
if (!file.exists())
return null;
return new BufferedInputStream(new FileInputStream(file));
}
/*
* (non-Javadoc)
*
* @see edu.umd.cs.findbugs.URLClassPath.Entry#getURL()
*/
public String getURL() {
return dirName;
}
public void close() {
// Nothing to do here
}
}
/**
* Classpath entry class to load files from a remote archive URL. It uses
* jar URLs to specify individual files within the remote archive.
*/
private static class RemoteArchiveEntry implements Entry {
private URL remoteArchiveURL;
/**
* Constructor.
*
* @param remoteArchiveURL
* the remote zip/jar file URL
*/
public RemoteArchiveEntry(URL remoteArchiveURL) {
this.remoteArchiveURL = remoteArchiveURL;
}
/*
* (non-Javadoc)
*
* @see
* edu.umd.cs.findbugs.URLClassPath.Entry#openStream(java.lang.String)
*/
public InputStream openStream(String resourceName) throws IOException {
URL remoteFileURL = new URL("jar:" + remoteArchiveURL.toString() + "/" + resourceName);
try {
return remoteFileURL.openStream();
} catch (IOException e) {
return null;
}
}
/*
* (non-Javadoc)
*
* @see edu.umd.cs.findbugs.URLClassPath.Entry#getURL()
*/
public String getURL() {
return remoteArchiveURL.toString();
}
public void close() {
// Nothing to do
}
}
/**
* Classpath entry class to load files from a remote directory URL.
*/
private static class RemoteDirectoryEntry implements Entry {
private URL remoteDirURL;
/**
* Constructor.
*
* @param remoteDirURL
* URL of the remote directory; must end in "/"
*/
public RemoteDirectoryEntry(URL remoteDirURL) {
this.remoteDirURL = remoteDirURL;
}
/*
* (non-Javadoc)
*
* @see
* edu.umd.cs.findbugs.URLClassPath.Entry#openStream(java.lang.String)
*/
public InputStream openStream(String resourceName) throws IOException {
URL remoteFileURL = new URL(remoteDirURL.toString() + resourceName);
try {
return remoteFileURL.openStream();
} catch (IOException e) {
return null;
}
}
/*
* (non-Javadoc)
*
* @see edu.umd.cs.findbugs.URLClassPath.Entry#getURL()
*/
public String getURL() {
return remoteDirURL.toString();
}
public void close() {
// Nothing to do
}
}
// Fields
private List<Entry> entryList;
/**
* Constructor. Creates a classpath with no elements.
*/
public URLClassPath() {
this.entryList = new LinkedList<Entry>();
}
/**
* Add given filename/URL to the classpath. If no URL protocol is given, the
* filename is assumed to be a local file or directory. Remote directories
* must be specified with a "/" character at the end of the URL.
*
* @param fileName
* filename or URL of codebase (directory or archive file)
* @throws IOException
* if entry is invalid or does not exist
*/
public void addURL(String fileName) throws IOException {
String protocol = URLClassPath.getURLProtocol(fileName);
if (protocol == null) {
fileName = "file:" + fileName;
protocol = "file";
}
String fileExtension = URLClassPath.getFileExtension(fileName);
boolean isArchive = fileExtension != null && URLClassPath.isArchiveExtension(fileExtension);
Entry entry;
if (protocol.equals("file")) {
String localFileName = fileName.substring("file:".length());
if (fileName.endsWith("/") || new File(localFileName).isDirectory())
entry = new LocalDirectoryEntry(localFileName);
else if (isArchive)
entry = new LocalArchiveEntry(localFileName);
else
throw new IOException("Classpath entry " + fileName + " is not a directory or archive file");
} else {
if (fileName.endsWith("/"))
entry = new RemoteDirectoryEntry(new URL(fileName));
else if (isArchive)
entry = new RemoteArchiveEntry(new URL(fileName));
else
throw new IOException("Classpath entry " + fileName + " is not a remote directory or archive file");
}
entryList.add(entry);
}
/**
* Return the classpath string.
*
* @return the classpath string
*/
public String getClassPath() {
StringBuilder buf = new StringBuilder();
for (Entry entry : entryList) {
if (buf.length() > 0)
buf.append(File.pathSeparator);
buf.append(entry.getURL());
}
return buf.toString();
}
/**
* Open a stream to read given resource.
*
* @param resourceName
* name of resource to load, e.g. "java/lang/Object.class"
* @return input stream to read resource, or null if resource could not be
* found
* @throws IOException
* if an IO error occurs trying to determine whether or not the
* resource exists
*/
private InputStream getInputStreamForResource(String resourceName) {
// Try each classpath entry, in order, until we find one
// that has the resource. Catch and ignore IOExceptions.
// FIXME: The following code should throw IOException.
//
// URL.openStream() does not seem to distinguish
// whether the resource does not exist, vs. some
// transient error occurring while trying to access it.
// This is unfortunate, because we really should throw
// an exception out of this method in the latter case,
// since it means our knowledge of the classpath is
// incomplete.
//
// Short of reimplementing HTTP, etc., ourselves,
// there is probably nothing we can do about this problem.
for (Entry entry : entryList) {
InputStream in;
try {
in = entry.openStream(resourceName);
if (in != null) {
if (URLClassPathRepository.DEBUG) {
System.out.println("\t==> found " + resourceName + " in " + entry.getURL());
}
return in;
}
} catch (IOException ignore) {
// Ignore
}
}
if (URLClassPathRepository.DEBUG) {
System.out.println("\t==> could not find " + resourceName + " on classpath");
}
return null;
}
private Set<String> classesThatCantBeFound = new HashSet<String>();
/**
* Look up a class from the classpath.
*
* @param className
* name of class to look up
* @return the JavaClass object for the class
* @throws ClassNotFoundException
* if the class couldn't be found
*/
public JavaClass lookupClass(String className) throws ClassNotFoundException {
if (classesThatCantBeFound.contains(className)) {
throw new ClassNotFoundException("Error while looking for class " + className + ": class not found");
}
String resourceName = className.replace('.', '/') + ".class";
InputStream in = null;
boolean parsedClass = false;
try {
in = getInputStreamForResource(resourceName);
if (in == null) {
classesThatCantBeFound.add(className);
throw new ClassNotFoundException("Error while looking for class " + className + ": class not found");
}
ClassParser classParser = new ClassParser(in, resourceName);
JavaClass javaClass = classParser.parse();
parsedClass = true;
return javaClass;
} catch (IOException e) {
classesThatCantBeFound.add(className);
throw new ClassNotFoundException("IOException while looking for class " + className, e);
} finally {
if (in != null && !parsedClass) {
try {
in.close();
} catch (IOException ignore) {
// Ignore
}
}
}
}
/**
* Close all underlying resources.
*/
public void close() {
for (Entry entry : entryList) {
entry.close();
}
entryList.clear();
}
/**
* Get the URL protocol of given URL string.
*
* @param urlString
* the URL string
* @return the protocol name ("http", "file", etc.), or null if there is no
* protocol
*/
public static String getURLProtocol(String urlString) {
String protocol = null;
int firstColon = urlString.indexOf(':');
if (firstColon >= 0) {
String specifiedProtocol = urlString.substring(0, firstColon);
if (FindBugs.knownURLProtocolSet.contains(specifiedProtocol))
protocol = specifiedProtocol;
}
return protocol;
}
/**
* Get the file extension of given fileName.
*
* @return the file extension, or null if there is no file extension
*/
public static String getFileExtension(String fileName) {
int lastDot = fileName.lastIndexOf('.');
return (lastDot >= 0) ? fileName.substring(lastDot) : null;
}
/**
* Determine if given file extension indicates an archive file.
*
* @param fileExtension
* the file extension (e.g., ".jar")
* @return true if the file extension indicates an archive, false otherwise
*/
public static boolean isArchiveExtension(String fileExtension) {
return Archive.ARCHIVE_EXTENSION_SET.contains(fileExtension);
}
}
// vim:ts=4