org.sonarsource.sonarlint.core.plugin.PluginInstancesLoader Maven / Gradle / Ivy
/*
* SonarLint Core - Implementation
* Copyright (C) 2016-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.core.plugin;
import com.google.common.base.Strings;
import java.io.Closeable;
import java.io.File;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.CheckForNull;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.SystemUtils;
import org.sonar.api.Plugin;
import org.sonar.api.utils.TempFolder;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import static org.apache.commons.lang.StringUtils.isNotEmpty;
/**
* Loads the plugin JAR files by creating the appropriate classloaders and by instantiating
* the entry point classes as defined in manifests. It assumes that JAR files are compatible with current
* environment (minimal sonarqube version, compatibility between plugins, ...):
*
* - server verifies compatibility of JARs before deploying them at startup (see ServerPluginRepository)
* - batch loads only the plugins deployed on server (see BatchPluginRepository)
*
*
* Plugins have their own isolated classloader, inheriting only from API classes.
* Some plugins can extend a "base" plugin, sharing the same classloader.
*
* This class is stateless. It does not keep pointers to classloaders and {@link org.sonar.api.Plugin}.
*/
public class PluginInstancesLoader {
private static final Logger LOG = Loggers.get(PluginInstancesLoader.class);
private static final String[] DEFAULT_SHARED_RESOURCES = {"org/sonar/plugins", "com/sonar/plugins", "com/sonarsource/plugins"};
private static final String SLF4J_ADAPTER_JAR_NAME = "sonarlint-slf4j-sonar-log";
private final PluginJarExploder jarExploder;
private final PluginClassloaderFactory classloaderFactory;
private final TempFolder tempFolder;
public PluginInstancesLoader(PluginJarExploder jarExploder, PluginClassloaderFactory classloaderFactory, TempFolder tempFolder) {
this.jarExploder = jarExploder;
this.classloaderFactory = classloaderFactory;
this.tempFolder = tempFolder;
}
public Map load(Map infoByKeys) {
File slf4jAdapter = extractSlf4jAdapterJar();
Collection defs = defineClassloaders(infoByKeys, slf4jAdapter);
Map classloaders = classloaderFactory.create(defs);
return instantiatePluginClasses(classloaders);
}
/**
* Defines the different classloaders to be created. Number of classloaders can be
* different than number of plugins.
*/
Collection defineClassloaders(Map infoByKeys, File slf4jAdapter) {
Map classloadersByBasePlugin = new HashMap<>();
for (PluginInfo info : infoByKeys.values()) {
String baseKey = basePluginKey(info, infoByKeys);
if (baseKey == null) {
continue;
}
PluginClassLoaderDef def = classloadersByBasePlugin.get(baseKey);
if (def == null) {
def = new PluginClassLoaderDef(baseKey);
classloadersByBasePlugin.put(baseKey, def);
}
ExplodedPlugin explodedPlugin = jarExploder.explode(info);
def.addFiles(Collections.singletonList(slf4jAdapter));
def.addFiles(Collections.singletonList(explodedPlugin.getMain()));
def.addFiles(explodedPlugin.getLibs());
def.addMainClass(info.getKey(), info.getMainClass());
for (String defaultSharedResource : DEFAULT_SHARED_RESOURCES) {
def.getExportMask().addInclusion(String.format("%s/%s/api/", defaultSharedResource, info.getKey()));
}
// The plugins that extend other plugins can only add some files to classloader.
// They can't change metadata like ordering strategy or compatibility mode.
if (Strings.isNullOrEmpty(info.getBasePlugin())) {
def.setSelfFirstStrategy(info.isUseChildFirstClassLoader());
}
}
return classloadersByBasePlugin.values();
}
private File extractSlf4jAdapterJar() {
InputStream jarInputStream = PluginInstancesLoader.class.getResourceAsStream("/" + SLF4J_ADAPTER_JAR_NAME + ".jar");
try {
File extractedJar = tempFolder.newFile(SLF4J_ADAPTER_JAR_NAME, ".jar");
FileUtils.copyInputStreamToFile(jarInputStream, extractedJar);
return extractedJar;
} catch (Exception e) {
throw new IllegalStateException("Failed to extract the jar '" + SLF4J_ADAPTER_JAR_NAME + ".jar'");
}
}
/**
* Instantiates collection of {@link org.sonar.api.Plugin} according to given metadata and classloaders
*
* @return the instances grouped by plugin key
* @throws IllegalStateException if at least one plugin can't be correctly loaded
*/
Map instantiatePluginClasses(Map classloaders) {
// instantiate plugins
Map instancesByPluginKey = new HashMap<>();
for (Map.Entry entry : classloaders.entrySet()) {
PluginClassLoaderDef def = entry.getKey();
ClassLoader classLoader = entry.getValue();
// the same classloader can be used by multiple plugins
for (Map.Entry mainClassEntry : def.getMainClassesByPluginKey().entrySet()) {
String pluginKey = mainClassEntry.getKey();
String mainClass = mainClassEntry.getValue();
try {
instancesByPluginKey.put(pluginKey, (Plugin) classLoader.loadClass(mainClass).newInstance());
} catch (UnsupportedClassVersionError e) {
throw new IllegalStateException(String.format("The plugin [%s] does not support Java %s",
pluginKey, SystemUtils.JAVA_VERSION_TRIMMED), e);
} catch (Throwable e) {
throw new IllegalStateException(String.format(
"Fail to instantiate class [%s] of plugin [%s]", mainClass, pluginKey), e);
}
}
}
return instancesByPluginKey;
}
public void unload(Collection plugins) {
for (Plugin plugin : plugins) {
ClassLoader classLoader = plugin.getClass().getClassLoader();
if (classLoader instanceof Closeable && classLoader != classloaderFactory.baseClassLoader()) {
try {
((Closeable) classLoader).close();
} catch (Exception e) {
Loggers.get(getClass()).error("Fail to close classloader " + classLoader.toString(), e);
}
}
}
}
/**
* Get the root key of a tree of plugins. For example if plugin C depends on B, which depends on A, then
* B and C must be attached to the classloader of A. The method returns A in the three cases.
*/
@CheckForNull
static String basePluginKey(PluginInfo plugin, Map allPluginsPerKey) {
String base = plugin.getKey();
String parentKey = plugin.getBasePlugin();
while (isNotEmpty(parentKey)) {
PluginInfo parentPlugin = allPluginsPerKey.get(parentKey);
if (parentPlugin == null) {
LOG.warn("Unable to find base plugin '{}' referenced by plugin '{}'", parentKey, base);
return null;
}
base = parentPlugin.getKey();
parentKey = parentPlugin.getBasePlugin();
}
return base;
}
}