/*
* Licensed to Crate under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership. Crate 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.
*
* However, if you have executed another commercial license agreement
* with Crate these terms will supersede the license and you may use the
* software solely pursuant to the terms of the relevant commercial
* agreement.
*/
package io.crate.plugin;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import io.crate.Plugin;
import org.apache.logging.log4j.Logger;
import org.apache.xbean.finder.ResourceFinder;
import org.elasticsearch.bootstrap.JarHell;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.component.LifecycleComponent;
import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.plugins.PluginInfo;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import static org.elasticsearch.common.io.FileSystemUtils.isAccessibleDirectory;
public class PluginLoader {
static final Setting<String> SETTING_CRATE_PLUGINS_PATH = Setting.simpleString(
"path.crate_plugins", Setting.Property.NodeScope);
private static final String RESOURCE_PATH = "META-INF/services/";
private final Settings settings;
private final Map<Plugin, List<OnModuleReference>> onModuleReferences;
private final Logger logger;
@VisibleForTesting
final List<Plugin> plugins;
private final Path pluginsPath;
private final List<URL> jarsToLoad = new ArrayList<>();
PluginLoader(Settings settings) {
this.settings = settings;
String pluginFolder = SETTING_CRATE_PLUGINS_PATH.get(settings);
if (pluginFolder.isEmpty()) {
pluginsPath = PathUtils.get(Strings.cleanPath(settings.get("path.home"))).resolve("plugins");
} else {
pluginsPath = PathUtils.get(Strings.cleanPath(pluginFolder));
}
logger = Loggers.getLogger(getClass().getPackage().getName(), settings);
Collection<Class<? extends Plugin>> implementations = findImplementations();
MapBuilder<Plugin, List<OnModuleReference>> onModuleReferences = MapBuilder.newMapBuilder();
ImmutableList.Builder<Plugin> builder = ImmutableList.builder();
for (Class<? extends Plugin> pluginClass : implementations) {
Plugin plugin;
try {
plugin = loadPlugin(pluginClass);
} catch (Throwable t) {
logger.error("error loading plugin: " + pluginClass.getSimpleName(), t);
continue;
}
try {
List<OnModuleReference> onModuleReferenceList = loadModuleReferences(plugin);
if (!onModuleReferenceList.isEmpty()) {
onModuleReferences.put(plugin, onModuleReferenceList);
}
} catch (Throwable t) {
logger.error("error loading moduleReferences from plugin: " + plugin.name(), t);
continue;
}
builder.add(plugin);
}
plugins = builder.build();
this.onModuleReferences = onModuleReferences.immutableMap();
if (logger.isInfoEnabled()) {
logger.info("plugins loaded: {} ", plugins.stream().map(Plugin::name).collect(Collectors.toList()));
}
}
private Collection<Class<? extends Plugin>> findImplementations() {
if (!isAccessibleDirectory(pluginsPath, logger)) {
return Collections.emptyList();
}
File[] plugins = pluginsPath.toFile().listFiles();
if (plugins == null) {
return Collections.emptyList();
}
Collection<Class<? extends Plugin>> allImplementations = new ArrayList<>();
for (File plugin : plugins) {
if (!plugin.canRead()) {
logger.debug("[{}] is not readable.", plugin.getAbsolutePath());
continue;
}
// check if its an elasticsearch plugin
Path esDescriptorFile = plugin.toPath().resolve(PluginInfo.ES_PLUGIN_PROPERTIES);
try {
if (esDescriptorFile.toFile().exists()) {
continue;
}
} catch (Exception e) {
// ignore
}
List<URL> pluginUrls = new ArrayList<>();
logger.trace("--- adding plugin [{}]", plugin.getAbsolutePath());
try {
URL pluginURL = plugin.toURI().toURL();
// jar-hell check the plugin against the parent classloader
try {
checkJarHell(pluginURL);
} catch (Exception e) {
String msg = String.format(Locale.ENGLISH,
"failed to load plugin %s due to jar hell", pluginURL);
logger.error(msg, e);
throw new RuntimeException(msg, e);
}
pluginUrls.add(pluginURL);
if (!plugin.isFile()) {
// gather files to add
List<File> libFiles = new ArrayList<>();
File[] pluginFiles = plugin.listFiles();
if (pluginFiles != null) {
libFiles.addAll(Arrays.asList(pluginFiles));
}
File libLocation = new File(plugin, "lib");
if (libLocation.exists() && libLocation.isDirectory()) {
File[] pluginLibFiles = libLocation.listFiles();
if (pluginLibFiles != null) {
libFiles.addAll(Arrays.asList(pluginLibFiles));
}
}
// if there are jars in it, add it as well
for (File libFile : libFiles) {
if (!(libFile.getName().endsWith(".jar") || libFile.getName().endsWith(".zip"))) {
continue;
}
URL libURL = libFile.toURI().toURL();
// jar-hell check the plugin lib against the parent classloader
try {
checkJarHell(libURL);
pluginUrls.add(libURL);
} catch (Exception e) {
String msg = String.format(Locale.ENGLISH,
"Library %s of plugin %s already loaded", libURL, pluginURL);
logger.error(msg, e);
throw new RuntimeException(msg, e);
}
}
}
} catch (MalformedURLException e) {
String msg = String.format(Locale.ENGLISH, "failed to add plugin [%s]", plugin);
logger.error(msg, e);
throw new RuntimeException(msg, e);
}
Collection<Class<? extends Plugin>> implementations = findImplementations(pluginUrls);
if (implementations == null || implementations.isEmpty()) {
String msg = String.format(Locale.ENGLISH,
"Path [%s] does not contain a valid Crate or Elasticsearch plugin", plugin.getAbsolutePath());
RuntimeException e = new RuntimeException(msg);
logger.error(msg, e);
throw e;
}
jarsToLoad.addAll(pluginUrls);
allImplementations.addAll(implementations);
}
return allImplementations;
}
@Nullable
private Collection<Class<? extends Plugin>> findImplementations(Collection<URL> pluginUrls) {
URL[] urls = pluginUrls.toArray(new URL[pluginUrls.size()]);
ClassLoader loader = URLClassLoader.newInstance(urls, getClass().getClassLoader());
ResourceFinder finder = new ResourceFinder(RESOURCE_PATH, loader, urls);
try {
return finder.findAllImplementations(Plugin.class);
} catch (ClassCastException e) {
logger.error("plugin does not implement io.crate.Plugin interface", e);
} catch (ClassNotFoundException e) {
logger.error("error while loading plugin, misconfigured plugin", e);
} catch (Throwable t) {
logger.error("error while loading plugins", t);
}
return null;
}
private Plugin loadPlugin(Class<? extends Plugin> pluginClass) {
Constructor<? extends Plugin> constructor;
try {
constructor = pluginClass.getConstructor(Settings.class);
try {
return constructor.newInstance(settings);
} catch (Exception e) {
throw new PluginException("Failed to create plugin [" + pluginClass + "]", e);
}
} catch (NoSuchMethodException e) {
try {
constructor = pluginClass.getConstructor();
try {
return constructor.newInstance();
} catch (Exception e1) {
throw new PluginException("Failed to create plugin [" + pluginClass + "]", e);
}
} catch (NoSuchMethodException e1) {
throw new PluginException("No constructor for [" + pluginClass + "]");
}
}
}
private List<OnModuleReference> loadModuleReferences(Plugin plugin) {
List<OnModuleReference> list = new ArrayList<>();
for (Method method : plugin.getClass().getDeclaredMethods()) {
if (!method.getName().equals("onModule")) {
continue;
}
if (method.getParameterTypes().length == 0 || method.getParameterTypes().length > 1) {
logger.warn("Plugin: {} implementing onModule with no parameters or more than one parameter", plugin.name());
continue;
}
Class moduleClass = method.getParameterTypes()[0];
if (!Module.class.isAssignableFrom(moduleClass)) {
logger.warn("Plugin: {} implementing onModule by the type is not of Module type {}", plugin.name(), moduleClass);
continue;
}
method.setAccessible(true);
//noinspection unchecked
list.add(new OnModuleReference(moduleClass, method));
}
return list;
}
Collection<Module> createGuiceModules() {
List<Module> modules = new ArrayList<>();
for (Plugin plugin : plugins) {
modules.addAll(plugin.createGuiceModules());
}
return modules;
}
Collection<Class<? extends LifecycleComponent>> getGuiceServiceClasses() {
List<Class<? extends LifecycleComponent>> services = new ArrayList<>();
for (Plugin plugin : plugins) {
services.addAll(plugin.getGuiceServiceClasses());
}
return services;
}
Settings additionalSettings() {
Settings.Builder builder = Settings.builder();
for (Plugin plugin : plugins) {
builder.put(plugin.additionalSettings());
}
return builder.build();
}
List<Setting<?>> getSettings() {
List<Setting<?>> settings = new ArrayList<>();
for (Plugin plugin : plugins) {
settings.addAll(plugin.getSettings());
}
return settings;
}
public void processModule(Module module) {
for (Plugin plugin : plugins) {
// see if there are onModule references
List<OnModuleReference> references = onModuleReferences.get(plugin);
if (references != null) {
for (OnModuleReference reference : references) {
if (reference.moduleClass.isAssignableFrom(module.getClass())) {
try {
reference.onModuleMethod.invoke(plugin, module);
} catch (Exception e) {
logger.warn("Plugin {}, failed to invoke custom onModule method", e, plugin.name());
}
}
}
}
}
}
private void checkJarHell(URL url) throws Exception {
final List<URL> loadedJars = new ArrayList<>(Arrays.asList(JarHell.parseClassPath()));
loadedJars.addAll(jarsToLoad);
loadedJars.add(url);
JarHell.checkJarHell(loadedJars.toArray(new URL[0]));
}
}