org.graylog2.shared.plugins.PluginLoader Maven / Gradle / Ivy
/**
* This file is part of Graylog.
*
* Graylog is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Graylog 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Graylog. If not, see .
*/
package org.graylog2.shared.plugins;
import com.google.common.base.Function;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import org.graylog2.plugin.Plugin;
import org.graylog2.plugin.PluginMetaData;
import org.graylog2.plugin.PluginModule;
import org.graylog2.shared.SuppressForbidden;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Objects.requireNonNull;
public class PluginLoader {
private static final Logger LOG = LoggerFactory.getLogger(PluginLoader.class);
private final File pluginDir;
private final ChainingClassLoader classLoader;
public PluginLoader(File pluginDir, ChainingClassLoader classLoader) {
this.pluginDir = requireNonNull(pluginDir);
this.classLoader = requireNonNull(classLoader);
}
public Set loadPlugins() {
return ImmutableSortedSet.orderedBy(new PluginComparator())
.addAll(Iterables.transform(loadJarPlugins(), new PluginAdapterFunction()))
.addAll(Iterables.transform(loadClassPathPlugins(), new PluginAdapterFunction()))
.build();
}
private Iterable loadClassPathPlugins() {
return ServiceLoader.load(Plugin.class);
}
@SuppressForbidden("Deliberate invocation of URL#getFile()")
private Iterable loadJarPlugins() {
if (!pluginDir.exists()) {
LOG.warn("Plugin directory {} does not exist, not loading plugins.", pluginDir.getAbsolutePath());
return Collections.emptySet();
}
if (!pluginDir.isDirectory()) {
LOG.warn("Path {} is not a directory, cannot load plugins.", pluginDir);
return Collections.emptySet();
}
LOG.debug("Scanning directory <{}> for plugins...", pluginDir.getAbsolutePath());
final File[] files = pluginDir.listFiles();
if (files == null) {
LOG.warn("Could not list files in {}, cannot load plugins.", pluginDir);
return Collections.emptySet();
}
LOG.debug("Loading [{}] plugins", files.length);
final List urls = Arrays.stream(files)
.filter(File::isFile)
.map(jar -> {
try {
LOG.debug("Loading <" + jar.getAbsolutePath() + ">");
return jar.toURI().toURL();
} catch (MalformedURLException e) {
LOG.error("Cannot open JAR file for discovering plugins", e);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
final List sharedClassLoaderUrls = new ArrayList<>();
urls.forEach(url -> {
final PluginProperties properties = PluginProperties.fromJarFile(url.getFile());
// Decide whether to create a separate, isolated class loader for the plugin. When the plugin is isolated
// (the default), it gets its own class loader and cannot see other plugins. Plugins which are not
// isolated share one class loader so they can see each other. (makes plugin inter-dependencies work)
if (properties.isIsolated()) {
LOG.debug("Creating isolated class loader for <{}>", url);
classLoader.addClassLoader(URLClassLoader.newInstance(new URL[]{url}));
} else {
LOG.debug("Using shared class loader for <{}>", url);
sharedClassLoaderUrls.add(url);
}
});
// Only create the shared class loader if any plugin requests to be shared.
if (!sharedClassLoaderUrls.isEmpty()) {
LOG.debug("Creating shared class loader for {} plugins: {}", sharedClassLoaderUrls.size(), sharedClassLoaderUrls);
classLoader.addClassLoader(URLClassLoader.newInstance(sharedClassLoaderUrls.toArray(new URL[sharedClassLoaderUrls.size()])));
}
final ServiceLoader pluginServiceLoader = ServiceLoader.load(Plugin.class, classLoader);
return ImmutableSet.copyOf(pluginServiceLoader);
}
public static class PluginComparator implements Comparator {
/**
* {@inheritDoc}
*/
@Override
public int compare(Plugin o1, Plugin o2) {
return ComparisonChain.start()
.compare(o1.metadata().getUniqueId(), o2.metadata().getUniqueId())
.compare(o1.metadata().getName(), o2.metadata().getName())
.compare(o1.metadata().getVersion(), o2.metadata().getVersion())
.result();
}
}
/**
* Adapter for {@link org.graylog2.plugin.Plugin} which implements {@link #equals(Object)} and {@link #hashCode()}
* only taking {@link org.graylog2.plugin.PluginMetaData#getUniqueId()} into account.
*/
public static class PluginAdapter implements Plugin {
private final Plugin plugin;
public PluginAdapter(Plugin plugin) {
this.plugin = checkNotNull(plugin);
}
@Override
public PluginMetaData metadata() {
return plugin.metadata();
}
@Override
public Collection modules() {
return plugin.modules();
}
public String getPluginClassName() {
return plugin.getClass().getCanonicalName();
}
@Override
public int hashCode() {
return Objects.hash(plugin.metadata().getUniqueId());
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Plugin) {
final Plugin that = (Plugin) obj;
return Objects.equals(this.metadata().getUniqueId(), that.metadata().getUniqueId());
}
return false;
}
@Override
public String toString() {
final PluginMetaData metadata = plugin.metadata();
return metadata.getName() + " " + metadata.getVersion() + " [" + metadata.getUniqueId() + "]";
}
}
private static class PluginAdapterFunction implements Function {
@Override
public Plugin apply(Plugin input) {
return new PluginAdapter(input);
}
}
}