<dependency> | <dependency> | ||||
<groupId>com.google.guava</groupId> | <groupId>com.google.guava</groupId> | ||||
<artifactId>guava</artifactId> | <artifactId>guava</artifactId> | ||||
</dependency> | |||||
<dependency> | |||||
<groupId>com.google.code.findbugs</groupId> | |||||
<artifactId>jsr305</artifactId> | |||||
<version>3.0.0</version> | |||||
<scope>provided</scope> | |||||
<version>18.0</version> | |||||
</dependency> | </dependency> | ||||
<dependency> | <dependency> | ||||
<groupId>commons-io</groupId> | <groupId>commons-io</groupId> | ||||
<artifactId>commons-io</artifactId> | <artifactId>commons-io</artifactId> | ||||
<version>2.4</version> | |||||
</dependency> | </dependency> | ||||
<dependency> | <dependency> | ||||
<groupId>commons-lang</groupId> | |||||
<artifactId>commons-lang</artifactId> | |||||
<groupId>org.apache.commons</groupId> | |||||
<artifactId>commons-lang3</artifactId> | |||||
<version>3.3.2</version> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>com.google.code.findbugs</groupId> | |||||
<artifactId>jsr305</artifactId> | |||||
<version>3.0.0</version> | |||||
<scope>provided</scope> | |||||
</dependency> | </dependency> | ||||
<dependency> | <dependency> | ||||
<groupId>org.codehaus.sonar</groupId> | <groupId>org.codehaus.sonar</groupId> | ||||
<artifactId>sonar-plugin-api</artifactId> | <artifactId>sonar-plugin-api</artifactId> | ||||
<version>${project.version}</version> | |||||
<scope>provided</scope> | <scope>provided</scope> | ||||
</dependency> | </dependency> | ||||
import com.google.common.base.Splitter; | import com.google.common.base.Splitter; | ||||
import org.apache.commons.io.FileUtils; | import org.apache.commons.io.FileUtils; | ||||
import org.apache.commons.lang.StringUtils; | |||||
import org.apache.commons.lang3.StringUtils; | |||||
import org.sonar.api.batch.fs.InputFile; | import org.sonar.api.batch.fs.InputFile; | ||||
import org.sonar.api.batch.sensor.Sensor; | import org.sonar.api.batch.sensor.Sensor; | ||||
import org.sonar.api.batch.sensor.SensorContext; | import org.sonar.api.batch.sensor.SensorContext; |
package org.sonar.xoo.lang; | package org.sonar.xoo.lang; | ||||
import org.apache.commons.io.FileUtils; | import org.apache.commons.io.FileUtils; | ||||
import org.apache.commons.lang.StringUtils; | |||||
import org.apache.commons.lang3.StringUtils; | |||||
import org.sonar.api.batch.fs.InputFile; | import org.sonar.api.batch.fs.InputFile; | ||||
import org.sonar.api.batch.measure.MetricFinder; | import org.sonar.api.batch.measure.MetricFinder; | ||||
import org.sonar.api.batch.sensor.Sensor; | import org.sonar.api.batch.sensor.Sensor; |
import com.google.common.base.Splitter; | import com.google.common.base.Splitter; | ||||
import org.apache.commons.io.FileUtils; | import org.apache.commons.io.FileUtils; | ||||
import org.apache.commons.lang.StringUtils; | |||||
import org.apache.commons.lang3.StringUtils; | |||||
import org.sonar.api.batch.fs.InputFile; | import org.sonar.api.batch.fs.InputFile; | ||||
import org.sonar.api.batch.sensor.Sensor; | import org.sonar.api.batch.sensor.Sensor; | ||||
import org.sonar.api.batch.sensor.SensorContext; | import org.sonar.api.batch.sensor.SensorContext; |
import com.google.common.base.Splitter; | import com.google.common.base.Splitter; | ||||
import org.apache.commons.io.FileUtils; | import org.apache.commons.io.FileUtils; | ||||
import org.apache.commons.lang.StringUtils; | |||||
import org.apache.commons.lang3.StringUtils; | |||||
import org.sonar.api.batch.fs.InputFile; | import org.sonar.api.batch.fs.InputFile; | ||||
import org.sonar.api.batch.sensor.Sensor; | import org.sonar.api.batch.sensor.Sensor; | ||||
import org.sonar.api.batch.sensor.SensorContext; | import org.sonar.api.batch.sensor.SensorContext; |
import com.google.common.annotations.VisibleForTesting; | import com.google.common.annotations.VisibleForTesting; | ||||
import org.apache.commons.io.FileUtils; | import org.apache.commons.io.FileUtils; | ||||
import org.apache.commons.lang.StringUtils; | |||||
import org.apache.commons.lang3.StringUtils; | |||||
import org.sonar.api.batch.fs.InputFile; | import org.sonar.api.batch.fs.InputFile; | ||||
import org.sonar.api.batch.scm.BlameCommand; | import org.sonar.api.batch.scm.BlameCommand; | ||||
import org.sonar.api.batch.scm.BlameLine; | import org.sonar.api.batch.scm.BlameLine; |
import com.google.common.base.Splitter; | import com.google.common.base.Splitter; | ||||
import org.apache.commons.io.FileUtils; | import org.apache.commons.io.FileUtils; | ||||
import org.apache.commons.lang.StringUtils; | |||||
import org.apache.commons.lang3.StringUtils; | |||||
import org.sonar.api.batch.DependsUpon; | import org.sonar.api.batch.DependsUpon; | ||||
import org.sonar.api.batch.fs.FilePredicates; | import org.sonar.api.batch.fs.FilePredicates; | ||||
import org.sonar.api.batch.fs.FileSystem; | import org.sonar.api.batch.fs.FileSystem; |
import com.google.common.base.Splitter; | import com.google.common.base.Splitter; | ||||
import org.apache.commons.io.FileUtils; | import org.apache.commons.io.FileUtils; | ||||
import org.apache.commons.lang.StringUtils; | |||||
import org.apache.commons.lang3.StringUtils; | |||||
import org.sonar.api.batch.DependedUpon; | import org.sonar.api.batch.DependedUpon; | ||||
import org.sonar.api.batch.fs.FilePredicates; | import org.sonar.api.batch.fs.FilePredicates; | ||||
import org.sonar.api.batch.fs.FileSystem; | import org.sonar.api.batch.fs.FileSystem; |
<dependency> | <dependency> | ||||
<groupId>com.google.guava</groupId> | <groupId>com.google.guava</groupId> | ||||
<artifactId>guava</artifactId> | <artifactId>guava</artifactId> | ||||
<version>10.0.1</version> | |||||
<version>17.0</version> | |||||
<exclusions> | <exclusions> | ||||
<exclusion> | <exclusion> | ||||
<!-- should be declared with scope provided --> | <!-- should be declared with scope provided --> |
package org.sonar.server.charts; | package org.sonar.server.charts; | ||||
import com.google.common.collect.Maps; | import com.google.common.collect.Maps; | ||||
import com.google.common.io.Closeables; | |||||
import java.awt.image.BufferedImage; | |||||
import java.io.IOException; | |||||
import java.io.OutputStream; | |||||
import java.util.Enumeration; | |||||
import java.util.Map; | |||||
import javax.servlet.ServletException; | |||||
import javax.servlet.http.HttpServlet; | |||||
import javax.servlet.http.HttpServletRequest; | |||||
import javax.servlet.http.HttpServletResponse; | |||||
import org.jfree.chart.encoders.KeypointPNGEncoderAdapter; | import org.jfree.chart.encoders.KeypointPNGEncoderAdapter; | ||||
import org.sonar.api.charts.Chart; | import org.sonar.api.charts.Chart; | ||||
import org.sonar.api.charts.ChartParameters; | import org.sonar.api.charts.ChartParameters; | ||||
import org.sonar.server.charts.deprecated.SparkLinesChart; | import org.sonar.server.charts.deprecated.SparkLinesChart; | ||||
import org.sonar.server.platform.Platform; | import org.sonar.server.platform.Platform; | ||||
import javax.servlet.ServletException; | |||||
import javax.servlet.http.HttpServlet; | |||||
import javax.servlet.http.HttpServletRequest; | |||||
import javax.servlet.http.HttpServletResponse; | |||||
import java.awt.image.BufferedImage; | |||||
import java.io.IOException; | |||||
import java.io.OutputStream; | |||||
import java.util.Enumeration; | |||||
import java.util.Map; | |||||
public class ChartsServlet extends HttpServlet { | public class ChartsServlet extends HttpServlet { | ||||
private static final Logger LOG = Loggers.get(ChartsServlet.class); | private static final Logger LOG = Loggers.get(ChartsServlet.class); | ||||
} | } | ||||
if (chart != null) { | if (chart != null) { | ||||
OutputStream out = null; | |||||
try { | |||||
out = response.getOutputStream(); | |||||
try (OutputStream out = response.getOutputStream()) { | |||||
response.setContentType("image/png"); | response.setContentType("image/png"); | ||||
chart.exportChartAsPNG(out); | chart.exportChartAsPNG(out); | ||||
} catch (Exception e) { | } catch (Exception e) { | ||||
LOG.error("Generating chart " + chart.getClass().getName(), e); | LOG.error("Generating chart " + chart.getClass().getName(), e); | ||||
} finally { | |||||
Closeables.closeQuietly(out); | |||||
} | } | ||||
} | } | ||||
} | } |
import org.sonar.api.utils.Durations; | import org.sonar.api.utils.Durations; | ||||
import org.sonar.core.i18n.DefaultI18n; | import org.sonar.core.i18n.DefaultI18n; | ||||
import org.sonar.core.i18n.RuleI18nManager; | import org.sonar.core.i18n.RuleI18nManager; | ||||
import org.sonar.core.platform.PluginClassloaderFactory; | |||||
import org.sonar.core.platform.PluginLoader; | import org.sonar.core.platform.PluginLoader; | ||||
import org.sonar.server.db.migrations.DatabaseMigrator; | import org.sonar.server.db.migrations.DatabaseMigrator; | ||||
import org.sonar.server.db.migrations.PlatformDatabaseMigration; | import org.sonar.server.db.migrations.PlatformDatabaseMigration; | ||||
import org.sonar.server.platform.RailsAppsDeployer; | import org.sonar.server.platform.RailsAppsDeployer; | ||||
import org.sonar.server.plugins.InstalledPluginReferentialFactory; | import org.sonar.server.plugins.InstalledPluginReferentialFactory; | ||||
import org.sonar.server.plugins.ServerExtensionInstaller; | import org.sonar.server.plugins.ServerExtensionInstaller; | ||||
import org.sonar.server.plugins.ServerPluginExploder; | |||||
import org.sonar.server.plugins.ServerPluginJarExploder; | |||||
import org.sonar.server.plugins.ServerPluginRepository; | import org.sonar.server.plugins.ServerPluginRepository; | ||||
import org.sonar.server.ruby.PlatformRubyBridge; | import org.sonar.server.ruby.PlatformRubyBridge; | ||||
import org.sonar.server.ui.JRubyI18n; | import org.sonar.server.ui.JRubyI18n; | ||||
// plugins | // plugins | ||||
ServerPluginRepository.class, | ServerPluginRepository.class, | ||||
ServerPluginExploder.class, | |||||
ServerPluginJarExploder.class, | |||||
PluginLoader.class, | PluginLoader.class, | ||||
PluginClassloaderFactory.class, | |||||
InstalledPluginReferentialFactory.class, | InstalledPluginReferentialFactory.class, | ||||
ServerExtensionInstaller.class, | ServerExtensionInstaller.class, | ||||
container.declareExtension(pluginInfo, extension); | container.declareExtension(pluginInfo, extension); | ||||
} | } | ||||
} | } | ||||
} catch (Exception e) { | |||||
} catch (Throwable e) { | |||||
// catch Throwable because we want to catch Error too (IncompatibleClassChangeError, ...) | |||||
throw new IllegalStateException(String.format("Fail to load plugin %s [%s]", pluginInfo.getName(), pluginInfo.getKey()), e); | throw new IllegalStateException(String.format("Fail to load plugin %s [%s]", pluginInfo.getName(), pluginInfo.getKey()), e); | ||||
} | } | ||||
} | } | ||||
ExtensionProvider provider = (ExtensionProvider) container.getComponentByKey(extension); | ExtensionProvider provider = (ExtensionProvider) container.getComponentByKey(extension); | ||||
installProvider(container, pluginInfo, provider); | installProvider(container, pluginInfo, provider); | ||||
} | } | ||||
} catch (Exception e) { | |||||
} catch (Throwable e) { | |||||
// catch Throwable because we want to catch Error too (IncompatibleClassChangeError, ...) | |||||
throw new IllegalStateException(String.format("Fail to load plugin %s [%s]", pluginInfo.getName(), pluginInfo.getKey()), e); | throw new IllegalStateException(String.format("Fail to load plugin %s [%s]", pluginInfo.getName(), pluginInfo.getKey()), e); | ||||
} | } | ||||
} | } |
import org.sonar.api.server.ServerSide; | import org.sonar.api.server.ServerSide; | ||||
import org.sonar.api.utils.ZipUtils; | import org.sonar.api.utils.ZipUtils; | ||||
import org.sonar.core.platform.ExplodedPlugin; | import org.sonar.core.platform.ExplodedPlugin; | ||||
import org.sonar.core.platform.PluginExploder; | |||||
import org.sonar.core.platform.PluginJarExploder; | |||||
import org.sonar.core.platform.PluginInfo; | import org.sonar.core.platform.PluginInfo; | ||||
import org.sonar.server.platform.DefaultServerFileSystem; | import org.sonar.server.platform.DefaultServerFileSystem; | ||||
import static org.apache.commons.io.FileUtils.forceMkdir; | import static org.apache.commons.io.FileUtils.forceMkdir; | ||||
@ServerSide | @ServerSide | ||||
public class ServerPluginExploder extends PluginExploder { | |||||
public class ServerPluginJarExploder extends PluginJarExploder { | |||||
private final DefaultServerFileSystem fs; | private final DefaultServerFileSystem fs; | ||||
public ServerPluginExploder(DefaultServerFileSystem fs) { | |||||
public ServerPluginJarExploder(DefaultServerFileSystem fs) { | |||||
this.fs = fs; | this.fs = fs; | ||||
} | } | ||||
import static org.sonar.core.platform.PluginInfo.jarToPluginInfo; | import static org.sonar.core.platform.PluginInfo.jarToPluginInfo; | ||||
/** | /** | ||||
* Manages installation and loading of plugins: | |||||
* Entry point to install and load plugins on server startup. It manages | |||||
* <ul> | * <ul> | ||||
* <li>installation of bundled plugins on first server startup</li> | * <li>installation of bundled plugins on first server startup</li> | ||||
* <li>installation of new plugins (effective after server startup)</li> | * <li>installation of new plugins (effective after server startup)</li> | ||||
* <li>un-installation of plugins (effective after server startup)</li> | * <li>un-installation of plugins (effective after server startup)</li> | ||||
* <li>cancel pending installations/un-installations</li> | * <li>cancel pending installations/un-installations</li> | ||||
* <li>load plugin bytecode</li> | |||||
* <li>instantiation of plugin entry-points</li> | |||||
* </ul> | * </ul> | ||||
*/ | */ | ||||
public class ServerPluginRepository implements PluginRepository, Startable { | public class ServerPluginRepository implements PluginRepository, Startable { |
import static org.mockito.Mockito.mock; | import static org.mockito.Mockito.mock; | ||||
import static org.mockito.Mockito.when; | import static org.mockito.Mockito.when; | ||||
public class ServerPluginExploderTest { | |||||
public class ServerPluginJarExploderTest { | |||||
@Rule | @Rule | ||||
public TemporaryFolder temp = new TemporaryFolder(); | public TemporaryFolder temp = new TemporaryFolder(); | ||||
DefaultServerFileSystem fs = mock(DefaultServerFileSystem.class); | DefaultServerFileSystem fs = mock(DefaultServerFileSystem.class); | ||||
ServerPluginExploder underTest = new ServerPluginExploder(fs); | |||||
ServerPluginJarExploder underTest = new ServerPluginJarExploder(fs); | |||||
@Test | @Test | ||||
public void copy_all_classloader_files_to_dedicated_directory() throws Exception { | public void copy_all_classloader_files_to_dedicated_directory() throws Exception { |
import com.google.common.collect.ImmutableMap; | import com.google.common.collect.ImmutableMap; | ||||
import com.google.common.collect.ImmutableSet; | import com.google.common.collect.ImmutableSet; | ||||
import java.io.File; | |||||
import java.io.IOException; | |||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.Map; | import java.util.Map; | ||||
import org.apache.commons.io.FileUtils; | import org.apache.commons.io.FileUtils; | ||||
import org.sonar.server.platform.DefaultServerFileSystem; | import org.sonar.server.platform.DefaultServerFileSystem; | ||||
import org.sonar.updatecenter.common.Version; | import org.sonar.updatecenter.common.Version; | ||||
import java.io.File; | |||||
import java.io.IOException; | |||||
import static org.assertj.core.api.Assertions.assertThat; | import static org.assertj.core.api.Assertions.assertThat; | ||||
import static org.junit.Assert.fail; | import static org.junit.Assert.fail; | ||||
import static org.mockito.Mockito.mock; | import static org.mockito.Mockito.mock; | ||||
Server server = mock(Server.class); | Server server = mock(Server.class); | ||||
ServerUpgradeStatus upgradeStatus = mock(ServerUpgradeStatus.class); | ServerUpgradeStatus upgradeStatus = mock(ServerUpgradeStatus.class); | ||||
DefaultServerFileSystem fs = mock(DefaultServerFileSystem.class, Mockito.RETURNS_DEEP_STUBS); | DefaultServerFileSystem fs = mock(DefaultServerFileSystem.class, Mockito.RETURNS_DEEP_STUBS); | ||||
PluginLoader pluginLoader = new PluginLoader(new ServerPluginExploder(fs)); | |||||
PluginLoader pluginLoader = mock(PluginLoader.class); | |||||
ServerPluginRepository underTest = new ServerPluginRepository(server, upgradeStatus, fs, pluginLoader); | ServerPluginRepository underTest = new ServerPluginRepository(server, upgradeStatus, fs, pluginLoader); | ||||
@Before | @Before | ||||
// both plugins are installed | // both plugins are installed | ||||
assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("core", "testbase"); | assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("core", "testbase"); | ||||
assertThat(underTest.getPluginInstance("core").getClass().getName()).isEqualTo("CorePlugin"); | |||||
assertThat(underTest.getPluginInstance("testbase").getClass().getName()).isEqualTo("BasePlugin"); | |||||
assertThat(underTest.hasPlugin("testbase")).isTrue(); | |||||
} | } | ||||
@Test | @Test | ||||
// both plugins are installed | // both plugins are installed | ||||
assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("core", "testbase"); | assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("core", "testbase"); | ||||
assertThat(underTest.getPluginInstance("core").getClass().getName()).isEqualTo("CorePlugin"); | |||||
assertThat(underTest.getPluginInstance("testbase").getClass().getName()).isEqualTo("BasePlugin"); | |||||
} | } | ||||
/** | /** | ||||
assertThat(downloadedJar).doesNotExist(); | assertThat(downloadedJar).doesNotExist(); | ||||
assertThat(new File(fs.getInstalledPluginsDir(), downloadedJar.getName())).isFile().exists(); | assertThat(new File(fs.getInstalledPluginsDir(), downloadedJar.getName())).isFile().exists(); | ||||
assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase"); | assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase"); | ||||
assertThat(underTest.getPluginInstance("testbase").getClass().getName()).isEqualTo("BasePlugin"); | |||||
} | } | ||||
@Test | @Test |
<groupId>org.codehaus.sonar</groupId> | <groupId>org.codehaus.sonar</groupId> | ||||
<artifactId>sonar-persistit</artifactId> | <artifactId>sonar-persistit</artifactId> | ||||
</dependency> | </dependency> | ||||
<dependency> | |||||
<groupId>com.google.guava</groupId> | |||||
<artifactId>guava</artifactId> | |||||
</dependency> | |||||
<dependency> | <dependency> | ||||
<groupId>org.codehaus.sonar</groupId> | <groupId>org.codehaus.sonar</groupId> |
import org.sonar.api.batch.BatchSide; | import org.sonar.api.batch.BatchSide; | ||||
import org.sonar.api.utils.ZipUtils; | import org.sonar.api.utils.ZipUtils; | ||||
import org.sonar.core.platform.ExplodedPlugin; | import org.sonar.core.platform.ExplodedPlugin; | ||||
import org.sonar.core.platform.PluginExploder; | |||||
import org.sonar.core.platform.PluginJarExploder; | |||||
import org.sonar.core.platform.PluginInfo; | import org.sonar.core.platform.PluginInfo; | ||||
import org.sonar.home.cache.FileCache; | import org.sonar.home.cache.FileCache; | ||||
import java.io.IOException; | import java.io.IOException; | ||||
@BatchSide | @BatchSide | ||||
public class BatchPluginExploder extends PluginExploder { | |||||
public class BatchPluginJarExploder extends PluginJarExploder { | |||||
private final FileCache fileCache; | private final FileCache fileCache; | ||||
public BatchPluginExploder(FileCache fileCache) { | |||||
public BatchPluginJarExploder(FileCache fileCache) { | |||||
this.fileCache = fileCache; | this.fileCache = fileCache; | ||||
} | } | ||||
/* | |||||
* 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.batch.bootstrap; | |||||
import org.sonar.api.batch.BatchSide; | |||||
import org.sonar.core.platform.PluginExploder; | |||||
import org.sonar.core.platform.PluginLoader; | |||||
/** | |||||
* The {@link PluginLoader} on batch side requires to use thread context | |||||
* classloader as base classloader in order to support plugins like Groovy | |||||
* (at least its version 1.1). | |||||
*/ | |||||
@BatchSide | |||||
public class BatchPluginLoader extends PluginLoader { | |||||
public BatchPluginLoader(PluginExploder exploder) { | |||||
super(exploder); | |||||
} | |||||
@Override | |||||
protected ClassLoader baseClassloader() { | |||||
return Thread.currentThread().getContextClassLoader(); | |||||
} | |||||
} |
import org.sonar.core.persistence.SemaphoreUpdater; | import org.sonar.core.persistence.SemaphoreUpdater; | ||||
import org.sonar.core.persistence.SemaphoresImpl; | import org.sonar.core.persistence.SemaphoresImpl; | ||||
import org.sonar.core.platform.ComponentContainer; | import org.sonar.core.platform.ComponentContainer; | ||||
import org.sonar.core.platform.PluginClassloaderFactory; | |||||
import org.sonar.core.platform.PluginInfo; | import org.sonar.core.platform.PluginInfo; | ||||
import org.sonar.core.platform.PluginLoader; | |||||
import org.sonar.core.platform.PluginRepository; | import org.sonar.core.platform.PluginRepository; | ||||
import org.sonar.core.purge.PurgeProfiler; | import org.sonar.core.purge.PurgeProfiler; | ||||
import org.sonar.core.rule.CacheRuleFinder; | import org.sonar.core.rule.CacheRuleFinder; | ||||
add( | add( | ||||
// plugins | // plugins | ||||
BatchPluginRepository.class, | BatchPluginRepository.class, | ||||
BatchPluginLoader.class, | |||||
BatchPluginExploder.class, | |||||
PluginLoader.class, | |||||
PluginClassloaderFactory.class, | |||||
BatchPluginJarExploder.class, | |||||
BatchPluginPredicate.class, | BatchPluginPredicate.class, | ||||
ExtensionInstaller.class, | ExtensionInstaller.class, | ||||
import static org.assertj.core.api.Assertions.assertThat; | import static org.assertj.core.api.Assertions.assertThat; | ||||
public class BatchPluginExploderTest { | |||||
public class BatchPluginJarExploderTest { | |||||
@ClassRule | @ClassRule | ||||
public static TemporaryFolder temp = new TemporaryFolder(); | public static TemporaryFolder temp = new TemporaryFolder(); | ||||
File userHome; | File userHome; | ||||
BatchPluginExploder underTest; | |||||
BatchPluginJarExploder underTest; | |||||
@Before | @Before | ||||
public void setUp() throws IOException { | public void setUp() throws IOException { | ||||
userHome = temp.newFolder(); | userHome = temp.newFolder(); | ||||
FileCache fileCache = new FileCacheBuilder().setUserHome(userHome).build(); | FileCache fileCache = new FileCacheBuilder().setUserHome(userHome).build(); | ||||
underTest = new BatchPluginExploder(fileCache); | |||||
underTest = new BatchPluginJarExploder(fileCache); | |||||
} | } | ||||
@Test | @Test | ||||
} | } | ||||
File getFileFromCache(String filename) throws IOException { | File getFileFromCache(String filename) throws IOException { | ||||
File src = FileUtils.toFile(BatchPluginExploderTest.class.getResource("/org/sonar/batch/bootstrap/BatchPluginUnzipperTest/" + filename)); | |||||
File src = FileUtils.toFile(BatchPluginJarExploderTest.class.getResource("/org/sonar/batch/bootstrap/BatchPluginUnzipperTest/" + filename)); | |||||
File destFile = new File(new File(userHome, "" + filename.hashCode()), filename); | File destFile = new File(new File(userHome, "" + filename.hashCode()), filename); | ||||
FileUtils.copyFile(src, destFile); | FileUtils.copyFile(src, destFile); | ||||
return destFile; | return destFile; |
<groupId>org.codehaus.sonar</groupId> | <groupId>org.codehaus.sonar</groupId> | ||||
<artifactId>sonar-plugin-api-deps</artifactId> | <artifactId>sonar-plugin-api-deps</artifactId> | ||||
<version>${project.version}</version> | <version>${project.version}</version> | ||||
<optional>true</optional> | |||||
<scope>runtime</scope> | <scope>runtime</scope> | ||||
</dependency> | </dependency> | ||||
<executions> | <executions> | ||||
<execution> | <execution> | ||||
<id>copy-deprecated-api-deps</id> | <id>copy-deprecated-api-deps</id> | ||||
<phase>process-resources</phase> | |||||
<phase>generate-resources</phase> | |||||
<goals> | <goals> | ||||
<goal>copy</goal> | <goal>copy</goal> | ||||
</goals> | </goals> |
import com.google.common.base.Preconditions; | import com.google.common.base.Preconditions; | ||||
import com.google.common.base.Strings; | 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.io.File; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Collection; | |||||
import java.util.HashMap; | import java.util.HashMap; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Map; | 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 String basePluginKey; | ||||
private final Map<String, String> mainClassesByPluginKey = new HashMap<>(); | private final Map<String, String> mainClassesByPluginKey = new HashMap<>(); | ||||
private final List<File> files = new ArrayList<>(); | private final List<File> files = new ArrayList<>(); | ||||
private final Mask mask = new Mask(); | private final Mask mask = new Mask(); | ||||
private boolean selfFirstStrategy = false; | 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; | this.basePluginKey = basePluginKey; | ||||
} | } | ||||
return basePluginKey; | return basePluginKey; | ||||
} | } | ||||
Map<String, String> getMainClassesByPluginKey() { | |||||
return mainClassesByPluginKey; | |||||
} | |||||
List<File> getFiles() { | List<File> getFiles() { | ||||
return files; | return files; | ||||
} | } | ||||
Mask getMask() { | |||||
void addFiles(Collection<File> f) { | |||||
this.files.addAll(f); | |||||
} | |||||
Mask getExportMask() { | |||||
return mask; | return mask; | ||||
} | } | ||||
this.selfFirstStrategy = selfFirstStrategy; | 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(); | |||||
} | } | ||||
} | } |
/* | |||||
* 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/"); | |||||
} | |||||
} |
import static org.apache.commons.io.FileUtils.listFiles; | 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"; | protected static final String LIB_RELATIVE_PATH_IN_JAR = "META-INF/lib"; | ||||
} | } | ||||
protected ExplodedPlugin explodeFromUnzippedDir(String pluginKey, File jarFile, File unzippedDir) { | 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; | Collection<File> libs; | ||||
if (libDir.isDirectory() && libDir.exists()) { | if (libDir.isDirectory() && libDir.exists()) { | ||||
libs = listFiles(libDir, null, false); | libs = listFiles(libDir, null, false); |
import com.google.common.annotations.VisibleForTesting; | import com.google.common.annotations.VisibleForTesting; | ||||
import com.google.common.base.Strings; | 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.Closeable; | ||||
import java.io.File; | |||||
import java.net.MalformedURLException; | |||||
import java.net.URL; | |||||
import java.util.Collection; | import java.util.Collection; | ||||
import java.util.HashMap; | import java.util.HashMap; | ||||
import java.util.Map; | 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 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 | * Loads the plugin JAR files by creating the appropriate classloaders and by instantiating | ||||
* environment (minimal sonarqube version, compatibility between plugins, ...): | * environment (minimal sonarqube version, compatibility between plugins, ...): | ||||
* <ul> | * <ul> | ||||
* <li>server verifies compatibility of JARs before deploying them at startup (see ServerPluginRepository)</li> | * <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> | * </ul> | ||||
* <p/> | * <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/> | * <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 { | public class PluginLoader { | ||||
private static final String[] DEFAULT_SHARED_RESOURCES = {"org/sonar/plugins", "com/sonar/plugins", "com/sonarsource/plugins"}; | 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) { | 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. | * different than number of plugins. | ||||
*/ | */ | ||||
@VisibleForTesting | @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()) { | for (PluginInfo info : infoByKeys.values()) { | ||||
String baseKey = basePluginKey(info, infoByKeys); | String baseKey = basePluginKey(info, infoByKeys); | ||||
ClassloaderDef def = classloadersByBasePlugin.get(baseKey); | |||||
PluginClassloaderDef def = classloadersByBasePlugin.get(baseKey); | |||||
if (def == null) { | if (def == null) { | ||||
def = new ClassloaderDef(baseKey); | |||||
def = new PluginClassloaderDef(baseKey); | |||||
classloadersByBasePlugin.put(baseKey, def); | classloadersByBasePlugin.put(baseKey, def); | ||||
} | } | ||||
ExplodedPlugin explodedPlugin = exploder.explode(info); | |||||
ExplodedPlugin explodedPlugin = jarExploder.explode(info); | |||||
def.addFiles(asList(explodedPlugin.getMain())); | def.addFiles(asList(explodedPlugin.getMain())); | ||||
def.addFiles(explodedPlugin.getLibs()); | def.addFiles(explodedPlugin.getLibs()); | ||||
def.addMainClass(info.getKey(), info.getMainClass()); | def.addMainClass(info.getKey(), info.getMainClass()); | ||||
for (String defaultSharedResource : DEFAULT_SHARED_RESOURCES) { | 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())) { | 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()); | 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 | * @return the instances grouped by plugin key | ||||
* @throws IllegalStateException if at least one plugin can't be correctly loaded | * @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 | // instantiate plugins | ||||
Map<String, Plugin> instancesByPluginKey = new HashMap<>(); | 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 | // 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 { | try { | ||||
instancesByPluginKey.put(pluginKey, (Plugin) def.getBuiltClassloader().loadClass(mainClass).newInstance()); | |||||
instancesByPluginKey.put(pluginKey, (Plugin) classLoader.loadClass(mainClass).newInstance()); | |||||
} catch (UnsupportedClassVersionError e) { | } catch (UnsupportedClassVersionError e) { | ||||
throw new IllegalStateException(String.format("The plugin [%s] does not support Java %s", | throw new IllegalStateException(String.format("The plugin [%s] does not support Java %s", | ||||
pluginKey, SystemUtils.JAVA_VERSION_TRIMMED), e); | pluginKey, SystemUtils.JAVA_VERSION_TRIMMED), e); | ||||
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { | |||||
} catch (Throwable e) { | |||||
throw new IllegalStateException(String.format( | throw new IllegalStateException(String.format( | ||||
"Fail to instantiate class [%s] of plugin [%s]", mainClass, pluginKey), e); | "Fail to instantiate class [%s] of plugin [%s]", mainClass, pluginKey), e); | ||||
} | } | ||||
public void unload(Collection<Plugin> plugins) { | public void unload(Collection<Plugin> plugins) { | ||||
for (Plugin plugin : plugins) { | for (Plugin plugin : plugins) { | ||||
ClassLoader classLoader = plugin.getClass().getClassLoader(); | ClassLoader classLoader = plugin.getClass().getClassLoader(); | ||||
if (classLoader instanceof Closeable && classLoader != baseClassloader()) { | |||||
if (classLoader instanceof Closeable && classLoader != classloaderFactory.baseClassloader()) { | |||||
try { | try { | ||||
((Closeable) classLoader).close(); | ((Closeable) classLoader).close(); | ||||
} catch (Exception e) { | } catch (Exception e) { | ||||
} | } | ||||
} | } | ||||
/** | |||||
* 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 | * 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. | * B and C must be attached to the classloader of A. The method returns A in the three cases. | ||||
} | } | ||||
return base; | return base; | ||||
} | } | ||||
private static URL fileToUrl(File file) { | |||||
try { | |||||
return file.toURI().toURL(); | |||||
} catch (MalformedURLException e) { | |||||
throw new IllegalStateException(e); | |||||
} | |||||
} | |||||
} | } |
/* | |||||
* 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; | |||||
} | |||||
} | |||||
} |
import static org.assertj.core.api.Assertions.assertThat; | import static org.assertj.core.api.Assertions.assertThat; | ||||
public class PluginExploderTest { | |||||
public class PluginJarExploderTest { | |||||
@Rule | @Rule | ||||
public TemporaryFolder temp = new TemporaryFolder(); | public TemporaryFolder temp = new TemporaryFolder(); | ||||
final File toDir = temp.newFolder(); | final File toDir = temp.newFolder(); | ||||
PluginInfo pluginInfo = new PluginInfo("checkstyle").setJarFile(jarFile); | PluginInfo pluginInfo = new PluginInfo("checkstyle").setJarFile(jarFile); | ||||
PluginExploder exploder = new PluginExploder() { | |||||
PluginJarExploder exploder = new PluginJarExploder() { | |||||
@Override | @Override | ||||
public ExplodedPlugin explode(PluginInfo info) { | public ExplodedPlugin explode(PluginInfo info) { | ||||
try { | try { | ||||
final File toDir = temp.newFolder(); | final File toDir = temp.newFolder(); | ||||
PluginInfo pluginInfo = new PluginInfo("foo").setJarFile(jarFile); | PluginInfo pluginInfo = new PluginInfo("foo").setJarFile(jarFile); | ||||
PluginExploder exploder = new PluginExploder() { | |||||
PluginJarExploder exploder = new PluginJarExploder() { | |||||
@Override | @Override | ||||
public ExplodedPlugin explode(PluginInfo info) { | public ExplodedPlugin explode(PluginInfo info) { | ||||
return explodeFromUnzippedDir("foo", info.getNonNullJarFile(), toDir); | return explodeFromUnzippedDir("foo", info.getNonNullJarFile(), toDir); |
package org.sonar.core.platform; | package org.sonar.core.platform; | ||||
import com.google.common.collect.ImmutableMap; | 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.assertj.core.data.MapEntry; | ||||
import org.junit.Rule; | import org.junit.Rule; | ||||
import org.junit.Test; | import org.junit.Test; | ||||
import org.junit.rules.TemporaryFolder; | import org.junit.rules.TemporaryFolder; | ||||
import org.sonar.api.Plugin; | import org.sonar.api.Plugin; | ||||
import org.sonar.api.SonarPlugin; | |||||
import org.sonar.api.utils.ZipUtils; | 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.assertThat; | ||||
import static org.assertj.core.api.Assertions.entry; | 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 { | public class PluginLoaderTest { | ||||
@Rule | @Rule | ||||
public TemporaryFolder temp = new TemporaryFolder(); | 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 | @Test | ||||
File jarFile = temp.newFile(); | File jarFile = temp.newFile(); | ||||
PluginInfo info = new PluginInfo("foo") | PluginInfo info = new PluginInfo("foo") | ||||
.setJarFile(jarFile) | .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); | assertThat(defs).hasSize(1); | ||||
ClassloaderDef def = defs.iterator().next(); | |||||
PluginClassloaderDef def = defs.iterator().next(); | |||||
assertThat(def.getBasePluginKey()).isEqualTo("foo"); | assertThat(def.getBasePluginKey()).isEqualTo("foo"); | ||||
assertThat(def.isSelfFirstStrategy()).isFalse(); | assertThat(def.isSelfFirstStrategy()).isFalse(); | ||||
assertThat(def.getFiles()).containsOnly(jarFile); | assertThat(def.getFiles()).containsOnly(jarFile); | ||||
assertThat(def.getMainClassesByPluginKey()).containsOnly(MapEntry.entry("foo", "org.foo.FooPlugin")); | assertThat(def.getMainClassesByPluginKey()).containsOnly(MapEntry.entry("foo", "org.foo.FooPlugin")); | ||||
// TODO test mask - require change in sonar-classloader | // 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 | @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(); | File baseJarFile = temp.newFile(), extensionJar1 = temp.newFile(), extensionJar2 = temp.newFile(); | ||||
PluginInfo base = new PluginInfo("foo") | PluginInfo base = new PluginInfo("foo") | ||||
.setJarFile(baseJarFile) | .setJarFile(baseJarFile) | ||||
.setBasePlugin("foo") | .setBasePlugin("foo") | ||||
.setUseChildFirstClassLoader(true); | .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)); | base.getKey(), base, extension1.getKey(), extension1, extension2.getKey(), extension2)); | ||||
assertThat(defs).hasSize(1); | assertThat(defs).hasSize(1); | ||||
ClassloaderDef def = defs.iterator().next(); | |||||
PluginClassloaderDef def = defs.iterator().next(); | |||||
assertThat(def.getBasePluginKey()).isEqualTo("foo"); | assertThat(def.getBasePluginKey()).isEqualTo("foo"); | ||||
assertThat(def.isSelfFirstStrategy()).isFalse(); | assertThat(def.isSelfFirstStrategy()).isFalse(); | ||||
assertThat(def.getFiles()).containsOnly(baseJarFile, extensionJar1, extensionJar2); | assertThat(def.getFiles()).containsOnly(baseJarFile, extensionJar1, extensionJar2); | ||||
// TODO test mask - require change in sonar-classloader | // TODO test mask - require change in sonar-classloader | ||||
} | } | ||||
/** | /** | ||||
* Does not unzip jar file. It directly returns the JAR file defined on PluginInfo. | * 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 | @Override | ||||
public ExplodedPlugin explode(PluginInfo info) { | public ExplodedPlugin explode(PluginInfo info) { | ||||
return new ExplodedPlugin(info.getKey(), info.getNonNullJarFile(), Collections.<File>emptyList()); | return new ExplodedPlugin(info.getKey(), info.getNonNullJarFile(), Collections.<File>emptyList()); | ||||
} | } | ||||
} | } | ||||
private class TempPluginExploder extends PluginExploder { | |||||
public static class FakePlugin extends SonarPlugin { | |||||
@Override | @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(); | |||||
} | } | ||||
} | } | ||||
} | } |
# see README.txt | |||||
!*/target/ | |||||
*/target/classes/ | |||||
*/target/maven-archiver/ | |||||
*/target/maven-status/ | |||||
*/target/test-*/ | |||||
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. |
<?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> |
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(); | |||||
} | |||||
} |
package org.sonar.plugins.base.api; | |||||
public class BaseApi { | |||||
public void doNothing() { | |||||
} | |||||
} |
<?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> |
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(); | |||||
} | |||||
} |
<?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> |
</parent> | </parent> | ||||
<artifactId>sonar-plugin-api-deps</artifactId> | <artifactId>sonar-plugin-api-deps</artifactId> | ||||
<packaging>jar</packaging> | |||||
<name>SonarQube :: Plugin API Dependencies</name> | <name>SonarQube :: Plugin API Dependencies</name> | ||||
<description>Deprecated transitive dependencies of sonar-plugin-api</description> | <description>Deprecated transitive dependencies of sonar-plugin-api</description> | ||||
<dependencies> | <dependencies> | ||||
<!-- | |||||
Versions must not be changed and overridden from parent pom. These are | |||||
the versions defined in SQ 5.1 | |||||
--> | |||||
<dependency> | <dependency> | ||||
<groupId>com.google.code.gson</groupId> | <groupId>com.google.code.gson</groupId> | ||||
<artifactId>gson</artifactId> | <artifactId>gson</artifactId> | ||||
<version>2.3.1</version> | |||||
</dependency> | </dependency> | ||||
<dependency> | <dependency> | ||||
<groupId>com.google.guava</groupId> | <groupId>com.google.guava</groupId> | ||||
<artifactId>guava</artifactId> | <artifactId>guava</artifactId> | ||||
<version>10.0.1</version> | |||||
</dependency> | </dependency> | ||||
<dependency> | <dependency> | ||||
<groupId>commons-codec</groupId> | <groupId>commons-codec</groupId> | ||||
<artifactId>commons-codec</artifactId> | <artifactId>commons-codec</artifactId> | ||||
<version>1.8</version> | |||||
</dependency> | </dependency> | ||||
<dependency> | <dependency> | ||||
<groupId>commons-collections</groupId> | <groupId>commons-collections</groupId> | ||||
<artifactId>commons-collections</artifactId> | <artifactId>commons-collections</artifactId> | ||||
<version>3.2.1</version> | |||||
</dependency> | </dependency> | ||||
<dependency> | <dependency> | ||||
<groupId>commons-io</groupId> | <groupId>commons-io</groupId> | ||||
<artifactId>commons-io</artifactId> | <artifactId>commons-io</artifactId> | ||||
<version>2.4</version> | |||||
</dependency> | </dependency> | ||||
<dependency> | <dependency> | ||||
<groupId>commons-lang</groupId> | <groupId>commons-lang</groupId> | ||||
<artifactId>commons-lang</artifactId> | <artifactId>commons-lang</artifactId> | ||||
<version>2.6</version> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>dom4j</groupId> | |||||
<artifactId>dom4j</artifactId> | |||||
<version>1.6.1</version> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>xml-apis</groupId> | |||||
<artifactId>xml-apis</artifactId> | |||||
<version>1.4.01</version> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.picocontainer</groupId> | |||||
<artifactId>picocontainer</artifactId> | |||||
<version>2.14.3</version> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.slf4j</groupId> | |||||
<artifactId>slf4j-api</artifactId> | |||||
<version>1.7.10</version> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>ch.qos.logback</groupId> | |||||
<artifactId>logback-classic</artifactId> | |||||
<version>1.1.2</version> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>ch.qos.logback</groupId> | |||||
<artifactId>logback-core</artifactId> | |||||
<version>1.1.2</version> | |||||
</dependency> | </dependency> | ||||
<!-- Needed by old versions of Java plugin (JavaClasspath) --> | <!-- Needed by old versions of Java plugin (JavaClasspath) --> | ||||
<dependency> | <dependency> | ||||
</goals> | </goals> | ||||
<configuration> | <configuration> | ||||
<minimizeJar>false</minimizeJar> | <minimizeJar>false</minimizeJar> | ||||
<createDependencyReducedPom>true</createDependencyReducedPom> | |||||
</configuration> | </configuration> | ||||
</execution> | </execution> | ||||
</executions> | </executions> |
</properties> | </properties> | ||||
<dependencies> | <dependencies> | ||||
<!-- | |||||
The following artifacts are shaded and relocated in an internal package. | |||||
They are not visible by plugins | |||||
--> | |||||
<dependency> | <dependency> | ||||
<groupId>com.google.code.gson</groupId> | <groupId>com.google.code.gson</groupId> | ||||
<artifactId>gson</artifactId> | <artifactId>gson</artifactId> | ||||
<groupId>com.google.guava</groupId> | <groupId>com.google.guava</groupId> | ||||
<artifactId>guava</artifactId> | <artifactId>guava</artifactId> | ||||
</dependency> | </dependency> | ||||
<dependency> | |||||
<groupId>commons-codec</groupId> | |||||
<artifactId>commons-codec</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>commons-collections</groupId> | |||||
<artifactId>commons-collections</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>commons-io</groupId> | |||||
<artifactId>commons-io</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>commons-lang</groupId> | |||||
<artifactId>commons-lang</artifactId> | |||||
</dependency> | |||||
<!-- | |||||
The following artifacts are shaded but not relocated. They | |||||
are provided at runtime, so plugins can use them but | |||||
can not change their version. | |||||
Long-term target is to remove them from API. They should be | |||||
embedded by plugins. | |||||
--> | |||||
<dependency> | <dependency> | ||||
<groupId>org.codehaus.sonar</groupId> | <groupId>org.codehaus.sonar</groupId> | ||||
<artifactId>sonar-check-api</artifactId> | <artifactId>sonar-check-api</artifactId> | ||||
</exclusion> | </exclusion> | ||||
</exclusions> | </exclusions> | ||||
</dependency> | </dependency> | ||||
<dependency> | |||||
<groupId>org.codehaus.sonar</groupId> | |||||
<artifactId>sonar-graph</artifactId> | |||||
</dependency> | |||||
<!-- TODO to be clarified --> | |||||
<dependency> | |||||
<groupId>org.codehaus.woodstox</groupId> | |||||
<artifactId>woodstox-core-lgpl</artifactId> | |||||
<exclusions> | |||||
<exclusion> | |||||
<groupId>stax</groupId> | |||||
<artifactId>stax-api</artifactId> | |||||
</exclusion> | |||||
</exclusions> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.codehaus.woodstox</groupId> | |||||
<artifactId>stax2-api</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.codehaus.staxmate</groupId> | |||||
<artifactId>staxmate</artifactId> | |||||
</dependency> | |||||
<dependency> | <dependency> | ||||
<groupId>jfree</groupId> | <groupId>jfree</groupId> | ||||
<artifactId>jfreechart</artifactId> | <artifactId>jfreechart</artifactId> | ||||
<scope>provided</scope> | <scope>provided</scope> | ||||
</dependency> | </dependency> | ||||
<dependency> | <dependency> | ||||
<groupId>com.google.code.findbugs</groupId> | <groupId>com.google.code.findbugs</groupId> | ||||
<artifactId>jsr305</artifactId> | <artifactId>jsr305</artifactId> | ||||
<scope>provided</scope> | <scope>provided</scope> | ||||
</dependency> | </dependency> | ||||
<dependency> | |||||
<groupId>org.codehaus.sonar</groupId> | |||||
<artifactId>sonar-graph</artifactId> | |||||
<!-- Set to provided to not be visible by plugins --> | |||||
<scope>provided</scope> | |||||
</dependency> | |||||
<!-- TODO we can't remove hibernate-annotations, because currently it's used | <!-- TODO we can't remove hibernate-annotations, because currently it's used | ||||
moreover it contains transitive dependency on dom4j, which is used in some plugins | moreover it contains transitive dependency on dom4j, which is used in some plugins | ||||
--> | --> | ||||
</dependency> | </dependency> | ||||
<dependency> | |||||
<groupId>commons-codec</groupId> | |||||
<artifactId>commons-codec</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>commons-collections</groupId> | |||||
<artifactId>commons-collections</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>commons-io</groupId> | |||||
<artifactId>commons-io</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>commons-lang</groupId> | |||||
<artifactId>commons-lang</artifactId> | |||||
</dependency> | |||||
<dependency> | <dependency> | ||||
<groupId>org.slf4j</groupId> | <groupId>org.slf4j</groupId> | ||||
<artifactId>slf4j-api</artifactId> | <artifactId>slf4j-api</artifactId> | ||||
<artifactId>xpp3</artifactId> | <artifactId>xpp3</artifactId> | ||||
<scope>provided</scope> | <scope>provided</scope> | ||||
</dependency> | </dependency> | ||||
<dependency> | |||||
<groupId>org.codehaus.woodstox</groupId> | |||||
<artifactId>woodstox-core-lgpl</artifactId> | |||||
<exclusions> | |||||
<exclusion> | |||||
<groupId>stax</groupId> | |||||
<artifactId>stax-api</artifactId> | |||||
</exclusion> | |||||
</exclusions> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.codehaus.woodstox</groupId> | |||||
<artifactId>stax2-api</artifactId> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.codehaus.staxmate</groupId> | |||||
<artifactId>staxmate</artifactId> | |||||
</dependency> | |||||
<dependency> | <dependency> | ||||
<groupId>javax.servlet</groupId> | <groupId>javax.servlet</groupId> | ||||
<artifactId>javax.servlet-api</artifactId> | <artifactId>javax.servlet-api</artifactId> |