/*
* Bytecode Analysis Framework
* Copyright (C) 2003,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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import javax.annotation.WillClose;
import edu.umd.cs.findbugs.Project;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.io.IO;
import edu.umd.cs.findbugs.util.Util;
/**
* Class to open input streams on source files. It maintains a "source path",
* which is like a classpath, but for finding source files instead of class
* files.
*/
public class SourceFinder {
private static final boolean DEBUG = SystemProperties.getBoolean("srcfinder.debug");
private static final int CACHE_SIZE = 50;
/*
* ----------------------------------------------------------------------
* Helper classes
* ----------------------------------------------------------------------
*/
/**
* Cache of SourceFiles. We use this to avoid repeatedly having to read
* frequently accessed source files.
*/
private static class Cache extends LinkedHashMap<String, SourceFile> {
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
protected boolean removeEldestEntry(Map.Entry<String, SourceFile> eldest) {
return size() >= CACHE_SIZE;
}
}
/**
* A repository of source files.
*/
private interface SourceRepository {
public boolean contains(String fileName);
public boolean isPlatformDependent();
public SourceFileDataSource getDataSource(String fileName);
}
/**
* A directory containing source files.
*/
private static class DirectorySourceRepository implements SourceRepository {
private String baseDir;
public DirectorySourceRepository(String baseDir) {
this.baseDir = baseDir;
}
@Override
public String toString() {
return "DirectorySourceRepository:" + baseDir;
}
public boolean contains(String fileName) {
File file = new File(getFullFileName(fileName));
boolean exists = file.exists();
if (DEBUG)
System.out.println("Exists " + exists + " for " + file);
return exists;
}
public boolean isPlatformDependent() {
return true;
}
public SourceFileDataSource getDataSource(String fileName) {
return new FileSourceFileDataSource(getFullFileName(fileName));
}
private String getFullFileName(String fileName) {
return baseDir + File.separator + fileName;
}
}
private static class InMemorySourceRepository implements SourceRepository {
Map<String, byte[]> contents = new HashMap<String, byte[]>();
Map<String, Long> lastModified = new HashMap<String, Long>();
InMemorySourceRepository(@WillClose ZipInputStream in) throws IOException {
try {
while (true) {
ZipEntry e = in.getNextEntry();
if (e == null)
break;
if (!e.isDirectory()) {
String name = e.getName();
long size = e.getSize();
if (size > Integer.MAX_VALUE)
throw new IOException(name + " is too big at " + size + " bytes");
ByteArrayOutputStream out;
if (size <= 0)
out = new ByteArrayOutputStream();
else
out = new ByteArrayOutputStream((int) size);
GZIPOutputStream gOut = new GZIPOutputStream(out);
IO.copy(in, gOut);
gOut.close();
byte data[] = out.toByteArray();
contents.put(name, data);
lastModified.put(name, e.getTime());
}
in.closeEntry();
}
} finally {
Util.closeSilently(in);
}
}
/*
* (non-Javadoc)
*
* @see
* edu.umd.cs.findbugs.ba.SourceFinder.SourceRepository#contains(java
* .lang.String)
*/
public boolean contains(String fileName) {
return contents.containsKey(fileName);
}
/*
* (non-Javadoc)
*
* @see
* edu.umd.cs.findbugs.ba.SourceFinder.SourceRepository#getDataSource
* (java.lang.String)
*/
public SourceFileDataSource getDataSource(final String fileName) {
return new SourceFileDataSource() {
public String getFullFileName() {
return fileName;
}
public InputStream open() throws IOException {
return new GZIPInputStream(new ByteArrayInputStream(contents.get(fileName)));
}
public long getLastModified() {
Long when = lastModified.get(fileName);
if (when == null || when < 0)
return 0;
return when;
}
};
}
/*
* (non-Javadoc)
*
* @see
* edu.umd.cs.findbugs.ba.SourceFinder.SourceRepository#isPlatformDependent
* ()
*/
public boolean isPlatformDependent() {
return false;
}
}
SourceRepository makeInMemorySourceRepository(final String url) {
final BlockingSourceRepository r = new BlockingSourceRepository();
Thread t = new Thread(new Runnable() {
public void run() {
InputStream in = null;
try {
URLConnection connection = new URL(url).openConnection();
in = connection.getInputStream();
if (getProject().isGuiAvaliable())
in = getProject().getGuiCallback().getProgressMonitorInputStream(in, connection.getContentLength(),
"Downloading project source code...");
if (url.endsWith(".z0p.gz"))
in = new GZIPInputStream(in);
r.setBase(new InMemorySourceRepository(new ZipInputStream(in)));
} catch (IOException e) {
if (getProject().isGuiAvaliable()) {
getProject().getGuiCallback().setErrorMessage("Unable to load " + url + "; " + e.getMessage());
}
AnalysisContext.logError("Unable to load " + url, e);
Util.closeSilently(in);
}
}
}, "Source loading thread");
t.setDaemon(true);
t.start();
return r;
}
SourceRepository makeJarURLConnectionSourceRepository(final String url) throws MalformedURLException, IOException {
final File file = File.createTempFile("jar_cache", null);
file.deleteOnExit();
final BlockingSourceRepository r = new BlockingSourceRepository();
Thread t = new Thread(new Runnable() {
public void run() {
InputStream in = null;
OutputStream out = null;
try {
URLConnection connection = new URL(url).openConnection();
if (getProject().isGuiAvaliable()) {
int size = connection.getContentLength();
in = getProject().getGuiCallback().getProgressMonitorInputStream(connection.getInputStream(), size,
"Loading source via url");
} else {
in = connection.getInputStream();
}
out = new FileOutputStream(file);
IO.copy(in, out);
r.setBase(new ZipSourceRepository(new ZipFile(file)));
} catch (IOException e) {
assert true;
} finally {
Util.closeSilently(in);
Util.closeSilently(out);
}
}
}, "Source loading thread");
t.setDaemon(true);
t.start();
return r;
}
static class BlockingSourceRepository implements SourceRepository {
SourceRepository base;
final CountDownLatch ready = new CountDownLatch(1);
public BlockingSourceRepository() {
}
public boolean isReady() {
return ready.getCount() == 0;
}
public void setBase(SourceRepository base) {
this.base = base;
ready.countDown();
}
private void await() {
try {
ready.await();
} catch (InterruptedException e) {
throw new IllegalStateException("Unexpected interrupt", e);
}
}
public boolean contains(String fileName) {
await();
return base.contains(fileName);
}
public SourceFileDataSource getDataSource(String fileName) {
await();
return base.getDataSource(fileName);
}
public boolean isPlatformDependent() {
await();
return base.isPlatformDependent();
}
}
/**
* A zip or jar archive containing source files.
*/
static class ZipSourceRepository implements SourceRepository {
ZipFile zipFile;
public ZipSourceRepository(ZipFile zipFile) {
this.zipFile = zipFile;
}
public boolean contains(String fileName) {
return zipFile.getEntry(fileName) != null;
}
public boolean isPlatformDependent() {
return false;
}
public SourceFileDataSource getDataSource(String fileName) {
return new ZipSourceFileDataSource(zipFile, fileName);
}
}
/*
* ----------------------------------------------------------------------
* Fields
* ----------------------------------------------------------------------
*/
private List<SourceRepository> repositoryList;
private Cache cache;
private Project project;
/*
* ----------------------------------------------------------------------
* Public methods
* ----------------------------------------------------------------------
*/
public SourceFinder(Project project) {
setProject(project);
}
/**
* @return Returns the project.
*/
public Project getProject() {
return project;
}
/**
* Set the list of source directories.
*/
void setSourceBaseList(Iterable<String> sourceBaseList) {
for (String repos : sourceBaseList) {
if (repos.endsWith(".zip") || repos.endsWith(".jar") || repos.endsWith(".z0p.gz")) {
// Zip or jar archive
try {
if (repos.startsWith("http:") || repos.startsWith("https:") || repos.startsWith("file:")) {
String url = SystemProperties.rewriteURLAccordingToProperties(repos);
repositoryList.add(makeInMemorySourceRepository(url));
} else
repositoryList.add(new ZipSourceRepository(new ZipFile(repos)));
} catch (IOException e) {
// Ignored - we won't use this archive
AnalysisContext.logError("Unable to load " + repos, e);
}
} else {
File dir = new File(repos);
if (dir.canRead() && dir.isDirectory())
repositoryList.add(new DirectorySourceRepository(repos));
else {
AnalysisContext.logError("Unable to load " + repos);
}
}
}
}
/**
* Open an input stream on a source file in given package.
*
* @param packageName
* the name of the package containing the class whose source file
* is given
* @param fileName
* the unqualified name of the source file
* @return an InputStream on the source file
* @throws IOException
* if a matching source file cannot be found
*/
public InputStream openSource(String packageName, String fileName) throws IOException {
SourceFile sourceFile = findSourceFile(packageName, fileName);
return sourceFile.getInputStream();
}
public InputStream openSource(SourceLineAnnotation source) throws IOException {
SourceFile sourceFile = findSourceFile(source);
return sourceFile.getInputStream();
}
public SourceFile findSourceFile(SourceLineAnnotation source) throws IOException {
return findSourceFile(source.getPackageName(), getOrGuessSourceFile(source));
}
/**
* Open a source file in given package.
*
* @param packageName
* the name of the package containing the class whose source file
* is given
* @param fileName
* the unqualified name of the source file
* @return the source file
* @throws IOException
* if a matching source file cannot be found
*/
public SourceFile findSourceFile(String packageName, String fileName) throws IOException {
// On windows the fileName specification is different between a file in
// a directory tree, and a
// file in a zip file. In a directory tree the separator used is '\',
// while in a zip it's '/'
// Therefore for each repository figure out what kind it is and use the
// appropriate separator.
// In all practicality, this code could just use the hardcoded '/' char,
// as windows can open
// files with this separator, but to allow for the mythical 'other'
// platform that uses an
// alternate separator, make a distinction
String platformName = getPlatformName(packageName, fileName);
String canonicalName = getCanonicalName(packageName, fileName);
// Is the file in the cache already? Always cache it with the canonical
// name
SourceFile sourceFile = cache.get(canonicalName);
if (sourceFile != null)
return sourceFile;
// Find this source file, add its data to the cache
if (DEBUG)
System.out.println("Trying " + fileName + " in package " + packageName + "...");
// Query each element of the source path to find the requested source
// file
for (SourceRepository repos : repositoryList) {
if (repos instanceof BlockingSourceRepository && !((BlockingSourceRepository) repos).isReady())
continue;
fileName = repos.isPlatformDependent() ? platformName : canonicalName;
if (DEBUG)
System.out.println("Looking in " + repos + " for " + fileName);
if (repos.contains(fileName)) {
// Found it
sourceFile = new SourceFile(repos.getDataSource(fileName));
cache.put(canonicalName, sourceFile); // always cache with
// canonicalName
return sourceFile;
}
}
throw new FileNotFoundException("Can't find source file " + fileName);
}
/**
* @param packageName
* @param fileName
* @return
*/
public static String getPlatformName(String packageName, String fileName) {
String platformName = packageName.replace('.', File.separatorChar) + (packageName.length() > 0 ? File.separator : "")
+ fileName;
return platformName;
}
public static String getPlatformName(SourceLineAnnotation source) {
return getPlatformName(source.getPackageName(), getOrGuessSourceFile(source));
}
public static String getCanonicalName(SourceLineAnnotation source) {
return getCanonicalName(source.getPackageName(), getOrGuessSourceFile(source));
}
/**
* @param packageName
* @param fileName
* @return
*/
public static String getCanonicalName(String packageName, String fileName) {
String canonicalName = packageName.replace('.', '/') + (packageName.length() > 0 ? "/" : "") + fileName;
return canonicalName;
}
public static String getOrGuessSourceFile(SourceLineAnnotation source) {
if (source.isSourceFileKnown())
return source.getSourceFile();
String baseClassName = source.getClassName();
int i = baseClassName.lastIndexOf('.');
baseClassName = baseClassName.substring(i + 1);
int j = baseClassName.indexOf("$");
if (j >= 0)
baseClassName = baseClassName.substring(0, j);
return baseClassName + ".java";
}
public boolean hasSourceFile(SourceLineAnnotation source) {
return hasSourceFile(source.getPackageName(), getOrGuessSourceFile(source));
}
public boolean hasSourceFile(String packageName, String fileName) {
// On windows the fileName specification is different between a file in
// a directory tree, and a
// file in a zip file. In a directory tree the separator used is '\',
// while in a zip it's '/'
// Therefore for each repository figure out what kind it is and use the
// appropriate separator.
// In all practicality, this code could just use the hardcoded '/' char,
// as windows can open
// files with this separator, but to allow for the mythical 'other'
// platform that uses an
// alternate separator, make a distinction
// Create a fully qualified source filename using the package name for
// both directories and zips
String platformName = getPlatformName(packageName, fileName);
String canonicalName = getCanonicalName(packageName, fileName);
// Is the file in the cache already? Always cache it with the canonical
// name
SourceFile sourceFile = cache.get(canonicalName);
if (sourceFile != null)
return true;
// Find this source file, add its data to the cache
if (DEBUG)
System.out.println("Trying " + fileName + " in package " + packageName + "...");
// Query each element of the source path to find the requested source
// file
for (SourceRepository repos : repositoryList) {
if (repos instanceof BlockingSourceRepository && !((BlockingSourceRepository) repos).isReady())
continue;
fileName = repos.isPlatformDependent() ? platformName : canonicalName;
if (DEBUG)
System.out.println("Looking in " + repos + " for " + fileName);
if (repos.contains(fileName)) {
return true;
}
}
return false;
}
/**
* @param project
*/
private void setProject(Project project) {
this.project = project;
repositoryList = new LinkedList<SourceRepository>();
cache = new Cache();
setSourceBaseList(project.getResolvedSourcePaths());
}
}
// vim:ts=4