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
17package vogar.target;
18
19import dalvik.system.DexFile;
20import java.io.File;
21import java.io.IOException;
22import java.util.Comparator;
23import java.util.Enumeration;
24import java.util.HashMap;
25import java.util.HashSet;
26import java.util.Map;
27import java.util.Set;
28import java.util.TreeSet;
29import java.util.regex.Pattern;
30import java.util.zip.ZipEntry;
31import 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 */
40final 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
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
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     */
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     */
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 {
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 {
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         */
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 {
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
233        ApkClassFinder(Map<String, DexFile> dexFiles) {
234            this.dexFiles = dexFiles;
235        }
236
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     */
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     */
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     */
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