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