1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17
18package java.net;
19
20import java.io.BufferedReader;
21import java.io.File;
22import java.io.FileInputStream;
23import java.io.FileNotFoundException;
24import java.io.FilePermission;
25import java.io.IOException;
26import java.io.InputStream;
27import java.io.InputStreamReader;
28import java.io.UnsupportedEncodingException;
29import java.nio.charset.StandardCharsets;
30import java.security.CodeSource;
31import java.security.PermissionCollection;
32import java.security.SecureClassLoader;
33import java.security.cert.Certificate;
34import java.util.ArrayList;
35import java.util.Collections;
36import java.util.Enumeration;
37import java.util.HashMap;
38import java.util.List;
39import java.util.Map;
40import java.util.StringTokenizer;
41import java.util.jar.Attributes;
42import java.util.jar.JarEntry;
43import java.util.jar.JarFile;
44import java.util.jar.Manifest;
45import libcore.io.IoUtils;
46import libcore.io.Streams;
47
48/**
49 * This class loader is responsible for loading classes and resources from a
50 * list of URLs which can refer to either directories or JAR files. Classes
51 * loaded by this {@code URLClassLoader} are granted permission to access the
52 * URLs contained in the URL search list.
53 */
54@FindBugsSuppressWarnings({ "DMI_COLLECTION_OF_URLS", "DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED" })
55public class URLClassLoader extends SecureClassLoader {
56
57    ArrayList<URL> originalUrls;
58
59    List<URL> searchList;
60    ArrayList<URLHandler> handlerList;
61    Map<URL, URLHandler> handlerMap = new HashMap<URL, URLHandler>();
62
63    private URLStreamHandlerFactory factory;
64
65    static class IndexFile {
66
67        private HashMap<String, ArrayList<URL>> map;
68        //private URLClassLoader host;
69
70
71        static IndexFile readIndexFile(JarFile jf, JarEntry indexEntry, URL url) {
72            BufferedReader in = null;
73            InputStream is = null;
74            try {
75                // Add mappings from resource to jar file
76                String parentURLString = getParentURL(url).toExternalForm();
77                String prefix = "jar:" + parentURLString + "/";
78                is = jf.getInputStream(indexEntry);
79                in = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
80                HashMap<String, ArrayList<URL>> pre_map = new HashMap<String, ArrayList<URL>>();
81                // Ignore the 2 first lines (index version)
82                if (in.readLine() == null) return null;
83                if (in.readLine() == null) return null;
84                TOP_CYCLE:
85                while (true) {
86                    String line = in.readLine();
87                    if (line == null) {
88                        break;
89                    }
90                    URL jar = new URL(prefix + line + "!/");
91                    while (true) {
92                        line = in.readLine();
93                        if (line == null) {
94                            break TOP_CYCLE;
95                        }
96                        if (line.isEmpty()) {
97                            break;
98                        }
99                        ArrayList<URL> list;
100                        if (pre_map.containsKey(line)) {
101                            list = pre_map.get(line);
102                        } else {
103                            list = new ArrayList<URL>();
104                            pre_map.put(line, list);
105                        }
106                        list.add(jar);
107                    }
108                }
109                if (!pre_map.isEmpty()) {
110                    return new IndexFile(pre_map);
111                }
112            } catch (MalformedURLException e) {
113                // Ignore this jar's index
114            } catch (IOException e) {
115                // Ignore this jar's index
116            } finally {
117                IoUtils.closeQuietly(in);
118                IoUtils.closeQuietly(is);
119            }
120            return null;
121        }
122
123        private static URL getParentURL(URL url) throws IOException {
124            URL fileURL = ((JarURLConnection) url.openConnection()).getJarFileURL();
125            String file = fileURL.getFile();
126            String parentFile = new File(file).getParent();
127            parentFile = parentFile.replace(File.separatorChar, '/');
128            if (parentFile.charAt(0) != '/') {
129                parentFile = "/" + parentFile;
130            }
131            URL parentURL = new URL(fileURL.getProtocol(), fileURL
132                    .getHost(), fileURL.getPort(), parentFile);
133            return parentURL;
134        }
135
136        public IndexFile(HashMap<String, ArrayList<URL>> map) {
137            this.map = map;
138        }
139
140        ArrayList<URL> get(String name) {
141            return map.get(name);
142        }
143    }
144
145    class URLHandler {
146        URL url;
147        URL codeSourceUrl;
148
149        public URLHandler(URL url) {
150            this.url = url;
151            this.codeSourceUrl = url;
152        }
153
154        void findResources(String name, ArrayList<URL> resources) {
155            URL res = findResource(name);
156            if (res != null && !resources.contains(res)) {
157                resources.add(res);
158            }
159        }
160
161        Class<?> findClass(String packageName, String name, String origName) {
162            URL resURL = targetURL(url, name);
163            if (resURL != null) {
164                try {
165                    InputStream is = resURL.openStream();
166                    return createClass(is, packageName, origName);
167                } catch (IOException e) {
168                }
169            }
170            return null;
171        }
172
173
174        Class<?> createClass(InputStream is, String packageName, String origName) {
175            if (is == null) {
176                return null;
177            }
178            byte[] clBuf;
179            try {
180                clBuf = Streams.readFully(is);
181            } catch (IOException e) {
182                return null;
183            }
184            if (packageName != null) {
185                String packageDotName = packageName.replace('/', '.');
186                Package packageObj = getPackage(packageDotName);
187                if (packageObj == null) {
188                    definePackage(packageDotName, null, null,
189                            null, null, null, null, null);
190                } else {
191                    if (packageObj.isSealed()) {
192                        throw new SecurityException("Package is sealed");
193                    }
194                }
195            }
196            return defineClass(origName, clBuf, 0, clBuf.length, new CodeSource(codeSourceUrl, (Certificate[]) null));
197        }
198
199        URL findResource(String name) {
200            URL resURL = targetURL(url, name);
201            if (resURL != null) {
202                try {
203                    URLConnection uc = resURL.openConnection();
204                    uc.getInputStream().close();
205                    // HTTP can return a stream on a non-existent file
206                    // So check for the return code;
207                    if (!resURL.getProtocol().equals("http")) {
208                        return resURL;
209                    }
210                    int code;
211                    if ((code = ((HttpURLConnection) uc).getResponseCode()) >= 200
212                            && code < 300) {
213                        return resURL;
214                    }
215                } catch (SecurityException e) {
216                    return null;
217                } catch (IOException e) {
218                    return null;
219                }
220            }
221            return null;
222        }
223
224        URL targetURL(URL base, String name) {
225            try {
226                StringBuilder fileBuilder = new StringBuilder();
227                fileBuilder.append(base.getFile());
228                URI.PATH_ENCODER.appendEncoded(fileBuilder, name);
229                String file = fileBuilder.toString();
230
231                return new URL(base.getProtocol(), base.getHost(), base.getPort(), file, null);
232            } catch (MalformedURLException e) {
233                return null;
234            }
235        }
236
237    }
238
239    class URLJarHandler extends URLHandler {
240        final JarFile jf;
241        final String prefixName;
242        final IndexFile index;
243        final Map<URL, URLHandler> subHandlers = new HashMap<URL, URLHandler>();
244
245        public URLJarHandler(URL url, URL jarURL, JarFile jf, String prefixName) {
246            super(url);
247            this.jf = jf;
248            this.prefixName = prefixName;
249            this.codeSourceUrl = jarURL;
250            final JarEntry je = jf.getJarEntry("META-INF/INDEX.LIST");
251            this.index = (je == null ? null : IndexFile.readIndexFile(jf, je, url));
252        }
253
254        public URLJarHandler(URL url, URL jarURL, JarFile jf, String prefixName, IndexFile index) {
255            super(url);
256            this.jf = jf;
257            this.prefixName = prefixName;
258            this.index = index;
259            this.codeSourceUrl = jarURL;
260        }
261
262        IndexFile getIndex() {
263            return index;
264        }
265
266        @Override
267        void findResources(String name, ArrayList<URL> resources) {
268            URL res = findResourceInOwn(name);
269            if (res != null && !resources.contains(res)) {
270                resources.add(res);
271            }
272            if (index != null) {
273                int pos = name.lastIndexOf("/");
274                // only keep the directory part of the resource
275                // as index.list only keeps track of directories and root files
276                String indexedName = (pos > 0) ? name.substring(0, pos) : name;
277                ArrayList<URL> urls = index.get(indexedName);
278                if (urls != null) {
279                    urls.remove(url);
280                    for (URL url : urls) {
281                        URLHandler h = getSubHandler(url);
282                        if (h != null) {
283                            h.findResources(name, resources);
284                        }
285                    }
286                }
287            }
288
289        }
290
291        @Override
292        Class<?> findClass(String packageName, String name, String origName) {
293            String entryName = prefixName + name;
294            JarEntry entry = jf.getJarEntry(entryName);
295            if (entry != null) {
296                /**
297                 * Avoid recursive load class, especially the class
298                 * is an implementation class of security provider
299                 * and the jar is signed.
300                 */
301                try {
302                    Manifest manifest = jf.getManifest();
303                    return createClass(entry, manifest, packageName, origName);
304                } catch (IOException e) {
305                }
306            }
307            if (index != null) {
308                ArrayList<URL> urls;
309                if (packageName == null) {
310                    urls = index.get(name);
311                } else {
312                    urls = index.get(packageName);
313                }
314                if (urls != null) {
315                    urls.remove(url);
316                    for (URL url : urls) {
317                        URLHandler h = getSubHandler(url);
318                        if (h != null) {
319                            Class<?> res = h.findClass(packageName, name, origName);
320                            if (res != null) {
321                                return res;
322                            }
323                        }
324                    }
325                }
326            }
327            return null;
328        }
329
330        private Class<?> createClass(JarEntry entry, Manifest manifest, String packageName, String origName) {
331            byte[] clBuf;
332            try {
333                InputStream is = jf.getInputStream(entry);
334                clBuf = Streams.readFully(is);
335            } catch (IOException e) {
336                return null;
337            }
338            if (packageName != null) {
339                String packageDotName = packageName.replace('/', '.');
340                Package packageObj = getPackage(packageDotName);
341                if (packageObj == null) {
342                    if (manifest != null) {
343                        definePackage(packageDotName, manifest,
344                                codeSourceUrl);
345                    } else {
346                        definePackage(packageDotName, null, null,
347                                null, null, null, null, null);
348                    }
349                } else {
350                    boolean exception = packageObj.isSealed();
351                    if (manifest != null) {
352                        if (isSealed(manifest, packageName + "/")) {
353                            exception = !packageObj
354                                    .isSealed(codeSourceUrl);
355                        }
356                    }
357                    if (exception) {
358                        throw new SecurityException(String.format("Package %s is sealed",
359                                packageName));
360                    }
361                }
362            }
363            CodeSource codeS = new CodeSource(codeSourceUrl, entry.getCertificates());
364            return defineClass(origName, clBuf, 0, clBuf.length, codeS);
365        }
366
367        URL findResourceInOwn(String name) {
368            String entryName = prefixName + name;
369            if (jf.getEntry(entryName) != null) {
370                return targetURL(url, name);
371            }
372            return null;
373        }
374
375        @Override
376        URL findResource(String name) {
377            URL res = findResourceInOwn(name);
378            if (res != null) {
379                return res;
380            }
381            if (index != null) {
382                int pos = name.lastIndexOf("/");
383                // only keep the directory part of the resource
384                // as index.list only keeps track of directories and root files
385                String indexedName = (pos > 0) ? name.substring(0, pos) : name;
386                ArrayList<URL> urls = index.get(indexedName);
387                if (urls != null) {
388                    urls.remove(url);
389                    for (URL url : urls) {
390                        URLHandler h = getSubHandler(url);
391                        if (h != null) {
392                            res = h.findResource(name);
393                            if (res != null) {
394                                return res;
395                            }
396                        }
397                    }
398                }
399            }
400            return null;
401        }
402
403        private synchronized URLHandler getSubHandler(URL url) {
404            URLHandler sub = subHandlers.get(url);
405            if (sub != null) {
406                return sub;
407            }
408            String protocol = url.getProtocol();
409            if (protocol.equals("jar")) {
410                sub = createURLJarHandler(url);
411            } else if (protocol.equals("file")) {
412                sub = createURLSubJarHandler(url);
413            } else {
414                sub = createURLHandler(url);
415            }
416            if (sub != null) {
417                subHandlers.put(url, sub);
418            }
419            return sub;
420        }
421
422        private URLHandler createURLSubJarHandler(URL url) {
423            String prefixName;
424            String file = url.getFile();
425            if (url.getFile().endsWith("!/")) {
426                prefixName = "";
427            } else {
428                int sepIdx = file.lastIndexOf("!/");
429                if (sepIdx == -1) {
430                    // Invalid URL, don't look here again
431                    return null;
432                }
433                sepIdx += 2;
434                prefixName = file.substring(sepIdx);
435            }
436            try {
437                URL jarURL = ((JarURLConnection) url
438                        .openConnection()).getJarFileURL();
439                JarURLConnection juc = (JarURLConnection) new URL(
440                        "jar", "",
441                        jarURL.toExternalForm() + "!/").openConnection();
442                JarFile jf = juc.getJarFile();
443                URLJarHandler jarH = new URLJarHandler(url, jarURL, jf, prefixName, null);
444                // TODO : to think what we should do with indexes & manifest.class file here
445                return jarH;
446            } catch (IOException e) {
447            }
448            return null;
449        }
450
451    }
452
453    class URLFileHandler extends URLHandler {
454        private String prefix;
455
456        public URLFileHandler(URL url) {
457            super(url);
458            String baseFile = url.getFile();
459            String host = url.getHost();
460            int hostLength = 0;
461            if (host != null) {
462                hostLength = host.length();
463            }
464            StringBuilder buf = new StringBuilder(2 + hostLength
465                    + baseFile.length());
466            if (hostLength > 0) {
467                buf.append("//").append(host);
468            }
469            // baseFile always ends with '/'
470            buf.append(baseFile);
471            prefix = buf.toString();
472        }
473
474        @Override
475        Class<?> findClass(String packageName, String name, String origName) {
476            String filename = prefix + name;
477            try {
478                filename = URLDecoder.decode(filename, "UTF-8");
479            } catch (IllegalArgumentException e) {
480                return null;
481            } catch (UnsupportedEncodingException e) {
482                return null;
483            }
484
485            File file = new File(filename);
486            if (file.exists()) {
487                try {
488                    InputStream is = new FileInputStream(file);
489                    return createClass(is, packageName, origName);
490                } catch (FileNotFoundException e) {
491                }
492            }
493            return null;
494        }
495
496        @Override
497        URL findResource(String name) {
498            int idx = 0;
499            String filename;
500
501            // Do not create a UNC path, i.e. \\host
502            while (idx < name.length() &&
503                   ((name.charAt(idx) == '/') || (name.charAt(idx) == '\\'))) {
504                idx++;
505            }
506
507            if (idx > 0) {
508                name = name.substring(idx);
509            }
510
511            try {
512                filename = URLDecoder.decode(prefix, "UTF-8") + name;
513
514                if (new File(filename).exists()) {
515                    return targetURL(url, name);
516                }
517                return null;
518            } catch (IllegalArgumentException e) {
519                return null;
520            } catch (UnsupportedEncodingException e) {
521                // must not happen
522                throw new AssertionError(e);
523            }
524        }
525
526    }
527
528
529    /**
530     * Constructs a new {@code URLClassLoader} instance. The newly created
531     * instance will have the system ClassLoader as its parent. URLs that end
532     * with "/" are assumed to be directories, otherwise they are assumed to be
533     * JAR files.
534     *
535     * @param urls
536     *            the list of URLs where a specific class or file could be
537     *            found.
538     */
539    public URLClassLoader(URL[] urls) {
540        this(urls, ClassLoader.getSystemClassLoader(), null);
541    }
542
543    /**
544     * Constructs a new URLClassLoader instance. The newly created instance will
545     * have the system ClassLoader as its parent. URLs that end with "/" are
546     * assumed to be directories, otherwise they are assumed to be JAR files.
547     *
548     * @param urls
549     *            the list of URLs where a specific class or file could be
550     *            found.
551     * @param parent
552     *            the class loader to assign as this loader's parent.
553     */
554    public URLClassLoader(URL[] urls, ClassLoader parent) {
555        this(urls, parent, null);
556    }
557
558    /**
559     * Adds the specified URL to the search list.
560     *
561     * @param url
562     *            the URL which is to add.
563     */
564    protected void addURL(URL url) {
565        try {
566            originalUrls.add(url);
567            searchList.add(createSearchURL(url));
568        } catch (MalformedURLException e) {
569        }
570    }
571
572    /**
573     * Returns all known URLs which point to the specified resource.
574     *
575     * @param name
576     *            the name of the requested resource.
577     * @return the enumeration of URLs which point to the specified resource.
578     * @throws IOException
579     *             if an I/O error occurs while attempting to connect.
580     */
581    @Override
582    public Enumeration<URL> findResources(final String name) throws IOException {
583        if (name == null) {
584            return null;
585        }
586        ArrayList<URL> result = new ArrayList<URL>();
587        int n = 0;
588        while (true) {
589            URLHandler handler = getHandler(n++);
590            if (handler == null) {
591                break;
592            }
593            handler.findResources(name, result);
594        }
595        return Collections.enumeration(result);
596    }
597
598    /**
599     * Gets all permissions for the specified {@code codesource}. First, this
600     * method retrieves the permissions from the system policy. If the protocol
601     * is "file:/" then a new permission, {@code FilePermission}, granting the
602     * read permission to the file is added to the permission collection.
603     * Otherwise, connecting to and accepting connections from the URL is
604     * granted.
605     *
606     * @param codesource
607     *            the code source object whose permissions have to be known.
608     * @return the list of permissions according to the code source object.
609     */
610    @Override
611    protected PermissionCollection getPermissions(final CodeSource codesource) {
612        PermissionCollection pc = super.getPermissions(codesource);
613        URL u = codesource.getLocation();
614        if (u.getProtocol().equals("jar")) {
615            try {
616                // Create a URL for the resource the jar refers to
617                u = ((JarURLConnection) u.openConnection()).getJarFileURL();
618            } catch (IOException e) {
619                // This should never occur. If it does continue using the jar
620                // URL
621            }
622        }
623        if (u.getProtocol().equals("file")) {
624            String path = u.getFile();
625            String host = u.getHost();
626            if (host != null && host.length() > 0) {
627                path = "//" + host + path;
628            }
629
630            if (File.separatorChar != '/') {
631                path = path.replace('/', File.separatorChar);
632            }
633            if (isDirectory(u)) {
634                pc.add(new FilePermission(path + "-", "read"));
635            } else {
636                pc.add(new FilePermission(path, "read"));
637            }
638        } else {
639            String host = u.getHost();
640            if (host.length() == 0) {
641                host = "localhost";
642            }
643            pc.add(new SocketPermission(host, "connect, accept"));
644        }
645        return pc;
646    }
647
648    /**
649     * Returns the search list of this {@code URLClassLoader}.
650     *
651     * @return the list of all known URLs of this instance.
652     */
653    public URL[] getURLs() {
654        return originalUrls.toArray(new URL[originalUrls.size()]);
655    }
656
657    /**
658     * Determines if the URL is pointing to a directory.
659     */
660    private static boolean isDirectory(URL url) {
661        String file = url.getFile();
662        return (file.length() > 0 && file.charAt(file.length() - 1) == '/');
663    }
664
665    /**
666     * Returns a new {@code URLClassLoader} instance for the given URLs and the
667     * system {@code ClassLoader} as its parent.
668     *
669     * @param urls
670     *            the list of URLs that is passed to the new {@code
671     *            URLClassLoader}.
672     * @return the created {@code URLClassLoader} instance.
673     */
674    public static URLClassLoader newInstance(final URL[] urls) {
675        return new URLClassLoader(urls, ClassLoader.getSystemClassLoader());
676    }
677
678    /**
679     * Returns a new {@code URLClassLoader} instance for the given URLs and the
680     * specified {@code ClassLoader} as its parent.
681     *
682     * @param urls
683     *            the list of URLs that is passed to the new URLClassLoader.
684     * @param parentCl
685     *            the parent class loader that is passed to the new
686     *            URLClassLoader.
687     * @return the created {@code URLClassLoader} instance.
688     */
689    public static URLClassLoader newInstance(final URL[] urls, final ClassLoader parentCl) {
690        return new URLClassLoader(urls, parentCl);
691    }
692
693    /**
694     * Constructs a new {@code URLClassLoader} instance. The newly created
695     * instance will have the specified {@code ClassLoader} as its parent and
696     * use the specified factory to create stream handlers. URLs that end with
697     * "/" are assumed to be directories, otherwise they are assumed to be JAR
698     * files.
699     *
700     * @param searchUrls
701     *            the list of URLs where a specific class or file could be
702     *            found.
703     * @param parent
704     *            the {@code ClassLoader} to assign as this loader's parent.
705     * @param factory
706     *            the factory that will be used to create protocol-specific
707     *            stream handlers.
708     */
709    public URLClassLoader(URL[] searchUrls, ClassLoader parent, URLStreamHandlerFactory factory) {
710        super(parent);
711        this.factory = factory;
712        int nbUrls = searchUrls.length;
713        originalUrls = new ArrayList<URL>(nbUrls);
714        handlerList = new ArrayList<URLHandler>(nbUrls);
715        searchList = Collections.synchronizedList(new ArrayList<URL>(nbUrls));
716        for (int i = 0; i < nbUrls; i++) {
717            originalUrls.add(searchUrls[i]);
718            try {
719                searchList.add(createSearchURL(searchUrls[i]));
720            } catch (MalformedURLException e) {
721            }
722        }
723    }
724
725    /**
726     * Tries to locate and load the specified class using the known URLs. If the
727     * class could be found, a class object representing the loaded class will
728     * be returned.
729     *
730     * @throws ClassNotFoundException
731     *             if the specified class cannot be loaded.
732     */
733    @Override
734    protected Class<?> findClass(final String className) throws ClassNotFoundException {
735        String partialName = className.replace('.', '/');
736        final String classFileName = new StringBuilder(partialName).append(".class").toString();
737        String packageName = null;
738        int position = partialName.lastIndexOf('/');
739        if ((position = partialName.lastIndexOf('/')) != -1) {
740            packageName = partialName.substring(0, position);
741        }
742        int n = 0;
743        while (true) {
744            URLHandler handler = getHandler(n++);
745            if (handler == null) {
746                break;
747            }
748            Class<?> res = handler.findClass(packageName, classFileName, className);
749            if (res != null) {
750                return res;
751            }
752        }
753        throw new ClassNotFoundException(className);
754    }
755
756    /**
757     * Returns an URL that will be checked if it contains the class or resource.
758     * If the file component of the URL is not a directory, a Jar URL will be
759     * created.
760     *
761     * @return java.net.URL a test URL
762     */
763    private URL createSearchURL(URL url) throws MalformedURLException {
764        if (url == null) {
765            return url;
766        }
767
768        String protocol = url.getProtocol();
769
770        if (isDirectory(url) || protocol.equals("jar")) {
771            return url;
772        }
773        if (factory == null) {
774            return new URL("jar", "",
775                    -1, url.toString() + "!/");
776        }
777        // use jar protocol as the stream handler protocol
778        return new URL("jar", "",
779                -1, url.toString() + "!/",
780                factory.createURLStreamHandler("jar"));
781    }
782
783    /**
784     * Returns an URL referencing the specified resource or {@code null} if the
785     * resource could not be found.
786     *
787     * @param name
788     *            the name of the requested resource.
789     * @return the URL which points to the given resource.
790     */
791    @Override
792    public URL findResource(final String name) {
793        if (name == null) {
794            return null;
795        }
796        int n = 0;
797        while (true) {
798            URLHandler handler = getHandler(n++);
799            if (handler == null) {
800                break;
801            }
802            URL res = handler.findResource(name);
803            if (res != null) {
804                return res;
805            }
806        }
807        return null;
808    }
809
810    private URLHandler getHandler(int num) {
811        if (num < handlerList.size()) {
812            return handlerList.get(num);
813        }
814        makeNewHandler();
815        if (num < handlerList.size()) {
816            return handlerList.get(num);
817        }
818        return null;
819    }
820
821    private synchronized void makeNewHandler() {
822        while (!searchList.isEmpty()) {
823            URL nextCandidate = searchList.remove(0);
824            if (nextCandidate == null) {
825                throw new NullPointerException("nextCandidate == null");
826            }
827            if (!handlerMap.containsKey(nextCandidate)) {
828                URLHandler result;
829                String protocol = nextCandidate.getProtocol();
830                if (protocol.equals("jar")) {
831                    result = createURLJarHandler(nextCandidate);
832                } else if (protocol.equals("file")) {
833                    result = createURLFileHandler(nextCandidate);
834                } else {
835                    result = createURLHandler(nextCandidate);
836                }
837                if (result != null) {
838                    handlerMap.put(nextCandidate, result);
839                    handlerList.add(result);
840                    return;
841                }
842            }
843        }
844    }
845
846    private URLHandler createURLHandler(URL url) {
847        return new URLHandler(url);
848    }
849
850    private URLHandler createURLFileHandler(URL url) {
851        return new URLFileHandler(url);
852    }
853
854    private URLHandler createURLJarHandler(URL url) {
855        String prefixName;
856        String file = url.getFile();
857        if (url.getFile().endsWith("!/")) {
858            prefixName = "";
859        } else {
860            int sepIdx = file.lastIndexOf("!/");
861            if (sepIdx == -1) {
862                // Invalid URL, don't look here again
863                return null;
864            }
865            sepIdx += 2;
866            prefixName = file.substring(sepIdx);
867        }
868        try {
869            URL jarURL = ((JarURLConnection) url
870                    .openConnection()).getJarFileURL();
871            JarURLConnection juc = (JarURLConnection) new URL(
872                    "jar", "",
873                    jarURL.toExternalForm() + "!/").openConnection();
874            JarFile jf = juc.getJarFile();
875            URLJarHandler jarH = new URLJarHandler(url, jarURL, jf, prefixName);
876
877            if (jarH.getIndex() == null) {
878                try {
879                    Manifest manifest = jf.getManifest();
880                    if (manifest != null) {
881                        String classpath = manifest.getMainAttributes().getValue(
882                                Attributes.Name.CLASS_PATH);
883                        if (classpath != null) {
884                            searchList.addAll(0, getInternalURLs(url, classpath));
885                        }
886                    }
887                } catch (IOException e) {
888                }
889            }
890            return jarH;
891        } catch (IOException e) {
892        }
893        return null;
894    }
895
896    /**
897     * Defines a new package using the information extracted from the specified
898     * manifest.
899     *
900     * @param packageName
901     *            the name of the new package.
902     * @param manifest
903     *            the manifest containing additional information for the new
904     *            package.
905     * @param url
906     *            the URL to the code source for the new package.
907     * @return the created package.
908     * @throws IllegalArgumentException
909     *             if a package with the given name already exists.
910     */
911    protected Package definePackage(String packageName, Manifest manifest,
912                                    URL url) throws IllegalArgumentException {
913        Attributes mainAttributes = manifest.getMainAttributes();
914        String dirName = packageName.replace('.', '/') + "/";
915        Attributes packageAttributes = manifest.getAttributes(dirName);
916        boolean noEntry = false;
917        if (packageAttributes == null) {
918            noEntry = true;
919            packageAttributes = mainAttributes;
920        }
921        String specificationTitle = packageAttributes
922                .getValue(Attributes.Name.SPECIFICATION_TITLE);
923        if (specificationTitle == null && !noEntry) {
924            specificationTitle = mainAttributes
925                    .getValue(Attributes.Name.SPECIFICATION_TITLE);
926        }
927        String specificationVersion = packageAttributes
928                .getValue(Attributes.Name.SPECIFICATION_VERSION);
929        if (specificationVersion == null && !noEntry) {
930            specificationVersion = mainAttributes
931                    .getValue(Attributes.Name.SPECIFICATION_VERSION);
932        }
933        String specificationVendor = packageAttributes
934                .getValue(Attributes.Name.SPECIFICATION_VENDOR);
935        if (specificationVendor == null && !noEntry) {
936            specificationVendor = mainAttributes
937                    .getValue(Attributes.Name.SPECIFICATION_VENDOR);
938        }
939        String implementationTitle = packageAttributes
940                .getValue(Attributes.Name.IMPLEMENTATION_TITLE);
941        if (implementationTitle == null && !noEntry) {
942            implementationTitle = mainAttributes
943                    .getValue(Attributes.Name.IMPLEMENTATION_TITLE);
944        }
945        String implementationVersion = packageAttributes
946                .getValue(Attributes.Name.IMPLEMENTATION_VERSION);
947        if (implementationVersion == null && !noEntry) {
948            implementationVersion = mainAttributes
949                    .getValue(Attributes.Name.IMPLEMENTATION_VERSION);
950        }
951        String implementationVendor = packageAttributes
952                .getValue(Attributes.Name.IMPLEMENTATION_VENDOR);
953        if (implementationVendor == null && !noEntry) {
954            implementationVendor = mainAttributes
955                    .getValue(Attributes.Name.IMPLEMENTATION_VENDOR);
956        }
957
958        return definePackage(packageName, specificationTitle,
959                specificationVersion, specificationVendor, implementationTitle,
960                implementationVersion, implementationVendor, isSealed(manifest,
961                dirName) ? url : null);
962    }
963
964    private boolean isSealed(Manifest manifest, String dirName) {
965        Attributes attributes = manifest.getAttributes(dirName);
966        if (attributes != null) {
967            String value = attributes.getValue(Attributes.Name.SEALED);
968            if (value != null) {
969                return value.equalsIgnoreCase("true");
970            }
971        }
972        Attributes mainAttributes = manifest.getMainAttributes();
973        String value = mainAttributes.getValue(Attributes.Name.SEALED);
974        return (value != null && value.equalsIgnoreCase("true"));
975    }
976
977    /**
978     * returns URLs referenced in the string classpath.
979     *
980     * @param root
981     *            the jar URL that classpath is related to
982     * @param classpath
983     *            the relative URLs separated by spaces
984     * @return URL[] the URLs contained in the string classpath.
985     */
986    private ArrayList<URL> getInternalURLs(URL root, String classpath) {
987        // Class-path attribute is composed of space-separated values.
988        StringTokenizer tokenizer = new StringTokenizer(classpath);
989        ArrayList<URL> addedURLs = new ArrayList<URL>();
990        String file = root.getFile();
991        int jarIndex = file.lastIndexOf("!/") - 1;
992        int index = file.lastIndexOf("/", jarIndex) + 1;
993        if (index == 0) {
994            index = file.lastIndexOf(
995                    System.getProperty("file.separator"), jarIndex) + 1;
996        }
997        file = file.substring(0, index);
998        while (tokenizer.hasMoreElements()) {
999            String element = tokenizer.nextToken();
1000            if (!element.isEmpty()) {
1001                try {
1002                    // Take absolute path case into consideration
1003                    URL url = new URL(new URL(file), element);
1004                    addedURLs.add(createSearchURL(url));
1005                } catch (MalformedURLException e) {
1006                    // Nothing is added
1007                }
1008            }
1009        }
1010        return addedURLs;
1011    }
1012}
1013