@@ -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> | |||
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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; |
@@ -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 --> |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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, | |||
@@ -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); | |||
} | |||
} |
@@ -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; | |||
} | |||
@@ -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 { |
@@ -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 { |
@@ -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 |
@@ -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> |
@@ -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; | |||
} | |||
@@ -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(); | |||
} | |||
} |
@@ -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, | |||
@@ -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; |
@@ -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> |
@@ -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(); | |||
} | |||
} |
@@ -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/"); | |||
} | |||
} |
@@ -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); |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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); |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,7 @@ | |||
# see README.txt | |||
!*/target/ | |||
*/target/classes/ | |||
*/target/maven-archiver/ | |||
*/target/maven-status/ | |||
*/target/test-*/ | |||
@@ -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. |
@@ -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> |
@@ -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(); | |||
} | |||
} |
@@ -0,0 +1,6 @@ | |||
package org.sonar.plugins.base.api; | |||
public class BaseApi { | |||
public void doNothing() { | |||
} | |||
} |
@@ -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> |
@@ -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(); | |||
} | |||
} |
@@ -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> |
@@ -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> |
@@ -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> |