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