/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package org.pepsoft.util; import java.io.*; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.Charset; import java.security.PublicKey; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarFile; /** * A general purpose plugin manager. * * @author pepijn */ public final class PluginManager { private PluginManager() { // Prevent instantiation } /** * Load plugin jars from a directory, which are signed with a particular * private key. * * <p>This method should be invoked only once. Any discovered and properly * signed plugin jars will be available to be returned by later invocations * of the {@link #findPlugins(Class, String, ClassLoader)} method. * * @param pluginDir The directory from which to load the plugins. * @param publicKey The public key corresponding to the private key with * which the plugins must have been signed. */ public static void loadPlugins(File pluginDir, PublicKey publicKey) { if (logger.isDebugEnabled()) { logger.debug("Loading plugins"); } File[] pluginFiles = pluginDir.listFiles((dir, name) -> name.toLowerCase().endsWith(".jar")); if (pluginFiles != null) { for (File pluginFile: pluginFiles) { try { JarFile jarFile = new JarFile(pluginFile); if (! isSigned(jarFile, publicKey)) { logger.error(jarFile.getName() + " is not official or has been tampered with; not loading it"); continue; } ClassLoader pluginClassLoader = new URLClassLoader(new URL[] {pluginFile.toURI().toURL()}); pluginJars.put(jarFile, pluginClassLoader); } catch (MalformedURLException e) { throw new RuntimeException("Malformed URL exception while trying to load plugins", e); } catch (IOException e) { throw new RuntimeException("I/O error while trying to load plugins", e); } } } } /** * Obtain a list of instances of all plugins available through a particular * classloader, or from plugin jars discovered by a previous invocation of * {@link #loadPlugins(File, PublicKey)}, which implement a particular type. * * @param type The type of plugin to return. * @param filename The resource path of the file containing the plugin * implementation classnames. * @param classLoader The classloader from which to discover plugins. * @param <T> The type of plugin to return. * @return A list of newly instantiated plugin objects of the specified * type available from the specified classloader and/or any earlier * discovered plugin jars. */ public static <T> List<T> findPlugins(Class<T> type, String filename, ClassLoader classLoader) { try { List<T> plugins = new ArrayList<>(); findPlugins(type, filename, classLoader, plugins); for (JarFile pluginJar: pluginJars.keySet()) { findPlugins(type, filename, pluginJar, plugins); } return plugins; } catch (IOException e) { throw new RuntimeException("I/O error while loading plugins", e); } } /** * Get a classloader which gives access to the classes of all the plugins. * * @return A classloader which gives access to the classes of all the * plugins. */ public static ClassLoader getPluginClassLoader() { return classLoader; } @SuppressWarnings({"empty-statement", "StatementWithEmptyBody"}) private static boolean isSigned(JarFile jarFile, PublicKey publicKey) throws IOException { for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements(); ) { JarEntry jarEntry = e.nextElement(); if (jarEntry.isDirectory() || jarEntry.getName().startsWith("META-INF/")) { continue; } byte[] buffer = new byte[BUFFER_SIZE]; try (InputStream in = jarFile.getInputStream(jarEntry)) { while (in.read(buffer) != -1) ; } Certificate[] certificates = jarEntry.getCertificates(); boolean signed = false; if (certificates != null) { for (Certificate certificate: certificates) { if (certificate.getPublicKey().equals(publicKey)) { signed = true; break; } } } if (! signed) { return false; } } return true; } private static <T> void findPlugins(Class<T> type, String filename, JarFile jarFile, List<T> plugins) throws IOException { JarEntry jarEntry = jarFile.getJarEntry(filename); try (BufferedReader in = new BufferedReader(new InputStreamReader(jarFile.getInputStream(jarEntry), Charset.forName("UTF-8")))) { String line; while ((line = in.readLine()) != null) { try { @SuppressWarnings("unchecked") Class<T> pluginType = (Class<T>) pluginJars.get(jarFile).loadClass(line); if (type.isAssignableFrom(pluginType)) { plugins.add(pluginType.newInstance()); } } catch (ClassNotFoundException e) { throw new RuntimeException("Class not found while instantiating plugin " + line, e); } catch (IllegalAccessException e) { throw new RuntimeException("Access denied while instantiating plugin " + line, e); } catch (InstantiationException e) { throw new RuntimeException("Exception thrown while instantiating plugin " + line, e); } } } } private static <T> void findPlugins(Class<T> type, String filename, ClassLoader classLoader, List<T> plugins) throws IOException { Enumeration<URL> resources = classLoader.getResources(filename); while (resources.hasMoreElements()) { URL url = resources.nextElement(); try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream(), Charset.forName("UTF-8")))) { String line; while ((line = in.readLine()) != null) { try { @SuppressWarnings("unchecked") Class<T> pluginType = (Class<T>) classLoader.loadClass(line); if (type.isAssignableFrom(pluginType)) { plugins.add(pluginType.newInstance()); } } catch (ClassNotFoundException e) { throw new RuntimeException("Class not found while instantiating plugin " + line, e); } catch (IllegalAccessException e) { throw new RuntimeException("Access denied while instantiating plugin " + line, e); } catch (InstantiationException e) { throw new RuntimeException("Exception thrown while instantiating plugin " + line, e); } } } } } private static final Map<JarFile, ClassLoader> pluginJars = new HashMap<>(); private static final int BUFFER_SIZE = 32768; private static final ClassLoader classLoader = new ClassLoader(ClassLoader.getSystemClassLoader()) { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { for (Map.Entry<JarFile, ClassLoader> entry: pluginJars.entrySet()) { Class<?> _class; try { _class = entry.getValue().loadClass(name); } catch (ClassNotFoundException e) { continue; } logger.debug("Loading " + name + " from " + entry.getKey().getName()); return _class; } throw new ClassNotFoundException("Class " + name + " not found in plugin class loaders"); } }; private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(PluginManager.class); }