diff options
author | Simon Brandhof <simon.brandhof@sonarsource.com> | 2015-05-29 10:29:04 +0200 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@sonarsource.com> | 2015-06-05 09:54:04 +0200 |
commit | 0956511c8c9d6aa6639d4378f47d73877cdc18de (patch) | |
tree | c7a14388d2eec8a5f1f1d7d25fb48894ee4aae9f /sonar-core | |
parent | b1a02efd650918249cd828bd19f4ebf2118d8a79 (diff) | |
download | sonarqube-0956511c8c9d6aa6639d4378f47d73877cdc18de.tar.gz sonarqube-0956511c8c9d6aa6639d4378f47d73877cdc18de.zip |
SONAR-6370 isolate plugin classloader from core classes
Diffstat (limited to 'sonar-core')
18 files changed, 608 insertions, 164 deletions
diff --git a/sonar-core/pom.xml b/sonar-core/pom.xml index 39383aa7962..069d94c38ff 100644 --- a/sonar-core/pom.xml +++ b/sonar-core/pom.xml @@ -118,6 +118,7 @@ <groupId>org.codehaus.sonar</groupId> <artifactId>sonar-plugin-api-deps</artifactId> <version>${project.version}</version> + <optional>true</optional> <scope>runtime</scope> </dependency> @@ -199,7 +200,7 @@ <executions> <execution> <id>copy-deprecated-api-deps</id> - <phase>process-resources</phase> + <phase>generate-resources</phase> <goals> <goal>copy</goal> </goals> diff --git a/sonar-core/src/main/java/org/sonar/core/platform/ClassloaderDef.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderDef.java index 3ebe5ccb0fb..9939c1a1c42 100644 --- a/sonar-core/src/main/java/org/sonar/core/platform/ClassloaderDef.java +++ b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderDef.java @@ -21,30 +21,33 @@ package org.sonar.core.platform; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import java.util.Collection; -import javax.annotation.Nullable; -import org.sonar.classloader.Mask; - import java.io.File; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.annotation.Nullable; +import org.sonar.classloader.Mask; /** - * Information about the classloader to be created for a set of plugins. + * Temporary information about the classloader to be created for a plugin (or a group of plugins). */ -class ClassloaderDef { +class PluginClassloaderDef { private final String basePluginKey; private final Map<String, String> mainClassesByPluginKey = new HashMap<>(); private final List<File> files = new ArrayList<>(); private final Mask mask = new Mask(); private boolean selfFirstStrategy = false; - private ClassLoader classloader = null; - ClassloaderDef(String basePluginKey) { - Preconditions.checkNotNull(basePluginKey); + /** + * Compatibility with API classloader as defined before version 5.2 + */ + private boolean compatibilityMode = false; + + PluginClassloaderDef(String basePluginKey) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(basePluginKey)); this.basePluginKey = basePluginKey; } @@ -52,15 +55,15 @@ class ClassloaderDef { return basePluginKey; } - Map<String, String> getMainClassesByPluginKey() { - return mainClassesByPluginKey; - } - List<File> getFiles() { return files; } - Mask getMask() { + void addFiles(Collection<File> f) { + this.files.addAll(f); + } + + Mask getExportMask() { return mask; } @@ -72,26 +75,38 @@ class ClassloaderDef { this.selfFirstStrategy = selfFirstStrategy; } - /** - * Returns the newly created classloader. Throws an exception - * if null, for example because called before {@link #setBuiltClassloader(ClassLoader)} - */ - ClassLoader getBuiltClassloader() { - Preconditions.checkState(classloader != null); - return classloader; + Map<String, String> getMainClassesByPluginKey() { + return mainClassesByPluginKey; + } + + void addMainClass(String pluginKey, @Nullable String mainClass) { + if (!Strings.isNullOrEmpty(mainClass)) { + mainClassesByPluginKey.put(pluginKey, mainClass); + } } - void setBuiltClassloader(ClassLoader c) { - this.classloader = c; + boolean isCompatibilityMode() { + return compatibilityMode; } - void addFiles(Collection<File> c) { - this.files.addAll(c); + void setCompatibilityMode(boolean b) { + this.compatibilityMode = b; } - void addMainClass(String pluginKey, @Nullable String mainClass) { - if (!Strings.isNullOrEmpty(mainClass)) { - mainClassesByPluginKey.put(pluginKey, mainClass); + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; } + PluginClassloaderDef that = (PluginClassloaderDef) o; + return basePluginKey.equals(that.basePluginKey); + } + + @Override + public int hashCode() { + return basePluginKey.hashCode(); } } diff --git a/sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderFactory.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderFactory.java new file mode 100644 index 00000000000..a79ba09910d --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderFactory.java @@ -0,0 +1,163 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.sonar.core.platform; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import org.apache.commons.io.FileUtils; +import org.sonar.api.batch.BatchSide; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.TempFolder; +import org.sonar.classloader.ClassloaderBuilder; +import org.sonar.classloader.Mask; + +import static org.sonar.classloader.ClassloaderBuilder.LoadingOrder.PARENT_FIRST; +import static org.sonar.classloader.ClassloaderBuilder.LoadingOrder.SELF_FIRST; + +/** + * Builds the graph of classloaders to be used to instantiate plugins. It deals with: + * <ul> + * <li>isolation of plugins against core classes (except api)</li> + * <li>backward-compatibility with plugins built for versions of SQ lower than 5.2. At that time + * API declared transitive dependencies that were automatically available to plugins</li> + * <li>sharing of some packages between plugins</li> + * <li>loading of the libraries embedded in plugin JAR files (directory META-INF/libs)</li> + * </ul> + */ +@BatchSide +@ServerSide +public class PluginClassloaderFactory { + + // underscores are used to not conflict with plugin keys (if someday a plugin key is "api") + private static final String API_CLASSLOADER_KEY = "_api_"; + + private final TempFolder temp; + private URL compatibilityModeJar; + + public PluginClassloaderFactory(TempFolder temp) { + this.temp = temp; + } + + /** + * Creates as many classloaders as requested by the input parameter. + */ + public Map<PluginClassloaderDef, ClassLoader> create(Collection<PluginClassloaderDef> defs) { + ClassloaderBuilder builder = new ClassloaderBuilder(); + builder.newClassloader(API_CLASSLOADER_KEY, baseClassloader()); + builder.setMask(API_CLASSLOADER_KEY, apiMask()); + + for (PluginClassloaderDef def : defs) { + builder.newClassloader(def.getBasePluginKey()); + builder.setParent(def.getBasePluginKey(), API_CLASSLOADER_KEY, new Mask()); + builder.setLoadingOrder(def.getBasePluginKey(), def.isSelfFirstStrategy() ? SELF_FIRST : PARENT_FIRST); + for (File jar : def.getFiles()) { + builder.addURL(def.getBasePluginKey(), fileToUrl(jar)); + } + if (def.isCompatibilityMode()) { + builder.addURL(def.getBasePluginKey(), extractCompatibilityModeJar()); + } + exportResources(def, builder, defs); + } + + return build(defs, builder); + } + + /** + * A plugin can export some resources to other plugins + */ + private void exportResources(PluginClassloaderDef def, ClassloaderBuilder builder, Collection<PluginClassloaderDef> allPlugins) { + // export the resources to all other plugins + builder.setExportMask(def.getBasePluginKey(), def.getExportMask()); + for (PluginClassloaderDef other : allPlugins) { + if (!other.getBasePluginKey().equals(def.getBasePluginKey())) { + builder.addSibling(def.getBasePluginKey(), other.getBasePluginKey(), new Mask()); + } + } + } + + /** + * Builds classloaders and verifies that all of them are correctly defined + */ + private Map<PluginClassloaderDef, ClassLoader> build(Collection<PluginClassloaderDef> defs, ClassloaderBuilder builder) { + Map<PluginClassloaderDef, ClassLoader> result = new HashMap<>(); + Map<String, ClassLoader> classloadersByBasePluginKey = builder.build(); + for (PluginClassloaderDef def : defs) { + ClassLoader classloader = classloadersByBasePluginKey.get(def.getBasePluginKey()); + if (classloader == null) { + throw new IllegalStateException(String.format("Fail to create classloader for plugin [%s]", def.getBasePluginKey())); + } + result.put(def, classloader); + } + return result; + } + + ClassLoader baseClassloader() { + return getClass().getClassLoader(); + } + + private URL extractCompatibilityModeJar() { + if (compatibilityModeJar == null) { + File jar = temp.newFile("sonar-plugin-api-deps", "jar"); + try { + FileUtils.copyURLToFile(getClass().getResource("/sonar-plugin-api-deps.jar"), jar); + compatibilityModeJar = jar.toURI().toURL(); + } catch (Exception e) { + throw new IllegalStateException("Can not extract sonar-plugin-api-deps.jar to " + jar.getAbsolutePath(), e); + } + } + return compatibilityModeJar; + } + + private static URL fileToUrl(File file) { + try { + return file.toURI().toURL(); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * The resources (packages) that API exposes to plugins. Other core classes (SonarQube, MyBatis, ...) + * can't be accessed. + * <p>To sum-up, these are the classes packaged in sonar-plugin-api.jar or available as + * a transitive dependency of sonar-plugin-api</p> + */ + private static Mask apiMask() { + return new Mask() + // inclusions + .addInclusion("org/sonar/api/") + .addInclusion("org/sonar/channel/") + .addInclusion("org/sonar/check/") + .addInclusion("org/sonar/colorizer/") + .addInclusion("org/sonar/duplications/") + .addInclusion("org/sonar/graph/") + .addInclusion("org/sonar/plugins/emailnotifications/api/") + .addInclusion("net/sourceforge/pmd/") + .addInclusion("org/apache/maven/") + .addInclusion("org/slf4j/") + + // exclusions + .addExclusion("org/sonar/api/internal/"); + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/platform/PluginExploder.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginJarExploder.java index 50681f13e3d..fb96c72ae9e 100644 --- a/sonar-core/src/main/java/org/sonar/core/platform/PluginExploder.java +++ b/sonar-core/src/main/java/org/sonar/core/platform/PluginJarExploder.java @@ -28,7 +28,7 @@ import java.util.zip.ZipEntry; import static org.apache.commons.io.FileUtils.listFiles; -public abstract class PluginExploder { +public abstract class PluginJarExploder { protected static final String LIB_RELATIVE_PATH_IN_JAR = "META-INF/lib"; @@ -39,7 +39,7 @@ public abstract class PluginExploder { } protected ExplodedPlugin explodeFromUnzippedDir(String pluginKey, File jarFile, File unzippedDir) { - File libDir = new File(unzippedDir, PluginExploder.LIB_RELATIVE_PATH_IN_JAR); + File libDir = new File(unzippedDir, PluginJarExploder.LIB_RELATIVE_PATH_IN_JAR); Collection<File> libs; if (libDir.isDirectory() && libDir.exists()) { libs = listFiles(libDir, null, false); diff --git a/sonar-core/src/main/java/org/sonar/core/platform/PluginLoader.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginLoader.java index f0cbc1defe3..1012c8ca438 100644 --- a/sonar-core/src/main/java/org/sonar/core/platform/PluginLoader.java +++ b/sonar-core/src/main/java/org/sonar/core/platform/PluginLoader.java @@ -21,23 +21,16 @@ package org.sonar.core.platform; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import org.apache.commons.lang.SystemUtils; -import org.sonar.api.Plugin; -import org.sonar.api.utils.log.Loggers; -import org.sonar.classloader.ClassloaderBuilder; -import org.sonar.classloader.Mask; - import java.io.Closeable; -import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import org.apache.commons.lang.SystemUtils; +import org.sonar.api.Plugin; +import org.sonar.api.utils.log.Loggers; +import org.sonar.updatecenter.common.Version; import static java.util.Arrays.asList; -import static org.sonar.classloader.ClassloaderBuilder.LoadingOrder.PARENT_FIRST; -import static org.sonar.classloader.ClassloaderBuilder.LoadingOrder.SELF_FIRST; /** * Loads the plugin JAR files by creating the appropriate classloaders and by instantiating @@ -45,117 +38,98 @@ import static org.sonar.classloader.ClassloaderBuilder.LoadingOrder.SELF_FIRST; * environment (minimal sonarqube version, compatibility between plugins, ...): * <ul> * <li>server verifies compatibility of JARs before deploying them at startup (see ServerPluginRepository)</li> - * <li>batch loads only the plugins deployed on server</li> + * <li>batch loads only the plugins deployed on server (see BatchPluginRepository)</li> * </ul> * <p/> - * Standard plugins have their own isolated classloader. Some others can extend a "base" plugin. - * In this case they share the same classloader then the base plugin. + * Plugins have their own isolated classloader, inheriting only from API classes. + * Some plugins can extend a "base" plugin, sharing the same classloader. * <p/> - * This class is stateless. It does not keep classloaders and {@link Plugin} in memory. + * This class is stateless. It does not keep pointers to classloaders and {@link Plugin}. */ public class PluginLoader { private static final String[] DEFAULT_SHARED_RESOURCES = {"org/sonar/plugins", "com/sonar/plugins", "com/sonarsource/plugins"}; + public static final Version COMPATIBILITY_MODE_MAX_VERSION = Version.create("5.2"); - // underscores are used to not conflict with plugin keys (if someday a plugin key is "api") - private static final String API_CLASSLOADER_KEY = "_api_"; + private final PluginJarExploder jarExploder; + private final PluginClassloaderFactory classloaderFactory; - private final PluginExploder exploder; - - public PluginLoader(PluginExploder exploder) { - this.exploder = exploder; + public PluginLoader(PluginJarExploder jarExploder, PluginClassloaderFactory classloaderFactory) { + this.jarExploder = jarExploder; + this.classloaderFactory = classloaderFactory; } public Map<String, Plugin> load(Map<String, PluginInfo> infoByKeys) { - Collection<ClassloaderDef> defs = defineClassloaders(infoByKeys); - buildClassloaders(defs); - return instantiatePluginInstances(defs); + Collection<PluginClassloaderDef> defs = defineClassloaders(infoByKeys); + Map<PluginClassloaderDef, ClassLoader> classloaders = classloaderFactory.create(defs); + return instantiatePluginClasses(classloaders); } /** - * Step 1 - define the different classloaders to be created. Number of classloaders can be + * Defines the different classloaders to be created. Number of classloaders can be * different than number of plugins. */ @VisibleForTesting - Collection<ClassloaderDef> defineClassloaders(Map<String, PluginInfo> infoByKeys) { - Map<String, ClassloaderDef> classloadersByBasePlugin = new HashMap<>(); + Collection<PluginClassloaderDef> defineClassloaders(Map<String, PluginInfo> infoByKeys) { + Map<String, PluginClassloaderDef> classloadersByBasePlugin = new HashMap<>(); for (PluginInfo info : infoByKeys.values()) { String baseKey = basePluginKey(info, infoByKeys); - ClassloaderDef def = classloadersByBasePlugin.get(baseKey); + PluginClassloaderDef def = classloadersByBasePlugin.get(baseKey); if (def == null) { - def = new ClassloaderDef(baseKey); + def = new PluginClassloaderDef(baseKey); classloadersByBasePlugin.put(baseKey, def); } - ExplodedPlugin explodedPlugin = exploder.explode(info); + ExplodedPlugin explodedPlugin = jarExploder.explode(info); def.addFiles(asList(explodedPlugin.getMain())); def.addFiles(explodedPlugin.getLibs()); def.addMainClass(info.getKey(), info.getMainClass()); for (String defaultSharedResource : DEFAULT_SHARED_RESOURCES) { - def.getMask().addInclusion(String.format("%s/%s/api/", defaultSharedResource, info.getKey())); + 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())) { - // The plugins that extend other plugins can only add some files to classloader. - // They can't change ordering strategy. def.setSelfFirstStrategy(info.isUseChildFirstClassLoader()); - } - } - return classloadersByBasePlugin.values(); - } - - /** - * Step 2 - create classloaders with appropriate constituents and metadata - */ - private void buildClassloaders(Collection<ClassloaderDef> defs) { - ClassloaderBuilder builder = new ClassloaderBuilder(); - builder.newClassloader(API_CLASSLOADER_KEY, baseClassloader()); - for (ClassloaderDef def : defs) { - builder - .newClassloader(def.getBasePluginKey()) - .setParent(def.getBasePluginKey(), API_CLASSLOADER_KEY, new Mask()) - // resources to be exported to other plugin classloaders (siblings) - .setExportMask(def.getBasePluginKey(), def.getMask()) - .setLoadingOrder(def.getBasePluginKey(), def.isSelfFirstStrategy() ? SELF_FIRST : PARENT_FIRST); - for (File file : def.getFiles()) { - builder.addURL(def.getBasePluginKey(), fileToUrl(file)); - } - for (ClassloaderDef sibling : defs) { - if (!sibling.getBasePluginKey().equals(def.getBasePluginKey())) { - builder.addSibling(def.getBasePluginKey(), sibling.getBasePluginKey(), new Mask()); + Version minSqVersion = info.getMinimalSqVersion(); + boolean compatibilityMode = (minSqVersion != null && minSqVersion.compareToIgnoreQualifier(COMPATIBILITY_MODE_MAX_VERSION) < 0); + def.setCompatibilityMode(compatibilityMode); + if (compatibilityMode) { + Loggers.get(getClass()).info("API compatibility mode is enabled on plugin {} [{}] " + + "(built with API lower than {})", + info.getName(), info.getKey(), COMPATIBILITY_MODE_MAX_VERSION); } } } - Map<String, ClassLoader> classloadersByBasePluginKey = builder.build(); - for (ClassloaderDef def : defs) { - ClassLoader builtClassloader = classloadersByBasePluginKey.get(def.getBasePluginKey()); - if (builtClassloader == null) { - throw new IllegalStateException(String.format("Fail to create classloader for plugin [%s]", def.getBasePluginKey())); - } - def.setBuiltClassloader(builtClassloader); - } + return classloadersByBasePlugin.values(); } /** - * Step 3 - instantiate plugin instances ({@link Plugin} + * Instantiates collection of ({@link 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 */ - private Map<String, Plugin> instantiatePluginInstances(Collection<ClassloaderDef> defs) { + @VisibleForTesting + Map<String, Plugin> instantiatePluginClasses(Map<PluginClassloaderDef, ClassLoader> classloaders) { // instantiate plugins Map<String, Plugin> instancesByPluginKey = new HashMap<>(); - for (ClassloaderDef def : defs) { + for (Map.Entry<PluginClassloaderDef, ClassLoader> entry : classloaders.entrySet()) { + PluginClassloaderDef def = entry.getKey(); + ClassLoader classLoader = entry.getValue(); + // the same classloader can be used by multiple plugins - for (Map.Entry<String, String> entry : def.getMainClassesByPluginKey().entrySet()) { - String pluginKey = entry.getKey(); - String mainClass = entry.getValue(); + for (Map.Entry<String, String> mainClassEntry : def.getMainClassesByPluginKey().entrySet()) { + String pluginKey = mainClassEntry.getKey(); + String mainClass = mainClassEntry.getValue(); try { - instancesByPluginKey.put(pluginKey, (Plugin) def.getBuiltClassloader().loadClass(mainClass).newInstance()); + 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 (ClassNotFoundException | IllegalAccessException | InstantiationException e) { + } catch (Throwable e) { throw new IllegalStateException(String.format( "Fail to instantiate class [%s] of plugin [%s]", mainClass, pluginKey), e); } @@ -167,7 +141,7 @@ public class PluginLoader { public void unload(Collection<Plugin> plugins) { for (Plugin plugin : plugins) { ClassLoader classLoader = plugin.getClass().getClassLoader(); - if (classLoader instanceof Closeable && classLoader != baseClassloader()) { + if (classLoader instanceof Closeable && classLoader != classloaderFactory.baseClassloader()) { try { ((Closeable) classLoader).close(); } catch (Exception e) { @@ -178,13 +152,6 @@ public class PluginLoader { } /** - * This method can be overridden to change the base classloader. - */ - protected ClassLoader baseClassloader() { - return getClass().getClassLoader(); - } - - /** * 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. */ @@ -198,12 +165,4 @@ public class PluginLoader { } return base; } - - private static URL fileToUrl(File file) { - try { - return file.toURI().toURL(); - } catch (MalformedURLException e) { - throw new IllegalStateException(e); - } - } } diff --git a/sonar-core/src/test/java/org/sonar/core/platform/PluginClassloaderFactoryTest.java b/sonar-core/src/test/java/org/sonar/core/platform/PluginClassloaderFactoryTest.java new file mode 100644 index 00000000000..9b5f6cf2dc5 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/platform/PluginClassloaderFactoryTest.java @@ -0,0 +1,132 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.sonar.core.platform; + +import java.io.File; +import java.util.Map; +import org.apache.commons.lang.StringUtils; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.api.utils.internal.JUnitTempFolder; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class PluginClassloaderFactoryTest { + + static final String BASE_PLUGIN_CLASSNAME = "org.sonar.plugins.base.BasePlugin"; + static final String DEPENDENT_PLUGIN_CLASSNAME = "org.sonar.plugins.dependent.DependentPlugin"; + static final String BASE_PLUGIN_KEY = "base"; + static final String DEPENDENT_PLUGIN_KEY = "dependent"; + + @Rule + public JUnitTempFolder temp = new JUnitTempFolder(); + + PluginClassloaderFactory factory = new PluginClassloaderFactory(temp); + + @Test + public void create_isolated_classloader() throws Exception { + PluginClassloaderDef def = basePluginDef(); + Map<PluginClassloaderDef, ClassLoader> map = factory.create(asList(def)); + + assertThat(map).containsOnlyKeys(def); + ClassLoader classLoader = map.get(def); + + // plugin can access to API classes, and of course to its own classes ! + assertThat(canLoadClass(classLoader, RulesDefinition.class.getCanonicalName())).isTrue(); + assertThat(canLoadClass(classLoader, BASE_PLUGIN_CLASSNAME)).isTrue(); + + // plugin can not access to core classes + assertThat(canLoadClass(classLoader, PluginClassloaderFactory.class.getCanonicalName())).isFalse(); + assertThat(canLoadClass(classLoader, Test.class.getCanonicalName())).isFalse(); + assertThat(canLoadClass(classLoader, StringUtils.class.getCanonicalName())).isFalse(); + } + + @Test + public void create_classloader_compatible_with_with_old_api_dependencies() throws Exception { + PluginClassloaderDef def = basePluginDef(); + def.setCompatibilityMode(true); + ClassLoader classLoader = factory.create(asList(def)).get(def); + + // Plugin can access to API and its transitive dependencies as defined in version 5.1. + // It can not access to core classes though, even if it was possible in previous versions. + assertThat(canLoadClass(classLoader, RulesDefinition.class.getCanonicalName())).isTrue(); + assertThat(canLoadClass(classLoader, StringUtils.class.getCanonicalName())).isTrue(); + assertThat(canLoadClass(classLoader, BASE_PLUGIN_CLASSNAME)).isTrue(); + assertThat(canLoadClass(classLoader, PluginClassloaderFactory.class.getCanonicalName())).isFalse(); + } + + @Test + public void classloader_exports_resources_to_other_classloaders() throws Exception { + PluginClassloaderDef baseDef = basePluginDef(); + PluginClassloaderDef dependentDef = dependentPluginDef(); + Map<PluginClassloaderDef, ClassLoader> map = factory.create(asList(baseDef, dependentDef)); + ClassLoader baseClassloader = map.get(baseDef); + ClassLoader dependentClassloader = map.get(dependentDef); + + // base-plugin exports its API package to other plugins + assertThat(canLoadClass(dependentClassloader, "org.sonar.plugins.base.api.BaseApi")).isTrue(); + assertThat(canLoadClass(dependentClassloader, BASE_PLUGIN_CLASSNAME)).isFalse(); + assertThat(canLoadClass(dependentClassloader, DEPENDENT_PLUGIN_CLASSNAME)).isTrue(); + + // dependent-plugin does not export its classes + assertThat(canLoadClass(baseClassloader, DEPENDENT_PLUGIN_CLASSNAME)).isFalse(); + assertThat(canLoadClass(baseClassloader, BASE_PLUGIN_CLASSNAME)).isTrue(); + } + + private static PluginClassloaderDef basePluginDef() { + PluginClassloaderDef def = new PluginClassloaderDef(BASE_PLUGIN_KEY); + def.addMainClass(BASE_PLUGIN_KEY, BASE_PLUGIN_CLASSNAME); + def.getExportMask().addInclusion("org/sonar/plugins/base/api/"); + def.addFiles(asList(fakePluginJar("base-plugin/target/base-plugin-0.1-SNAPSHOT.jar"))); + return def; + } + + private static PluginClassloaderDef dependentPluginDef() { + PluginClassloaderDef def = new PluginClassloaderDef(DEPENDENT_PLUGIN_KEY); + def.addMainClass(DEPENDENT_PLUGIN_KEY, DEPENDENT_PLUGIN_CLASSNAME); + def.getExportMask().addInclusion("org/sonar/plugins/dependent/api/"); + def.addFiles(asList(fakePluginJar("dependent-plugin/target/dependent-plugin-0.1-SNAPSHOT.jar"))); + return def; + } + + private static File fakePluginJar(String path) { + // Maven way + File file = new File("src/test/projects/" + path); + if (!file.exists()) { + // Intellij way + file = new File("sonar-core/src/test/projects/" + path); + if (!file.exists()) { + throw new IllegalArgumentException("Fake projects are not built: " + path); + } + } + return file; + } + + private static boolean canLoadClass(ClassLoader classloader, String classname) { + try { + classloader.loadClass(classname); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/platform/PluginExploderTest.java b/sonar-core/src/test/java/org/sonar/core/platform/PluginJarExploderTest.java index 85906b1e8ce..6e760f551e3 100644 --- a/sonar-core/src/test/java/org/sonar/core/platform/PluginExploderTest.java +++ b/sonar-core/src/test/java/org/sonar/core/platform/PluginJarExploderTest.java @@ -29,7 +29,7 @@ import java.io.File; import static org.assertj.core.api.Assertions.assertThat; -public class PluginExploderTest { +public class PluginJarExploderTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); @@ -40,7 +40,7 @@ public class PluginExploderTest { final File toDir = temp.newFolder(); PluginInfo pluginInfo = new PluginInfo("checkstyle").setJarFile(jarFile); - PluginExploder exploder = new PluginExploder() { + PluginJarExploder exploder = new PluginJarExploder() { @Override public ExplodedPlugin explode(PluginInfo info) { try { @@ -63,7 +63,7 @@ public class PluginExploderTest { final File toDir = temp.newFolder(); PluginInfo pluginInfo = new PluginInfo("foo").setJarFile(jarFile); - PluginExploder exploder = new PluginExploder() { + PluginJarExploder exploder = new PluginJarExploder() { @Override public ExplodedPlugin explode(PluginInfo info) { return explodeFromUnzippedDir("foo", info.getNonNullJarFile(), toDir); diff --git a/sonar-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java b/sonar-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java index e2da9f4c85f..76ddb9f2c3d 100644 --- a/sonar-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java +++ b/sonar-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java @@ -20,43 +20,57 @@ package org.sonar.core.platform; import com.google.common.collect.ImmutableMap; -import org.apache.commons.io.FileUtils; +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; import org.assertj.core.data.MapEntry; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.sonar.api.Plugin; +import org.sonar.api.SonarPlugin; import org.sonar.api.utils.ZipUtils; +import org.sonar.updatecenter.common.Version; -import java.io.File; -import java.io.IOException; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class PluginLoaderTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); - @Test - public void load_and_unload_plugins() { - File checkstyleJar = FileUtils.toFile(getClass().getResource("/org/sonar/core/plugins/sonar-checkstyle-plugin-2.8.jar")); - PluginInfo checkstyleInfo = PluginInfo.create(checkstyleJar); + PluginClassloaderFactory classloaderFactory = mock(PluginClassloaderFactory.class); + PluginLoader loader = new PluginLoader(new FakePluginExploder(), classloaderFactory); - PluginLoader loader = new PluginLoader(new TempPluginExploder()); - Map<String, Plugin> instances = loader.load(ImmutableMap.of("checkstyle", checkstyleInfo)); + @Test + public void instantiate_plugin_entry_point() { + PluginClassloaderDef def = new PluginClassloaderDef("fake"); + def.addMainClass("fake", FakePlugin.class.getName()); - assertThat(instances).containsOnlyKeys("checkstyle"); - Plugin checkstyleInstance = instances.get("checkstyle"); - assertThat(checkstyleInstance.getClass().getName()).isEqualTo("org.sonar.plugins.checkstyle.CheckstylePlugin"); + Map<String, Plugin> instances = loader.instantiatePluginClasses(ImmutableMap.of(def, getClass().getClassLoader())); + assertThat(instances).containsOnlyKeys("fake"); + assertThat(instances.get("fake")).isInstanceOf(FakePlugin.class); + } - loader.unload(instances.values()); - // TODO test that classloaders are closed. Two strategies: - // + @Test + public void plugin_entry_point_must_be_no_arg_public() { + PluginClassloaderDef def = new PluginClassloaderDef("fake"); + def.addMainClass("fake", IncorrectPlugin.class.getName()); + + try { + loader.instantiatePluginClasses(ImmutableMap.of(def, getClass().getClassLoader())); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Fail to instantiate class [org.sonar.core.platform.PluginLoaderTest$IncorrectPlugin] of plugin [fake]"); + } } @Test @@ -64,26 +78,40 @@ public class PluginLoaderTest { File jarFile = temp.newFile(); PluginInfo info = new PluginInfo("foo") .setJarFile(jarFile) - .setMainClass("org.foo.FooPlugin"); + .setMainClass("org.foo.FooPlugin") + .setMinimalSqVersion(Version.create("5.2")); - PluginLoader loader = new PluginLoader(new FakePluginExploder()); - Collection<ClassloaderDef> defs = loader.defineClassloaders(ImmutableMap.of("foo", info)); + Collection<PluginClassloaderDef> defs = loader.defineClassloaders(ImmutableMap.of("foo", info)); assertThat(defs).hasSize(1); - ClassloaderDef def = defs.iterator().next(); + PluginClassloaderDef def = defs.iterator().next(); assertThat(def.getBasePluginKey()).isEqualTo("foo"); assertThat(def.isSelfFirstStrategy()).isFalse(); assertThat(def.getFiles()).containsOnly(jarFile); assertThat(def.getMainClassesByPluginKey()).containsOnly(MapEntry.entry("foo", "org.foo.FooPlugin")); // TODO test mask - require change in sonar-classloader + + // built with SQ 5.2+ -> does not need API compatibility mode + assertThat(def.isCompatibilityMode()).isFalse(); + } + + @Test + public void enable_compatibility_mode_if_plugin_is_built_before_5_2() throws Exception { + File jarFile = temp.newFile(); + PluginInfo info = new PluginInfo("foo") + .setJarFile(jarFile) + .setMainClass("org.foo.FooPlugin") + .setMinimalSqVersion(Version.create("4.5.2")); + + Collection<PluginClassloaderDef> defs = loader.defineClassloaders(ImmutableMap.of("foo", info)); + assertThat(defs.iterator().next().isCompatibilityMode()).isTrue(); } /** - * A plugin can be extended by other plugins. In this case they share the same classloader. - * The first plugin is named "base plugin". + * A plugin (the "base" plugin) can be extended by other plugins. In this case they share the same classloader. */ @Test - public void define_same_classloader_for_multiple_plugins() throws Exception { + public void test_plugins_sharing_the_same_classloader() throws Exception { File baseJarFile = temp.newFile(), extensionJar1 = temp.newFile(), extensionJar2 = temp.newFile(); PluginInfo base = new PluginInfo("foo") .setJarFile(baseJarFile) @@ -105,13 +133,11 @@ public class PluginLoaderTest { .setBasePlugin("foo") .setUseChildFirstClassLoader(true); - PluginLoader loader = new PluginLoader(new FakePluginExploder()); - - Collection<ClassloaderDef> defs = loader.defineClassloaders(ImmutableMap.of( + Collection<PluginClassloaderDef> defs = loader.defineClassloaders(ImmutableMap.of( base.getKey(), base, extension1.getKey(), extension1, extension2.getKey(), extension2)); assertThat(defs).hasSize(1); - ClassloaderDef def = defs.iterator().next(); + PluginClassloaderDef def = defs.iterator().next(); assertThat(def.getBasePluginKey()).isEqualTo("foo"); assertThat(def.isSelfFirstStrategy()).isFalse(); assertThat(def.getFiles()).containsOnly(baseJarFile, extensionJar1, extensionJar2); @@ -122,27 +148,35 @@ public class PluginLoaderTest { // TODO test mask - require change in sonar-classloader } + + /** * Does not unzip jar file. It directly returns the JAR file defined on PluginInfo. */ - private static class FakePluginExploder extends PluginExploder { + private static class FakePluginExploder extends PluginJarExploder { @Override public ExplodedPlugin explode(PluginInfo info) { return new ExplodedPlugin(info.getKey(), info.getNonNullJarFile(), Collections.<File>emptyList()); } } - private class TempPluginExploder extends PluginExploder { + public static class FakePlugin extends SonarPlugin { @Override - public ExplodedPlugin explode(PluginInfo info) { - try { - File tempDir = temp.newFolder(); - ZipUtils.unzip(info.getNonNullJarFile(), tempDir, newLibFilter()); - return explodeFromUnzippedDir(info.getKey(), info.getNonNullJarFile(), tempDir); - - } catch (IOException e) { - throw new IllegalStateException(e); - } + public List getExtensions() { + return Collections.emptyList(); + } + } + + /** + * No public empty-param constructor + */ + public static class IncorrectPlugin extends SonarPlugin { + public IncorrectPlugin(String s) { + } + + @Override + public List getExtensions() { + return Collections.emptyList(); } } } diff --git a/sonar-core/src/test/projects/.gitignore b/sonar-core/src/test/projects/.gitignore new file mode 100644 index 00000000000..a945b8525e6 --- /dev/null +++ b/sonar-core/src/test/projects/.gitignore @@ -0,0 +1,7 @@ +# see README.txt +!*/target/ +*/target/classes/ +*/target/maven-archiver/ +*/target/maven-status/ +*/target/test-*/ + diff --git a/sonar-core/src/test/projects/README.txt b/sonar-core/src/test/projects/README.txt new file mode 100644 index 00000000000..c53a66d52f2 --- /dev/null +++ b/sonar-core/src/test/projects/README.txt @@ -0,0 +1,3 @@ +This directory provides the fake plugins used by tests. These tests are rarely changed, so generated +artifacts are stored in Git repository (see .gitignore). It avoids from adding unnecessary modules +to build. diff --git a/sonar-core/src/test/projects/base-plugin/pom.xml b/sonar-core/src/test/projects/base-plugin/pom.xml new file mode 100644 index 00000000000..1f15c59228c --- /dev/null +++ b/sonar-core/src/test/projects/base-plugin/pom.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>org.codehaus.sonar.tests</groupId> + <artifactId>base-plugin</artifactId> + <version>0.1-SNAPSHOT</version> + <packaging>sonar-plugin</packaging> + <name>Base Plugin</name> + <description>Fake plugin used to verify building of plugin classloaders</description> + + <dependencies> + <dependency> + <groupId>org.codehaus.sonar</groupId> + <artifactId>sonar-plugin-api</artifactId> + <version>5.2-SNAPSHOT</version> + <scope>provided</scope> + </dependency> + </dependencies> + <build> + <sourceDirectory>src</sourceDirectory> + <plugins> + <plugin> + <groupId>org.codehaus.sonar</groupId> + <artifactId>sonar-packaging-maven-plugin</artifactId> + <version>1.13</version> + <extensions>true</extensions> + <configuration> + <pluginKey>base</pluginKey> + <pluginClass>org.sonar.plugins.base.BasePlugin</pluginClass> + </configuration> + </plugin> + </plugins> + </build> + +</project> diff --git a/sonar-core/src/test/projects/base-plugin/src/org/sonar/plugins/base/BasePlugin.java b/sonar-core/src/test/projects/base-plugin/src/org/sonar/plugins/base/BasePlugin.java new file mode 100644 index 00000000000..e4c41e585e5 --- /dev/null +++ b/sonar-core/src/test/projects/base-plugin/src/org/sonar/plugins/base/BasePlugin.java @@ -0,0 +1,13 @@ +package org.sonar.plugins.base; + +import org.sonar.api.SonarPlugin; + +import java.util.Collections; +import java.util.List; + +public class BasePlugin extends SonarPlugin { + + public List getExtensions() { + return Collections.emptyList(); + } +} diff --git a/sonar-core/src/test/projects/base-plugin/src/org/sonar/plugins/base/api/BaseApi.java b/sonar-core/src/test/projects/base-plugin/src/org/sonar/plugins/base/api/BaseApi.java new file mode 100644 index 00000000000..6bc9947358c --- /dev/null +++ b/sonar-core/src/test/projects/base-plugin/src/org/sonar/plugins/base/api/BaseApi.java @@ -0,0 +1,6 @@ +package org.sonar.plugins.base.api; + +public class BaseApi { + public void doNothing() { + } +} diff --git a/sonar-core/src/test/projects/base-plugin/target/base-plugin-0.1-SNAPSHOT.jar b/sonar-core/src/test/projects/base-plugin/target/base-plugin-0.1-SNAPSHOT.jar Binary files differnew file mode 100644 index 00000000000..c2bdb08c95c --- /dev/null +++ b/sonar-core/src/test/projects/base-plugin/target/base-plugin-0.1-SNAPSHOT.jar diff --git a/sonar-core/src/test/projects/dependent-plugin/pom.xml b/sonar-core/src/test/projects/dependent-plugin/pom.xml new file mode 100644 index 00000000000..d414af74fe6 --- /dev/null +++ b/sonar-core/src/test/projects/dependent-plugin/pom.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>org.codehaus.sonar.tests</groupId> + <artifactId>dependent-plugin</artifactId> + <version>0.1-SNAPSHOT</version> + <packaging>sonar-plugin</packaging> + <name>Dependent Plugin</name> + <description>Fake plugin used to verify that plugins can export some resources to other plugins</description> + + <dependencies> + <dependency> + <groupId>org.codehaus.sonar</groupId> + <artifactId>sonar-plugin-api</artifactId> + <version>5.2-SNAPSHOT</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.codehaus.sonar.tests</groupId> + <artifactId>base-plugin</artifactId> + <version>0.1-SNAPSHOT</version> + <type>sonar-plugin</type> + <scope>provided</scope> + </dependency> + </dependencies> + <build> + <sourceDirectory>src</sourceDirectory> + <plugins> + <plugin> + <groupId>org.codehaus.sonar</groupId> + <artifactId>sonar-packaging-maven-plugin</artifactId> + <version>1.13</version> + <extensions>true</extensions> + <configuration> + <pluginKey>dependent</pluginKey> + <pluginClass>org.sonar.plugins.dependent.DependentPlugin</pluginClass> + </configuration> + </plugin> + </plugins> + </build> + +</project> diff --git a/sonar-core/src/test/projects/dependent-plugin/src/org/sonar/plugins/dependent/DependentPlugin.java b/sonar-core/src/test/projects/dependent-plugin/src/org/sonar/plugins/dependent/DependentPlugin.java new file mode 100644 index 00000000000..5d320db62f9 --- /dev/null +++ b/sonar-core/src/test/projects/dependent-plugin/src/org/sonar/plugins/dependent/DependentPlugin.java @@ -0,0 +1,18 @@ +package org.sonar.plugins.dependent; + +import org.sonar.api.SonarPlugin; +import org.sonar.plugins.base.api.BaseApi; +import java.util.Collections; +import java.util.List; + +public class DependentPlugin extends SonarPlugin { + + public DependentPlugin() { + // uses a class that is exported by base-plugin + new BaseApi().doNothing(); + } + + public List getExtensions() { + return Collections.emptyList(); + } +} diff --git a/sonar-core/src/test/projects/dependent-plugin/target/dependent-plugin-0.1-SNAPSHOT.jar b/sonar-core/src/test/projects/dependent-plugin/target/dependent-plugin-0.1-SNAPSHOT.jar Binary files differnew file mode 100644 index 00000000000..b828a8c6386 --- /dev/null +++ b/sonar-core/src/test/projects/dependent-plugin/target/dependent-plugin-0.1-SNAPSHOT.jar diff --git a/sonar-core/src/test/projects/pom.xml b/sonar-core/src/test/projects/pom.xml new file mode 100644 index 00000000000..0af144c8b18 --- /dev/null +++ b/sonar-core/src/test/projects/pom.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>org.codehaus.sonar.tests</groupId> + <artifactId>parent</artifactId> + <version>0.1-SNAPSHOT</version> + <packaging>pom</packaging> + <modules> + <module>base-plugin</module> + <module>dependent-plugin</module> + </modules> + +</project> |