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