blob: bd71421d5728a9d96c9ba689fdee4400052414e1 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package vogar;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
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.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import vogar.commands.Mkdir;
import vogar.util.Strings;
/**
* Indexes the locations of commonly used classes to assist in constructing correct Vogar commands.
*/
public final class ClassFileIndex {
/** how many milliseconds before the cache expires and we reindex jars */
private static final long CACHE_EXPIRY = 86400000; // = one day
/** regular expressions representing things that make sense on the classpath */
private static final List<String> JAR_PATTERN_STRINGS = Arrays.asList(
"classes\\.jar"
);
/** regular expressions representing failures probably due to things missing on the classpath */
private static final List<String> FAILURE_PATTERN_STRINGS = Arrays.asList(
".*package (.*) does not exist.*",
".*import (.*);.*",
".*ClassNotFoundException: (\\S*).*",
".*NoClassDefFoundError: Could not initialize class (\\S*).*"
);
private static final List<Pattern> JAR_PATTERNS = new ArrayList<Pattern>();
static {
for (String patternString : JAR_PATTERN_STRINGS) {
JAR_PATTERNS.add(Pattern.compile(patternString));
}
}
private static final List<Pattern> FAILURE_PATTERNS = new ArrayList<Pattern>();
static {
for (String patternString : FAILURE_PATTERN_STRINGS) {
// DOTALL flag allows proper handling of multiline strings
FAILURE_PATTERNS.add(Pattern.compile(patternString, Pattern.DOTALL));
}
}
private final Log log;
private final Mkdir mkdir;
private final String DELIMITER = "\t";
private final File classFileIndexFile =
new File(System.getProperty("user.home"), ".vogar/classfileindex");
private final Map<String, Set<File>> classFileMap = new HashMap<String, Set<File>>();
private final List<File> jarSearchDirs;
public ClassFileIndex(Log log, Mkdir mkdir, List<File> jarSearchDirs) {
this.log = log;
this.mkdir = mkdir;
this.jarSearchDirs = jarSearchDirs;
}
public Set<File> suggestClasspaths(String testOutput) {
Set<File> suggestedClasspaths = new HashSet<File>();
for (Pattern pattern : FAILURE_PATTERNS) {
Matcher matcher = pattern.matcher(testOutput);
if (!matcher.matches()) {
continue;
}
for (int i = 1; i <= matcher.groupCount(); i++) {
String missingPackageOrClass = matcher.group(i);
Set<File> containingJars = classFileMap.get(missingPackageOrClass);
if (containingJars != null) {
suggestedClasspaths.addAll(containingJars);
}
}
}
return suggestedClasspaths;
}
/**
* Search through the jar search directories to find .jars to index.
*
* If this has already been done, instead just use the cached version in .vogar
*/
public void createIndex() {
if (!classFileMap.isEmpty()) {
return;
}
if (classFileIndexFile.exists()) {
long lastModified = classFileIndexFile.lastModified();
long curTime = new Date().getTime();
boolean cacheExpired = lastModified < curTime - CACHE_EXPIRY;
if (cacheExpired) {
log.verbose("class file index expired, rebuilding");
} else {
readIndexCache();
return;
}
}
log.verbose("building class file index");
// Create index
for (File jarSearchDir : jarSearchDirs) {
if (!jarSearchDir.exists()) {
log.warn("directory \"" + jarSearchDir + "\" in jar paths doesn't exist");
continue;
}
// traverse the jar directory, looking for files called ending in .jar
log.verbose("looking in " + jarSearchDir + " for .jar files");
Set<File> jarFiles = new HashSet<File>();
getJarFiles(jarFiles, jarSearchDir);
for (File file : jarFiles) {
indexJarFile(file);
}
}
// save for use on subsequent runs
writeIndexCache();
}
private void indexJarFile(File file) {
try {
JarFile jarFile = new JarFile(file);
for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements(); ) {
JarEntry jarEntry = e.nextElement();
// change paths into classes/packages, strip trailing period, and strip
// trailing .class extension
String classPath = jarEntry.getName()
.replaceAll("/", ".")
.replaceFirst("\\.$", "")
.replaceFirst("\\.class$", "");
if (classFileMap.containsKey(classPath)) {
classFileMap.get(classPath).add(file);
} else {
Set<File> classPathJars = new HashSet<File>();
classPathJars.add(file);
classFileMap.put(classPath, classPathJars);
}
}
} catch (IOException e) {
log.warn("failed to read " + file + ": " + e.getMessage());
}
}
private void getJarFiles(Set<File> jarFiles, File dir) {
List<File> files = Arrays.asList(dir.listFiles());
for (File file : files) {
if (file.isDirectory() && file.exists() && file.canRead()) {
getJarFiles(jarFiles, file);
continue;
}
for (Pattern pattern : JAR_PATTERNS) {
Matcher matcher = pattern.matcher(file.getName());
if (matcher.matches()) {
jarFiles.add(file);
}
}
}
}
private void writeIndexCache() {
log.verbose("writing index cache");
BufferedWriter indexCacheWriter;
mkdir.mkdirs(classFileIndexFile.getParentFile());
try {
indexCacheWriter = new BufferedWriter(new FileWriter(classFileIndexFile));
for (Map.Entry<String, Set<File>> entry : classFileMap.entrySet()) {
indexCacheWriter.write(entry.getKey() + DELIMITER
+ Strings.join(entry.getValue(), DELIMITER));
indexCacheWriter.newLine();
}
indexCacheWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void readIndexCache() {
log.verbose("reading class file index cache");
BufferedReader reader;
try {
reader = new BufferedReader(new FileReader(classFileIndexFile));
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
try {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
// Each line is a mapping of a class, package or file to the .jar files that
// contain its definition within VOGAR_JAR_PATH. Each component is separated
// by a delimiter.
String[] parts = line.split(DELIMITER);
if (parts.length < 2) {
throw new RuntimeException("classfileindex contains invalid line: " + line);
}
String resource = parts[0];
Set<File> jarFiles = new HashSet<File>();
for (int i = 1; i < parts.length; i++) {
jarFiles.add(new File(parts[i]));
}
classFileMap.put(resource, jarFiles);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}