Home | History | Annotate | Download | only in target
      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.HashSet;
     25 import java.util.Set;
     26 import java.util.TreeSet;
     27 import java.util.regex.Pattern;
     28 import java.util.zip.ZipEntry;
     29 import 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  */
     38 final 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