Explorar el Código

SONAR-6370 isolate plugin classloader from core classes

tags/5.2-RC1
Simon Brandhof hace 9 años
padre
commit
0956511c8c
Se han modificado 41 ficheros con 772 adiciones y 310 borrados
  1. 12
    8
      plugins/sonar-xoo-plugin/pom.xml
  2. 1
    1
      plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/coverage/AbstractCoverageSensor.java
  3. 1
    1
      plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/lang/MeasureSensor.java
  4. 1
    1
      plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/lang/SymbolReferencesSensor.java
  5. 1
    1
      plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/lang/SyntaxHighlightingSensor.java
  6. 1
    1
      plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/scm/XooBlameCommand.java
  7. 1
    1
      plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/test/CoveragePerTestSensor.java
  8. 1
    1
      plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/test/TestExecutionSensor.java
  9. 1
    1
      pom.xml
  10. 10
    17
      server/sonar-server/src/main/java/org/sonar/server/charts/ChartsServlet.java
  11. 4
    2
      server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java
  12. 4
    2
      server/sonar-server/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java
  13. 3
    3
      server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java
  14. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java
  15. 2
    2
      server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarExploderTest.java
  16. 3
    10
      server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java
  17. 4
    0
      sonar-batch/pom.xml
  18. 3
    3
      sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchPluginJarExploder.java
  19. 0
    41
      sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchPluginLoader.java
  20. 5
    2
      sonar-batch/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java
  21. 4
    4
      sonar-batch/src/test/java/org/sonar/batch/bootstrap/BatchPluginJarExploderTest.java
  22. 2
    1
      sonar-core/pom.xml
  23. 43
    28
      sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderDef.java
  24. 163
    0
      sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderFactory.java
  25. 2
    2
      sonar-core/src/main/java/org/sonar/core/platform/PluginJarExploder.java
  26. 48
    89
      sonar-core/src/main/java/org/sonar/core/platform/PluginLoader.java
  27. 132
    0
      sonar-core/src/test/java/org/sonar/core/platform/PluginClassloaderFactoryTest.java
  28. 3
    3
      sonar-core/src/test/java/org/sonar/core/platform/PluginJarExploderTest.java
  29. 75
    41
      sonar-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java
  30. 7
    0
      sonar-core/src/test/projects/.gitignore
  31. 3
    0
      sonar-core/src/test/projects/README.txt
  32. 36
    0
      sonar-core/src/test/projects/base-plugin/pom.xml
  33. 13
    0
      sonar-core/src/test/projects/base-plugin/src/org/sonar/plugins/base/BasePlugin.java
  34. 6
    0
      sonar-core/src/test/projects/base-plugin/src/org/sonar/plugins/base/api/BaseApi.java
  35. BIN
      sonar-core/src/test/projects/base-plugin/target/base-plugin-0.1-SNAPSHOT.jar
  36. 43
    0
      sonar-core/src/test/projects/dependent-plugin/pom.xml
  37. 18
    0
      sonar-core/src/test/projects/dependent-plugin/src/org/sonar/plugins/dependent/DependentPlugin.java
  38. BIN
      sonar-core/src/test/projects/dependent-plugin/target/dependent-plugin-0.1-SNAPSHOT.jar
  39. 14
    0
      sonar-core/src/test/projects/pom.xml
  40. 43
    2
      sonar-plugin-api-deps/pom.xml
  41. 57
    40
      sonar-plugin-api/pom.xml

+ 12
- 8
plugins/sonar-xoo-plugin/pom.xml Ver fichero

@@ -17,24 +17,28 @@
<dependency>
<groupId>com.google.guava</groupId>
<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>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</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>
<groupId>org.codehaus.sonar</groupId>
<artifactId>sonar-plugin-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>


+ 1
- 1
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/coverage/AbstractCoverageSensor.java Ver fichero

@@ -21,7 +21,7 @@ package org.sonar.xoo.coverage;

import com.google.common.base.Splitter;
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.sensor.Sensor;
import org.sonar.api.batch.sensor.SensorContext;

+ 1
- 1
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/lang/MeasureSensor.java Ver fichero

@@ -20,7 +20,7 @@
package org.sonar.xoo.lang;

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.measure.MetricFinder;
import org.sonar.api.batch.sensor.Sensor;

+ 1
- 1
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/lang/SymbolReferencesSensor.java Ver fichero

@@ -21,7 +21,7 @@ package org.sonar.xoo.lang;

import com.google.common.base.Splitter;
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.sensor.Sensor;
import org.sonar.api.batch.sensor.SensorContext;

+ 1
- 1
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/lang/SyntaxHighlightingSensor.java Ver fichero

@@ -21,7 +21,7 @@ package org.sonar.xoo.lang;

import com.google.common.base.Splitter;
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.sensor.Sensor;
import org.sonar.api.batch.sensor.SensorContext;

+ 1
- 1
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/scm/XooBlameCommand.java Ver fichero

@@ -21,7 +21,7 @@ package org.sonar.xoo.scm;

import com.google.common.annotations.VisibleForTesting;
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.scm.BlameCommand;
import org.sonar.api.batch.scm.BlameLine;

+ 1
- 1
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/test/CoveragePerTestSensor.java Ver fichero

@@ -21,7 +21,7 @@ package org.sonar.xoo.test;

import com.google.common.base.Splitter;
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.fs.FilePredicates;
import org.sonar.api.batch.fs.FileSystem;

+ 1
- 1
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/test/TestExecutionSensor.java Ver fichero

@@ -21,7 +21,7 @@ package org.sonar.xoo.test;

import com.google.common.base.Splitter;
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.fs.FilePredicates;
import org.sonar.api.batch.fs.FileSystem;

+ 1
- 1
pom.xml Ver fichero

@@ -721,7 +721,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>10.0.1</version>
<version>17.0</version>
<exclusions>
<exclusion>
<!-- should be declared with scope provided -->

+ 10
- 17
server/sonar-server/src/main/java/org/sonar/server/charts/ChartsServlet.java Ver fichero

@@ -20,7 +20,15 @@
package org.sonar.server.charts;

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.sonar.api.charts.Chart;
import org.sonar.api.charts.ChartParameters;
@@ -34,17 +42,6 @@ import org.sonar.server.charts.deprecated.PieChart;
import org.sonar.server.charts.deprecated.SparkLinesChart;
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 {

private static final Logger LOG = Loggers.get(ChartsServlet.class);
@@ -145,15 +142,11 @@ public class ChartsServlet extends HttpServlet {
}

if (chart != null) {
OutputStream out = null;
try {
out = response.getOutputStream();
try (OutputStream out = response.getOutputStream()) {
response.setContentType("image/png");
chart.exportChartAsPNG(out);
} catch (Exception e) {
LOG.error("Generating chart " + chart.getClass().getName(), e);
} finally {
Closeables.closeQuietly(out);
}
}
}

+ 4
- 2
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java Ver fichero

@@ -22,6 +22,7 @@ package org.sonar.server.platform.platformlevel;
import org.sonar.api.utils.Durations;
import org.sonar.core.i18n.DefaultI18n;
import org.sonar.core.i18n.RuleI18nManager;
import org.sonar.core.platform.PluginClassloaderFactory;
import org.sonar.core.platform.PluginLoader;
import org.sonar.server.db.migrations.DatabaseMigrator;
import org.sonar.server.db.migrations.PlatformDatabaseMigration;
@@ -30,7 +31,7 @@ import org.sonar.server.platform.DefaultServerUpgradeStatus;
import org.sonar.server.platform.RailsAppsDeployer;
import org.sonar.server.plugins.InstalledPluginReferentialFactory;
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.ruby.PlatformRubyBridge;
import org.sonar.server.ui.JRubyI18n;
@@ -51,8 +52,9 @@ public class PlatformLevel2 extends PlatformLevel {

// plugins
ServerPluginRepository.class,
ServerPluginExploder.class,
ServerPluginJarExploder.class,
PluginLoader.class,
PluginClassloaderFactory.class,
InstalledPluginReferentialFactory.class,
ServerExtensionInstaller.class,


+ 4
- 2
server/sonar-server/src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java Ver fichero

@@ -59,7 +59,8 @@ public class ServerExtensionInstaller {
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);
}
}
@@ -71,7 +72,8 @@ public class ServerExtensionInstaller {
ExtensionProvider provider = (ExtensionProvider) container.getComponentByKey(extension);
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);
}
}

server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginExploder.java → server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java Ver fichero

@@ -23,7 +23,7 @@ import org.apache.commons.io.FileUtils;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.ZipUtils;
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.server.platform.DefaultServerFileSystem;

@@ -33,11 +33,11 @@ import static org.apache.commons.io.FileUtils.cleanDirectory;
import static org.apache.commons.io.FileUtils.forceMkdir;

@ServerSide
public class ServerPluginExploder extends PluginExploder {
public class ServerPluginJarExploder extends PluginJarExploder {

private final DefaultServerFileSystem fs;

public ServerPluginExploder(DefaultServerFileSystem fs) {
public ServerPluginJarExploder(DefaultServerFileSystem fs) {
this.fs = fs;
}


+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java Ver fichero

@@ -61,13 +61,13 @@ import static org.apache.commons.io.FileUtils.moveFileToDirectory;
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>
* <li>installation of bundled plugins on first server startup</li>
* <li>installation of new plugins (effective after server startup)</li>
* <li>un-installation of plugins (effective after server startup)</li>
* <li>cancel pending installations/un-installations</li>
* <li>load plugin bytecode</li>
* <li>instantiation of plugin entry-points</li>
* </ul>
*/
public class ServerPluginRepository implements PluginRepository, Startable {

server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginExploderTest.java → server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarExploderTest.java Ver fichero

@@ -32,13 +32,13 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class ServerPluginExploderTest {
public class ServerPluginJarExploderTest {

@Rule
public TemporaryFolder temp = new TemporaryFolder();

DefaultServerFileSystem fs = mock(DefaultServerFileSystem.class);
ServerPluginExploder underTest = new ServerPluginExploder(fs);
ServerPluginJarExploder underTest = new ServerPluginJarExploder(fs);

@Test
public void copy_all_classloader_files_to_dedicated_directory() throws Exception {

+ 3
- 10
server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java Ver fichero

@@ -21,6 +21,8 @@ package org.sonar.server.plugins;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import org.apache.commons.io.FileUtils;
@@ -39,9 +41,6 @@ import org.sonar.core.platform.PluginLoader;
import org.sonar.server.platform.DefaultServerFileSystem;
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.junit.Assert.fail;
import static org.mockito.Mockito.mock;
@@ -58,7 +57,7 @@ public class ServerPluginRepositoryTest {
Server server = mock(Server.class);
ServerUpgradeStatus upgradeStatus = mock(ServerUpgradeStatus.class);
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);

@Before
@@ -91,9 +90,6 @@ public class ServerPluginRepositoryTest {

// both plugins are installed
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
@@ -115,8 +111,6 @@ public class ServerPluginRepositoryTest {

// both plugins are installed
assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("core", "testbase");
assertThat(underTest.getPluginInstance("core").getClass().getName()).isEqualTo("CorePlugin");
assertThat(underTest.getPluginInstance("testbase").getClass().getName()).isEqualTo("BasePlugin");
}

/**
@@ -160,7 +154,6 @@ public class ServerPluginRepositoryTest {
assertThat(downloadedJar).doesNotExist();
assertThat(new File(fs.getInstalledPluginsDir(), downloadedJar.getName())).isFile().exists();
assertThat(underTest.getPluginInfosByKeys()).containsOnlyKeys("testbase");
assertThat(underTest.getPluginInstance("testbase").getClass().getName()).isEqualTo("BasePlugin");
}

@Test

+ 4
- 0
sonar-batch/pom.xml Ver fichero

@@ -28,6 +28,10 @@
<groupId>org.codehaus.sonar</groupId>
<artifactId>sonar-persistit</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.sonar</groupId>

sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchPluginExploder.java → sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchPluginJarExploder.java Ver fichero

@@ -23,7 +23,7 @@ import org.apache.commons.io.FileUtils;
import org.sonar.api.batch.BatchSide;
import org.sonar.api.utils.ZipUtils;
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.home.cache.FileCache;

@@ -32,11 +32,11 @@ import java.io.FileOutputStream;
import java.io.IOException;

@BatchSide
public class BatchPluginExploder extends PluginExploder {
public class BatchPluginJarExploder extends PluginJarExploder {

private final FileCache fileCache;

public BatchPluginExploder(FileCache fileCache) {
public BatchPluginJarExploder(FileCache fileCache) {
this.fileCache = fileCache;
}


+ 0
- 41
sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchPluginLoader.java Ver fichero

@@ -1,41 +0,0 @@
/*
* 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();
}
}

+ 5
- 2
sonar-batch/src/main/java/org/sonar/batch/bootstrap/GlobalContainer.java Ver fichero

@@ -54,7 +54,9 @@ import org.sonar.core.persistence.MyBatis;
import org.sonar.core.persistence.SemaphoreUpdater;
import org.sonar.core.persistence.SemaphoresImpl;
import org.sonar.core.platform.ComponentContainer;
import org.sonar.core.platform.PluginClassloaderFactory;
import org.sonar.core.platform.PluginInfo;
import org.sonar.core.platform.PluginLoader;
import org.sonar.core.platform.PluginRepository;
import org.sonar.core.purge.PurgeProfiler;
import org.sonar.core.rule.CacheRuleFinder;
@@ -98,8 +100,9 @@ public class GlobalContainer extends ComponentContainer {
add(
// plugins
BatchPluginRepository.class,
BatchPluginLoader.class,
BatchPluginExploder.class,
PluginLoader.class,
PluginClassloaderFactory.class,
BatchPluginJarExploder.class,
BatchPluginPredicate.class,
ExtensionInstaller.class,


sonar-batch/src/test/java/org/sonar/batch/bootstrap/BatchPluginExploderTest.java → sonar-batch/src/test/java/org/sonar/batch/bootstrap/BatchPluginJarExploderTest.java Ver fichero

@@ -34,19 +34,19 @@ import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThat;

public class BatchPluginExploderTest {
public class BatchPluginJarExploderTest {

@ClassRule
public static TemporaryFolder temp = new TemporaryFolder();

File userHome;
BatchPluginExploder underTest;
BatchPluginJarExploder underTest;

@Before
public void setUp() throws IOException {
userHome = temp.newFolder();
FileCache fileCache = new FileCacheBuilder().setUserHome(userHome).build();
underTest = new BatchPluginExploder(fileCache);
underTest = new BatchPluginJarExploder(fileCache);
}

@Test
@@ -72,7 +72,7 @@ public class BatchPluginExploderTest {
}

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);
FileUtils.copyFile(src, destFile);
return destFile;

+ 2
- 1
sonar-core/pom.xml Ver fichero

@@ -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>

sonar-core/src/main/java/org/sonar/core/platform/ClassloaderDef.java → sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderDef.java Ver fichero

@@ -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();
}
}

+ 163
- 0
sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderFactory.java Ver fichero

@@ -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/");
}
}

sonar-core/src/main/java/org/sonar/core/platform/PluginExploder.java → sonar-core/src/main/java/org/sonar/core/platform/PluginJarExploder.java Ver fichero

@@ -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);

+ 48
- 89
sonar-core/src/main/java/org/sonar/core/platform/PluginLoader.java Ver fichero

@@ -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) {
@@ -177,13 +151,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);
}
}
}

+ 132
- 0
sonar-core/src/test/java/org/sonar/core/platform/PluginClassloaderFactoryTest.java Ver fichero

@@ -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;
}
}
}

sonar-core/src/test/java/org/sonar/core/platform/PluginExploderTest.java → sonar-core/src/test/java/org/sonar/core/platform/PluginJarExploderTest.java Ver fichero

@@ -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);

+ 75
- 41
sonar-core/src/test/java/org/sonar/core/platform/PluginLoaderTest.java Ver fichero

@@ -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();
}
}
}

+ 7
- 0
sonar-core/src/test/projects/.gitignore Ver fichero

@@ -0,0 +1,7 @@
# see README.txt
!*/target/
*/target/classes/
*/target/maven-archiver/
*/target/maven-status/
*/target/test-*/


+ 3
- 0
sonar-core/src/test/projects/README.txt Ver fichero

@@ -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.

+ 36
- 0
sonar-core/src/test/projects/base-plugin/pom.xml Ver fichero

@@ -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>

+ 13
- 0
sonar-core/src/test/projects/base-plugin/src/org/sonar/plugins/base/BasePlugin.java Ver fichero

@@ -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();
}
}

+ 6
- 0
sonar-core/src/test/projects/base-plugin/src/org/sonar/plugins/base/api/BaseApi.java Ver fichero

@@ -0,0 +1,6 @@
package org.sonar.plugins.base.api;

public class BaseApi {
public void doNothing() {
}
}

BIN
sonar-core/src/test/projects/base-plugin/target/base-plugin-0.1-SNAPSHOT.jar Ver fichero


+ 43
- 0
sonar-core/src/test/projects/dependent-plugin/pom.xml Ver fichero

@@ -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>

+ 18
- 0
sonar-core/src/test/projects/dependent-plugin/src/org/sonar/plugins/dependent/DependentPlugin.java Ver fichero

@@ -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();
}
}

BIN
sonar-core/src/test/projects/dependent-plugin/target/dependent-plugin-0.1-SNAPSHOT.jar Ver fichero


+ 14
- 0
sonar-core/src/test/projects/pom.xml Ver fichero

@@ -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>

+ 43
- 2
sonar-plugin-api-deps/pom.xml Ver fichero

@@ -9,35 +9,76 @@
</parent>

<artifactId>sonar-plugin-api-deps</artifactId>
<packaging>jar</packaging>

<name>SonarQube :: Plugin API Dependencies</name>
<description>Deprecated transitive dependencies of sonar-plugin-api</description>
<dependencies>

<!--
Versions must not be changed and overridden from parent pom. These are
the versions defined in SQ 5.1
-->

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>10.0.1</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.8</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<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>
<!-- Needed by old versions of Java plugin (JavaClasspath) -->
<dependency>
@@ -63,7 +104,7 @@
</goals>
<configuration>
<minimizeJar>false</minimizeJar>
<createDependencyReducedPom>true</createDependencyReducedPom>
</configuration>
</execution>
</executions>

+ 57
- 40
sonar-plugin-api/pom.xml Ver fichero

@@ -18,6 +18,11 @@
</properties>
<dependencies>

<!--
The following artifacts are shaded and relocated in an internal package.
They are not visible by plugins
-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
@@ -26,6 +31,30 @@
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</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>
<groupId>org.codehaus.sonar</groupId>
<artifactId>sonar-check-api</artifactId>
@@ -50,23 +79,45 @@
</exclusion>
</exclusions>
</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>
<groupId>jfree</groupId>
<artifactId>jfreechart</artifactId>
<scope>provided</scope>
</dependency>


<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</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
moreover it contains transitive dependency on dom4j, which is used in some plugins
-->
@@ -87,22 +138,6 @@
</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>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
@@ -114,24 +149,6 @@
<artifactId>xpp3</artifactId>
<scope>provided</scope>
</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>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>

Cargando…
Cancelar
Guardar