/*
* Copyright (C) 2012 Jason Gedge <http://www.gedge.ca>
*
* This file is part of the OpGraph project.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.gedge.opgraph.util;
import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Logger;
/**
* A default service discovery class which mimics the standard library's
* {@link java.util.ServiceLoader} for service discovery.
*
* Static methods are provided ({@link #addClassLoader(ClassLoader)} and
* {@link #removeClassLoader(ClassLoader)}) to provide additional, custom
* class loaders to search through for service providers. Note that the
* system class loader will always be used, along with the class loader used
* to load the class given to {@link #findProviders(Class)}.
*
* TODO caching
*/
public class DefaultServiceDiscovery extends ServiceDiscovery {
/** The resource prefix to search through */
private static final String SERVICE_PREFIX = "META-INF/services/";
/** Logger */
private static final Logger LOGGER = Logger.getLogger(DefaultServiceDiscovery.class.getName());
/** An additional set of classloaders to search through */
private final static Set<ClassLoader> classloaders = new HashSet<ClassLoader>();
/**
* Adds a custom classloader to search through for service providers.
*
* @param classloader the classloader to add
*/
public static void addClassLoader(ClassLoader classloader) {
classloaders.add(classloader);
}
/**
* Removes a custom classloader from the searchable classloaders.
*
* @param classloader the classloader to remove
*/
public static void removeClassLoader(ClassLoader classloader) {
classloaders.remove(classloader);
}
@Override
public <T> List<Class<? extends T>> findProviders(final Class<T> service) {
final Set<ClassLoader> classloaders = new HashSet<ClassLoader>();
classloaders.addAll(DefaultServiceDiscovery.classloaders);
classloaders.add(service.getClassLoader());
classloaders.add(ClassLoader.getSystemClassLoader());
// Get the resource URL and iterate through them
final List<DiscoveryData> dataList = getResourceURLs(classloaders, SERVICE_PREFIX + service.getName(), false);
final List<Class<? extends T>> providersList = new ArrayList<Class<? extends T>>();
for(DiscoveryData data : dataList) {
try {
Iterator<String> linesPending = new LineIterator(data.url.openStream());
while(linesPending.hasNext()) {
final String line = linesPending.next().trim();
try {
if(line.length() > 0) {
final Class<?> rawClass = Class.forName(line, false, data.classloader);
providersList.add(rawClass.asSubclass(service));
}
} catch(ClassNotFoundException exc) {
LOGGER.warning("Classloader '" + data.classloader + "' could not find class");
} catch(ClassCastException exc) {
LOGGER.warning("URL '" + data.url + "' contains invalid provider: " + line);
}
}
} catch(IOException exc) {
LOGGER.warning("Could not open service provider file " + data.url);
}
}
return providersList;
}
@Override
public List<URL> findResources(String path) {
final Set<ClassLoader> classloaders = new HashSet<ClassLoader>();
classloaders.addAll(DefaultServiceDiscovery.classloaders);
classloaders.add(ClassLoader.getSystemClassLoader());
final List<URL> resourceURLs = new ArrayList<URL>();
final List<DiscoveryData> dataList = getResourceURLs(classloaders, path, false);
for(DiscoveryData data : dataList)
resourceURLs.add(data.url);
return resourceURLs;
}
/**
* Data pertaining to provider discovery.
*/
private static class DiscoveryData {
/** The classloader that discovered the URL */
public final ClassLoader classloader;
/** URL to the resource */
public final URL url;
/** A key associated with this resource */
@SuppressWarnings("unused")
public final String key;
/**
* Default constructor.
*
* @param classloader the classloader used to find the class/resournce
* @param url URL to the resource
* @param key key associated with the resource
*/
public DiscoveryData(ClassLoader classloader, URL url, String key) {
this.classloader = classloader;
this.url = url;
this.key = key;
}
}
/**
* Gets a list resource URLs for a service.
*
* @param path the path to look for
* @param mapped if <code>true</code>, the service acts as a prefix for
* a directory containing files where the filename is the
* key. Otherwise, the service itself is the file.
*/
private List<DiscoveryData> getResourceURLs(Iterable<ClassLoader> classloaders, String path, boolean mapped) {
final ArrayList<DiscoveryData> data = new ArrayList<DiscoveryData>();
for(ClassLoader classloader : classloaders) {
String basePath = path;
Enumeration<URL> baseURLs = null;
try {
baseURLs = classloader.getResources(basePath);
} catch(IOException exc) {
LOGGER.warning("Could not loaded resource URLs from classloader " + classloader);
continue;
}
while(baseURLs.hasMoreElements()) {
final URL baseURL = baseURLs.nextElement();
if(mapped) {
try {
if(baseURL.getProtocol().equals("jar")) {
// Make sure there's a path separator at the end
if(!basePath.endsWith("/"))
basePath += '/';
// Jar file, look inside for entries
final URLConnection connection = baseURL.openConnection();
if(connection instanceof JarURLConnection) {
final JarURLConnection jarConnection = (JarURLConnection)connection;
final JarFile jarFile = jarConnection.getJarFile();
// Iterate through entries
final Enumeration<JarEntry> entries = jarFile.entries();
while(entries.hasMoreElements()) {
// Don't accept directories, or entries that aren't a part of
// the base path
final JarEntry jarEntry = entries.nextElement();
if(jarEntry.isDirectory() || !jarEntry.getName().startsWith(basePath))
continue;
final String key = jarEntry.getName().substring(basePath.length());
data.add(new DiscoveryData(classloader, new URL(baseURL, key), key));
}
}
} else if(baseURL.getProtocol().equals("file")) {
// Find files in directory
final File dir = new File(URLDecoder.decode(baseURL.getPath(), "UTF-8"));
if(dir.isDirectory()) {
for(File file : dir.listFiles()) {
if(!file.isDirectory()) {
final URL url = file.toURI().toURL();
data.add(new DiscoveryData(classloader, url, file.getName()));
}
}
}
}
} catch(IOException exc) {
LOGGER.warning("IOException getting resource url from " + baseURL);
}
} else {
data.add(new DiscoveryData(classloader, baseURL, null));
}
}
}
return data;
}
}