/*
* Written by Dawid Kurzyniec and released to the public domain, as explained
* at http://creativecommons.org/licenses/publicdomain
*/
package edu.emory.mathcs.util.classloader;
import edu.emory.mathcs.util.classloader.jar.JarURLStreamHandler;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.*;
import java.security.Permission;
import java.security.cert.Certificate;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
/**
* This class aids in accessing remote resources referred by URLs.
* The URLs are resolved into {@link edu.emory.mathcs.util.classloader.ResourceHandle resource handles} which can
* be used to access the resources directly and uniformly, regardless of the
* URL type. The resource loader
* is particularly useful when dealing with resources fetched from JAR files.
* It maintains the cache of opened JAR files (so that so that
* subsequent requests for resources coming from the same base Jar file can be
* handled efficiently). It fully supports JAR class-path (references from
* a JAR file to other JAR files) and JAR index (JAR containing information
* about content of other JARs). The caching policy of downloaded JAR files can
* be customized via the constructor parameter <code>jarHandler</code>; the
* default policy is to use separate cache per each
* ResourceLoader instance.
* <p>
* This class is particularly useful when implementing custom class loaders.
* It provides bottom-level resource fetching functionality. By using one of
* the loader methods which accepts an array of URLs, it
* is straightforward to implement class-path searching similar to that
* of {@link java.net.URLClassLoader}, with JAR dependencies (Class-Path)
* properly resolved and with JAR indexes properly handled.
* <p>
* This class provides two set of methods: <i>get</i> methods that return
* {@link edu.emory.mathcs.util.classloader.ResourceHandle}s (or their enumerations) and <i>find</i> methods that
* return URLs (or their enumerations). If the resource is not found,
* null (or empty enumeration) is returned. Resource handles represent a
* connection to the resource and they should be closed when done
* processing, just like input streams. In contrast, find methods return
* URLs that can be used to open multiple connections to the resource. In
* typical class loader applications, when a single retrieval is sufficient,
* it is preferable to use <i>get</i> methods since they pose slightly smaller
* communication overhead.
*
* @author Dawid Kurzyniec
* @version 1.0
*/
@SuppressWarnings({"unchecked", "PMD"})
public class ResourceLoader {
private static final String JAR_INDEX_ENTRY_NAME = "META-INF/INDEX.LIST";
final URLStreamHandler jarHandler;
final Map url2jarInfo = new HashMap();
/**
* Constructs new ResourceLoadeer with default JAR caching policy, that is,
* to create and use separate cache for this ResourceLoader instance.
*/
public ResourceLoader() {
this(new JarURLStreamHandler());
}
/**
* Constructs new ResourceLoader with specified JAR file handler which can
* implement custom JAR caching policy.
* @param jarHandler JAR file handler
*/
public ResourceLoader(URLStreamHandler jarHandler) {
this.jarHandler = jarHandler;
}
/**
* Gets resource with given name at the given source URL. If the URL points
* to a directory, the name is the file path relative to this directory.
* If the URL points to a JAR file, the name identifies an entry in that
* JAR file. If the URL points to a JAR file, the resource is not found
* in that JAR file, and the JAR file has Class-Path attribute, the
* JAR files identified in the Class-Path are also searched for the
* resource.
* @param source the source URL
* @param name the resource name
* @return handle representing the resource, or null if not found
*/
public ResourceHandle getResource(URL source, String name) {
return getResource(source, name, new HashSet(), null);
}
/**
* Gets resource with given name at the given search path. The path is
* searched iteratively, one URL at a time. If the URL points
* to a directory, the name is the file path relative to this directory.
* If the URL points to the JAR file, the name identifies an entry in that
* JAR file. If the URL points to the JAR file, the resource is not found
* in that JAR file, and the JAR file has Class-Path attribute, the
* JAR files identified in the Class-Path are also searched for the
* resource.
* @param sources the source URL path
* @param name the resource name
* @return handle representing the resource, or null if not found
*/
public ResourceHandle getResource(URL[] sources, String name) {
Set visited = new HashSet();
for (int i=0; i<sources.length; i++) {
ResourceHandle h = getResource(sources[i], name, visited, null);
if (h != null) return h;
}
return null;
}
/**
* Gets all resources with given name at the given source URL. If the URL
* points to a directory, the name is the file path relative to this
* directory. If the URL points to a JAR file, the name identifies an entry
* in that JAR file. If the URL points to a JAR file, the resource is not
* found in that JAR file, and the JAR file has Class-Path attribute, the
* JAR files identified in the Class-Path are also searched for the
* resource.
*
* @param source the source URL
* @param name the resource name
* @return enumeration of resource handles representing the resources
*/
public Enumeration getResources(URL source, String name) {
return new ResourceEnumeration(new URL[] {source}, name, false);
}
/**
* Gets all resources with given name at the given search path. If the URL
* points to a directory, the name is the file path relative to this
* directory. If the URL points to a JAR file, the name identifies an entry
* in that JAR file. If the URL points to a JAR file, the resource is not
* found in that JAR file, and the JAR file has Class-Path attribute, the
* JAR files identified in the Class-Path are also searched for the
* resource.
*
* @param sources the source URL path
* @param name the resource name
* @return enumeration of resource handles representing the resources
*/
public Enumeration getResources(URL[] sources, String name) {
return new ResourceEnumeration((URL[])sources.clone(), name, false);
}
// public URL[] getResolvedSearchPath(URL[] sources) {
// List path = new ArrayList();
// for (int i=0; i<sources.length; i++) {
// appendToResolvedSearchPath(path, sources[i]);
// }
// return (URL[])path.toArray(new URL[path.size()]);
// }
//
// private void appendToResolvedSearchPath(List path, final URL source) {
// if (isDir(source)) {
// path.add(source);
// }
// else {
// // URL
// try {
// getJarInfo(source).appendToResolvedClassPath(path);
// }
// catch (MalformedURLException e) {}
// }
// }
//
private ResourceHandle getResource(final URL source, String name,
Set visitedJars, Set skip) {
name = ResourceUtils.canonizePath(name);
if (isDir(source)) {
// plain resource
final URL url;
try {
// escape spaces etc. to make sure url is well-formed
URI relUri = new URI(null, null, null, -1, name, null, null);
url = new URL(source, relUri.getRawPath());
}
catch (URISyntaxException e) {
throw new IllegalArgumentException("Illegal resource name: " + name);
}
catch (MalformedURLException e) {
return null;
}
if (skip != null && skip.contains(url)) return null;
final URLConnection conn;
try {
conn = url.openConnection();
conn.getInputStream();
}
catch (IOException e) {
return null;
}
final String finalName = name;
return new ResourceHandle() {
public String getName() { return finalName; }
public URL getURL() { return url; }
public URL getCodeSourceURL() { return source; }
public InputStream getInputStream() throws IOException {
return conn.getInputStream();
}
public int getContentLength() {
return conn.getContentLength();
}
public void close() {
try {
getInputStream().close();
} catch (IOException e) {}
}
};
}
else {
// we deal with a JAR file here
try {
return getJarInfo(source).getResource(name, visitedJars, skip);
}
catch (MalformedURLException e) {
return null;
}
}
}
/**
* Fined resource with given name at the given source URL. If the URL points
* to a directory, the name is the file path relative to this directory.
* If the URL points to a JAR file, the name identifies an entry in that
* JAR file. If the URL points to a JAR file, the resource is not found
* in that JAR file, and the JAR file has Class-Path attribute, the
* JAR files identified in the Class-Path are also searched for the
* resource.
* @param source the source URL
* @param name the resource name
* @return URL of the resource, or null if not found
*/
public URL findResource(URL source, String name) {
return findResource(source, name, new HashSet(), null);
}
/**
* Finds resource with given name at the given search path. The path is
* searched iteratively, one URL at a time. If the URL points
* to a directory, the name is the file path relative to this directory.
* If the URL points to the JAR file, the name identifies an entry in that
* JAR file. If the URL points to the JAR file, the resource is not found
* in that JAR file, and the JAR file has Class-Path attribute, the
* JAR files identified in the Class-Path are also searched for the
* resource.
* @param sources the source URL path
* @param name the resource name
* @return URL of the resource, or null if not found
*/
public URL findResource(URL[] sources, String name) {
Set visited = new HashSet();
for (int i=0; i<sources.length; i++) {
URL url = findResource(sources[i], name, visited, null);
if (url != null) return url;
}
return null;
}
/**
* Finds all resources with given name at the given source URL. If the URL
* points to a directory, the name is the file path relative to this
* directory. If the URL points to a JAR file, the name identifies an entry
* in that JAR file. If the URL points to a JAR file, the resource is not
* found in that JAR file, and the JAR file has Class-Path attribute, the
* JAR files identified in the Class-Path are also searched for the
* resource.
*
* @param source the source URL
* @param name the resource name
* @return enumeration of URLs of the resources
*/
public Enumeration findResources(URL source, String name) {
return new ResourceEnumeration(new URL[] {source}, name, true);
}
/**
* Finds all resources with given name at the given search path. If the URL
* points to a directory, the name is the file path relative to this
* directory. If the URL points to a JAR file, the name identifies an entry
* in that JAR file. If the URL points to a JAR file, the resource is not
* found in that JAR file, and the JAR file has Class-Path attribute, the
* JAR files identified in the Class-Path are also searched for the
* resource.
*
* @param sources the source URL path
* @param name the resource name
* @return enumeration of URLs of the resources
*/
public Enumeration findResources(URL[] sources, String name) {
return new ResourceEnumeration((URL[])sources.clone(), name, true);
}
private URL findResource(final URL source, String name,
Set visitedJars, Set skip) {
URL url;
name = ResourceUtils.canonizePath(name);
if (isDir(source)) {
// plain resource
try {
url = new URL(source, name);
}
catch (MalformedURLException e) {
return null;
}
if (skip != null && skip.contains(url)) return null;
final URLConnection conn;
try {
conn = url.openConnection();
if (conn instanceof HttpURLConnection) {
HttpURLConnection httpConn = (HttpURLConnection) conn;
httpConn.setRequestMethod("HEAD");
if (httpConn.getResponseCode() >= 400) return null;
} else {
conn.getInputStream().close();
}
}
catch (IOException e) {
return null;
}
return url;
} else {
// we deal with a JAR file here
try {
ResourceHandle rh = getJarInfo(source).getResource(name,
visitedJars,
skip);
return (rh != null) ? rh.getURL() : null;
}
catch (MalformedURLException e) {
return null;
}
}
}
/**
* Test whether given URL points to a directory. URL is deemed to point
* to a directory if has non-null "file" component ending with "/".
*
* @param url the URL to test
* @return true if the URL points to a directory, false otherwise
*/
protected static boolean isDir(URL url) {
String file = url.getFile();
return (file != null && file.endsWith("/"));
}
private static class JarInfo {
final ResourceLoader loader;
final URL source; // "real" jar file path
final URL base; // "jar:{base}!/"
JarFile jar;
boolean resolved;
Permission perm;
URL[] classPath;
String[] index;
Map package2url;
JarInfo(ResourceLoader loader, URL source) throws MalformedURLException {
this.loader = loader;
this.source = source;
this.base = new URL("jar", "", -1, source + "!/", loader.jarHandler);
}
public ResourceHandle getResource(String name) {
return getResource(name, new HashSet());
}
ResourceHandle getResource(String name, Set visited) {
return getResource(name, visited, null);
}
// void appendToResolvedClassPath(List path) {
// path.add(source);
// URL[] classPath;
// synchronized (this) {
// classPath = this.classPath;
// }
// if (classPath == null) return;
// for (int i=0; i<classPath.length; i++) {
// URL url = classPath[i];
// if (path.contains(url)) continue;
// loader.appendToResolvedSearchPath(path, url);
// }
// }
ResourceHandle getResource(String name, Set visited, Set skip) {
visited.add(source);
URL url;
try {
// escape spaces etc. to make sure url is well-formed
URI relUri = new URI(null, null, null, -1, name, null, null);
url = new URL(base, relUri.getRawPath());
}
catch (URISyntaxException e) {
throw new IllegalArgumentException("Illegal resource name: " +
name);
}
catch (MalformedURLException e) {
return null;
}
try {
JarFile jfile = getJarFileIfPossiblyContains(name);
if (jfile != null) {
JarEntry jentry = jar.getJarEntry(name);
if (jentry != null && (skip == null || !skip.contains(url))) {
return new JarResourceHandle(jfile, jentry, url, source);
}
}
}
catch (IOException e) {
return null;
}
// not in here, but check also the dependencies
URL[] dependencies;
synchronized (this) {
if (package2url != null) {
int idx = name.lastIndexOf("/");
String prefix = (idx > 0) ? name.substring(0, idx) : name;
dependencies = (URL[]) package2url.get(prefix);
}
else {
// classpath might be null only if it was a dependency of
// an indexed JAR with out-of-date index (the index brought
// us here but resource was not found in the JAR). But this
// (out-of-sync index) should be captured by
// getJarFileIfPossiblyContains.
assert classPath != null;
dependencies = classPath;
}
}
if (dependencies == null) return null;
for (int i=0; i<dependencies.length; i++) {
URL cpUrl = dependencies[i];
if (visited.contains(cpUrl)) continue;
JarInfo depJInfo;
try {
depJInfo = loader.getJarInfo(cpUrl);
ResourceHandle rh = depJInfo.getResource(name, visited, skip);
if (rh != null) return rh;
}
catch (MalformedURLException e) {
// continue with other URLs
}
}
// not found
return null;
}
synchronized void setIndex(List newIndex) {
if (jar != null) {
// already loaded; no need for index
return;
}
if (index != null) {
// verification - previously declared content must remain there
Set violating = new HashSet(Arrays.asList(index));
violating.removeAll(newIndex);
if (!violating.isEmpty()) {
throw new RuntimeException("Invalid JAR index: " +
"the following entries were previously declared, but " +
"they are not present in the new index: " +
violating.toString());
}
}
this.index = (String[])newIndex.toArray(new String[newIndex.size()]);
Arrays.sort(this.index);
}
public JarFile getJarFileIfPossiblyContains(String name) throws IOException {
Map indexes;
synchronized (this) {
if (jar != null) {
// make sure we would be allowed to load it ourselves
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkPermission(perm);
}
// other thread may still be updating indexes of dependent
// JAR files
try {
while (!resolved) wait();
}
catch (InterruptedException e) {
throw new IOException("Interrupted");
}
return jar;
}
if (index != null) {
// we may be able to respond negatively w/o loading the JAR
int pos = name.lastIndexOf('/');
if (pos > 0) name = name.substring(0, pos);
if (Arrays.binarySearch(index, name) < 0) return null;
}
// load the JAR
JarURLConnection conn = (JarURLConnection)base.openConnection();
this.perm = conn.getPermission();
JarFile jar = conn.getJarFile();
// conservatively check if index is accurate, that is, does not
// contain entries which are not in the JAR file
if (index != null) {
Set indices = new HashSet(Arrays.asList(index));
Enumeration entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = (JarEntry)entries.nextElement();
String indexEntry = entry.getName();
// for non-top, find the package name
int pos = indexEntry.lastIndexOf('/');
if (pos > 0) {
indexEntry = indexEntry.substring(0, pos);
}
indices.remove(indexEntry);
}
if (!indices.isEmpty()) {
throw new RuntimeException("Invalid JAR index: " +
"the following entries not found in JAR: " +
indices);
}
}
this.jar = jar;
this.classPath = parseClassPath(jar, source);
indexes = parseJarIndex(this.source, jar);
indexes.remove(this.source.toExternalForm());
if (!indexes.isEmpty()) {
this.package2url = package2url(indexes);
}
}
// just loaded the JAR - need to resolve the index
try {
for (Iterator itr = indexes.entrySet().iterator(); itr.hasNext();) {
Map.Entry entry = (Map.Entry)itr.next();
URL url = (URL)entry.getKey();
if (url.toExternalForm().equals(this.source.toExternalForm())) {
continue;
}
List index = (List)entry.getValue();
loader.getJarInfo(url).setIndex(index);
}
}
finally {
synchronized (this) {
this.resolved = true;
notifyAll();
}
}
return jar;
}
}
private static Map package2url(Map indexes) {
Map prefix2url = new HashMap();
for (Iterator i = indexes.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry)i.next();
URL url = (URL)entry.getKey();
List idx = (List)entry.getValue();
for (Iterator j = idx.iterator(); j.hasNext();) {
String prefix = (String)j.next();
List prefixList = (List)prefix2url.get(prefix);
if (prefixList == null) {
prefixList = new ArrayList();
prefix2url.put(prefix, prefixList);
}
prefixList.add(url);
}
}
// replace lists with arrays
for (Iterator i = prefix2url.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry)i.next();
List list = (List)entry.getValue();
entry.setValue(list.toArray(new URL[list.size()]));
}
return prefix2url;
}
private JarInfo getJarInfo(URL url) throws MalformedURLException {
JarInfo jinfo;
synchronized (url2jarInfo) {
// fix: no longer use url.equals, since it distinguishes between
// "" and null in the host part of file URLs. The ""-type urls are
// correct but "null"-type ones come from file.toURI().toURL()
// on 1.4.1. (It is fixed in 1.4.2)
jinfo = (JarInfo)url2jarInfo.get(url.toExternalForm());
if (jinfo == null) {
jinfo = new JarInfo(this, url);
url2jarInfo.put(url.toExternalForm(), jinfo);
}
}
return jinfo;
}
private static class JarResourceHandle extends ResourceHandle {
final JarFile jar;
final JarEntry jentry;
final URL url;
final URL codeSource;
JarResourceHandle(JarFile jar, JarEntry jentry, URL url, URL codeSource) {
this.jar = jar;
this.jentry = jentry;
this.url = url;
this.codeSource = codeSource;
}
public String getName() {
return jentry.getName();
}
public URL getURL() {
return url;
}
public URL getCodeSourceURL() {
return codeSource;
}
public InputStream getInputStream() throws IOException {
return jar.getInputStream(jentry);
}
public int getContentLength() {
return (int)jentry.getSize();
}
public Manifest getManifest() throws IOException {
return jar.getManifest();
}
public Attributes getAttributes() throws IOException {
return jentry.getAttributes();
}
public Certificate[] getCertificates() {
return jentry.getCertificates();
}
public void close() {}
}
private static Map parseJarIndex(URL cxt, JarFile jar) throws IOException {
JarEntry entry = jar.getJarEntry(JAR_INDEX_ENTRY_NAME);
if (entry == null) return Collections.EMPTY_MAP;
InputStream is = jar.getInputStream(entry);
BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
Map result = new LinkedHashMap();
String line;
// skip version-info
do { line = reader.readLine(); }
while (line != null && line.trim().length() > 0);
URL currentURL;
List currentList = null;
while (true) {
// skip the blank line
line = reader.readLine();
if (line == null) {
return result;
}
currentURL = new URL(cxt, line);
currentList = new ArrayList();
result.put(currentURL, currentList);
while (true) {
line = reader.readLine();
if (line == null || line.trim().length() == 0) break;
currentList.add(line);
}
}
}
private static URL[] parseClassPath(JarFile jar, URL source) throws IOException {
Manifest man = jar.getManifest();
if (man == null) return new URL[0];
Attributes attr = man.getMainAttributes();
if (attr == null) return new URL[0];
String cp = attr.getValue(Attributes.Name.CLASS_PATH);
if (cp == null) return new URL[0];
StringTokenizer tokenizer = new StringTokenizer(cp);
List cpList = new ArrayList();
URI sourceURI = URI.create(source.toString());
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
try {
try {
URI uri = new URI(token);
if (!uri.isAbsolute()) {
uri = sourceURI.resolve(uri);
}
cpList.add(uri.toURL());
}
catch (URISyntaxException e) {
// tolerate malformed URIs for backward-compatibility
URL url = new URL(source, token);
cpList.add(url);
}
}
catch (MalformedURLException e) {
throw new IOException(e.getMessage());
}
}
return (URL[])cpList.toArray(new URL[cpList.size()]);
}
private class ResourceEnumeration implements Enumeration {
Object[] resources;
int cur = -1;
ResourceEnumeration(URL[] urls, String name, boolean findOnly) {
Set previousURLs = new HashSet();
Set visited = new HashSet();
List list = new ArrayList();
for (int i=0; i<urls.length; i++) {
if (findOnly) {
URL url = findResource(urls[i], name, visited, null);
if (url != null) {
list.add(url);
}
} else {
ResourceHandle h = getResource(urls[i],
name,
new HashSet(),
previousURLs);
if (h != null) {
previousURLs.add(h.getURL());
list.add(h);
}
}
}
resources = list.toArray(new Object[list.size()]);
}
public boolean hasMoreElements() {
if ((cur + 1) >= resources.length)
return false;
else
return true;
}
public Object nextElement() {
cur++;
if (cur >= resources.length)
throw new NoSuchElementException();
return resources[cur];
}
}
}