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