• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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.target;
18 
19 import dalvik.system.DexFile;
20 import java.io.File;
21 import java.io.IOException;
22 import java.util.Comparator;
23 import java.util.Enumeration;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.TreeSet;
29 import java.util.regex.Pattern;
30 import java.util.zip.ZipEntry;
31 import java.util.zip.ZipFile;
32 
33 /**
34  * Inspects the classpath to return the classes in a requested package. This
35  * class doesn't yet traverse directories on the classpath.
36  *
37  * <p>Adapted from android.test.ClassPathPackageInfo. Unlike that class, this
38  * runs on both Dalvik and Java VMs.
39  */
40 final class ClassPathScanner {
41 
42     static final Comparator<Class<?>> ORDER_CLASS_BY_NAME = new Comparator<Class<?>>() {
43         @Override public int compare(Class<?> a, Class<?> b) {
44             return a.getName().compareTo(b.getName());
45         }
46     };
47     private static final String DOT_CLASS = ".class";
48 
49     private final String[] classPath;
50     private final ClassFinder classFinder;
51 
createDexFiles(String[] classPath)52     private static Map<String, DexFile> createDexFiles(String[] classPath) {
53         Map<String, DexFile> result = new HashMap<String, DexFile>();
54         for (String entry : classPath) {
55             File classPathEntry = new File(entry);
56             if (!classPathEntry.exists() || classPathEntry.isDirectory()) {
57                 continue;
58             }
59 
60             try {
61                 result.put(classPathEntry.getName(), new DexFile(classPathEntry));
62             } catch (IOException ignore) {
63                 // okay, presumably the dex file didn't contain any classes
64             }
65         }
66         return result;
67     }
68 
ClassPathScanner()69     ClassPathScanner() {
70         classPath = getClassPath();
71         if ("Dalvik".equals(System.getProperty("java.vm.name"))) {
72             classFinder = new ApkClassFinder(createDexFiles(classPath));
73         } else {
74             // When running vogar tests under an IDE the classes are not held in a .jar file.
75             // This system properties can be set to make it possible to run the vogar tests from an
76             // IDE. It is not intended for normal usage.
77             if (Boolean.parseBoolean(System.getProperty("vogar-scan-directories-for-tests"))) {
78                 classFinder = new DirectoryClassFinder();
79             } else {
80                 classFinder = new JarClassFinder();
81             }
82         }
83     }
84 
85     /**
86      * Returns a package describing the loadable classes whose package name is
87      * {@code packageName}.
88      */
scan(String packageName)89     public Package scan(String packageName) throws IOException {
90         Set<String> subpackageNames = new TreeSet<>();
91         Set<String> classNames = new TreeSet<>();
92         Set<Class<?>> topLevelClasses = new TreeSet<>(ORDER_CLASS_BY_NAME);
93         findClasses(packageName, classNames, subpackageNames);
94         for (String className : classNames) {
95             try {
96                 topLevelClasses.add(Class.forName(className, false, getClass().getClassLoader()));
97             } catch (ClassNotFoundException e) {
98                 throw new RuntimeException(e);
99             }
100         }
101         return new Package(this, subpackageNames, topLevelClasses);
102     }
103 
104     /**
105      * Finds all classes and subpackages that are below the packageName and
106      * add them to the respective sets. Searches the package on the whole class
107      * path.
108      */
findClasses(String packageName, Set<String> classNames, Set<String> subpackageNames)109     private void findClasses(String packageName, Set<String> classNames,
110             Set<String> subpackageNames) throws IOException {
111         String packagePrefix = packageName + '.';
112         String pathPrefix = packagePrefix.replace('.', '/');
113         for (String entry : classPath) {
114             File entryFile = new File(entry);
115             if (entryFile.exists()) {
116                 classFinder.find(entryFile, pathPrefix, packageName, classNames, subpackageNames);
117             }
118         }
119     }
120 
121     interface ClassFinder {
find(File classPathEntry, String pathPrefix, String packageName, Set<String> classNames, Set<String> subpackageNames)122         void find(File classPathEntry, String pathPrefix, String packageName,
123                 Set<String> classNames, Set<String> subpackageNames) throws IOException;
124     }
125 
126     /**
127      * Finds all classes and subpackages that are below the packageName and
128      * add them to the respective sets. Searches the package in a single jar file.
129      */
130     private static class JarClassFinder implements ClassFinder {
find(File classPathEntry, String pathPrefix, String packageName, Set<String> classNames, Set<String> subpackageNames)131         public void find(File classPathEntry, String pathPrefix, String packageName,
132                 Set<String> classNames, Set<String> subpackageNames) throws IOException {
133             if (classPathEntry.isDirectory()) {
134                 return;
135             }
136 
137             Set<String> entryNames = getJarEntries(classPathEntry);
138             // check if the Jar contains the package.
139             if (!entryNames.contains(pathPrefix)) {
140                 return;
141             }
142             int prefixLength = pathPrefix.length();
143             for (String entryName : entryNames) {
144                 if (entryName.startsWith(pathPrefix)) {
145                     if (entryName.endsWith(DOT_CLASS)) {
146                         // check if the class is in the package itself or in one of its
147                         // subpackages.
148                         int index = entryName.indexOf('/', prefixLength);
149                         if (index >= 0) {
150                             String p = entryName.substring(0, index).replace('/', '.');
151                             subpackageNames.add(p);
152                         } else if (isToplevelClass(entryName)) {
153                             classNames.add(getClassName(entryName).replace('/', '.'));
154                         }
155                     }
156                 }
157             }
158         }
159 
160         /**
161          * Gets the class and package entries from a Jar.
162          */
getJarEntries(File jarFile)163         private Set<String> getJarEntries(File jarFile) throws IOException {
164             Set<String> entryNames = new HashSet<>();
165             ZipFile zipFile = new ZipFile(jarFile);
166             for (Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements(); ) {
167                 String entryName = e.nextElement().getName();
168                 if (!entryName.endsWith(DOT_CLASS)) {
169                     continue;
170                 }
171 
172                 entryNames.add(entryName);
173 
174                 // add the entry name of the classes package, i.e. the entry name of
175                 // the directory that the class is in. Used to quickly skip jar files
176                 // if they do not contain a certain package.
177                 //
178                 // Also add parent packages so that a JAR that contains
179                 // pkg1/pkg2/Foo.class will be marked as containing pkg1/ in addition
180                 // to pkg1/pkg2/ and pkg1/pkg2/Foo.class.  We're still interested in
181                 // JAR files that contains subpackages of a given package, even if
182                 // an intermediate package contains no direct classes.
183                 //
184                 // Classes in the default package will cause a single package named
185                 // "" to be added instead.
186                 int lastIndex = entryName.lastIndexOf('/');
187                 do {
188                     String packageName = entryName.substring(0, lastIndex + 1);
189                     entryNames.add(packageName);
190                     lastIndex = entryName.lastIndexOf('/', lastIndex - 1);
191                 } while (lastIndex > 0);
192             }
193 
194             return entryNames;
195         }
196     }
197 
198     /**
199      * Finds all classes and subpackages that are below the packageName and
200      * add them to the respective sets. Searches the package from a class directory.
201      */
202     private static class DirectoryClassFinder implements ClassFinder {
find(File classPathEntry, String pathPrefix, String packageName, Set<String> classNames, Set<String> subpackageNames)203         public void find(File classPathEntry, String pathPrefix, String packageName,
204                 Set<String> classNames, Set<String> subpackageNames) throws IOException {
205 
206             File subDir = new File(classPathEntry, pathPrefix);
207             if (subDir.exists() && subDir.isDirectory()) {
208                 File[] files = subDir.listFiles();
209                 if (files != null) {
210                     for (File subFile : files) {
211                         String fileName = subFile.getName();
212                         if (fileName.endsWith(DOT_CLASS)) {
213                             classNames.add(packageName + "." + getClassName(fileName));
214                         } else if (subFile.isDirectory()) {
215                             subpackageNames.add(packageName + "." + fileName);
216                         }
217                     }
218                 }
219             }
220         }
221     }
222 
223     /**
224      * Finds all classes and sub packages that are below the packageName and
225      * add them to the respective sets. Searches the package in a single APK.
226      *
227      * <p>This class uses the Android-only class DexFile. This class will fail
228      * to load on non-Android VMs.
229      */
230     private static class ApkClassFinder implements ClassFinder {
231         private final Map<String, DexFile> dexFiles;
232 
ApkClassFinder(Map<String, DexFile> dexFiles)233         ApkClassFinder(Map<String, DexFile> dexFiles) {
234             this.dexFiles = dexFiles;
235         }
236 
find(File classPathEntry, String pathPrefix, String packageName, Set<String> classNames, Set<String> subpackageNames)237         public void find(File classPathEntry, String pathPrefix, String packageName,
238                 Set<String> classNames, Set<String> subpackageNames) {
239             if (classPathEntry.isDirectory()) {
240                 return;
241             }
242 
243             DexFile dexFile = dexFiles.get(classPathEntry.getName());
244             if (dexFile == null) {
245                 return;
246             }
247             Enumeration<String> apkClassNames = dexFile.entries();
248             while (apkClassNames.hasMoreElements()) {
249                 String className = apkClassNames.nextElement();
250                 if (!className.startsWith(packageName)) {
251                     continue;
252                 }
253 
254                 String subPackageName = packageName;
255                 int lastPackageSeparator = className.lastIndexOf('.');
256                 if (lastPackageSeparator > 0) {
257                     subPackageName = className.substring(0, lastPackageSeparator);
258                 }
259                 if (subPackageName.length() > packageName.length()) {
260                     subpackageNames.add(subPackageName);
261                 } else if (isToplevelClass(className)) {
262                     classNames.add(className);
263                 }
264             }
265         }
266     }
267 
268     /**
269      * Returns true if a given file name represents a toplevel class.
270      */
isToplevelClass(String fileName)271     private static boolean isToplevelClass(String fileName) {
272         return fileName.indexOf('$') < 0;
273     }
274 
275     /**
276      * Given the absolute path of a class file, return the class name.
277      */
getClassName(String className)278     private static String getClassName(String className) {
279         int classNameEnd = className.length() - DOT_CLASS.length();
280         return className.substring(0, classNameEnd);
281     }
282 
283     /**
284      * Gets the class path from the System Property "java.class.path" and splits
285      * it up into the individual elements.
286      */
getClassPath()287     public static String[] getClassPath() {
288         String classPath = System.getProperty("java.class.path");
289         String separator = System.getProperty("path.separator", ":");
290         return classPath.split(Pattern.quote(separator));
291     }
292 }
293