1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package vogar; 18 19import java.io.BufferedReader; 20import java.io.BufferedWriter; 21import java.io.File; 22import java.io.FileNotFoundException; 23import java.io.FileReader; 24import java.io.FileWriter; 25import java.io.IOException; 26import java.util.ArrayList; 27import java.util.Arrays; 28import java.util.Date; 29import java.util.Enumeration; 30import java.util.HashMap; 31import java.util.HashSet; 32import java.util.List; 33import java.util.Map; 34import java.util.Set; 35import java.util.jar.JarEntry; 36import java.util.jar.JarFile; 37import java.util.regex.Matcher; 38import java.util.regex.Pattern; 39import vogar.commands.Mkdir; 40import vogar.util.Strings; 41 42/** 43 * Indexes the locations of commonly used classes to assist in constructing correct Vogar commands. 44 */ 45public final class ClassFileIndex { 46 47 /** how many milliseconds before the cache expires and we reindex jars */ 48 private static final long CACHE_EXPIRY = 86400000; // = one day 49 50 /** regular expressions representing things that make sense on the classpath */ 51 private static final List<String> JAR_PATTERN_STRINGS = Arrays.asList( 52 "classes\\.jar" 53 ); 54 /** regular expressions representing failures probably due to things missing on the classpath */ 55 private static final List<String> FAILURE_PATTERN_STRINGS = Arrays.asList( 56 ".*package (.*) does not exist.*", 57 ".*import (.*);.*", 58 ".*ClassNotFoundException: (\\S*).*", 59 ".*NoClassDefFoundError: Could not initialize class (\\S*).*" 60 ); 61 private static final List<Pattern> JAR_PATTERNS = new ArrayList<Pattern>(); 62 static { 63 for (String patternString : JAR_PATTERN_STRINGS) { 64 JAR_PATTERNS.add(Pattern.compile(patternString)); 65 } 66 } 67 private static final List<Pattern> FAILURE_PATTERNS = new ArrayList<Pattern>(); 68 static { 69 for (String patternString : FAILURE_PATTERN_STRINGS) { 70 // DOTALL flag allows proper handling of multiline strings 71 FAILURE_PATTERNS.add(Pattern.compile(patternString, Pattern.DOTALL)); 72 } 73 } 74 75 private final Log log; 76 private final Mkdir mkdir; 77 private final String DELIMITER = "\t"; 78 private final File classFileIndexFile = 79 new File(System.getProperty("user.home"), ".vogar/classfileindex"); 80 private final Map<String, Set<File>> classFileMap = new HashMap<String, Set<File>>(); 81 private final List<File> jarSearchDirs; 82 83 public ClassFileIndex(Log log, Mkdir mkdir, List<File> jarSearchDirs) { 84 this.log = log; 85 this.mkdir = mkdir; 86 this.jarSearchDirs = jarSearchDirs; 87 } 88 89 public Set<File> suggestClasspaths(String testOutput) { 90 Set<File> suggestedClasspaths = new HashSet<File>(); 91 92 for (Pattern pattern : FAILURE_PATTERNS) { 93 Matcher matcher = pattern.matcher(testOutput); 94 if (!matcher.matches()) { 95 continue; 96 } 97 98 for (int i = 1; i <= matcher.groupCount(); i++) { 99 String missingPackageOrClass = matcher.group(i); 100 Set<File> containingJars = classFileMap.get(missingPackageOrClass); 101 if (containingJars != null) { 102 suggestedClasspaths.addAll(containingJars); 103 } 104 } 105 } 106 107 return suggestedClasspaths; 108 } 109 110 /** 111 * Search through the jar search directories to find .jars to index. 112 * 113 * If this has already been done, instead just use the cached version in .vogar 114 */ 115 public void createIndex() { 116 if (!classFileMap.isEmpty()) { 117 return; 118 } 119 120 if (classFileIndexFile.exists()) { 121 long lastModified = classFileIndexFile.lastModified(); 122 long curTime = new Date().getTime(); 123 boolean cacheExpired = lastModified < curTime - CACHE_EXPIRY; 124 if (cacheExpired) { 125 log.verbose("class file index expired, rebuilding"); 126 } else { 127 readIndexCache(); 128 return; 129 } 130 } 131 132 log.verbose("building class file index"); 133 134 // Create index 135 for (File jarSearchDir : jarSearchDirs) { 136 if (!jarSearchDir.exists()) { 137 log.warn("directory \"" + jarSearchDir + "\" in jar paths doesn't exist"); 138 continue; 139 } 140 141 // traverse the jar directory, looking for files called ending in .jar 142 log.verbose("looking in " + jarSearchDir + " for .jar files"); 143 144 Set<File> jarFiles = new HashSet<File>(); 145 getJarFiles(jarFiles, jarSearchDir); 146 for (File file : jarFiles) { 147 indexJarFile(file); 148 } 149 } 150 151 // save for use on subsequent runs 152 writeIndexCache(); 153 } 154 155 private void indexJarFile(File file) { 156 try { 157 JarFile jarFile = new JarFile(file); 158 for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements(); ) { 159 JarEntry jarEntry = e.nextElement(); 160 161 // change paths into classes/packages, strip trailing period, and strip 162 // trailing .class extension 163 String classPath = jarEntry.getName() 164 .replaceAll("/", ".") 165 .replaceFirst("\\.$", "") 166 .replaceFirst("\\.class$", ""); 167 if (classFileMap.containsKey(classPath)) { 168 classFileMap.get(classPath).add(file); 169 } else { 170 Set<File> classPathJars = new HashSet<File>(); 171 classPathJars.add(file); 172 classFileMap.put(classPath, classPathJars); 173 } 174 } 175 } catch (IOException e) { 176 log.warn("failed to read " + file + ": " + e.getMessage()); 177 } 178 } 179 180 private void getJarFiles(Set<File> jarFiles, File dir) { 181 List<File> files = Arrays.asList(dir.listFiles()); 182 for (File file : files) { 183 if (file.isDirectory()) { 184 getJarFiles(jarFiles, file); 185 continue; 186 } 187 188 for (Pattern pattern : JAR_PATTERNS) { 189 Matcher matcher = pattern.matcher(file.getName()); 190 if (matcher.matches()) { 191 jarFiles.add(file); 192 } 193 } 194 } 195 } 196 197 private void writeIndexCache() { 198 log.verbose("writing index cache"); 199 200 BufferedWriter indexCacheWriter; 201 mkdir.mkdirs(classFileIndexFile.getParentFile()); 202 try { 203 indexCacheWriter = new BufferedWriter(new FileWriter(classFileIndexFile)); 204 for (Map.Entry<String, Set<File>> entry : classFileMap.entrySet()) { 205 indexCacheWriter.write(entry.getKey() + DELIMITER 206 + Strings.join(entry.getValue(), DELIMITER)); 207 indexCacheWriter.newLine(); 208 } 209 indexCacheWriter.close(); 210 } catch (IOException e) { 211 throw new RuntimeException(e); 212 } 213 } 214 215 private void readIndexCache() { 216 log.verbose("reading class file index cache"); 217 218 BufferedReader reader; 219 try { 220 reader = new BufferedReader(new FileReader(classFileIndexFile)); 221 } catch (FileNotFoundException e) { 222 throw new RuntimeException(e); 223 } 224 try { 225 String line; 226 while ((line = reader.readLine()) != null) { 227 line = line.trim(); 228 229 // Each line is a mapping of a class, package or file to the .jar files that 230 // contain its definition within VOGAR_JAR_PATH. Each component is separated 231 // by a delimiter. 232 String[] parts = line.split(DELIMITER); 233 if (parts.length < 2) { 234 throw new RuntimeException("classfileindex contains invalid line: " + line); 235 } 236 String resource = parts[0]; 237 Set<File> jarFiles = new HashSet<File>(); 238 for (int i = 1; i < parts.length; i++) { 239 jarFiles.add(new File(parts[i])); 240 } 241 classFileMap.put(resource, jarFiles); 242 } 243 } catch (IOException e) { 244 throw new RuntimeException(e); 245 } 246 } 247} 248