PathClassLoader.java revision d1c610c2a641157df80aa8aefefc49393074f507
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.InputStream;
23import java.io.IOException;
24import java.io.RandomAccessFile;
25import java.net.URL;
26import java.util.ArrayList;
27import java.util.Enumeration;
28import java.util.NoSuchElementException;
29import java.util.zip.ZipEntry;
30import java.util.zip.ZipFile;
31import java.net.MalformedURLException;
32
33import dalvik.system.DexFile;
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 String[] mLibPaths;
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         * Prep for native library loading.
148         */
149        String pathList = System.getProperty("java.library.path", ".");
150        String pathSep = System.getProperty("path.separator", ":");
151        String fileSep = System.getProperty("file.separator", "/");
152
153        if (libPath != null) {
154            if (pathList.length() > 0) {
155                pathList += pathSep + libPath;
156            }
157            else {
158                pathList = libPath;
159            }
160        }
161
162        mLibPaths = pathList.split(pathSep);
163        length = mLibPaths.length;
164
165        // Add a '/' to the end so we don't have to do the property lookup
166        // and concatenation later.
167        for (int i = 0; i < length; i++) {
168            if (!mLibPaths[i].endsWith(fileSep))
169                mLibPaths[i] += fileSep;
170            if (false)
171                System.out.println("Native lib path:  " + mLibPaths[i]);
172        }
173    }
174
175    /**
176     * Finds a class. This method is called by {@code loadClass()} after the
177     * parent ClassLoader has failed to find a loaded class of the same name.
178     *
179     * @param name
180     *            The "binary name" of the class to search for, in a
181     *            human-readable form like "java.lang.String" or
182     *            "java.net.URLClassLoader$3$1".
183     * @return the {@link Class} object representing the class
184     * @throws ClassNotFoundException
185     *             if the class cannot be found
186     */
187    @Override
188    protected Class<?> findClass(String name) throws ClassNotFoundException
189    {
190        ensureInit();
191
192        //System.out.println("PathClassLoader " + this + ": findClass '" + name + "'");
193
194        byte[] data = null;
195        int length = mPaths.length;
196
197        for (int i = 0; i < length; i++) {
198            //System.out.println("My path is: " + mPaths[i]);
199
200            if (mDexs[i] != null) {
201                Class clazz = mDexs[i].loadClassBinaryName(name, this);
202                if (clazz != null)
203                    return clazz;
204            } else if (mZips[i] != null) {
205                String fileName = name.replace('.', '/') + ".class";
206                data = loadFromArchive(mZips[i], fileName);
207            } else {
208                File pathFile = mFiles[i];
209                if (pathFile.isDirectory()) {
210                    String fileName =
211                        mPaths[i] + "/" + name.replace('.', '/') + ".class";
212                    data = loadFromDirectory(fileName);
213                } else {
214                    //System.out.println("PathClassLoader: can't find '"
215                    //    + mPaths[i] + "'");
216                }
217
218            }
219
220            /* --this doesn't work in current version of Dalvik--
221            if (data != null) {
222                System.out.println("--- Found class " + name
223                    + " in zip[" + i + "] '" + mZips[i].getName() + "'");
224                int dotIndex = name.lastIndexOf('.');
225                if (dotIndex != -1) {
226                    String packageName = name.substring(0, dotIndex);
227                    synchronized (this) {
228                        Package packageObj = getPackage(packageName);
229                        if (packageObj == null) {
230                            definePackage(packageName, null, null,
231                                    null, null, null, null, null);
232                        }
233                    }
234                }
235
236                return defineClass(name, data, 0, data.length);
237            }
238            */
239        }
240
241        throw new ClassNotFoundException(name + " in loader " + this);
242    }
243
244    /**
245     * Finds a resource. This method is called by {@code getResource()} after
246     * the parent ClassLoader has failed to find a loaded resource of the same
247     * name.
248     *
249     * @param name
250     *            The name of the resource to find
251     * @return the location of the resource as a URL, or {@code null} if the
252     *         resource is not found.
253     */
254    @Override
255    protected URL findResource(String name) {
256        ensureInit();
257
258        //java.util.logging.Logger.global.severe("findResource: " + name);
259
260        int length = mPaths.length;
261
262        for (int i = 0; i < length; i++) {
263            URL result = findResource(name, i);
264            if(result != null) {
265                return result;
266            }
267        }
268
269        return null;
270    }
271
272    /**
273     * Finds an enumeration of URLs for the resource with the specified name.
274     *
275     * @param resName
276     *            the name of the resource to find.
277     * @return an enumeration of {@code URL} objects for the requested resource.
278     */
279    @Override
280    protected Enumeration<URL> findResources(String resName) {
281        ensureInit();
282
283        int length = mPaths.length;
284        ArrayList<URL> results = new ArrayList<URL>();
285
286        for (int i = 0; i < length; i++) {
287            URL result = findResource(resName, i);
288            if(result != null) {
289                results.add(result);
290            }
291        }
292        return new EnumerateListArray<URL>(results);
293    }
294
295    private URL findResource(String name, int i) {
296        File pathFile = mFiles[i];
297        ZipFile zip = mZips[i];
298        if (zip != null) {
299            if (isInArchive(zip, name)) {
300                //System.out.println("  found " + name + " in " + pathFile);
301                try {
302                    // File.toURL() is compliant with RFC 1738 in always
303                    // creating absolute path names. If we construct the
304                    // URL by concatenating strings, we might end up with
305                    // illegal URLs for relative names.
306                    return new URL("jar:" + pathFile.toURL() + "!/" + name);
307                }
308                catch (MalformedURLException e) {
309                    throw new RuntimeException(e);
310                }
311            }
312        } else if (pathFile.isDirectory()) {
313            File dataFile = new File(mPaths[i] + "/" + name);
314            if (dataFile.exists()) {
315                //System.out.println("  found resource " + name);
316                try {
317                    // Same as archive case regarding URL construction.
318                    return dataFile.toURL();
319                }
320                catch (MalformedURLException e) {
321                    throw new RuntimeException(e);
322                }
323            }
324        } else if (pathFile.isFile()) {
325        } else {
326            System.err.println("PathClassLoader: can't find '"
327                + mPaths[i] + "'");
328        }
329        return null;
330    }
331
332    /*
333     * Load the contents of a file from a file in a directory.
334     *
335     * Returns null if the class wasn't found.
336     */
337    private byte[] loadFromDirectory(String path) {
338        RandomAccessFile raf;
339        byte[] fileData;
340
341        //System.out.println("Trying to load from " + path);
342        try {
343            raf = new RandomAccessFile(path, "r");
344        }
345        catch (FileNotFoundException fnfe) {
346            //System.out.println("  Not found: " + path);
347            return null;
348        }
349
350        try {
351            fileData = new byte[(int) raf.length()];
352            raf.read(fileData);
353            raf.close();
354        }
355        catch (IOException ioe) {
356            System.err.println("Error reading from " + path);
357            // swallow it, return null instead
358            fileData = null;
359        }
360
361        return fileData;
362    }
363
364    /*
365     * Load a class from a file in an archive.  We currently assume that
366     * the file is a Zip archive.
367     *
368     * Returns null if the class wasn't found.
369     */
370    private byte[] loadFromArchive(ZipFile zip, String name) {
371        ZipEntry entry;
372
373        entry = zip.getEntry(name);
374        if (entry == null)
375            return null;
376
377        ByteArrayOutputStream byteStream;
378        InputStream stream;
379        int count;
380
381        /*
382         * Copy the data out of the stream.  Because we got the ZipEntry
383         * from a ZipFile, the uncompressed size is known, and we can set
384         * the initial size of the ByteArrayOutputStream appropriately.
385         */
386        try {
387            stream = zip.getInputStream(entry);
388            byteStream = new ByteArrayOutputStream((int) entry.getSize());
389            byte[] buf = new byte[4096];
390            while ((count = stream.read(buf)) > 0)
391                byteStream.write(buf, 0, count);
392
393            stream.close();
394        }
395        catch (IOException ioex) {
396            //System.out.println("Failed extracting '" + archive + "': " +ioex);
397            return null;
398        }
399
400        //System.out.println("  loaded from Zip");
401        return byteStream.toByteArray();
402    }
403
404    /*
405     * Figure out if "name" is a member of "archive".
406     */
407    private boolean isInArchive(ZipFile zip, String name) {
408        return zip.getEntry(name) != null;
409    }
410
411    /**
412     * Finds a native library. This method is called after the parent
413     * ClassLoader has failed to find a native library of the same name.
414     *
415     * @param libname
416     *            The name of the library to find
417     * @return the complete path of the library, or {@code null} if the library
418     *         is not found.
419     */
420    protected String findLibrary(String libname) {
421        ensureInit();
422
423        String fileName = System.mapLibraryName(libname);
424        for (int i = 0; i < mLibPaths.length; i++) {
425            String pathName = mLibPaths[i] + fileName;
426            File test = new File(pathName);
427
428            if (test.exists())
429                return pathName;
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 && !"".equals(name)) {
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