| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.any23.plugin; |
| |
| import org.apache.any23.cli.Tool; |
| import org.apache.any23.configuration.DefaultConfiguration; |
| import org.apache.any23.extractor.ExtractorFactory; |
| import org.apache.any23.extractor.ExtractorGroup; |
| import org.apache.any23.extractor.ExtractorRegistry; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.File; |
| import java.io.FilenameFilter; |
| import java.io.IOException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.ServiceLoader; |
| import java.util.Set; |
| |
| /** |
| * The <i>Any23PluginManager</i> is responsible for inspecting |
| * dynamically the classpath and retrieving useful classes. |
| * |
| * @author Michele Mostarda (mostarda@fbk.eu) |
| */ |
| public class Any23PluginManager { |
| |
| /** |
| * Any23 Command Line Interface package. |
| */ |
| public static final String CLI_PACKAGE = Tool.class.getPackage().getName(); |
| |
| /** |
| * Property where look for plugins. |
| */ |
| public static final String PLUGIN_DIRS_PROPERTY = "any23.plugin.dirs"; |
| |
| /** |
| * List separator for the string declaring the plugin list. |
| */ |
| public static final String PLUGIN_DIRS_LIST_SEPARATOR = ":"; |
| |
| /** |
| * Internal logger. |
| */ |
| private static final Logger logger = LoggerFactory.getLogger(Any23PluginManager.class); |
| |
| /** |
| * Singleton lazy instance. |
| */ |
| private static final Any23PluginManager instance = new Any23PluginManager(); |
| |
| /** |
| * Internal class loader used to dynamically load classes. |
| */ |
| private final DynamicClassLoader dynamicClassLoader; |
| |
| /** |
| * Constructor. |
| */ |
| private Any23PluginManager() { |
| dynamicClassLoader = new DynamicClassLoader(); |
| } |
| |
| /** |
| * @return a singleton instance of {@link Any23PluginManager}. |
| */ |
| public static synchronized Any23PluginManager getInstance() { |
| return instance; |
| } |
| |
| /** |
| * Loads a <i>JAR</i> file in the classpath. |
| * |
| * @param jar the JAR file to be loaded. |
| * @return <code>true</code> if the JAR is added for the first time to the classpath, |
| * <code>false</code> otherwise. |
| */ |
| public synchronized boolean loadJAR(File jar) { |
| if(jar == null) { |
| throw new NullPointerException("jar file cannot be null."); |
| } |
| if (!jar.isFile() && !jar.exists()) { |
| throw new IllegalArgumentException( |
| String.format(java.util.Locale.ROOT, "Invalid JAR [%s], must be an existing file.", jar.getAbsolutePath()) |
| ); |
| } |
| return dynamicClassLoader.addJAR(jar); |
| } |
| |
| /** |
| * Loads a list of <i>JAR</i>s in the classpath. |
| * |
| * @param jars list of JARs to be loaded. |
| * @return list of exceptions raised during the loading. |
| */ |
| public synchronized Throwable[] loadJARs(File... jars) { |
| final List<Throwable> result = new ArrayList<>(); |
| for (File jar : jars) { |
| try { |
| loadJAR(jar); |
| } catch (Throwable t) { |
| result.add( |
| new IllegalArgumentException( |
| String.format(java.util.Locale.ROOT, "Error while loading jar [%s]", jar.getAbsolutePath()), |
| t |
| ) |
| ); |
| } |
| } |
| return result.toArray(new Throwable[result.size()]); |
| } |
| |
| /** |
| * Loads a <i>classes</i> directory in the classpath. |
| * |
| * @param classDir the directory to be loaded. |
| * @return <code>true</code> if the directory is added for the first time to the classpath, |
| * <code>false</code> otherwise. |
| */ |
| public synchronized boolean loadClassDir(File classDir) { |
| if(classDir == null) { |
| throw new NullPointerException("classDir cannot be null."); |
| } |
| if (!classDir.isDirectory() && !classDir.exists()) { |
| throw new IllegalArgumentException( |
| String.format(java.util.Locale.ROOT, "Invalid class dir [%s], must be an existing file.", classDir.getAbsolutePath()) |
| ); |
| } |
| return dynamicClassLoader.addClassDir(classDir); |
| } |
| |
| /** |
| * Loads a list of class dirs in the classpath. |
| * |
| * @param classDirs list of class dirs to be loaded. |
| * @return list of exceptions raised during the loading. |
| */ |
| public synchronized Throwable[] loadClassDirs(File... classDirs) { |
| final List<Throwable> result = new ArrayList<>(); |
| for (File classDir : classDirs) { |
| try { |
| loadClassDir(classDir); |
| } catch (Throwable t) { |
| result.add( |
| new IllegalArgumentException( |
| String.format(java.util.Locale.ROOT, "Error while loading class dir [%s]", classDir.getAbsolutePath()), |
| t |
| ) |
| ); |
| } |
| } |
| return result.toArray(new Throwable[result.size()]); |
| } |
| |
| /** |
| * Loads all the JARs detected in a given directory. |
| * |
| * @param jarDir directory containing the JARs to be loaded. |
| * Example '/usr/local/apache-tomcat-7.0.72/webapps/apache-any23-service-2.2-SNAPSHOT/WEB-INF/lib/apache-any23-openie' |
| * @return <code>true</code> if all JARs in dir are loaded. |
| */ |
| public synchronized boolean loadJARDir(File jarDir) { |
| if(jarDir == null) |
| throw new NullPointerException("JAR dir must be not null."); |
| if(!jarDir.exists() ) |
| throw new IllegalArgumentException("Given directory doesn't exist:" + jarDir.getAbsolutePath()); |
| if(!jarDir.isDirectory() ) |
| throw new IllegalArgumentException( |
| "given file exists and it is not a directory: " + jarDir.getAbsolutePath() |
| ); |
| boolean loaded = true; |
| for (File jarFile : jarDir.listFiles( |
| new FilenameFilter() { |
| @Override |
| public boolean accept(File dir, String name) { |
| return name.endsWith(".jar"); |
| } |
| }) |
| ) { |
| loaded &= loadJAR(jarFile); |
| } |
| return loaded; |
| } |
| |
| /** |
| * Loads a generic list of files, trying to determine the type of every file. |
| * |
| * @param files list of files to be loaded. |
| * @return list of errors occurred during loading. |
| */ |
| public synchronized Throwable[] loadFiles(File... files) { |
| final List<Throwable> errors = new ArrayList<>(); |
| for(File file : files) { |
| try { |
| if (file.isFile() && file.getName().endsWith(".jar")) { |
| loadJAR(file); |
| } else if (file.isDirectory()) { |
| if (file.getName().endsWith("classes")) { |
| loadClassDir(file); |
| } else { |
| loadJARDir(file); |
| } |
| } else { |
| throw new IllegalArgumentException("Cannot handle file " + file.getAbsolutePath()); |
| } |
| } catch (Throwable t) { |
| errors.add(t); |
| } |
| } |
| return errors.toArray(new Throwable[errors.size()]); |
| } |
| |
| /** |
| * Returns all classes within the specified <code>packageName</code> satisfying the given class |
| * <code>filter</code>. The search is performed on the static classpath (the one the application |
| * started with) and the dynamic classpath (the one specified using the load methods). |
| * |
| * @param <T> type of filtered class. |
| * @param type of filtered class. |
| * @return list of matching classes. |
| * @throws IOException if there is an error obtaining plugins. |
| */ |
| public synchronized <T> Iterator<T> getPlugins(final Class<T> type) |
| throws IOException { |
| return ServiceLoader.load(type, dynamicClassLoader).iterator(); |
| } |
| |
| /** |
| * Returns the list of all the {@link Tool} classes declared within the classpath. |
| * |
| * @return not <code>null</code> list of tool classes. |
| * @throws IOException if there is an error obtaining {@link org.apache.any23.cli.Tool}'s |
| * from the classpath. |
| */ |
| public synchronized Iterator<Tool> getTools() throws IOException { |
| return getPlugins(Tool.class); |
| } |
| |
| /** |
| * List of {@link ExtractorPlugin} classes declared within the classpath. |
| * |
| * @return not <code>null</code> list of plugin classes. |
| * @throws IOException if there is an error obtaining Extractors. |
| */ |
| @SuppressWarnings("rawtypes") |
| public synchronized Iterator<ExtractorFactory> getExtractors() throws IOException { |
| return getPlugins(ExtractorFactory.class); |
| } |
| |
| /** |
| * Loads plugins from a list of specified locations. |
| * |
| * @param pluginLocations list of locations. |
| * @return a report about the loaded plugins. |
| */ |
| public synchronized String loadPlugins(File... pluginLocations) { |
| final StringBuilder report = new StringBuilder(); |
| report.append("\nLoading plugins from locations {\n"); |
| for (File pluginLocation : pluginLocations) { |
| report.append(pluginLocation.getAbsolutePath()).append('\n'); |
| } |
| report.append("}\n"); |
| |
| final Throwable[] errors = loadFiles(pluginLocations); |
| if (errors.length > 0) { |
| report.append("The following errors occurred while loading plugins {\n"); |
| for (Throwable error : errors) { |
| report.append(error); |
| report.append("\n\n\n"); |
| } |
| report.append("}\n"); |
| } |
| return report.toString(); |
| } |
| |
| /** |
| * Configures a new list of extractors containing the extractors declared in <code>initialExtractorGroup</code> |
| * and also the extractors detected in classpath specified by <code>pluginLocations</code>. |
| * |
| * @param pluginLocations path locations of plugins. |
| * @return full list of extractors. |
| * @throws java.io.IOException if there is an error locating the plugin(s). |
| * @throws IllegalAccessException if there are access permissions for plugin(s). |
| * @throws InstantiationException if there is an error instantiating plugin(s). |
| */ |
| public synchronized ExtractorGroup configureExtractors( |
| final File... pluginLocations |
| ) throws IOException, IllegalAccessException, InstantiationException { |
| |
| final String pluginsReport = loadPlugins(pluginLocations); |
| logger.info(pluginsReport); |
| |
| final StringBuilder report = new StringBuilder(); |
| try { |
| final List<ExtractorFactory<?>> newFactoryList = new ArrayList<>(); |
| @SuppressWarnings("rawtypes") |
| Iterator<ExtractorFactory> extractors = getExtractors(); |
| while (extractors.hasNext()) { |
| ExtractorFactory<?> factory = extractors.next(); |
| |
| report.append("\n - found plugin: ").append(factory.getExtractorName()).append("\n"); |
| |
| newFactoryList.add(factory); |
| } |
| |
| if (newFactoryList.isEmpty()) { |
| report.append("\n=== No plugins have been found.===\n"); |
| } |
| |
| return new ExtractorGroup(newFactoryList); |
| } finally { |
| logger.info(report.toString()); |
| } |
| } |
| |
| /** |
| * Configures a new list of extractors containing the extractors declared in <code>initialExtractorGroup</code> |
| * and also the extractors detected in classpath specified by the default configuration. |
| * |
| * @param initialExtractorGroup initial list of extractors. |
| * @return full list of extractors. |
| * @throws java.io.IOException if there is an error locating the extractor(s). |
| * @throws IllegalAccessException if there are access permissions for extractor(s). |
| * @throws InstantiationException if there is an error instantiating extractor(s). |
| */ |
| public synchronized ExtractorGroup configureExtractors(ExtractorGroup initialExtractorGroup) |
| throws IOException, InstantiationException, IllegalAccessException { |
| final String pluginDirs = DefaultConfiguration.singleton().getPropertyOrFail(PLUGIN_DIRS_PROPERTY); |
| final File[] pluginLocations = getPluginLocations(pluginDirs); |
| return configureExtractors(pluginLocations); |
| } |
| |
| /** |
| * Returns an extractor group containing both the default extractors declared by the |
| * {@link org.apache.any23.extractor.ExtractorRegistry} and the {@link ExtractorPlugin}s. |
| * @param registry an {@link org.apache.any23.extractor.ExtractorRegistry} |
| * @param pluginLocations optional list of plugin locations. |
| * |
| * @return a not <code>null</code> and not empty extractor group. |
| * @throws java.io.IOException if there is an error locating the extractor group. |
| * @throws IllegalAccessException if there are access permissions for the extractor group. |
| * @throws InstantiationException if there is an error instantiating the extractor group. |
| */ |
| public synchronized ExtractorGroup getApplicableExtractors(ExtractorRegistry registry, File... pluginLocations) |
| throws IOException, IllegalAccessException, InstantiationException { |
| return configureExtractors(pluginLocations); |
| } |
| |
| /** |
| * Returns an {@link Iterator} of tools that have been detected within the given list of locations. |
| * |
| * @param pluginLocations list of plugin locations. |
| * @return set of detected tools. |
| * @throws IOException if there is an error acessing {@link org.apache.any23.cli.Tool}'s. |
| */ |
| public synchronized Iterator<Tool> getApplicableTools(File... pluginLocations) throws IOException { |
| final String report = loadPlugins(pluginLocations); |
| logger.info(report); |
| return getTools(); |
| } |
| |
| /** |
| * Converts a column separated list of dirs in a list of files. |
| * |
| * @param pluginDirsList |
| * @return |
| */ |
| private File[] getPluginLocations(String pluginDirsList) { |
| final String[] locationsStr = pluginDirsList.split(PLUGIN_DIRS_LIST_SEPARATOR); |
| final List<File> locations = new ArrayList<>(); |
| for(String locationStr : locationsStr) { |
| final File location = new File(locationStr); |
| if( ! location.exists()) { |
| throw new IllegalArgumentException( |
| String.format(java.util.Locale.ROOT, "Plugin location '%s' cannot be found.", locationStr) |
| ); |
| } |
| locations.add(location); |
| } |
| return locations.toArray(new File[locations.size()]); |
| } |
| |
| /** |
| * Dynamic local file class loader. |
| */ |
| private static final class DynamicClassLoader extends URLClassLoader { |
| |
| private final Set<String> addedURLs = new HashSet<>(); |
| |
| private final List<File> jars; |
| |
| private final List<File> dirs; |
| |
| public DynamicClassLoader(URL[] urls) { |
| super(urls, Any23PluginManager.class.getClassLoader()); |
| jars = new ArrayList<>(); |
| dirs = new ArrayList<>(); |
| } |
| |
| public DynamicClassLoader() { |
| this(new URL[0]); |
| } |
| |
| public boolean addClassDir(File classDir) { |
| final String urlPath = "file://" + classDir.getAbsolutePath() + "/"; |
| try { |
| if( addURL(urlPath) ) { |
| dirs.add(classDir); |
| return true; |
| } |
| return false; |
| } catch (MalformedURLException murle) { |
| throw new RuntimeException("Invalid dir URL.", murle); |
| } |
| } |
| |
| public boolean addJAR(File jar) { |
| final String urlPath = "jar:file://" + jar.getAbsolutePath() + "!/"; |
| try { |
| if (addURL(urlPath)) { |
| jars.add(jar); |
| return true; |
| } |
| return false; |
| } catch (MalformedURLException murle) { |
| throw new RuntimeException("Invalid JAR URL.", murle); |
| } |
| } |
| |
| private boolean addURL(String urlPath) throws MalformedURLException { |
| if(addedURLs.contains(urlPath)) { |
| return false; |
| } |
| super.addURL(new URL(urlPath)); |
| addedURLs.add(urlPath); |
| return true; |
| } |
| } |
| |
| } |