/* * Copyright (c) 2008-2015 Maxifier Ltd. All Rights Reserved. */ package com.maxifier.guice.bootstrap; import com.google.inject.Injector; import com.google.inject.Module; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileFilter; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.HashMap; import java.util.Map; import static com.google.common.base.Preconditions.checkArgument; import static com.maxifier.guice.bootstrap.InjectorBuilder.loadModule; /** * Loads plugin modules from specified directory. * * @author Konstantin Lyamshin (2015-11-04 18:08) */ public class PluginLoader { private static final Logger logger = LoggerFactory.getLogger(PluginLoader.class); private static final FileFilter DIRECTORIES = new FileFilter() { @Override public boolean accept(File file) { return file.isDirectory(); } }; private static final FileFilter JARS = new FileFilter() { @Override public boolean accept(File file) { return file.isFile() && !file.isHidden() && file.getName().endsWith(".jar"); } }; private final Map<File, ClassLoader> pluginDirs = new HashMap<File, ClassLoader>(); /** * Create ClassLoader on top of every subdirectory of {@code pluginDir}. * * @param pluginDir parent directory for individual plugins */ public PluginLoader(File pluginDir) { this(pluginDir, false); } /** * Create ClassLoader on top of {@code pluginDir} or its subdirectories. * * @param pluginDir parent directory for plugins' classpath * @param flat true if pluginDir should be used as is without deep into subdirectories */ public PluginLoader(File pluginDir, boolean flat) { checkArgument(pluginDir.isDirectory(), "Plugin path is not directory"); ClassLoader parentCL = this.getClass().getClassLoader(); if (!flat) { for (File dir : pluginDir.listFiles(DIRECTORIES)) { pluginDirs.put(dir, initClassLoader(dir, parentCL)); } } if (pluginDirs.isEmpty()) { // fall back to a flat directory structure pluginDirs.put(pluginDir, initClassLoader(pluginDir, parentCL)); } } /** * @return parent ClassLoader for plugins' ClassLoaders */ public ClassLoader getClassLoader() { if (pluginDirs.isEmpty()) { // should never happen return this.getClass().getClassLoader(); } // extract parent class loader from existing childen return pluginDirs.values().iterator().next().getParent(); } /** * @param parentCL parent ClassLoader for plugins' ClassLoaders */ public void setClassLoader(ClassLoader parentCL) { // Reload existing classloaders for (Map.Entry<File, ClassLoader> entry : pluginDirs.entrySet()) { entry.setValue(initClassLoader(entry.getKey(), parentCL)); } } private static ClassLoader initClassLoader(File pluginDir, ClassLoader parentCL) { try { File[] jars = pluginDir.listFiles(JARS); URL[] urls = new URL[jars.length]; for (int i = 0; i < jars.length; i++) { urls[i] = jars[i].toURI().toURL(); } return new URLClassLoader(urls, parentCL); } catch (MalformedURLException e) { throw new IllegalArgumentException("Can't process plugin dir " + pluginDir, e); } } /** * Searches plugin ClassLoaders for {@code pluginClassName} and creates plugin using {@code injector}. * * @param injector configuration injector (see {@link InjectorBuilder#buildConfigurationInjector()}) * @param pluginClassName plugin module class name * @return configured plugin module * @throws IllegalArgumentException if plugin not found in classpath */ public Module loadPlugin(Injector injector, String pluginClassName) { Module plugin = null; for (ClassLoader classLoader : pluginDirs.values()) { try { Class<?> moduleClass = classLoader.loadClass(pluginClassName); if (plugin == null) { plugin = loadModule(injector, moduleClass); } else { logger.error("Duplicate plugin found in classpath: " + pluginClassName); } } catch (ClassNotFoundException ignored) { } } if (plugin == null) { throw new IllegalArgumentException("Plugin module not found in classpath: " + pluginClassName); } return plugin; } }