@@ -46,6 +46,8 @@ class PluginClassloaderDef { | |||
*/ | |||
private boolean compatibilityMode = false; | |||
private boolean serverExtension = false; | |||
PluginClassloaderDef(String basePluginKey) { | |||
Preconditions.checkArgument(!Strings.isNullOrEmpty(basePluginKey)); | |||
this.basePluginKey = basePluginKey; | |||
@@ -93,6 +95,14 @@ class PluginClassloaderDef { | |||
this.compatibilityMode = b; | |||
} | |||
boolean isServerExtension() { | |||
return serverExtension; | |||
} | |||
void setServerExtension(boolean serverExtension) { | |||
this.serverExtension = serverExtension; | |||
} | |||
@Override | |||
public boolean equals(@Nullable Object o) { | |||
if (this == o) { |
@@ -65,9 +65,9 @@ public class PluginClassloaderFactory { | |||
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.setMask(API_CLASSLOADER_KEY, def.isServerExtension() ? new Mask() : apiMask()); | |||
builder.newClassloader(def.getBasePluginKey()); | |||
builder.setParent(def.getBasePluginKey(), API_CLASSLOADER_KEY, new Mask()); | |||
builder.setLoadingOrder(def.getBasePluginKey(), def.isSelfFirstStrategy() ? SELF_FIRST : PARENT_FIRST); | |||
@@ -145,7 +145,7 @@ public class PluginClassloaderFactory { | |||
*/ | |||
private static Mask apiMask() { | |||
return new Mask() | |||
.addInclusion("org/sonar/api/") | |||
.addInclusion("org/sonar/api/") | |||
.addInclusion("org/sonar/channel/") | |||
.addInclusion("org/sonar/check/") | |||
.addInclusion("org/sonar/colorizer/") | |||
@@ -169,6 +169,7 @@ public class PluginClassloaderFactory { | |||
.addInclusion("org/sonar/server/platform/") | |||
.addInclusion("org/sonar/core/persistence/") | |||
.addInclusion("org/sonar/core/properties/") | |||
.addInclusion("org/sonar/server/views/") | |||
// API exclusions | |||
.addExclusion("org/sonar/api/internal/"); |
@@ -21,10 +21,12 @@ package org.sonar.core.platform; | |||
import com.google.common.annotations.VisibleForTesting; | |||
import com.google.common.base.Strings; | |||
import com.google.common.collect.ImmutableSet; | |||
import java.io.Closeable; | |||
import java.util.Collection; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import org.apache.commons.lang.SystemUtils; | |||
import org.sonar.api.Plugin; | |||
import org.sonar.api.utils.log.Loggers; | |||
@@ -44,11 +46,17 @@ import static java.util.Arrays.asList; | |||
* 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 pointers to classloaders and {@link Plugin}. | |||
* This class is stateless. It does not keep pointers to classloaders and {@link org.sonar.api.SonarPlugin}. | |||
*/ | |||
public class PluginLoader { | |||
private static final String[] DEFAULT_SHARED_RESOURCES = {"org/sonar/plugins", "com/sonar/plugins", "com/sonarsource/plugins"}; | |||
/** | |||
* Defines the base keys (defined by {@link #basePluginKey(PluginInfo, Map)}) of the plugins which are allowed to | |||
* run a full server extensions. | |||
*/ | |||
private static final Set<String> SYSTEM_EXTENSION_PLUGINS_BASE_KEYS = ImmutableSet.of("views"); | |||
public static final Version COMPATIBILITY_MODE_MAX_VERSION = Version.create("5.2"); | |||
private final PluginJarExploder jarExploder; | |||
@@ -85,9 +93,9 @@ public class PluginLoader { | |||
def.addFiles(explodedPlugin.getLibs()); | |||
def.addMainClass(info.getKey(), info.getMainClass()); | |||
for (String defaultSharedResource : DEFAULT_SHARED_RESOURCES) { | |||
def.getExportMask().addInclusion(String.format("%s/%s/api/", defaultSharedResource, info.getKey())); | |||
} | |||
for (String defaultSharedResource : DEFAULT_SHARED_RESOURCES) { | |||
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. | |||
@@ -96,6 +104,7 @@ public class PluginLoader { | |||
Version minSqVersion = info.getMinimalSqVersion(); | |||
boolean compatibilityMode = minSqVersion != null && minSqVersion.compareToIgnoreQualifier(COMPATIBILITY_MODE_MAX_VERSION) < 0; | |||
def.setCompatibilityMode(compatibilityMode); | |||
def.setServerExtension(isServerExtension(baseKey)); | |||
if (compatibilityMode) { | |||
Loggers.get(getClass()).debug("API compatibility mode is enabled on plugin {} [{}] " + | |||
"(built with API lower than {})", | |||
@@ -106,8 +115,12 @@ public class PluginLoader { | |||
return classloadersByBasePlugin.values(); | |||
} | |||
private static boolean isServerExtension(String basePluginKey) { | |||
return SYSTEM_EXTENSION_PLUGINS_BASE_KEYS.contains(basePluginKey); | |||
} | |||
/** | |||
* Instantiates collection of ({@link Plugin} according to given metadata and classloaders | |||
* Instantiates collection of {@link org.sonar.api.SonarPlugin} 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 |
@@ -0,0 +1,41 @@ | |||
/* | |||
* 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.classloader; | |||
import java.util.List; | |||
/** | |||
* Gives access to protected read methods of {@link Mask}. | |||
*/ | |||
public class MaskReader { | |||
private final Mask mask; | |||
public MaskReader(Mask mask) { | |||
this.mask = mask; | |||
} | |||
public List<String> getInclusions() { | |||
return mask.getInclusions(); | |||
} | |||
public List<String> getExclusions() { | |||
return mask.getExclusions(); | |||
} | |||
} |
@@ -21,6 +21,7 @@ package org.sonar.core.platform; | |||
import com.google.common.collect.ImmutableMap; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
import java.util.List; | |||
@@ -146,6 +147,34 @@ public class PluginLoaderTest { | |||
// TODO test mask - require change in sonar-classloader | |||
} | |||
@Test | |||
public void plugin_is_recognised_as_server_extension_if_key_is_views_and_extends_no_other_plugin_and_runs_in_compatibility_mode() throws IOException { | |||
PluginInfo views = create52PluginInfo("views"); | |||
Collection<PluginClassloaderDef> defs = loader.defineClassloaders(ImmutableMap.of("views", views)); | |||
assertThat(defs.iterator().next().isServerExtension()).isTrue(); | |||
} | |||
@Test | |||
public void plugin_is_not_recognised_as_system_extension_if_key_is_views_and_extends_another_plugin() throws IOException { | |||
PluginInfo foo = create52PluginInfo("foo"); | |||
PluginInfo views = create52PluginInfo("views") | |||
.setBasePlugin("foo"); | |||
Collection<PluginClassloaderDef> defs = loader.defineClassloaders(ImmutableMap.of("foo", foo, "views", views)); | |||
assertThat(defs).extracting("compatibilityMode").containsOnly(false, false); | |||
} | |||
private PluginInfo create52PluginInfo(String pluginKey) throws IOException { | |||
File jarFile = temp.newFile(); | |||
return new PluginInfo(pluginKey) | |||
.setJarFile(jarFile) | |||
.setMainClass("org.foo." + pluginKey + "Plugin") | |||
.setMinimalSqVersion(Version.create("5.2")); | |||
} | |||
/** | |||
* Does not unzip jar file. It directly returns the JAR file defined on PluginInfo. | |||
*/ |