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 }