blob: f286874fa874186cd96cea5c5504d02d41cad089 [file] [log] [blame]
/*
* 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;
}
}
}