// This file is part of OpenTSDB. // Copyright (C) 2013-2014 The OpenTSDB Authors. // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 2.1 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 Lesser // General Public License for more details. You should have received a copy // of the GNU Lesser General Public License along with this program. If not, // see <http://www.gnu.org/licenses/>. package net.opentsdb.utils; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.ServiceLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Super simple ServiceLoader based plugin framework for OpenTSDB that lets us * add files or directories to the class path after startup and then search for * a specific plugin type or any plugins that match a given class. This isn't * meant to be a rich plugin manager, it only handles the basics of searching * and instantiating a given class. * <p> * Before attempting any of the plugin loader calls, users should call one or * more of the jar loader methods to append files to the class path that may * have not been loaded on startup. This is particularly useful for plugins that * have dependencies not included by OpenTSDB. * <p> * For example, a typical process may be: * <ul> * <li>loadJARs(<plugin_path>) where <plugin_path> contains JARs of * the plugins and their dependencies</li> * <li>loadSpecificPlugin() or loadPlugins() to instantiate the proper plugin * types</li> * </ul> * <p> * Plugin creation is pretty simple, just implement the abstract plugin class, * create a Manifest file, add the "services" folder and plugin file and export * a jar file. * <p> * <b>Note:</b> All plugins must have a parameterless constructor for the * ServiceLoader to work. This means you can't have final class variables, but * we'll make a promise to call an initialize() method with the proper * parameters, such as configs or the TSDB object, immediately after loading a * plugin and before trying to access any of its methods. * <p> * <b>Note:</b> All plugins must also implement a shutdown() method to clean up * gracefully. * * @since 2.0 */ public final class PluginLoader { private static final Logger LOG = LoggerFactory.getLogger(PluginLoader.class); /** Static list of types for the class loader */ private static final Class<?>[] PARAMETER_TYPES = new Class[] { URL.class }; /** * Searches the class path for the specific plugin of a given type * <p> * <b>Note:</b> If you want to load JARs dynamically, you need to call * {@link #loadJAR} or {@link #loadJARs} methods with the proper file * or directory first, otherwise this will only search whatever was loaded * on startup. * <p> * <b>WARNING:</b> If there are multiple versions of the request plugin in the * class path, only one will be returned, so check the logs to see that the * correct version was loaded. * * @param name The specific name of a plugin to search for, e.g. * net.opentsdb.search.ElasticSearch * @param type The class type to search for * @return An instantiated object of the given type if found, null if the * class could not be found * @throws ServiceConfigurationError if the plugin cannot be instantiated * @throws IllegalArgumentName if the plugin name is null or empty */ public static <T> T loadSpecificPlugin(final String name, final Class<T> type) { if (name.isEmpty()) { throw new IllegalArgumentException("Missing plugin name"); } ServiceLoader<T> serviceLoader = ServiceLoader.load(type); Iterator<T> it = serviceLoader.iterator(); if (!it.hasNext()) { LOG.warn("Unable to locate any plugins of the type: " + type.getName()); return null; } while(it.hasNext()) { T plugin = it.next(); if (plugin.getClass().getName().equals(name) || plugin.getClass().getSuperclass().getName().equals(name)) { return plugin; } } LOG.warn("Unable to locate plugin: " + name); return null; } /** * Searches the class path for implementations of the given type, returning a * list of all plugins that were found * <p> * <b>Note:</b> If you want to load JARs dynamically, you need to call * {@link #loadJAR} or {@link #loadJARs} methods with the proper file * or directory first, otherwise this will only search whatever was loaded * on startup. * <p> * <b>WARNING:</b> If there are multiple versions of the request plugin in the * class path, only one will be returned, so check the logs to see that the * correct version was loaded. * * @param type The class type to search for * @return An instantiated list of objects of the given type if found, null * if no implementations of the type were found * @throws ServiceConfigurationError if any of the plugins could not be * instantiated */ public static <T> List<T> loadPlugins(final Class<T> type) { ServiceLoader<T> serviceLoader = ServiceLoader.load(type); Iterator<T> it = serviceLoader.iterator(); if (!it.hasNext()) { LOG.warn("Unable to locate any plugins of the type: " + type.getName()); return null; } ArrayList<T> plugins = new ArrayList<T>(); while(it.hasNext()) { plugins.add(it.next()); } if (plugins.size() > 0) { return plugins; } LOG.warn("Unable to locate plugins for type: " + type.getName()); return null; } /** * Attempts to load the given jar into the class path * @param jar Full path to a .jar file * @throws IOException if the file does not exist or cannot be accessed * @throws SecurityException if there is a security manager present and the * operation is denied * @throws IllegalArgumentException if the filename did not end with .jar * @throws NoSuchMethodException if there is an error with the class loader * @throws IllegalAccessException if a security manager is present and the * operation was denied * @throws InvocationTargetException if there is an issue loading the jar */ public static void loadJAR(String jar) throws IOException, SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { if (!jar.toLowerCase().endsWith(".jar")) { throw new IllegalArgumentException( "File specified did not end with .jar"); } File file = new File(jar); if (!file.exists()) { throw new FileNotFoundException(jar); } addFile(file); } /** * Recursively traverses a directory searching for files ending with .jar and * loads them into the class path * <p> * <b>WARNING:</b> This can be pretty slow if you have a directory with many * sub-directories. Keep the directory structure shallow. * * @param directory The directory * @throws IOException if the directory does not exist or cannot be accessed * @throws SecurityException if there is a security manager present and the * operation is denied * @throws IllegalArgumentException if the path was not a directory * @throws NoSuchMethodException if there is an error with the class loader * @throws IllegalAccessException if a security manager is present and the * operation was denied * @throws InvocationTargetException if there is an issue loading the jar */ public static void loadJARs(String directory) throws SecurityException, IllegalArgumentException, IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { File file = new File(directory); if (!file.isDirectory()) { throw new IllegalArgumentException( "The path specified was not a directory"); } ArrayList<File> jars = new ArrayList<File>(); searchForJars(file, jars); if (jars.size() < 1) { LOG.debug("No JAR files found in path: " + directory); return; } for (File jar : jars) { addFile(jar); } } /** * Recursive method to search for JAR files starting at a given level * @param file The directory to search in * @param jars A list of file objects that will be loaded with discovered * jar files * @throws SecurityException if a security manager exists and prevents reading */ private static void searchForJars(final File file, List<File> jars) { if (file.isFile()) { if (file.getAbsolutePath().toLowerCase().endsWith(".jar")) { jars.add(file); LOG.debug("Found a jar: " + file.getAbsolutePath()); } } else if (file.isDirectory()) { File[] files = file.listFiles(); if (files == null) { // if this is null, it's due to a security issue LOG.warn("Access denied to directory: " + file.getAbsolutePath()); } else { for (File f : files) { searchForJars(f, jars); } } } } /** * Attempts to add the given file object to the class loader * @param f The JAR file object to load * @throws IOException if the file does not exist or cannot be accessed * @throws SecurityException if there is a security manager present and the * operation is denied * @throws IllegalArgumentException if the file was invalid * @throws NoSuchMethodException if there is an error with the class loader * @throws IllegalAccessException if a security manager is present and the * operation was denied * @throws InvocationTargetException if there is an issue loading the jar */ private static void addFile(File f) throws IOException, SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { addURL(f.toURI().toURL()); } /** * Attempts to add the given file/URL to the class loader * @param url Full path to the file to add * @throws SecurityException if there is a security manager present and the * operation is denied * @throws IllegalArgumentException if the path was not a directory * @throws NoSuchMethodException if there is an error with the class loader * @throws IllegalAccessException if a security manager is present and the * operation was denied * @throws InvocationTargetException if there is an issue loading the jar */ private static void addURL(final URL url) throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException { URLClassLoader sysloader = (URLClassLoader)ClassLoader.getSystemClassLoader(); Class<?> sysclass = URLClassLoader.class; Method method = sysclass.getDeclaredMethod("addURL", PARAMETER_TYPES); method.setAccessible(true); method.invoke(sysloader, new Object[]{ url }); LOG.debug("Successfully added JAR to class loader: " + url.getFile()); } }