/*
 * 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;
	}
}

@@ -3712,17 +3712,25 @@
 nd class
-%22
+ %22 + line
 );%0A%09%09%09%09%09

Print name of class when not found