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