• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package vogar;
18 
19 import java.io.BufferedReader;
20 import java.io.BufferedWriter;
21 import java.io.File;
22 import java.io.FileNotFoundException;
23 import java.io.FileReader;
24 import java.io.FileWriter;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Date;
29 import java.util.Enumeration;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.jar.JarEntry;
36 import java.util.jar.JarFile;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39 import vogar.commands.Mkdir;
40 import vogar.util.Strings;
41 
42 /**
43  * Indexes the locations of commonly used classes to assist in constructing correct Vogar commands.
44  */
45 public 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) {
Pattern.compile(patternString)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
Pattern.compile(patternString, Pattern.DOTALL)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 
ClassFileIndex(Log log, Mkdir mkdir, List<File> jarSearchDirs)83     public ClassFileIndex(Log log, Mkdir mkdir, List<File> jarSearchDirs) {
84         this.log = log;
85         this.mkdir = mkdir;
86         this.jarSearchDirs = jarSearchDirs;
87     }
88 
suggestClasspaths(String testOutput)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      */
createIndex()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() && file.exists() && file.canRead()) {
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