Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: edu/emory/mathcs/util/classloader/ResourceLoader.java


1   /* ***** BEGIN LICENSE BLOCK *****
2    * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3    *
4    * The contents of this file are subject to the Mozilla Public License Version
5    * 1.1 (the "License"); you may not use this file except in compliance with
6    * the License. You may obtain a copy of the License at
7    * http://www.mozilla.org/MPL/
8    *
9    * Software distributed under the License is distributed on an "AS IS" basis,
10   * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11   * for the specific language governing rights and limitations under the
12   * License.
13   *
14   * The Original Code is the Emory Utilities.
15   *
16   * The Initial Developer of the Original Code is
17   * The Distributed Computing Laboratory, Emory University.
18   * Portions created by the Initial Developer are Copyright (C) 2002
19   * the Initial Developer. All Rights Reserved.
20   *
21   * Alternatively, the contents of this file may be used under the terms of
22   * either the GNU General Public License Version 2 or later (the "GPL"), or
23   * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
24   * in which case the provisions of the GPL or the LGPL are applicable instead
25   * of those above. If you wish to allow use of your version of this file only
26   * under the terms of either the GPL or the LGPL, and not to allow others to
27   * use your version of this file under the terms of the MPL, indicate your
28   * decision by deleting the provisions above and replace them with the notice
29   * and other provisions required by the GPL or the LGPL. If you do not delete
30   * the provisions above, a recipient may use your version of this file under
31   * the terms of any one of the MPL, the GPL or the LGPL.
32   *
33   * ***** END LICENSE BLOCK ***** */
34  
35  package edu.emory.mathcs.util.classloader;
36  
37  import java.io.*;
38  import java.net.*;
39  import java.security.*;
40  import java.util.*;
41  import java.util.jar.*;
42  import edu.emory.mathcs.util.classloader.jar.JarURLStreamHandler;
43  
44  /**
45   * This class aids in accessing remote resources referred by URLs.
46   * The URLs are resolved into {@link ResourceHandle resource handles} which can
47   * be used to access the resources directly and uniformly, regardless of the
48   * URL type. The resource loader
49   * is particularly useful when dealing with resources fetched from JAR files.
50   * It maintains the cache of opened JAR files (so that so that
51   * subsequent requests for resources coming from the same base Jar file can be
52   * handled efficiently). It fully supports JAR class-path (references from
53   * a JAR file to other JAR files) and JAR index (JAR containing information
54   * about content of other JARs). The caching policy of downloaded JAR files can
55   * be customized via the constructor parameter <code>jarHandler</code>; the
56   * default policy is to use separate cache per each
57   * ResourceLoader instance.
58   * <p>
59   * This class is particularly useful when implementing custom class loaders.
60   * It provides bottom-level resource fetching functionality. By using one of
61   * the loader methods which accepts an array of URLs, it
62   * is straightforward to implement class-path searching similar to that
63   * of {@link java.net.URLClassLoader}, with JAR dependencies (Class-Path)
64   * properly resolved and with JAR indexes properly handled.
65   * <p>
66   * This class provides two set of methods: <i>get</i> methods that return
67   * {@link ResourceHandle}s (or their enumerations) and <i>find</i> methods that
68   * return URLs (or their enumerations). If the resource is not found,
69   * null (or empty enumeration) is returned. Resource handles represent a
70   * connection to the resource and they should be closed when done
71   * processing, just like input streams. In contrast, find methods return
72   * URLs that can be used to open multiple connections to the resource. In
73   * typical class loader applications, when a single retrieval is sufficient,
74   * it is preferable to use <i>get</i> methods since they pose slightly smaller
75   * communication overhead.
76   *
77   * @author Dawid Kurzyniec
78   * @version 1.0
79   */
80  public class ResourceLoader {
81  
82      private static final String JAR_INDEX_ENTRY_NAME = "META-INF/INDEX.LIST";
83  
84      final URLStreamHandler jarHandler;
85  
86      final Map url2jarInfo = new HashMap();
87  
88      /**
89       * Constructs new ResourceLoadeer with default JAR caching policy, that is,
90       * to create and use separate cache for this ResourceLoader instance.
91       */
92      public ResourceLoader() {
93          this(new JarURLStreamHandler());
94      }
95  
96      /**
97       * Constructs new ResourceLoadeer with specified JAR file handler which can
98       * implement custom JAR caching policy.
99       * @param jarHandler JAR file handler
100      */
101     public ResourceLoader(URLStreamHandler jarHandler) {
102         this.jarHandler = jarHandler;
103     }
104 
105     /**
106      * Gets resource with given name at the given source URL. If the URL points
107      * to a directory, the name is the file path relative to this directory.
108      * If the URL points to a JAR file, the name identifies an entry in that
109      * JAR file. If the URL points to a JAR file, the resource is not found
110      * in that JAR file, and the JAR file has Class-Path attribute, the
111      * JAR files identified in the Class-Path are also searched for the
112      * resource.
113      * @param source the source URL
114      * @param name the resource name
115      * @return handle representing the resource, or null if not found
116      */
117     public ResourceHandle getResource(URL source, String name) {
118         return getResource(source, name, new HashSet(), null);
119     }
120 
121     /**
122      * Gets resource with given name at the given search path. The path is
123      * searched iteratively, one URL at a time. If the URL points
124      * to a directory, the name is the file path relative to this directory.
125      * If the URL points to the JAR file, the name identifies an entry in that
126      * JAR file. If the URL points to the JAR file, the resource is not found
127      * in that JAR file, and the JAR file has Class-Path attribute, the
128      * JAR files identified in the Class-Path are also searched for the
129      * resource.
130 
131      * @param source the source URL
132      * @param name the resource name
133      * @return handle representing the resource, or null if not found
134      */
135     public ResourceHandle getResource(URL[] sources, String name) {
136         Set visited = new HashSet();
137         for (int i=0; i<sources.length; i++) {
138             ResourceHandle h = getResource(sources[i], name, visited, null);
139             if (h != null) return h;
140         }
141         return null;
142     }
143 
144     /**
145      * Gets all resources with given name at the given source URL. If the URL
146      * points to a directory, the name is the file path relative to this
147      * directory. If the URL points to a JAR file, the name identifies an entry
148      * in that JAR file. If the URL points to a JAR file, the resource is not
149      * found in that JAR file, and the JAR file has Class-Path attribute, the
150      * JAR files identified in the Class-Path are also searched for the
151      * resource.
152      * <p>
153      * The search is lazy, i.e. "find next resource" operation is triggered
154      * by calling {@link Enumeration#hasMoreElements}.
155      *
156      * @param source the source URL
157      * @param name the resource name
158      * @return enumeration of resource handles representing the resources
159      */
160     public Enumeration getResources(URL source, String name) {
161         return new ResourceEnumeration(new URL[] {source}, name, false);
162     }
163 
164     /**
165      * Gets all resources with given name at the given search path. If the URL
166      * points to a directory, the name is the file path relative to this
167      * directory. If the URL points to a JAR file, the name identifies an entry
168      * in that JAR file. If the URL points to a JAR file, the resource is not
169      * found in that JAR file, and the JAR file has Class-Path attribute, the
170      * JAR files identified in the Class-Path are also searched for the
171      * resource.
172      * <p>
173      * The search is lazy, i.e. "find next resource" operation is triggered
174      * by calling {@link Enumeration#hasMoreElements}.
175      *
176      * @param source the source URL
177      * @param name the resource name
178      * @return enumeration of resource handles representing the resources
179      */
180     public Enumeration getResources(URL[] sources, String name) {
181         return new ResourceEnumeration((URL[])sources.clone(), name, false);
182     }
183 
184     private ResourceHandle getResource(final URL source, String name,
185                                        Set visitedJars, Set skip) {
186 
187         name = ResourceUtils.canonizePath(name);
188         if (isDir(source)) {
189             // plain resource
190             final URL url;
191             try {
192                 // escape spaces etc. to make sure url is well-formed
193                 URI relUri = new URI(null, null, null, -1, name, null, null);
194                 url = new URL(source, relUri.getRawPath());
195             }
196             catch (URISyntaxException e) {
197                 throw new IllegalArgumentException("Illegal resource name: " + name);
198             }
199             catch (MalformedURLException e) {
200                 return null;
201             }
202 
203             if (skip != null && skip.contains(url)) return null;
204             final URLConnection conn;
205             try {
206                 conn = url.openConnection();
207                 conn.getInputStream();
208             }
209             catch (IOException e) {
210                 return null;
211             }
212             final String finalName = name;
213             return new ResourceHandle() {
214                 public String getName()             { return finalName; }
215                 public URL getURL()                 { return url; }
216                 public URL getCodeSourceURL()       { return source; }
217                 public InputStream getInputStream() throws IOException {
218                     return conn.getInputStream();
219                 }
220                 public int getContentLength() throws IOException {
221                     return conn.getContentLength();
222                 }
223                 public void close() {
224                     try {
225                         getInputStream().close();
226                     } catch (IOException e) {}
227                 }
228             };
229         }
230         else {
231             // we deal with a JAR file here
232             try {
233                 return getJarInfo(source).getResource(name, visitedJars, skip);
234             }
235             catch (MalformedURLException e) {
236                 return null;
237             }
238         }
239     }
240 
241     /**
242      * Fined resource with given name at the given source URL. If the URL points
243      * to a directory, the name is the file path relative to this directory.
244      * If the URL points to a JAR file, the name identifies an entry in that
245      * JAR file. If the URL points to a JAR file, the resource is not found
246      * in that JAR file, and the JAR file has Class-Path attribute, the
247      * JAR files identified in the Class-Path are also searched for the
248      * resource.
249      * @param source the source URL
250      * @param name the resource name
251      * @return URL of the resource, or null if not found
252      */
253     public URL findResource(URL source, String name) {
254         return findResource(source, name, new HashSet(), null);
255     }
256 
257     /**
258      * Finds resource with given name at the given search path. The path is
259      * searched iteratively, one URL at a time. If the URL points
260      * to a directory, the name is the file path relative to this directory.
261      * If the URL points to the JAR file, the name identifies an entry in that
262      * JAR file. If the URL points to the JAR file, the resource is not found
263      * in that JAR file, and the JAR file has Class-Path attribute, the
264      * JAR files identified in the Class-Path are also searched for the
265      * resource.
266 
267      * @param source the source URL
268      * @param name the resource name
269      * @return URL of the resource, or null if not found
270      */
271     public URL findResource(URL[] sources, String name) {
272         Set visited = new HashSet();
273         for (int i=0; i<sources.length; i++) {
274             URL url = findResource(sources[i], name, visited, null);
275             if (url != null) return url;
276         }
277         return null;
278     }
279 
280     /**
281      * Finds all resources with given name at the given source URL. If the URL
282      * points to a directory, the name is the file path relative to this
283      * directory. If the URL points to a JAR file, the name identifies an entry
284      * in that JAR file. If the URL points to a JAR file, the resource is not
285      * found in that JAR file, and the JAR file has Class-Path attribute, the
286      * JAR files identified in the Class-Path are also searched for the
287      * resource.
288      * <p>
289      * The search is lazy, i.e. "find next resource" operation is triggered
290      * by calling {@link Enumeration#hasMoreElements}.
291      *
292      * @param source the source URL
293      * @param name the resource name
294      * @return enumeration of URLs of the resources
295      */
296     public Enumeration findResources(URL source, String name) {
297         return new ResourceEnumeration(new URL[] {source}, name, true);
298     }
299 
300     /**
301      * Finds all resources with given name at the given search path. If the URL
302      * points to a directory, the name is the file path relative to this
303      * directory. If the URL points to a JAR file, the name identifies an entry
304      * in that JAR file. If the URL points to a JAR file, the resource is not
305      * found in that JAR file, and the JAR file has Class-Path attribute, the
306      * JAR files identified in the Class-Path are also searched for the
307      * resource.
308      * <p>
309      * The search is lazy, i.e. "find next resource" operation is triggered
310      * by calling {@link Enumeration#hasMoreElements}.
311      *
312      * @param source the source URL
313      * @param name the resource name
314      * @return enumeration of URLs of the resources
315      */
316     public Enumeration findResources(URL[] sources, String name) {
317         return new ResourceEnumeration((URL[])sources.clone(), name, true);
318     }
319 
320 
321     private URL findResource(final URL source, String name,
322                              Set visitedJars, Set skip) {
323         URL url;
324         name = ResourceUtils.canonizePath(name);
325         if (isDir(source)) {
326             // plain resource
327             try {
328                 url = new URL(source, name);
329             }
330             catch (MalformedURLException e) {
331                 return null;
332             }
333             if (skip != null && skip.contains(url)) return null;
334             final URLConnection conn;
335             try {
336                 conn = url.openConnection();
337                 if (conn instanceof HttpURLConnection) {
338                     HttpURLConnection httpConn = (HttpURLConnection)conn;
339                     httpConn.setRequestMethod("HEAD");
340                     if (httpConn.getResponseCode() >= 400) return null;
341                 }
342                 else {
343                     conn.getInputStream().close();
344                 }
345             }
346             catch (IOException e) {
347                 return null;
348             }
349             return url;
350         }
351         else {
352             // we deal with a JAR file here
353             try {
354                 ResourceHandle rh = getJarInfo(source).getResource(name, visitedJars, skip);
355                 return (rh != null) ? rh.getURL() : null;
356             }
357             catch (MalformedURLException e) {
358                 return null;
359             }
360         }
361 
362     }
363 
364     /**
365      * Test whether given URL points to a directory. URL is deemed to point
366      * to a directory if has non-null "file" component ending with "/".
367      * @param url the URL to test
368      * @return true if the URL points to a directory, false otherwise
369      */
370     protected static boolean isDir(URL url) {
371         String file = url.getFile();
372         return (file != null && file.endsWith("/"));
373     }
374 
375     private static class JarInfo {
376         final ResourceLoader loader;
377         final URL source;         // "real" jar file path
378         final URL base;           // "jar:{base}!/"
379         JarFile jar;
380         boolean resolved;
381         Permission perm;
382         URL[] classPath;
383         String[] index;
384 
385         JarInfo(ResourceLoader loader, URL source) throws MalformedURLException {
386             this.loader = loader;
387             this.source = source;
388             this.base = new URL("jar", "", -1, source + "!/", loader.jarHandler);
389         }
390 
391         public ResourceHandle getResource(String name) {
392             return getResource(name, new HashSet());
393         }
394 
395         ResourceHandle getResource(String name, Set visited) {
396             return getResource(name, visited, null);
397         }
398 
399         ResourceHandle getResource(String name, Set visited, Set skip) {
400             visited.add(source);
401             URL url;
402             try {
403                 // escape spaces etc. to make sure url is well-formed
404                 URI relUri = new URI(null, null, null, -1, name, null, null);
405                 url = new URL(base, relUri.getRawPath());
406             }
407             catch (URISyntaxException e) {
408                 throw new IllegalArgumentException("Illegal resource name: " + name);
409             }
410             catch (MalformedURLException e) {
411                 return null;
412             }
413             try {
414                 JarFile jfile = getJarFileIfPossiblyContains(name);
415                 if (jfile != null) {
416                     JarEntry jentry = jar.getJarEntry(name);
417                     if (jentry != null && (skip == null || !skip.contains(url))) {
418                         return new JarResourceHandle(jfile, jentry, url, source);
419                     }
420                 }
421             }
422             catch (IOException e) {
423                 return null;
424             }
425 
426             // not in here, but check also the dependencies
427             for (int i=0; i<classPath.length; i++) {
428                 URL cpUrl = classPath[i];
429                 if (visited.contains(cpUrl)) continue;
430                 JarInfo depJInfo;
431                 try {
432                     depJInfo = loader.getJarInfo(cpUrl);
433                     ResourceHandle rh = depJInfo.getResource(name, visited, skip);
434                     if (rh != null) return rh;
435                 }
436                 catch (MalformedURLException e) {
437                     // continue with other URLs
438                 }
439             }
440 
441             // not found
442             return null;
443         }
444 
445         synchronized void setIndex(List newIndex) {
446             if (jar != null) {
447                 // already loaded; no need for index
448                 return;
449             }
450             if (index != null) {
451                 // verification - previously declared content must remain there
452                 Set violating = new HashSet(Arrays.asList(index));
453                 violating.removeAll(newIndex);
454                 if (!violating.isEmpty()) {
455                     throw new RuntimeException("Invalid JAR index: " +
456                         "the following entries were previously declared, but " +
457                         "they are not present in the new index: " +
458                         violating.toString());
459                 }
460             }
461             this.index = (String[])newIndex.toArray(new String[newIndex.size()]);
462             Arrays.sort(this.index);
463         }
464 
465         public JarFile getJarFileIfPossiblyContains(String name) throws IOException {
466             synchronized (this) {
467                 if (jar != null) {
468                     // make sure we would be allowed to load it ourselves
469                     SecurityManager security = System.getSecurityManager();
470                     if (security != null) {
471                         security.checkPermission(perm);
472                     }
473 
474                     // other thread may still be updating indexes of depentent
475                     // JAR files
476                     try {
477                         while (!resolved) wait();
478                     }
479                     catch (InterruptedException e) {
480                         throw new IOException("Interrupted");
481                     }
482                     return jar;
483                 }
484 
485                 if (index != null) {
486                     // we may be able to respond negatively w/o loading the JAR
487                     int pos = name.lastIndexOf('/');
488                     if (pos > 0) name = name.substring(0, pos);
489                     if (Arrays.binarySearch(index, name) < 0) return null;
490                 }
491 
492                 // load the JAR
493                 JarURLConnection conn = (JarURLConnection)base.openConnection();
494                 this.perm = conn.getPermission();
495                 JarFile jar = conn.getJarFile();
496                 // conservatively check if index is accurate, i.e. does not
497                 // contain entries which are not in the JAR file
498                 if (index != null) {
499                     Set indices = new HashSet(Arrays.asList(index));
500                     Enumeration entries = jar.entries();
501                     while (entries.hasMoreElements()) {
502                         JarEntry entry = (JarEntry)entries.nextElement();
503                         String indexEntry = entry.getName();
504                         // for non-top, find the package name
505                         int pos = indexEntry.lastIndexOf('/');
506                         if (pos > 0) {
507                             indexEntry = indexEntry.substring(0, pos);
508                         }
509                         indices.remove(indexEntry);
510                     }
511                     if (!indices.isEmpty()) {
512                         throw new RuntimeException("Invalid JAR index: " +
513                             "the following entries not found in JAR: " +
514                             indices);
515                     }
516                 }
517                 this.jar = jar;
518                 this.classPath = parseClassPath(jar, source);
519             }
520             // just loaded the JAR - need to resolve the index
521             try {
522                 Map indexes = parseDependentJarIndex(this.source, jar);
523                 for (Iterator itr = indexes.entrySet().iterator(); itr.hasNext();) {
524                     Map.Entry entry = (Map.Entry)itr.next();
525                     URL url = (URL)entry.getKey();
526                     List index = (List)entry.getValue();
527                     loader.getJarInfo(url).setIndex(index);
528                 }
529             }
530             finally {
531                 synchronized (this) {
532                     this.resolved = true;
533                     notifyAll();
534                 }
535             }
536             return jar;
537         }
538     }
539 
540     private JarInfo getJarInfo(URL url) throws MalformedURLException {
541         JarInfo jinfo;
542         synchronized (url2jarInfo) {
543             jinfo = (JarInfo)url2jarInfo.get(url);
544             if (jinfo == null) {
545                 jinfo = new JarInfo(this, url);
546                 url2jarInfo.put(url, jinfo);
547             }
548         }
549         return jinfo;
550     }
551 
552     private static class JarResourceHandle extends ResourceHandle {
553         final JarFile jar;
554         final JarEntry jentry;
555         final URL url;
556         final URL codeSource;
557         JarResourceHandle(JarFile jar, JarEntry jentry, URL url, URL codeSource) {
558             this.jar = jar;
559             this.jentry = jentry;
560             this.url = url;
561             this.codeSource = codeSource;
562         }
563         public String getName() {
564             return jentry.getName();
565         }
566         public URL getURL() {
567             return url;
568         }
569         public URL getCodeSourceURL() {
570             return codeSource;
571         }
572         public InputStream getInputStream() throws IOException {
573             return jar.getInputStream(jentry);
574         }
575         public int getContentLength() throws IOException {
576             return (int)jentry.getSize();
577         }
578         public void close() {}
579     }
580 
581     private static Map parseDependentJarIndex(URL cxt, JarFile jar) throws IOException {
582         JarEntry entry = jar.getJarEntry(JAR_INDEX_ENTRY_NAME);
583         if (entry == null) return Collections.EMPTY_MAP;
584         InputStream is = jar.getInputStream(entry);
585         BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
586 
587         Map result = new LinkedHashMap();
588 
589         String line;
590 
591         // ignore this JAR own's index
592         do { line = reader.readLine(); }
593         while (line != null && line.trim().length() > 0);
594 
595         URL currentURL;
596         List currentList = null;
597         while (true) {
598             // skip the blank line
599             line = reader.readLine();
600             if (line == null) {
601                 return result;
602             }
603 
604             currentURL = new URL(cxt, line);
605             currentList = new ArrayList();
606             result.put(currentURL, currentList);
607 
608             while (true) {
609                 line = reader.readLine();
610                 if (line == null || line.trim().length() == 0) break;
611                 currentList.add(line);
612             }
613         }
614     }
615 
616     private static URL[] parseClassPath(JarFile jar, URL source) throws IOException {
617         Manifest man = jar.getManifest();
618         if (man == null) return new URL[0];
619         Attributes attr = man.getMainAttributes();
620         if (attr == null) return new URL[0];
621         String cp = attr.getValue(Attributes.Name.CLASS_PATH);
622         if (cp == null) return new URL[0];
623         StringTokenizer tokenizer = new StringTokenizer(cp);
624         List cpList = new ArrayList();
625         URI sourceURI = URI.create(source.toString());
626         while (tokenizer.hasMoreTokens()) {
627             String token = tokenizer.nextToken();
628             try {
629                 try {
630                     URI uri = new URI(token);
631                     if (!uri.isAbsolute()) {
632                         uri = sourceURI.resolve(uri);
633                     }
634                     cpList.add(uri.toURL());
635                 }
636                 catch (URISyntaxException e) {
637                     // tolerate malformed URIs for backward-compatibility
638                     URL url = new URL(source, token);
639                     cpList.add(url);
640                 }
641             }
642             catch (MalformedURLException e) {
643                 throw new IOException(e.getMessage());
644             }
645         }
646         return (URL[])cpList.toArray(new URL[cpList.size()]);
647     }
648 
649     private class ResourceEnumeration implements Enumeration {
650         final URL[] urls;
651         final String name;
652         final boolean findOnly;
653         int idx;
654         Object next;
655         Set previousURLs = new HashSet();
656         ResourceEnumeration(URL[] urls, String name, boolean findOnly) {
657             this.urls = urls;
658             this.name = name;
659             this.findOnly = findOnly;
660             this.idx = 0;
661         }
662         public boolean hasMoreElements() {
663             fetchNext();
664             return (next != null);
665         }
666         public Object nextElement() {
667             fetchNext();
668             if (next == null) throw new NoSuchElementException();
669             return next;
670         }
671         private void fetchNext() {
672             if (next != null) return;
673             while (idx < urls.length) {
674                 Object found;
675                 if (findOnly) {
676                     URL url = findResource(urls[idx], name, new HashSet(), previousURLs);
677                     if (url != null) {
678                         previousURLs.add(url);
679                         next = url;
680                         return;
681                     }
682                 }
683                 else {
684                     ResourceHandle h = getResource(urls[idx], name, new HashSet(),
685                                                    previousURLs);
686                     if (h != null) {
687                         previousURLs.add(h.getURL());
688                         next = h;
689                         return;
690                     }
691                 }
692                 idx++;
693             }
694         }
695     }
696 }