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