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