PathClassLoader.java revision 92e51d7dedadb3e8a605eb00e455faf1e3446a02
1/*
2 * Copyright (C) 2007 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 dalvik.system;
18
19import java.io.ByteArrayOutputStream;
20import java.io.File;
21import java.io.FileNotFoundException;
22import java.io.IOException;
23import java.io.InputStream;
24import java.io.RandomAccessFile;
25import java.net.MalformedURLException;
26import java.net.URL;
27import java.util.ArrayList;
28import java.util.Collections;
29import java.util.Enumeration;
30import java.util.List;
31import java.util.NoSuchElementException;
32import java.util.zip.ZipEntry;
33import java.util.zip.ZipFile;
34import libcore.io.IoUtils;
35
36/**
37 * Provides a simple {@link ClassLoader} implementation that operates on a list
38 * of files and directories in the local file system, but does not attempt to
39 * load classes from the network. Android uses this class for its system class
40 * loader and for its application class loader(s).
41 */
42public class PathClassLoader extends ClassLoader {
43
44    private final String path;
45    private final String libPath;
46
47    /*
48     * Parallel arrays for jar/apk files.
49     *
50     * (could stuff these into an object and have a single array;
51     * improves clarity but adds overhead)
52     */
53    private final String[] mPaths;
54    private final File[] mFiles;
55    private final ZipFile[] mZips;
56    private final DexFile[] mDexs;
57
58    /**
59     * Native library path.
60     */
61    private final List<String> libraryPathElements;
62
63    /**
64     * Creates a {@code PathClassLoader} that operates on a given list of files
65     * and directories. This method is equivalent to calling
66     * {@link #PathClassLoader(String, String, ClassLoader)} with a
67     * {@code null} value for the second argument (see description there).
68     *
69     * @param path
70     *            the list of files and directories
71     *
72     * @param parent
73     *            the parent class loader
74     */
75    public PathClassLoader(String path, ClassLoader parent) {
76        this(path, null, parent);
77    }
78
79    /**
80     * Creates a {@code PathClassLoader} that operates on two given
81     * lists of files and directories. The entries of the first list
82     * should be one of the following:
83     *
84     * <ul>
85     * <li>Directories containing classes or resources.
86     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file.
87     * <li>"classes.dex" files.
88     * </ul>
89     *
90     * The entries of the second list should be directories containing
91     * native library files. Both lists are separated using the
92     * character specified by the "path.separator" system property,
93     * which, on Android, defaults to ":".
94     *
95     * @param path
96     *            the list of files and directories containing classes and
97     *            resources
98     *
99     * @param libPath
100     *            the list of directories containing native libraries
101     *
102     * @param parent
103     *            the parent class loader
104     */
105    public PathClassLoader(String path, String libPath, ClassLoader parent) {
106        super(parent);
107
108        if (path == null) {
109            throw new NullPointerException();
110        }
111
112        this.path = path;
113        this.libPath = libPath;
114
115        mPaths = path.split(":");
116        int length = mPaths.length;
117
118        mFiles = new File[length];
119        mZips = new ZipFile[length];
120        mDexs = new DexFile[length];
121
122        /* open all Zip and DEX files up front */
123        for (int i = 0; i < length; i++) {
124            File pathFile = new File(mPaths[i]);
125            mFiles[i] = pathFile;
126
127            if (pathFile.isFile()) {
128                if (!mPaths[i].endsWith(".dex")) {
129                    /*
130                     * If the name doesn't end with ".dex" assume it's a zip
131                     * file of some sort.
132                     */
133                    try {
134                        mZips[i] = new ZipFile(pathFile);
135                    }
136                    catch (IOException ioex) {
137                        // expecting IOException and ZipException
138                        //System.out.println("Failed opening '" + pathFile
139                        //    + "': " + ioex);
140                        //ioex.printStackTrace();
141                    }
142                }
143
144                /*
145                 * If we got a zip file, we still need to extract out
146                 * the dex file from it.
147                 */
148                try {
149                    mDexs[i] = new DexFile(pathFile);
150                }
151                catch (IOException ioex) {
152                    // It might be a resource-only zip.
153                    //System.out.println("Failed to construct DexFile '"
154                    //    + pathFile + "': " + ioex);
155                }
156            }
157        }
158
159        /*
160         * Native libraries may exist in both the system and application library
161         * paths, so we use this search order for these paths:
162         *   1. This class loader's library path for application libraries
163         *   2. The VM's library path from the system property for system libraries
164         * This order was reversed prior to Gingerbread; see http://b/2933456
165         */
166        libraryPathElements = new ArrayList<String>();
167        if (libPath != null) {
168            for (String pathElement : libPath.split(File.pathSeparator)) {
169                libraryPathElements.add(cleanupPathElement(pathElement));
170            }
171        }
172        String systemLibraryPath = System.getProperty("java.library.path", ".");
173        if (!systemLibraryPath.isEmpty()) {
174            for (String pathElement : systemLibraryPath.split(File.pathSeparator)) {
175                libraryPathElements.add(cleanupPathElement(pathElement));
176            }
177        }
178    }
179
180    /**
181     * Returns a path element that includes a trailing file separator.
182     */
183    private String cleanupPathElement(String path) {
184        return path.endsWith(File.separator) ? path : (path + File.separator);
185    }
186
187    /**
188     * Finds a class. This method is called by {@code loadClass()} after the
189     * parent ClassLoader has failed to find a loaded class of the same name.
190     *
191     * @param name
192     *            The "binary name" of the class to search for, in a
193     *            human-readable form like "java.lang.String" or
194     *            "java.net.URLClassLoader$3$1".
195     * @return the {@link Class} object representing the class
196     * @throws ClassNotFoundException
197     *             if the class cannot be found
198     */
199    @Override
200    protected Class<?> findClass(String name) throws ClassNotFoundException {
201        //System.out.println("PathClassLoader " + this + ": findClass '" + name + "'");
202
203        byte[] data = null;
204        int length = mPaths.length;
205
206        for (int i = 0; i < length; i++) {
207            //System.out.println("My path is: " + mPaths[i]);
208
209            if (mDexs[i] != null) {
210                Class clazz = mDexs[i].loadClassBinaryName(name, this);
211                if (clazz != null)
212                    return clazz;
213            } else if (mZips[i] != null) {
214                String fileName = name.replace('.', '/') + ".class";
215                data = loadFromArchive(mZips[i], fileName);
216            } else {
217                File pathFile = mFiles[i];
218                if (pathFile.isDirectory()) {
219                    String fileName =
220                        mPaths[i] + "/" + name.replace('.', '/') + ".class";
221                    data = loadFromDirectory(fileName);
222                } else {
223                    //System.out.println("PathClassLoader: can't find '"
224                    //    + mPaths[i] + "'");
225                }
226
227            }
228
229            /* --this doesn't work in current version of Dalvik--
230            if (data != null) {
231                System.out.println("--- Found class " + name
232                    + " in zip[" + i + "] '" + mZips[i].getName() + "'");
233                int dotIndex = name.lastIndexOf('.');
234                if (dotIndex != -1) {
235                    String packageName = name.substring(0, dotIndex);
236                    synchronized (this) {
237                        Package packageObj = getPackage(packageName);
238                        if (packageObj == null) {
239                            definePackage(packageName, null, null,
240                                    null, null, null, null, null);
241                        }
242                    }
243                }
244
245                return defineClass(name, data, 0, data.length);
246            }
247            */
248        }
249
250        throw new ClassNotFoundException(name + " in loader " + this);
251    }
252
253    /**
254     * Finds a resource. This method is called by {@code getResource()} after
255     * the parent ClassLoader has failed to find a loaded resource of the same
256     * name.
257     *
258     * @param name
259     *            The name of the resource to find
260     * @return the location of the resource as a URL, or {@code null} if the
261     *         resource is not found.
262     */
263    @Override
264    protected URL findResource(String name) {
265        //java.util.logging.Logger.global.severe("findResource: " + name);
266
267        int length = mPaths.length;
268
269        for (int i = 0; i < length; i++) {
270            URL result = findResource(name, i);
271            if (result != null) {
272                return result;
273            }
274        }
275
276        return null;
277    }
278
279    /**
280     * Finds an enumeration of URLs for the resource with the specified name.
281     *
282     * @param resName
283     *            the name of the resource to find.
284     * @return an enumeration of {@code URL} objects for the requested resource.
285     */
286    @Override
287    protected Enumeration<URL> findResources(String resName) {
288        int length = mPaths.length;
289        ArrayList<URL> results = new ArrayList<URL>();
290        for (int i = 0; i < length; i++) {
291            URL result = findResource(resName, i);
292            if (result != null) {
293                results.add(result);
294            }
295        }
296        return Collections.enumeration(results);
297    }
298
299    private URL findResource(String name, int i) {
300        File pathFile = mFiles[i];
301        ZipFile zip = mZips[i];
302        if (zip != null) {
303            if (isInArchive(zip, name)) {
304                //System.out.println("  found " + name + " in " + pathFile);
305                try {
306                    // File.toURL() is compliant with RFC 1738 in always
307                    // creating absolute path names. If we construct the
308                    // URL by concatenating strings, we might end up with
309                    // illegal URLs for relative names.
310                    return new URL("jar:" + pathFile.toURL() + "!/" + name);
311                }
312                catch (MalformedURLException e) {
313                    throw new RuntimeException(e);
314                }
315            }
316        } else if (pathFile.isDirectory()) {
317            File dataFile = new File(mPaths[i] + "/" + name);
318            if (dataFile.exists()) {
319                //System.out.println("  found resource " + name);
320                try {
321                    // Same as archive case regarding URL construction.
322                    return dataFile.toURL();
323                }
324                catch (MalformedURLException e) {
325                    throw new RuntimeException(e);
326                }
327            }
328        } else if (pathFile.isFile()) {
329        } else {
330            System.err.println("PathClassLoader: can't find '"
331                + mPaths[i] + "'");
332        }
333        return null;
334    }
335
336    /*
337     * Load the contents of a file from a file in a directory.
338     *
339     * Returns null if the class wasn't found.
340     */
341    private byte[] loadFromDirectory(String path) {
342        try {
343            return IoUtils.readFileAsByteArray(path);
344        } catch (IOException ex) {
345            System.err.println("Error reading from " + path);
346            // swallow it, return null instead
347            return null;
348        }
349    }
350
351    /*
352     * Load a class from a file in an archive.  We currently assume that
353     * the file is a Zip archive.
354     *
355     * Returns null if the class wasn't found.
356     */
357    private byte[] loadFromArchive(ZipFile zip, String name) {
358        ZipEntry entry;
359
360        entry = zip.getEntry(name);
361        if (entry == null)
362            return null;
363
364        ByteArrayOutputStream byteStream;
365        InputStream stream;
366        int count;
367
368        /*
369         * Copy the data out of the stream.  Because we got the ZipEntry
370         * from a ZipFile, the uncompressed size is known, and we can set
371         * the initial size of the ByteArrayOutputStream appropriately.
372         */
373        try {
374            stream = zip.getInputStream(entry);
375            byteStream = new ByteArrayOutputStream((int) entry.getSize());
376            byte[] buf = new byte[4096];
377            while ((count = stream.read(buf)) > 0)
378                byteStream.write(buf, 0, count);
379
380            stream.close();
381        }
382        catch (IOException ioex) {
383            //System.out.println("Failed extracting '" + archive + "': " +ioex);
384            return null;
385        }
386
387        //System.out.println("  loaded from Zip");
388        return byteStream.toByteArray();
389    }
390
391    /*
392     * Figure out if "name" is a member of "archive".
393     */
394    private boolean isInArchive(ZipFile zip, String name) {
395        return zip.getEntry(name) != null;
396    }
397
398    /**
399     * Finds a native library. This class loader first searches its own library
400     * path (as specified in the constructor) and then the system library path.
401     * In Android 2.2 and earlier, the search order was reversed.
402     *
403     * @param libname
404     *            The name of the library to find
405     * @return the complete path of the library, or {@code null} if the library
406     *         is not found.
407     */
408    public String findLibrary(String libname) {
409        String fileName = System.mapLibraryName(libname);
410        for (String pathElement : libraryPathElements) {
411            String pathName = pathElement + fileName;
412            File test = new File(pathName);
413
414            if (test.exists()) {
415                return pathName;
416            }
417        }
418
419        return null;
420    }
421
422    /**
423     * Returns package information for the given package. Unfortunately, the
424     * PathClassLoader doesn't really have this information, and as a non-secure
425     * ClassLoader, it isn't even required to, according to the spec. Yet, we
426     * want to provide it, in order to make all those hopeful callers of
427     * <code>myClass.getPackage().getName()</code> happy. Thus we construct a
428     * Package object the first time it is being requested and fill most of the
429     * fields with dummy values. The Package object is then put into the
430     * ClassLoader's Package cache, so we see the same one next time. We don't
431     * create Package objects for null arguments or for the default package.
432     * <p>
433     * There a limited chance that we end up with multiple Package objects
434     * representing the same package: It can happen when when a package is
435     * scattered across different JAR files being loaded by different
436     * ClassLoaders. Rather unlikely, and given that this whole thing is more or
437     * less a workaround, probably not worth the effort.
438     *
439     * @param name
440     *            the name of the class
441     * @return the package information for the class, or {@code null} if there
442     *         is not package information available for it
443     */
444    @Override
445    protected Package getPackage(String name) {
446        if (name != null && !name.isEmpty()) {
447            synchronized(this) {
448                Package pack = super.getPackage(name);
449
450                if (pack == null) {
451                    pack = definePackage(name, "Unknown", "0.0", "Unknown", "Unknown", "0.0", "Unknown", null);
452                }
453
454                return pack;
455            }
456        }
457        return null;
458    }
459
460    public String toString () {
461        return getClass().getName() + "[" + path + "]";
462    }
463}
464