aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-core
diff options
context:
space:
mode:
authorSimon Brandhof <simon.brandhof@sonarsource.com>2015-05-29 10:29:04 +0200
committerSimon Brandhof <simon.brandhof@sonarsource.com>2015-06-05 09:54:04 +0200
commit0956511c8c9d6aa6639d4378f47d73877cdc18de (patch)
treec7a14388d2eec8a5f1f1d7d25fb48894ee4aae9f /sonar-core
parentb1a02efd650918249cd828bd19f4ebf2118d8a79 (diff)
downloadsonarqube-0956511c8c9d6aa6639d4378f47d73877cdc18de.tar.gz
sonarqube-0956511c8c9d6aa6639d4378f47d73877cdc18de.zip
SONAR-6370 isolate plugin classloader from core classes
Diffstat (limited to 'sonar-core')
-rw-r--r--sonar-core/pom.xml3
-rw-r--r--sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderDef.java (renamed from sonar-core/src/main/java/org/sonar/core/platform/ClassloaderDef.java)71
-rw-r--r--sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderFactory.java163
-rw-r--r--sonar-core/src/main/java/org/sonar/core/platform/PluginJarExploder.java (renamed from sonar-core/src/main/java/org/sonar/core/platform/PluginExploder.java)4
-rw-r--r--sonar-core/src/main/java/org/sonar/core/platform/PluginLoader.java137
-rw-r--r--sonar-core/src/test/java/org/sonar/core/platform/PluginClassloaderFactoryTest.java132
-rw-r--r--sonar-core/src/test/java/org/sonar/core/platform/PluginJarExploderTest.java (renamed from sonar-core/src/test/java/org/sonar/core/platform/PluginExploderTest.java)6
-rw-r--r--sonar-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java116
-rw-r--r--sonar-core/src/test/projects/.gitignore7
-rw-r--r--sonar-core/src/test/projects/README.txt3
-rw-r--r--sonar-core/src/test/projects/base-plugin/pom.xml36
-rw-r--r--sonar-core/src/test/projects/base-plugin/src/org/sonar/plugins/base/BasePlugin.java13
-rw-r--r--sonar-core/src/test/projects/base-plugin/src/org/sonar/plugins/base/api/BaseApi.java6
-rw-r--r--sonar-core/src/test/projects/base-plugin/target/base-plugin-0.1-SNAPSHOT.jarbin0 -> 3421 bytes
-rw-r--r--sonar-core/src/test/projects/dependent-plugin/pom.xml43
-rw-r--r--sonar-core/src/test/projects/dependent-plugin/src/org/sonar/plugins/dependent/DependentPlugin.java18
-rw-r--r--sonar-core/src/test/projects/dependent-plugin/target/dependent-plugin-0.1-SNAPSHOT.jarbin0 -> 3077 bytes
-rw-r--r--sonar-core/src/test/projects/pom.xml14
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
new file mode 100644
index 00000000000..c2bdb08c95c
--- /dev/null
+++ b/sonar-core/src/test/projects/base-plugin/target/base-plugin-0.1-SNAPSHOT.jar
Binary files differ
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
new file mode 100644
index 00000000000..b828a8c6386
--- /dev/null
+++ b/sonar-core/src/test/projects/dependent-plugin/target/dependent-plugin-0.1-SNAPSHOT.jar
Binary files differ
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>