package org.xbib.classloader.uri;
import org.elasticsearch.common.collect.Lists;
import org.xbib.classloader.ResourceEnumeration;
import org.xbib.classloader.ResourceFinder;
import org.xbib.classloader.ResourceHandle;
import org.xbib.classloader.ResourceLocation;
import org.xbib.classloader.directory.DirectoryResourceLocation;
import org.xbib.classloader.jar.JarResourceLocation;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
public class URIResourceFinder implements ResourceFinder {
private final Object lock = new Object();
private final LinkedHashSet<URI> uris = new LinkedHashSet<URI>();
private final LinkedHashMap<URI, ResourceLocation> classPath = new LinkedHashMap<URI, ResourceLocation>();
private final LinkedHashSet<File> watchedFiles = new LinkedHashSet<File>();
private boolean destroyed = false;
public URIResourceFinder() {
}
public URIResourceFinder(URI[] uris) {
addURIs(uris);
}
public void destroy() {
synchronized (lock) {
if (destroyed) {
return;
}
destroyed = true;
uris.clear();
for (ResourceLocation resourceLocation : classPath.values()) {
resourceLocation.close();
}
classPath.clear();
}
}
public ResourceHandle getResource(String resourceName) {
synchronized (lock) {
if (destroyed) {
return null;
}
for (Map.Entry<URI, ResourceLocation> entry : getClassPath().entrySet()) {
ResourceLocation resourceLocation = entry.getValue();
ResourceHandle resourceHandle = resourceLocation.getResourceHandle(resourceName);
if (resourceHandle != null && !resourceHandle.isDirectory()) {
return resourceHandle;
}
}
}
return null;
}
public URL findResource(String resourceName) {
synchronized (lock) {
if (destroyed) {
return null;
}
for (Map.Entry<URI, ResourceLocation> entry : getClassPath().entrySet()) {
ResourceLocation resourceLocation = entry.getValue();
ResourceHandle resourceHandle = resourceLocation.getResourceHandle(resourceName);
if (resourceHandle != null) {
return resourceHandle.getUrl();
}
}
}
return null;
}
public Enumeration<URL> findResources(String resourceName) {
synchronized (lock) {
return new ResourceEnumeration(new ArrayList<ResourceLocation>(getClassPath().values()), resourceName);
}
}
public void addURI(URI uri) {
addUris(Collections.singletonList(uri));
}
public URI[] getURIs() {
synchronized (lock) {
return uris.toArray(new URI[uris.size()]);
}
}
/**
* Adds an array of uris to the end of this class loader.
*
* @param uris the URLs to add
*/
protected void addURIs(URI[] uris) {
addUris(Arrays.asList(uris));
}
/**
* Adds a list of uris to the end of this class loader.
*
* @param uris the URLs to add
*/
protected void addUris(List<URI> uris) {
synchronized (lock) {
if (destroyed) {
throw new IllegalStateException("UriResourceFinder has been destroyed");
}
boolean shouldRebuild = this.uris.addAll(uris);
if (shouldRebuild) {
rebuildClassPath();
}
}
}
private LinkedHashMap<URI, ResourceLocation> getClassPath() {
assert Thread.holdsLock(lock) : "This method can only be called while holding the lock";
for (File file : watchedFiles) {
if (file.canRead()) {
rebuildClassPath();
break;
}
}
return classPath;
}
/**
* Rebuilds the entire class path. This class is called when new URIs are
* added or one of the watched files becomes readable. This method will not
* open jar files again, but will add any new entries not alredy open to the
* class path. If any file based uri is does not exist, we will watch for
* that file to appear.
*/
private void rebuildClassPath() {
assert Thread.holdsLock(lock) : "This method can only be called while holding the lock";
// copy all of the existing locations into a temp map and clear the class path
Map<URI, ResourceLocation> existingJarFiles = new LinkedHashMap<URI, ResourceLocation>(classPath);
classPath.clear();
LinkedList<URI> locationStack = new LinkedList<URI>(uris);
try {
while (!locationStack.isEmpty()) {
URI uri = locationStack.removeFirst();
// Skip any duplicate uris in the classpath
if (classPath.containsKey(uri)) {
continue;
}
// Check is this URL has already been opened
ResourceLocation resourceLocation = existingJarFiles.remove(uri);
// If not opened, cache the uri and wrap it with a resource location
if (resourceLocation == null) {
try {
resourceLocation = createResourceLocation(uri.toURL(), cacheUri(uri));
} catch (FileNotFoundException e) {
// if this is a file URL, the file doesn't exist yet... watch to see if it appears later
if ("file".equals(uri.getScheme())) {
File file = new File(uri.getPath());
watchedFiles.add(file);
continue;
}
} catch (IOException ignored) {
// can't seem to open the file... this is most likely a bad jar file
// so don't keep a watch out for it because that would require lots of checking
// Dain: We may want to review this decision later
continue;
} catch (UnsupportedOperationException ex) {
// the protocol for the JAR file's URL is not supported. This can occur when
// the jar file is embedded in an EAR or CAR file.
continue;
}
try {
// add the jar to our class path
if (resourceLocation != null && resourceLocation.getCodeSource() != null) {
classPath.put(resourceLocation.getCodeSource().toURI(), resourceLocation);
}
} catch (URISyntaxException ex) {
// ignore
}
}
// push the manifest classpath on the stack (make sure to maintain the order)
List<URI> manifestClassPath = getManifestClassPath(resourceLocation);
locationStack.addAll(0, manifestClassPath);
}
} catch (Error e) {
destroy();
throw e;
}
for (ResourceLocation resourceLocation : existingJarFiles.values()) {
resourceLocation.close();
}
}
protected File cacheUri(URI uri) throws IOException {
if (!"file".equals(uri.getScheme())) {
// download the jar
throw new UnsupportedOperationException("Only local file jars are supported " + uri);
}
File file = new File(uri.getPath());
if (!file.exists()) {
throw new FileNotFoundException(file.getAbsolutePath());
}
if (!file.canRead()) {
throw new IOException("File is not readable: " + file.getAbsolutePath());
}
return file;
}
protected ResourceLocation createResourceLocation(URL codeSource, File cacheFile) throws IOException {
if (!cacheFile.exists()) {
throw new FileNotFoundException(cacheFile.getAbsolutePath());
}
if (!cacheFile.canRead()) {
throw new IOException("File is not readable: " + cacheFile.getAbsolutePath());
}
return cacheFile.isDirectory() ?
// DirectoryResourceLocation will only return "file" URLs within this directory
// do not use the DirectoryResourceLocation for non file based uris
new DirectoryResourceLocation(cacheFile) :
new JarResourceLocation(codeSource, cacheFile);
}
private List<URI> getManifestClassPath(ResourceLocation resourceLocation) {
try {
// get the manifest, if possible
Manifest manifest = resourceLocation.getManifest();
if (manifest == null) {
// some locations don't have a manifest
return Lists.newLinkedList();
}
// get the class-path attribute, if possible
String manifestClassPath = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH);
if (manifestClassPath == null) {
return Lists.newLinkedList();
}
// build the uris...
// the class-path attribute is space delimited
URL codeSource = resourceLocation.getCodeSource();
LinkedList<URI> classPathUrls = new LinkedList<URI>();
for (StringTokenizer tokenizer = new StringTokenizer(manifestClassPath, " "); tokenizer.hasMoreTokens(); ) {
String entry = tokenizer.nextToken();
try {
// the class path entry is relative to the resource location code source
URL entryUrl = new URL(codeSource, entry);
classPathUrls.addLast(entryUrl.toURI());
} catch (MalformedURLException ignored) {
// most likely a poorly named entry
} catch (URISyntaxException ignored) {
// most likely a poorly named entry
}
}
return classPathUrls;
} catch (IOException ignored) {
// error opening the manifest
return Lists.newLinkedList();
}
}
}