PathClassLoader.java revision 7365de1056414750d0a7d1fdd26025fd247f0d04
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.NoSuchElementException;
30import java.util.zip.ZipEntry;
31import java.util.zip.ZipFile;
32
33/**
34 * Provides a simple {@link ClassLoader} implementation that operates on a list
35 * of files and directories in the local file system, but does not attempt to
36 * load classes from the network. Android uses this class for its system class
37 * loader and for its application class loader(s).
38 */
39public class PathClassLoader extends ClassLoader {
40
41    private final String path;
42    private final String libPath;
43
44    private boolean initialized;
45
46    private String[] mPaths;
47    private File[] mFiles;
48    private ZipFile[] mZips;
49    private DexFile[] mDexs;
50    private String[] mLibPaths;
51
52    /**
53     * Creates a {@code PathClassLoader} that operates on a given list of files
54     * and directories. This method is equivalent to calling
55     * {@link #PathClassLoader(String, String, ClassLoader)} with a
56     * {@code null} value for the second argument (see description there).
57     *
58     * @param path
59     *            the list of files and directories
60     *
61     * @param parent
62     *            the parent class loader
63     */
64    public PathClassLoader(String path, ClassLoader parent) {
65        this(path, null, parent);
66    }
67
68    /**
69     * Creates a {@code PathClassLoader} that operates on two given lists of
70     * files and directories. The entries of the first list should be one of the
71     * following:
72     * <ul>
73     * <li>Directories containing classes or resources.
74     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file.
75     * <li>"classes.dex" files.
76     * </ul>
77     * The entries of the second list should be directories containing native
78     * library files. Both lists are separated using the character specified by
79     * the "path.separator" system property, which, on Android, defaults to ":".
80     *
81     * @param path
82     *            the list of files and directories containing classes and
83     *            resources
84     *
85     * @param libPath
86     *            the list of directories containing native libraries
87     *
88     * @param parent
89     *            the parent class loader
90     */
91    public PathClassLoader(String path, String libPath, ClassLoader parent) {
92        super(parent);
93
94        if (path == null)
95            throw new NullPointerException();
96
97        this.path = path;
98        this.libPath = libPath;
99    }
100
101    private synchronized void ensureInit() {
102        if (initialized) {
103            return;
104        }
105
106        initialized = true;
107
108        mPaths = path.split(":");
109        int length = mPaths.length;
110
111        //System.out.println("PathClassLoader: " + mPaths);
112        mFiles = new File[length];
113        mZips = new ZipFile[length];
114        mDexs = new DexFile[length];
115
116        boolean wantDex =
117            System.getProperty("android.vm.dexfile", "").equals("true");
118
119        /* open all Zip and DEX files up front */
120        for (int i = 0; i < length; i++) {
121            //System.out.println("My path is: " + mPaths[i]);
122            File pathFile = new File(mPaths[i]);
123            mFiles[i] = pathFile;
124
125            if (pathFile.isFile()) {
126                try {
127                    mZips[i] = new ZipFile(pathFile);
128                }
129                catch (IOException ioex) {
130                    // expecting IOException and ZipException
131                    //System.out.println("Failed opening '" + pathFile + "': " + ioex);
132                    //ioex.printStackTrace();
133                }
134                if (wantDex) {
135                    /* we need both DEX and Zip, because dex has no resources */
136                    try {
137                        mDexs[i] = new DexFile(pathFile);
138                    }
139                    catch (IOException ioex) {}
140                }
141            }
142        }
143
144        /*
145         * Prep for native library loading.
146         */
147        String pathList = System.getProperty("java.library.path", ".");
148        String pathSep = System.getProperty("path.separator", ":");
149        String fileSep = System.getProperty("file.separator", "/");
150
151        if (libPath != null) {
152            if (pathList.length() > 0) {
153                pathList += pathSep + libPath;
154            }
155            else {
156                pathList = libPath;
157            }
158        }
159
160        mLibPaths = pathList.split(pathSep);
161        length = mLibPaths.length;
162
163        // Add a '/' to the end so we don't have to do the property lookup
164        // and concatenation later.
165        for (int i = 0; i < length; i++) {
166            if (!mLibPaths[i].endsWith(fileSep))
167                mLibPaths[i] += fileSep;
168            if (false)
169                System.out.println("Native lib path:  " + mLibPaths[i]);
170        }
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 method is called after the parent
411     * ClassLoader has failed to find a native library of the same name.
412     *
413     * @param libname
414     *            The name of the library to find
415     * @return the complete path of the library, or {@code null} if the library
416     *         is not found.
417     */
418    public String findLibrary(String libname) {
419        ensureInit();
420
421        String fileName = System.mapLibraryName(libname);
422        for (int i = 0; i < mLibPaths.length; i++) {
423            String pathName = mLibPaths[i] + fileName;
424            File test = new File(pathName);
425
426            if (test.exists())
427                return pathName;
428        }
429
430        return null;
431    }
432
433    /**
434     * Returns package information for the given package. Unfortunately, the
435     * PathClassLoader doesn't really have this information, and as a non-secure
436     * ClassLoader, it isn't even required to, according to the spec. Yet, we
437     * want to provide it, in order to make all those hopeful callers of
438     * <code>myClass.getPackage().getName()</code> happy. Thus we construct a
439     * Package object the first time it is being requested and fill most of the
440     * fields with dummy values. The Package object is then put into the
441     * ClassLoader's Package cache, so we see the same one next time. We don't
442     * create Package objects for null arguments or for the default package.
443     * <p>
444     * There a limited chance that we end up with multiple Package objects
445     * representing the same package: It can happen when when a package is
446     * scattered across different JAR files being loaded by different
447     * ClassLoaders. Rather unlikely, and given that this whole thing is more or
448     * less a workaround, probably not worth the effort.
449     *
450     * @param name
451     *            the name of the class
452     * @return the package information for the class, or {@code null} if there
453     *         is not package information available for it
454     */
455    @Override
456    protected Package getPackage(String name) {
457        if (name != null && !name.isEmpty()) {
458            synchronized(this) {
459                Package pack = super.getPackage(name);
460
461                if (pack == null) {
462                    pack = definePackage(name, "Unknown", "0.0", "Unknown", "Unknown", "0.0", "Unknown", null);
463                }
464
465                return pack;
466            }
467        }
468
469        return null;
470    }
471
472    /*
473     * Create an Enumeration for an ArrayList.
474     */
475    private static class EnumerateListArray<T> implements Enumeration<T> {
476        private final ArrayList mList;
477        private int i = 0;
478
479        EnumerateListArray(ArrayList list) {
480            mList = list;
481        }
482
483        public boolean hasMoreElements() {
484            return i < mList.size();
485        }
486
487        public T nextElement() {
488            if (i >= mList.size())
489                throw new NoSuchElementException();
490            return (T) mList.get(i++);
491        }
492    };
493
494    public String toString () {
495        return getClass().getName() + "[" + path + "]";
496    }
497}
498