From 32e97e99a805f50e099b16ac55a65703d284979b Mon Sep 17 00:00:00 2001
From: Steve Marion
Date: Thu, 14 Dec 2023 15:09:22 +0100
Subject: [PATCH] SONAR-21195 allow plugins loaded in different containers to
access classLoader resources. Integrate sonarsource-classeloader library into
sonar-core source.
---
build.gradle | 1 -
sonar-core/build.gradle | 1 -
.../org/sonar/classloader/ClassRealm.java | 206 +++++
.../sonar/classloader/ClassloaderBuilder.java | 246 ++++++
.../org/sonar/classloader/ClassloaderRef.java | 53 ++
.../classloader/DefaultClassloaderRef.java | 69 ++
.../main/java/org/sonar/classloader/Mask.java | 208 +++++
.../sonar/classloader/NullClassloaderRef.java | 46 +
.../classloader/ParentFirstStrategy.java | 64 ++
.../sonar/classloader/SelfFirstStrategy.java | 66 ++
.../java/org/sonar/classloader/Strategy.java | 35 +
.../sonar/classloader/StrategyContext.java | 52 ++
.../org/sonar/classloader/package-info.java | 24 +
.../core/platform/PluginClassLoader.java | 14 +-
.../core/platform/PluginClassLoaderDef.java | 6 +-
.../platform/PluginClassloaderFactory.java | 66 +-
.../classloader/ClassloaderBuilderTest.java | 787 ++++++++++++++++++
.../java/org/sonar/classloader/MaskTest.java | 170 ++++
.../PluginClassloaderFactoryTest.java | 28 +-
sonar-core/tester/a.jar | Bin 0 -> 894 bytes
sonar-core/tester/a/A.class | Bin 0 -> 226 bytes
sonar-core/tester/a/A.java | 5 +
sonar-core/tester/a/a.txt | 1 +
sonar-core/tester/a_v2.jar | Bin 0 -> 892 bytes
sonar-core/tester/a_v2/A.class | Bin 0 -> 226 bytes
sonar-core/tester/a_v2/A.java | 6 +
sonar-core/tester/a_v2/a.txt | 1 +
sonar-core/tester/b.jar | Bin 0 -> 827 bytes
sonar-core/tester/b/B.class | Bin 0 -> 176 bytes
sonar-core/tester/b/B.java | 2 +
sonar-core/tester/b/b.txt | 1 +
sonar-core/tester/build.sh | 16 +
sonar-core/tester/c.jar | Bin 0 -> 826 bytes
sonar-core/tester/c/C.class | Bin 0 -> 176 bytes
sonar-core/tester/c/C.java | 2 +
sonar-core/tester/c/c.txt | 1 +
36 files changed, 2133 insertions(+), 44 deletions(-)
create mode 100644 sonar-core/src/main/java/org/sonar/classloader/ClassRealm.java
create mode 100644 sonar-core/src/main/java/org/sonar/classloader/ClassloaderBuilder.java
create mode 100644 sonar-core/src/main/java/org/sonar/classloader/ClassloaderRef.java
create mode 100644 sonar-core/src/main/java/org/sonar/classloader/DefaultClassloaderRef.java
create mode 100644 sonar-core/src/main/java/org/sonar/classloader/Mask.java
create mode 100644 sonar-core/src/main/java/org/sonar/classloader/NullClassloaderRef.java
create mode 100644 sonar-core/src/main/java/org/sonar/classloader/ParentFirstStrategy.java
create mode 100644 sonar-core/src/main/java/org/sonar/classloader/SelfFirstStrategy.java
create mode 100644 sonar-core/src/main/java/org/sonar/classloader/Strategy.java
create mode 100644 sonar-core/src/main/java/org/sonar/classloader/StrategyContext.java
create mode 100644 sonar-core/src/main/java/org/sonar/classloader/package-info.java
create mode 100644 sonar-core/src/test/java/org/sonar/classloader/ClassloaderBuilderTest.java
create mode 100644 sonar-core/src/test/java/org/sonar/classloader/MaskTest.java
create mode 100644 sonar-core/tester/a.jar
create mode 100644 sonar-core/tester/a/A.class
create mode 100644 sonar-core/tester/a/A.java
create mode 100644 sonar-core/tester/a/a.txt
create mode 100644 sonar-core/tester/a_v2.jar
create mode 100644 sonar-core/tester/a_v2/A.class
create mode 100644 sonar-core/tester/a_v2/A.java
create mode 100644 sonar-core/tester/a_v2/a.txt
create mode 100644 sonar-core/tester/b.jar
create mode 100644 sonar-core/tester/b/B.class
create mode 100644 sonar-core/tester/b/B.java
create mode 100644 sonar-core/tester/b/b.txt
create mode 100644 sonar-core/tester/build.sh
create mode 100644 sonar-core/tester/c.jar
create mode 100644 sonar-core/tester/c/C.class
create mode 100644 sonar-core/tester/c/C.java
create mode 100644 sonar-core/tester/c/c.txt
diff --git a/build.gradle b/build.gradle
index e1d932bc939..eac01d4dc97 100644
--- a/build.gradle
+++ b/build.gradle
@@ -376,7 +376,6 @@ subprojects {
dependency('org.codehaus.sonar:sonar-channel:4.2') {
exclude 'org.slf4j:slf4j-api'
}
- dependency 'org.codehaus.sonar:sonar-classloader:1.0'
dependency 'com.fasterxml.staxmate:staxmate:2.4.1'
dependencySet(group: 'org.eclipse.jetty', version: '9.4.6.v20170531') {
entry 'jetty-proxy'
diff --git a/sonar-core/build.gradle b/sonar-core/build.gradle
index 0a7ac004a70..79045ab8c46 100644
--- a/sonar-core/build.gradle
+++ b/sonar-core/build.gradle
@@ -17,7 +17,6 @@ dependencies {
api 'commons-lang:commons-lang'
api 'javax.annotation:javax.annotation-api'
api 'javax.inject:javax.inject'
- api 'org.codehaus.sonar:sonar-classloader'
api 'org.slf4j:slf4j-api'
api 'org.sonarsource.api.plugin:sonar-plugin-api'
api 'org.sonarsource.update-center:sonar-update-center-common'
diff --git a/sonar-core/src/main/java/org/sonar/classloader/ClassRealm.java b/sonar-core/src/main/java/org/sonar/classloader/ClassRealm.java
new file mode 100644
index 00000000000..f66def1ff1a
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/classloader/ClassRealm.java
@@ -0,0 +1,206 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.io.IOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import javax.annotation.CheckForNull;
+
+class ClassRealm extends URLClassLoader implements StrategyContext {
+
+ private final String key;
+ private Mask mask = Mask.ALL;
+ private Mask exportMask = Mask.ALL;
+ private ClassloaderRef parentRef = NullClassloaderRef.INSTANCE;
+ private List siblingRefs = new ArrayList<>();
+ private Strategy strategy;
+
+ ClassRealm(String key, ClassLoader baseClassloader) {
+ super(new URL[0], baseClassloader);
+ this.key = key;
+ }
+
+ String getKey() {
+ return key;
+ }
+
+ ClassRealm setMask(Mask mask) {
+ this.mask = mask;
+ return this;
+ }
+
+ Mask getExportMask() {
+ return exportMask;
+ }
+
+ ClassRealm setExportMask(Mask exportMask) {
+ this.exportMask = exportMask;
+ return this;
+ }
+
+ ClassRealm setParent(ClassloaderRef parentRef) {
+ this.parentRef = parentRef;
+ return this;
+ }
+
+ ClassRealm addSibling(ClassloaderRef ref) {
+ this.siblingRefs.add(ref);
+ return this;
+ }
+
+ ClassRealm setStrategy(Strategy strategy) {
+ this.strategy = strategy;
+ return this;
+ }
+
+ ClassRealm addConstituent(URL url) {
+ super.addURL(url);
+ return this;
+ }
+
+ @Override
+ public Class> loadClass(String name) throws ClassNotFoundException {
+ return loadClass(name, false);
+ }
+
+ @Override
+ protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ if (mask.acceptClass(name)) {
+ try {
+ // first, try loading bootstrap classes
+ return super.loadClass(name, resolve);
+ } catch (ClassNotFoundException ignored) {
+ // next, try loading via siblings, self and parent as controlled by strategy
+ return strategy.loadClass(this, name);
+ }
+ }
+ throw new ClassNotFoundException(String.format("Class %s is not accepted in classloader %s", name, this));
+ }
+
+
+ @Override
+ protected Class> findClass(String name) throws ClassNotFoundException {
+ // not supposed to be used. Replaced by loadClassFromSelf(String)
+ throw new ClassNotFoundException(name);
+ }
+
+ @CheckForNull
+ @Override
+ public URL getResource(String name) {
+ if (mask.acceptResource(name)) {
+ return strategy.getResource(this, name);
+ }
+ return null;
+ }
+
+ @Override
+ public Enumeration getResources(String name) throws IOException {
+ // Important note: do not use java.util.Set as equals and hashCode methods of
+ // java.net.URL perform domain name resolution. This can result in a big performance hit.
+ List resources = new ArrayList<>();
+ if (mask.acceptResource(name)) {
+ strategy.getResources(this, name, resources);
+ }
+ return Collections.enumeration(resources);
+ }
+
+ @Override
+ public Class> loadClassFromSelf(String name) {
+ Class> clazz = findLoadedClass(name);
+ if (clazz == null) {
+ try {
+ return super.findClass(name);
+ } catch (ClassNotFoundException ignored) {
+ // return null when class is not found, so that loading strategy
+ // can try parent or sibling classloaders.
+ }
+ }
+ return clazz;
+ }
+
+ @Override
+ public Class> loadClassFromSiblings(String name) {
+ for (ClassloaderRef siblingRef : siblingRefs) {
+ Class> clazz = siblingRef.loadClassIfPresent(name);
+ if (clazz != null) {
+ return clazz;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Class> loadClassFromParent(String name) {
+ return parentRef.loadClassIfPresent(name);
+ }
+
+ @Override
+ public URL loadResourceFromSelf(String name) {
+ return super.findResource(name);
+ }
+
+ @Override
+ public URL loadResourceFromSiblings(String name) {
+ for (ClassloaderRef siblingRef : siblingRefs) {
+ URL url = siblingRef.loadResourceIfPresent(name);
+ if (url != null) {
+ return url;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public URL loadResourceFromParent(String name) {
+ return parentRef.loadResourceIfPresent(name);
+ }
+
+ @Override
+ public void loadResourcesFromSelf(String name, Collection appendTo) {
+ try {
+ appendTo.addAll(Collections.list(super.findResources(name)));
+ } catch (IOException e) {
+ throw new IllegalStateException(String.format("Fail to load resources named '%s' from classloader %s", name, toString()), e);
+ }
+ }
+
+ @Override
+ public void loadResourcesFromSiblings(String name, Collection appendTo) {
+ for (ClassloaderRef siblingRef : siblingRefs) {
+ siblingRef.loadResources(name, appendTo);
+ }
+ }
+
+ @Override
+ public void loadResourcesFromParent(String name, Collection appendTo) {
+ parentRef.loadResources(name, appendTo);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ClassRealm{%s}", key);
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/classloader/ClassloaderBuilder.java b/sonar-core/src/main/java/org/sonar/classloader/ClassloaderBuilder.java
new file mode 100644
index 00000000000..0bea223a28a
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/classloader/ClassloaderBuilder.java
@@ -0,0 +1,246 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.net.URL;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Collections.emptyList;
+
+/**
+ * @since 0.1
+ */
+public class ClassloaderBuilder {
+ private final Map previouslyCreatedClassLoaders;
+
+ private final Map newRealmsByKey = new HashMap<>();
+
+ public ClassloaderBuilder() {
+ this(emptyList());
+ }
+
+ /**
+ * Creates a new classloader builder that can use a collection of previously created
+ * classloaders as parent or siblings when building the new classloaders.
+ *
+ * @param previouslyCreatedClassLoaders Collection of classloaders that can be used as a
+ * parent or sibling. Must be of type {@link ClassRealm}.
+ */
+ public ClassloaderBuilder(Collection previouslyCreatedClassLoaders) {
+ this.previouslyCreatedClassLoaders = new HashMap<>();
+ for (ClassLoader cl : previouslyCreatedClassLoaders) {
+ if (!(cl instanceof ClassRealm)) {
+ throw new IllegalArgumentException("classloader not of type ClassRealm: " + cl);
+ }
+ ClassRealm classRealm = (ClassRealm) cl;
+ this.previouslyCreatedClassLoaders.put(classRealm.getKey(), classRealm);
+ }
+ }
+
+ public enum LoadingOrder {
+ /**
+ * Order: siblings, then parent, then self
+ */
+ PARENT_FIRST(ParentFirstStrategy.INSTANCE),
+
+ /**
+ * Order: siblings, then self, then parent
+ */
+ SELF_FIRST(SelfFirstStrategy.INSTANCE);
+
+ private final Strategy strategy;
+
+ LoadingOrder(Strategy strategy) {
+ this.strategy = strategy;
+ }
+ }
+
+ /**
+ * Wrapper of {@link ClassRealm} as long as associations are not fully
+ * defined
+ */
+ private static class NewRealm {
+ private final ClassRealm realm;
+
+ // key of the optional parent classloader
+ private String parentKey;
+
+ private final List siblingKeys = new ArrayList<>();
+ private final Map associatedMasks = new HashMap<>();
+
+ private NewRealm(ClassRealm realm) {
+ this.realm = realm;
+ }
+ }
+
+ /**
+ * Declares a new classloader based on system classloader.
+ */
+ public ClassloaderBuilder newClassloader(String key) {
+ return newClassloader(key, getSystemClassloader());
+ }
+
+ /**
+ * Declares a new classloader based on a given parent classloader. Key must be unique. An
+ * {@link IllegalArgumentException} is thrown if the key is already referenced.
+ *
+ * Default loading order is {@link LoadingOrder#PARENT_FIRST}.
+ */
+ public ClassloaderBuilder newClassloader(final String key, final ClassLoader baseClassloader) {
+ if (newRealmsByKey.containsKey(key)) {
+ throw new IllegalStateException(String.format("The classloader '%s' already exists. Can not create it twice.", key));
+ }
+ if (previouslyCreatedClassLoaders.containsKey(key)) {
+ throw new IllegalStateException(String.format("The classloader '%s' already exists in the list of previously created classloaders."
+ + " Can not create it twice.", key));
+ }
+ ClassRealm realm = AccessController.>doPrivileged(() -> new ClassRealm(key, baseClassloader));
+ realm.setStrategy(LoadingOrder.PARENT_FIRST.strategy);
+ newRealmsByKey.put(key, new NewRealm(realm));
+ return this;
+ }
+
+ public ClassloaderBuilder setParent(String key, String parentKey, Mask mask) {
+ NewRealm newRealm = getOrFail(key);
+ newRealm.parentKey = parentKey;
+ newRealm.associatedMasks.put(parentKey, mask);
+ return this;
+ }
+
+ public ClassloaderBuilder setParent(String key, ClassLoader parent, Mask mask) {
+ NewRealm newRealm = getOrFail(key);
+ newRealm.realm.setParent(new DefaultClassloaderRef(parent, mask));
+ return this;
+ }
+
+ public ClassloaderBuilder addSibling(String key, String siblingKey, Mask mask) {
+ NewRealm newRealm = getOrFail(key);
+ newRealm.siblingKeys.add(siblingKey);
+ newRealm.associatedMasks.put(siblingKey, mask);
+ return this;
+ }
+
+ public ClassloaderBuilder addSibling(String key, ClassLoader sibling, Mask mask) {
+ NewRealm newRealm = getOrFail(key);
+ newRealm.realm.addSibling(new DefaultClassloaderRef(sibling, mask));
+ return this;
+ }
+
+ public ClassloaderBuilder addURL(String key, URL url) {
+ getOrFail(key).realm.addConstituent(url);
+ return this;
+ }
+
+ public ClassloaderBuilder setMask(String key, Mask mask) {
+ getOrFail(key).realm.setMask(mask);
+ return this;
+ }
+
+ public ClassloaderBuilder setExportMask(String key, Mask mask) {
+ getOrFail(key).realm.setExportMask(mask);
+ return this;
+ }
+
+ public ClassloaderBuilder setLoadingOrder(String key, LoadingOrder order) {
+ getOrFail(key).realm.setStrategy(order.strategy);
+ return this;
+ }
+
+ /**
+ * Returns the new classloaders, grouped by keys. The parent and sibling classloaders
+ * that are already existed (see {@link #setParent(String, ClassLoader, Mask)}
+ * and {@link #addSibling(String, ClassLoader, Mask)} are not included into result.
+ */
+ public Map build() {
+ Map result = new HashMap<>();
+
+ // all the classloaders are created. Associations can now be resolved.
+ for (Map.Entry entry : newRealmsByKey.entrySet()) {
+ NewRealm newRealm = entry.getValue();
+ if (newRealm.parentKey != null) {
+ ClassRealm parent = getNewOrPreviousClassloader(newRealm.parentKey);
+ Mask parentMask = newRealm.associatedMasks.get(newRealm.parentKey);
+ parentMask = mergeWithExportMask(parentMask, newRealm.parentKey);
+ newRealm.realm.setParent(new DefaultClassloaderRef(parent, parentMask));
+ }
+ for (String siblingKey : newRealm.siblingKeys) {
+ ClassRealm sibling = getNewOrPreviousClassloader(siblingKey);
+ Mask siblingMask = newRealm.associatedMasks.get(siblingKey);
+ siblingMask = mergeWithExportMask(siblingMask, siblingKey);
+ newRealm.realm.addSibling(new DefaultClassloaderRef(sibling, siblingMask));
+ }
+ result.put(newRealm.realm.getKey(), newRealm.realm);
+ }
+ return result;
+ }
+
+ private Mask mergeWithExportMask(Mask mask, String exportKey) {
+ NewRealm newRealm = newRealmsByKey.get(exportKey);
+ if (newRealm != null) {
+ return Mask.builder().copy(mask).merge(newRealm.realm.getExportMask()).build();
+ }
+ ClassRealm realm = previouslyCreatedClassLoaders.get(exportKey);
+ if (realm != null) {
+ return Mask.builder().copy(mask).merge(realm.getExportMask()).build();
+ }
+ return mask;
+ }
+
+ private NewRealm getOrFail(String key) {
+ NewRealm newRealm = newRealmsByKey.get(key);
+ if (newRealm == null) {
+ throw new IllegalStateException(String.format("The classloader '%s' does not exist", key));
+ }
+ return newRealm;
+ }
+
+ private ClassRealm getNewOrPreviousClassloader(String key) {
+ NewRealm newRealm = newRealmsByKey.get(key);
+ if (newRealm != null) {
+ return newRealm.realm;
+ }
+ ClassRealm previousClassloader = previouslyCreatedClassLoaders.get(key);
+ if (previousClassloader != null) {
+ return previousClassloader;
+ }
+
+ throw new IllegalStateException(String.format("The classloader '%s' does not exist", key));
+ }
+
+ /**
+ * JRE system classloader. In Oracle JVM:
+ * - ClassLoader.getSystemClassLoader() is sun.misc.Launcher$AppClassLoader. It contains app classpath.
+ * - ClassLoader.getSystemClassLoader().getParent() is sun.misc.Launcher$ExtClassLoader. It is the JRE core classloader.
+ */
+ private static ClassLoader getSystemClassloader() {
+ ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
+ ClassLoader systemParent = systemClassLoader.getParent();
+ if (systemParent != null) {
+ systemClassLoader = systemParent;
+ }
+ return systemClassLoader;
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/classloader/ClassloaderRef.java b/sonar-core/src/main/java/org/sonar/classloader/ClassloaderRef.java
new file mode 100644
index 00000000000..0e3b2ad7fd9
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/classloader/ClassloaderRef.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.net.URL;
+import java.util.Collection;
+import javax.annotation.CheckForNull;
+
+interface ClassloaderRef {
+
+ /**
+ * Does not throw {@link java.lang.ClassNotFoundException} but returns null
+ * when class is not found
+ *
+ * @param name name of class, for example "org.foo.Bar"
+ */
+ @CheckForNull
+ Class> loadClassIfPresent(String name);
+
+ /**
+ * Searches for a resource. Returns null if not found.
+ *
+ * @param name name of resource, for example "org/foo/Bar.class" or "org/foo/config.xml"
+ */
+ @CheckForNull
+ URL loadResourceIfPresent(String name);
+
+ /**
+ * Searches for all the occurrences of a resource from hierarchy of classloaders.
+ * Results are appended to the parameter "appendTo". Order of resources is given by the
+ * hierarchy order of classloaders.
+ *
+ * @see #loadResourceIfPresent(String) for the format of resource name
+ */
+ void loadResources(String name, Collection appendTo);
+}
diff --git a/sonar-core/src/main/java/org/sonar/classloader/DefaultClassloaderRef.java b/sonar-core/src/main/java/org/sonar/classloader/DefaultClassloaderRef.java
new file mode 100644
index 00000000000..1d7f98f42ed
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/classloader/DefaultClassloaderRef.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.io.IOException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Enumeration;
+
+class DefaultClassloaderRef implements ClassloaderRef {
+ private final Mask mask;
+ private final ClassLoader classloader;
+
+ DefaultClassloaderRef(ClassLoader classloader, Mask mask) {
+ this.classloader = classloader;
+ this.mask = mask;
+ }
+
+ @Override
+ public Class loadClassIfPresent(String classname) {
+ if (mask.acceptClass(classname)) {
+ try {
+ return classloader.loadClass(classname);
+ } catch (ClassNotFoundException ignored) {
+ // excepted behavior. Return null if class does not exist in this classloader
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public URL loadResourceIfPresent(String name) {
+ if (mask.acceptResource(name)) {
+ return classloader.getResource(name);
+ }
+ return null;
+ }
+
+ @Override
+ public void loadResources(String name, Collection appendTo) {
+ if (mask.acceptResource(name)) {
+ try {
+ Enumeration resources = classloader.getResources(name);
+ while (resources.hasMoreElements()) {
+ appendTo.add(resources.nextElement());
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException(String.format("Fail to load resources named '%s'", name), e);
+ }
+ }
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/classloader/Mask.java b/sonar-core/src/main/java/org/sonar/classloader/Mask.java
new file mode 100644
index 00000000000..6d40f525902
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/classloader/Mask.java
@@ -0,0 +1,208 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/**
+ * A mask restricts access of a classloader to resources through inclusion and exclusion patterns.
+ * By default all resources/classes are visible.
+ *
+ * Format of inclusion/exclusion patterns is the file path separated by slashes, for example
+ * "org/foo/Bar.class" or "org/foo/config.xml". Wildcard patterns are not supported. Directories must end with
+ * slash, for example "org/foo/" for excluding package org.foo and its sub-packages. Add the
+ * exclusion "/" to exclude everything.
+ *
+ * @since 0.1
+ */
+public class Mask {
+
+ private static final String ROOT = "/";
+
+ /**
+ * Accepts everything
+ *
+ * @since 1.1
+ */
+ public static final Mask ALL = Mask.builder().build();
+
+ /**
+ * Accepts nothing
+ *
+ * @since 1.1
+ */
+ public static final Mask NONE = Mask.builder().exclude(ROOT).build();
+
+ private final Set inclusions;
+ private final Set exclusions;
+
+ private Mask(Builder builder) {
+ this.inclusions = Collections.unmodifiableSet(builder.inclusions);
+ this.exclusions = Collections.unmodifiableSet(builder.exclusions);
+ }
+
+ /**
+ * Create a {@link Builder} for building immutable instances of {@link Mask}
+ *
+ * @since 1.1
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public Set getInclusions() {
+ return inclusions;
+ }
+
+ public Set getExclusions() {
+ return exclusions;
+ }
+
+ boolean acceptClass(String classname) {
+ if (inclusions.isEmpty() && exclusions.isEmpty()) {
+ return true;
+ }
+ return acceptResource(classToResource(classname));
+ }
+
+ boolean acceptResource(String name) {
+ boolean ok = true;
+ if (!inclusions.isEmpty()) {
+ ok = false;
+ for (String include : inclusions) {
+ if (matchPattern(name, include)) {
+ ok = true;
+ break;
+ }
+ }
+ }
+ if (ok) {
+ for (String exclude : exclusions) {
+ if (matchPattern(name, exclude)) {
+ ok = false;
+ break;
+ }
+ }
+ }
+ return ok;
+ }
+
+ private static boolean matchPattern(String name, String pattern) {
+ return pattern.equals(ROOT) || (pattern.endsWith("/") && name.startsWith(pattern)) || pattern.equals(name);
+ }
+
+ private static String classToResource(String classname) {
+ return classname.replace('.', '/') + ".class";
+ }
+
+
+ public static class Builder {
+ private final Set inclusions = new HashSet<>();
+ private final Set exclusions = new HashSet<>();
+
+ private Builder() {
+ }
+
+ public Builder include(String path, String... others) {
+ doInclude(path);
+ for (String other : others) {
+ doInclude(other);
+ }
+ return this;
+ }
+
+ public Builder exclude(String path, String... others) {
+ doExclude(path);
+ for (String other : others) {
+ doExclude(other);
+ }
+ return this;
+ }
+
+ public Builder copy(Mask with) {
+ this.inclusions.addAll(with.inclusions);
+ this.exclusions.addAll(with.exclusions);
+ return this;
+ }
+
+ public Builder merge(Mask with) {
+ List lowestIncludes = new ArrayList<>();
+
+ if (inclusions.isEmpty()) {
+ lowestIncludes.addAll(with.inclusions);
+ } else if (with.inclusions.isEmpty()) {
+ lowestIncludes.addAll(inclusions);
+ } else {
+ for (String include : inclusions) {
+ for (String fromInclude : with.inclusions) {
+ overlappingInclude(include, fromInclude)
+ .ifPresent(lowestIncludes::add);
+ }
+ }
+ }
+ inclusions.clear();
+ inclusions.addAll(lowestIncludes);
+ exclusions.addAll(with.exclusions);
+ return this;
+ }
+
+ private static Optional overlappingInclude(String include, String fromInclude) {
+ if (fromInclude.equals(include)) {
+ return Optional.of(fromInclude);
+ } else if (fromInclude.startsWith(include)) {
+ return Optional.of(fromInclude);
+ } else if (include.startsWith(fromInclude)) {
+ return Optional.of(include);
+ }
+ return Optional.empty();
+ }
+
+ public Mask build() {
+ return new Mask(this);
+ }
+
+ private void doInclude(@Nullable String path) {
+ this.inclusions.add(validatePath(path));
+ }
+
+ private void doExclude(@Nullable String path) {
+ this.exclusions.add(validatePath(path));
+ }
+
+ private static String validatePath(@Nullable String path) {
+ if (path == null) {
+ throw new IllegalArgumentException("Mask path must not be null");
+ }
+ if (path.startsWith("/") && path.length() > 1) {
+ throw new IllegalArgumentException("Mask path must not start with slash: ");
+ }
+ if (path.contains("*")) {
+ throw new IllegalArgumentException("Mask path is not a wildcard pattern and should not contain star characters (*): " + path);
+ }
+ return path;
+ }
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/classloader/NullClassloaderRef.java b/sonar-core/src/main/java/org/sonar/classloader/NullClassloaderRef.java
new file mode 100644
index 00000000000..1f8fe7ca009
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/classloader/NullClassloaderRef.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.net.URL;
+import java.util.Collection;
+
+class NullClassloaderRef implements ClassloaderRef {
+
+ public static final NullClassloaderRef INSTANCE = new NullClassloaderRef();
+
+ private NullClassloaderRef() {
+ }
+
+ @Override
+ public Class loadClassIfPresent(String classname) {
+ return null;
+ }
+
+ @Override
+ public URL loadResourceIfPresent(String name) {
+ return null;
+ }
+
+ @Override
+ public void loadResources(String name, Collection appendTo) {
+ // do nothing
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/classloader/ParentFirstStrategy.java b/sonar-core/src/main/java/org/sonar/classloader/ParentFirstStrategy.java
new file mode 100644
index 00000000000..a322f8227e0
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/classloader/ParentFirstStrategy.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.net.URL;
+import java.util.Collection;
+
+class ParentFirstStrategy implements Strategy {
+ static final Strategy INSTANCE = new ParentFirstStrategy();
+
+ private ParentFirstStrategy() {
+ }
+
+ @Override
+ public Class> loadClass(StrategyContext context, String name) throws ClassNotFoundException {
+ Class> clazz = context.loadClassFromSiblings(name);
+ if (clazz == null) {
+ clazz = context.loadClassFromParent(name);
+ if (clazz == null) {
+ clazz = context.loadClassFromSelf(name);
+ if (clazz == null) {
+ throw new ClassNotFoundException(name);
+ }
+ }
+ }
+ return clazz;
+ }
+
+ @Override
+ public URL getResource(StrategyContext context, String name) {
+ URL url = context.loadResourceFromSiblings(name);
+ if (url == null) {
+ url = context.loadResourceFromParent(name);
+ if (url == null) {
+ url = context.loadResourceFromSelf(name);
+ }
+ }
+ return url;
+ }
+
+ @Override
+ public void getResources(StrategyContext context, String name, Collection appendTo) {
+ context.loadResourcesFromSiblings(name, appendTo);
+ context.loadResourcesFromParent(name, appendTo);
+ context.loadResourcesFromSelf(name, appendTo);
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/classloader/SelfFirstStrategy.java b/sonar-core/src/main/java/org/sonar/classloader/SelfFirstStrategy.java
new file mode 100644
index 00000000000..48bbb9457d5
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/classloader/SelfFirstStrategy.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.net.URL;
+import java.util.Collection;
+
+class SelfFirstStrategy implements Strategy {
+
+ static final SelfFirstStrategy INSTANCE = new SelfFirstStrategy();
+
+ private SelfFirstStrategy() {
+ // singleton instance
+ }
+
+ @Override
+ public Class> loadClass(StrategyContext context, String name) throws ClassNotFoundException {
+ Class> clazz = context.loadClassFromSiblings(name);
+ if (clazz == null) {
+ clazz = context.loadClassFromSelf(name);
+ if (clazz == null) {
+ clazz = context.loadClassFromParent(name);
+ if (clazz == null) {
+ throw new ClassNotFoundException(name);
+ }
+ }
+ }
+ return clazz;
+ }
+
+ @Override
+ public URL getResource(StrategyContext context, String name) {
+ URL url = context.loadResourceFromSiblings(name);
+ if (url == null) {
+ url = context.loadResourceFromSelf(name);
+ if (url == null) {
+ url = context.loadResourceFromParent(name);
+ }
+ }
+ return url;
+ }
+
+ @Override
+ public void getResources(StrategyContext context, String name, Collection appendTo) {
+ context.loadResourcesFromSiblings(name, appendTo);
+ context.loadResourcesFromSelf(name, appendTo);
+ context.loadResourcesFromParent(name, appendTo);
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/classloader/Strategy.java b/sonar-core/src/main/java/org/sonar/classloader/Strategy.java
new file mode 100644
index 00000000000..39904cef37b
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/classloader/Strategy.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.net.URL;
+import java.util.Collection;
+import javax.annotation.CheckForNull;
+
+interface Strategy {
+
+ Class> loadClass(StrategyContext context, String name) throws ClassNotFoundException;
+
+ @CheckForNull
+ URL getResource(StrategyContext context, String name);
+
+ void getResources(StrategyContext context, String name, Collection urls);
+
+}
diff --git a/sonar-core/src/main/java/org/sonar/classloader/StrategyContext.java b/sonar-core/src/main/java/org/sonar/classloader/StrategyContext.java
new file mode 100644
index 00000000000..23e705b1ab5
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/classloader/StrategyContext.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.net.URL;
+import java.util.Collection;
+import javax.annotation.CheckForNull;
+
+interface StrategyContext {
+
+ @CheckForNull
+ Class> loadClassFromSiblings(String name);
+
+ @CheckForNull
+ Class> loadClassFromSelf(String name);
+
+ @CheckForNull
+ Class> loadClassFromParent(String name);
+
+ @CheckForNull
+ URL loadResourceFromSiblings(String name);
+
+ @CheckForNull
+ URL loadResourceFromSelf(String name);
+
+ @CheckForNull
+ URL loadResourceFromParent(String name);
+
+ void loadResourcesFromSiblings(String name, Collection appendTo);
+
+ void loadResourcesFromSelf(String name, Collection appendTo);
+
+ void loadResourcesFromParent(String name, Collection appendTo);
+
+}
diff --git a/sonar-core/src/main/java/org/sonar/classloader/package-info.java b/sonar-core/src/main/java/org/sonar/classloader/package-info.java
new file mode 100644
index 00000000000..a99d9c6da7d
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/classloader/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.classloader;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoader.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoader.java
index 8096cd012db..9952a5fe1a3 100644
--- a/sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoader.java
+++ b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoader.java
@@ -52,6 +52,7 @@ public class PluginClassLoader {
private static final Version COMPATIBILITY_MODE_MAX_VERSION = Version.create("5.2");
private final PluginClassloaderFactory classloaderFactory;
+ private final Map classLoaders = new HashMap<>();
public PluginClassLoader(PluginClassloaderFactory classloaderFactory) {
this.classloaderFactory = classloaderFactory;
@@ -63,8 +64,9 @@ public class PluginClassLoader {
public Map load(Map pluginsByKey) {
Collection defs = defineClassloaders(pluginsByKey);
- Map classloaders = classloaderFactory.create(defs);
- return instantiatePluginClasses(classloaders);
+ Map newClassloaders = classloaderFactory.create(classLoaders, defs);
+ classLoaders.putAll(newClassloaders);
+ return instantiatePluginClasses(newClassloaders);
}
/**
@@ -88,20 +90,22 @@ public class PluginClassLoader {
def.addMainClass(info.getKey(), info.getMainClass());
for (String defaultSharedResource : DEFAULT_SHARED_RESOURCES) {
- def.getExportMask().addInclusion(String.format("%s/%s/api/", defaultSharedResource, info.getKey()));
+ def.getExportMask().include(String.format("%s/%s/api/", defaultSharedResource, info.getKey()));
}
// The plugins that extend other plugins can only add some files to classloader.
// They can't change metadata like ordering strategy or compatibility mode.
if (Strings.isNullOrEmpty(info.getBasePlugin())) {
if (info.isUseChildFirstClassLoader()) {
- LoggerFactory.getLogger(getClass()).warn("Plugin {} [{}] uses a child first classloader which is deprecated", info.getName(), info.getKey());
+ LoggerFactory.getLogger(getClass()).warn("Plugin {} [{}] uses a child first classloader which is deprecated", info.getName(),
+ info.getKey());
}
def.setSelfFirstStrategy(info.isUseChildFirstClassLoader());
Version minSonarPluginApiVersion = info.getMinimalSonarPluginApiVersion();
boolean compatibilityMode = minSonarPluginApiVersion != null && minSonarPluginApiVersion.compareToIgnoreQualifier(COMPATIBILITY_MODE_MAX_VERSION) < 0;
if (compatibilityMode) {
- LoggerFactory.getLogger(getClass()).warn("API compatibility mode is no longer supported. In case of error, plugin {} [{}] should package its dependencies.",
+ LoggerFactory.getLogger(getClass()).warn("API compatibility mode is no longer supported. In case of error, plugin {} [{}] " +
+ "should package its dependencies.",
info.getName(), info.getKey());
}
}
diff --git a/sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoaderDef.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoaderDef.java
index 9d581daeebd..bb99e5fd17f 100644
--- a/sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoaderDef.java
+++ b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoaderDef.java
@@ -33,12 +33,12 @@ import org.sonar.classloader.Mask;
/**
* Temporary information about the classLoader to be created for a plugin (or a group of plugins).
*/
-class PluginClassLoaderDef {
+public class PluginClassLoaderDef {
private final String basePluginKey;
private final Map mainClassesByPluginKey = new HashMap<>();
private final List files = new ArrayList<>();
- private final Mask mask = new Mask();
+ private final Mask.Builder mask = Mask.builder();
private boolean selfFirstStrategy = false;
PluginClassLoaderDef(String basePluginKey) {
@@ -58,7 +58,7 @@ class PluginClassLoaderDef {
this.files.addAll(f);
}
- Mask getExportMask() {
+ Mask.Builder getExportMask() {
return mask;
}
diff --git a/sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderFactory.java b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderFactory.java
index 8205f78a9b5..1647bbfde10 100644
--- a/sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderFactory.java
+++ b/sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderFactory.java
@@ -24,9 +24,10 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
-import org.sonar.api.scanner.ScannerSide;
import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.scanner.ScannerSide;
import org.sonar.api.server.ServerSide;
import org.sonar.classloader.ClassloaderBuilder;
import org.sonar.classloader.Mask;
@@ -53,35 +54,41 @@ public class PluginClassloaderFactory {
/**
* Creates as many classloaders as requested by the input parameter.
*/
- public Map create(Collection defs) {
+ public Map create(Map previouslyCreatedClassloaders,
+ Collection newDefs) {
ClassLoader baseClassLoader = baseClassLoader();
- ClassloaderBuilder builder = new ClassloaderBuilder();
+ Collection allDefs = new HashSet<>();
+ allDefs.addAll(newDefs);
+ allDefs.addAll(previouslyCreatedClassloaders.keySet());
+
+ ClassloaderBuilder builder = new ClassloaderBuilder(previouslyCreatedClassloaders.values());
builder.newClassloader(API_CLASSLOADER_KEY, baseClassLoader);
builder.setMask(API_CLASSLOADER_KEY, apiMask());
- for (PluginClassLoaderDef def : defs) {
+ for (PluginClassLoaderDef def : newDefs) {
builder.newClassloader(def.getBasePluginKey());
- builder.setParent(def.getBasePluginKey(), API_CLASSLOADER_KEY, new Mask());
+ builder.setParent(def.getBasePluginKey(), API_CLASSLOADER_KEY, Mask.ALL);
builder.setLoadingOrder(def.getBasePluginKey(), def.isSelfFirstStrategy() ? SELF_FIRST : PARENT_FIRST);
for (File jar : def.getFiles()) {
builder.addURL(def.getBasePluginKey(), fileToUrl(jar));
}
- exportResources(def, builder, defs);
+ exportResources(def, builder, allDefs);
}
- return build(defs, builder);
+ return build(newDefs, builder);
}
/**
* A plugin can export some resources to other plugins
*/
- private static void exportResources(PluginClassLoaderDef def, ClassloaderBuilder builder, Collection allPlugins) {
+ private static void exportResources(PluginClassLoaderDef newDef, ClassloaderBuilder builder,
+ Collection allPlugins) {
// export the resources to all other plugins
- builder.setExportMask(def.getBasePluginKey(), def.getExportMask());
+ builder.setExportMask(newDef.getBasePluginKey(), newDef.getExportMask().build());
for (PluginClassLoaderDef other : allPlugins) {
- if (!other.getBasePluginKey().equals(def.getBasePluginKey())) {
- builder.addSibling(def.getBasePluginKey(), other.getBasePluginKey(), new Mask());
+ if (!other.getBasePluginKey().equals(newDef.getBasePluginKey())) {
+ builder.addSibling(newDef.getBasePluginKey(), other.getBasePluginKey(), Mask.ALL);
}
}
}
@@ -121,29 +128,30 @@ public class PluginClassloaderFactory {
* a transitive dependency of sonar-plugin-api
*/
private static Mask apiMask() {
- return new Mask()
- .addInclusion("org/sonar/api/")
- .addInclusion("org/sonar/check/")
- .addInclusion("org/codehaus/stax2/")
- .addInclusion("org/codehaus/staxmate/")
- .addInclusion("com/ctc/wstx/")
- .addInclusion("org/slf4j/")
+ return Mask.builder()
+ .include("org/sonar/api/",
+ "org/sonar/check/",
+ "org/codehaus/stax2/",
+ "org/codehaus/staxmate/",
+ "com/ctc/wstx/",
+ "org/slf4j/",
- // SLF4J bridges. Do not let plugins re-initialize and configure their logging system
- .addInclusion("org/apache/commons/logging/")
- .addInclusion("org/apache/log4j/")
- .addInclusion("ch/qos/logback/")
+ // SLF4J bridges. Do not let plugins re-initialize and configure their logging system
+ "org/apache/commons/logging/",
+ "org/apache/log4j/",
+ "ch/qos/logback/",
- // Exposed by org.sonar.api.server.authentication.IdentityProvider
- .addInclusion("javax/servlet/")
+ // Exposed by org.sonar.api.server.authentication.IdentityProvider
+ "javax/servlet/",
- // required for some internal SonarSource plugins (billing, orchestrator, ...)
- .addInclusion("org/sonar/server/platform/")
+ // required for some internal SonarSource plugins (billing, orchestrator, ...)
+ "org/sonar/server/platform/",
- // required for commercial plugins at SonarSource
- .addInclusion("com/sonarsource/plugins/license/api/")
+ // required for commercial plugins at SonarSource
+ "com/sonarsource/plugins/license/api/")
// API exclusions
- .addExclusion("org/sonar/api/internal/");
+ .exclude("org/sonar/api/internal/")
+ .build();
}
}
diff --git a/sonar-core/src/test/java/org/sonar/classloader/ClassloaderBuilderTest.java b/sonar-core/src/test/java/org/sonar/classloader/ClassloaderBuilderTest.java
new file mode 100644
index 00000000000..93945be52fd
--- /dev/null
+++ b/sonar-core/src/test/java/org/sonar/classloader/ClassloaderBuilderTest.java
@@ -0,0 +1,787 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.io.File;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.IOUtils;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+
+public class ClassloaderBuilderTest {
+
+ ClassloaderBuilder sut = new ClassloaderBuilder();
+
+ @Test
+ public void minimal_system_classloader() throws Exception {
+ // create a classloader based on system classloader
+ // -> access only to JRE
+ Map classloaders = sut.newClassloader("example").build();
+
+ assertThat(classloaders).hasSize(1);
+ ClassLoader classloader = classloaders.get("example");
+ assertThat(classloader).hasToString("ClassRealm{example}");
+ assertThat(canLoadClass(classloader, HashMap.class.getName())).isTrue();
+ assertThat(canLoadClass(classloader, Test.class.getName())).isFalse();
+ assertThat(canLoadClass(classloader, "A")).isFalse();
+ assertThat(canLoadResource(classloader, "a.txt")).isFalse();
+ }
+
+ @Test
+ public void previous_classloader_not_returned_again() throws Exception {
+ Map classloaders1 = sut.newClassloader("example1").build();
+ Map classloaders2 = new ClassloaderBuilder(classloaders1.values())
+ .newClassloader("example2").build();
+
+ assertThat(classloaders2).containsOnlyKeys("example2");
+ }
+
+ @Test
+ public void fail_if_setting_attribute_to_previously_loaded_classloader() throws Exception {
+ Map classloaders1 = sut.newClassloader("example1").build();
+ ClassloaderBuilder builder = new ClassloaderBuilder(classloaders1.values())
+ .newClassloader("example2");
+
+ try {
+ builder.setMask("example1", Mask.ALL);
+ fail();
+ } catch (IllegalStateException e) {
+ // ok
+ }
+ }
+
+ /**
+ * Classloader based on another one (the junit env in this example). No parent-child hierarchy.
+ */
+ @Test
+ public void base_classloader() throws Exception {
+ //
+ Map classloaders = sut.newClassloader("example", getClass().getClassLoader()).build();
+
+ assertThat(classloaders).hasSize(1);
+ ClassLoader classloader = classloaders.get("example");
+ assertThat(canLoadClass(classloader, HashMap.class.getName())).isTrue();
+ assertThat(canLoadClass(classloader, Test.class.getName())).isTrue();
+ assertThat(canLoadClass(classloader, "A")).isFalse();
+ assertThat(canLoadResource(classloader, "a.txt")).isFalse();
+ }
+
+ @Test
+ public void classloader_constituents() throws Exception {
+ Map classloaders = sut
+ .newClassloader("the-cl")
+ .addURL("the-cl", new File("tester/a.jar").toURL())
+ .addURL("the-cl", new File("tester/b.jar").toURL())
+ .build();
+
+ assertThat(classloaders).hasSize(1);
+ ClassLoader self = classloaders.get("the-cl");
+ assertThat(canLoadClass(self, "A")).isTrue();
+ assertThat(canLoadResource(self, "a.txt")).isTrue();
+ assertThat(canLoadClass(self, "B")).isTrue();
+ assertThat(canLoadResource(self, "b.txt")).isTrue();
+ assertThat(canLoadClass(self, "C")).isFalse();
+ assertThat(canLoadResource(self, "c.txt")).isFalse();
+ }
+
+ /**
+ * Parent -> child -> grand-child classloaders. Default order strategy is parent-first
+ */
+ @Test
+ public void parent_child_relation() throws Exception {
+ // parent contains class A -> access to only A
+ // child contains class B -> access to A and B
+ // grand-child contains class C -> access to A, B and C
+ Map classloaders = sut
+ .newClassloader("the-parent")
+ .addURL("the-parent", new File("tester/a.jar").toURL())
+
+ // order of declaration is not important -> declare grand-child before child
+ .newClassloader("the-grand-child")
+ .addURL("the-grand-child", new File("tester/c.jar").toURL())
+ .setParent("the-grand-child", "the-child", Mask.ALL)
+
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/b.jar").toURL())
+ .setParent("the-child", "the-parent", Mask.ALL)
+
+ .build();
+
+ assertThat(classloaders).hasSize(3);
+
+ ClassLoader parent = classloaders.get("the-parent");
+ assertThat(canLoadClass(parent, "A")).isTrue();
+ assertThat(canLoadClass(parent, "B")).isFalse();
+ assertThat(canLoadClass(parent, "C")).isFalse();
+ assertThat(canLoadResource(parent, "a.txt")).isTrue();
+ assertThat(canLoadResource(parent, "b.txt")).isFalse();
+ assertThat(canLoadResource(parent, "c.txt")).isFalse();
+
+ ClassLoader child = classloaders.get("the-child");
+ assertThat(canLoadClass(child, "A")).isTrue();
+ assertThat(canLoadClass(child, "B")).isTrue();
+ assertThat(canLoadClass(child, "C")).isFalse();
+ assertThat(canLoadResource(child, "a.txt")).isTrue();
+ assertThat(canLoadResource(child, "b.txt")).isTrue();
+ assertThat(canLoadResource(child, "c.txt")).isFalse();
+
+ ClassLoader grandChild = classloaders.get("the-grand-child");
+ assertThat(canLoadClass(grandChild, "A")).isTrue();
+ assertThat(canLoadClass(grandChild, "B")).isTrue();
+ assertThat(canLoadClass(grandChild, "C")).isTrue();
+ assertThat(canLoadResource(grandChild, "a.txt")).isTrue();
+ assertThat(canLoadResource(grandChild, "b.txt")).isTrue();
+ assertThat(canLoadResource(grandChild, "c.txt")).isTrue();
+ }
+
+ /**
+ * Parent classloader can be created outside {@link ClassloaderBuilder}.
+ * Default ordering strategy is parent-first.
+ */
+ @Test
+ public void existing_parent() throws Exception {
+ // parent contains JUnit
+ // child contains class A -> access to A and JUnit
+ ClassLoader parent = getClass().getClassLoader();
+ Map newClassloaders = sut
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/a.jar").toURL())
+ .setParent("the-child", parent, Mask.ALL)
+ .build();
+
+ assertThat(newClassloaders).hasSize(1);
+ assertThat(canLoadClass(parent, Test.class.getName())).isTrue();
+ assertThat(canLoadClass(parent, "A")).isFalse();
+ ClassLoader child = newClassloaders.get("the-child");
+ assertThat(canLoadClass(child, Test.class.getName())).isTrue();
+ assertThat(canLoadClass(child, "A")).isTrue();
+ }
+
+ @Test
+ public void parent_first_ordering() throws Exception {
+ // parent contains version 1 of A
+ // child contains version 2 of A
+ Map newClassloaders = sut
+ .newClassloader("the-parent")
+ .addURL("the-parent", new File("tester/a.jar").toURL())
+
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/a_v2.jar").toURL())
+ .setParent("the-child", "the-parent", Mask.ALL)
+ .build();
+
+ ClassLoader parent = newClassloaders.get("the-parent");
+ assertThat(canLoadMethod(parent, "A", "version1")).isTrue();
+ assertThat(canLoadMethod(parent, "A", "version2")).isFalse();
+ assertThat(IOUtils.toString(parent.getResource("a.txt"))).startsWith("version 1 of a.txt");
+
+ ClassLoader child = newClassloaders.get("the-child");
+ assertThat(canLoadMethod(child, "A", "version1")).isTrue();
+ assertThat(canLoadMethod(child, "A", "version2")).isFalse();
+ assertThat(IOUtils.toString(child.getResource("a.txt"))).startsWith("version 1 of a.txt");
+ }
+
+ /**
+ * - parent contains B and version 1 of A
+ * - child contains version 2 of A -> sees B and version 2 of A
+ */
+ @Test
+ public void self_first_ordering() throws Exception {
+
+ Map newClassloaders = sut
+ .newClassloader("the-parent")
+ .addURL("the-parent", new File("tester/a.jar").toURL())
+ .addURL("the-parent", new File("tester/b.jar").toURL())
+
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/a_v2.jar").toURL())
+ .setParent("the-child", "the-parent", Mask.ALL)
+ .setLoadingOrder("the-child", ClassloaderBuilder.LoadingOrder.SELF_FIRST)
+ .build();
+
+ ClassLoader parent = newClassloaders.get("the-parent");
+ assertThat(canLoadMethod(parent, "A", "version1")).isTrue();
+ assertThat(canLoadMethod(parent, "A", "version2")).isFalse();
+ assertThat(IOUtils.toString(parent.getResource("a.txt"))).startsWith("version 1 of a.txt");
+
+ ClassLoader child = newClassloaders.get("the-child");
+ assertThat(canLoadClass(child, "B")).isTrue();
+ assertThat(canLoadMethod(child, "A", "version1")).isFalse();
+ assertThat(canLoadMethod(child, "A", "version2")).isTrue();
+ assertThat(IOUtils.toString(child.getResource("a.txt"))).startsWith("version 2 of a.txt");
+ assertThat(Collections.list(child.getResources("b.txt"))).hasSize(1);
+ ArrayList resources = Collections.list(child.getResources("a.txt"));
+ assertThat(resources).hasSize(2);
+ assertThat(IOUtils.toString(resources.get(0))).startsWith("version 2 of a.txt");
+ assertThat(IOUtils.toString(resources.get(1))).startsWith("version 1 of a.txt");
+ }
+
+ /**
+ * Prevent a classloader from loading some resources that are available in its own constituents.
+ */
+ @Test
+ public void self_mask() throws Exception {
+ Map classloaders = sut
+ .newClassloader("the-cl")
+ .addURL("the-cl", new File("tester/a.jar").toURL())
+ .addURL("the-cl", new File("tester/b.jar").toURL())
+ .setMask("the-cl", Mask.builder().exclude("A.class", "a.txt").build())
+ .build();
+
+ ClassLoader cl = classloaders.get("the-cl");
+ assertThat(canLoadClass(cl, "A")).isFalse();
+ assertThat(canLoadClass(cl, "B")).isTrue();
+ assertThat(canLoadResource(cl, "a.txt")).isFalse();
+ assertThat(canLoadResource(cl, "b.txt")).isTrue();
+ }
+
+ /**
+ * Partial inheritance of parent classloader
+ */
+ @Test
+ public void parent_mask() throws Exception {
+ Map newClassloaders = sut
+ .newClassloader("the-parent")
+ .addURL("the-parent", new File("tester/a.jar").toURL())
+ .addURL("the-parent", new File("tester/b.jar").toURL())
+
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/c.jar").toURL())
+ .setParent("the-child", "the-parent", Mask.builder().exclude("A.class", "a.txt").build())
+ .build();
+
+ ClassLoader parent = newClassloaders.get("the-parent");
+ assertThat(canLoadClass(parent, "A")).isTrue();
+ assertThat(canLoadClass(parent, "B")).isTrue();
+ assertThat(canLoadClass(parent, "C")).isFalse();
+ assertThat(canLoadResource(parent, "a.txt")).isTrue();
+ assertThat(canLoadResource(parent, "b.txt")).isTrue();
+ assertThat(canLoadResource(parent, "c.txt")).isFalse();
+
+ ClassLoader child = newClassloaders.get("the-child");
+ assertThat(canLoadClass(child, "A")).isFalse();
+ assertThat(canLoadClass(child, "B")).isTrue();
+ assertThat(canLoadClass(child, "C")).isTrue();
+ assertThat(canLoadResource(child, "a.txt")).isFalse();
+ assertThat(canLoadResource(child, "b.txt")).isTrue();
+ assertThat(canLoadResource(child, "c.txt")).isTrue();
+ }
+
+ /**
+ * Parent classloader contains A and B, but exports only B to its children
+ */
+ @Test
+ public void export_mask() throws Exception {
+ Map newClassloaders = sut
+ .newClassloader("the-parent")
+ .addURL("the-parent", new File("tester/a.jar").toURL())
+ .addURL("the-parent", new File("tester/b.jar").toURL())
+ .setExportMask("the-parent", Mask.builder().exclude("A.class", "a.txt").build())
+
+ .newClassloader("the-child")
+ .setParent("the-child", "the-parent", Mask.ALL)
+ .build();
+
+ ClassLoader parent = newClassloaders.get("the-parent");
+ assertThat(canLoadClass(parent, "A")).isTrue();
+ assertThat(canLoadClass(parent, "B")).isTrue();
+ assertThat(canLoadResource(parent, "a.txt")).isTrue();
+ assertThat(canLoadResource(parent, "b.txt")).isTrue();
+
+ ClassLoader child = newClassloaders.get("the-child");
+ assertThat(canLoadClass(child, "A")).isFalse();
+ assertThat(canLoadClass(child, "B")).isTrue();
+ assertThat(canLoadResource(child, "a.txt")).isFalse();
+ assertThat(canLoadResource(child, "b.txt")).isTrue();
+ }
+
+ /**
+ * Parent classloader contains A, B and C, but exports only B and C to its children.
+ * On the other side child classloader excludes B from its parent, so it benefits
+ * only from C
+ */
+ @Test
+ public void mix_of_import_and_export_masks() throws Exception {
+ Map newClassloaders = sut
+ .newClassloader("the-parent")
+ .addURL("the-parent", new File("tester/a.jar").toURL())
+ .addURL("the-parent", new File("tester/b.jar").toURL())
+ .addURL("the-parent", new File("tester/c.jar").toURL())
+ .setExportMask("the-parent", Mask.builder().exclude("A.class", "a.txt").build())
+
+ .newClassloader("the-child")
+ .setParent("the-child", "the-parent", Mask.builder().exclude("B.class", "b.txt").build())
+ .build();
+
+ ClassLoader parent = newClassloaders.get("the-parent");
+ assertThat(canLoadClass(parent, "A")).isTrue();
+ assertThat(canLoadClass(parent, "B")).isTrue();
+ assertThat(canLoadClass(parent, "C")).isTrue();
+ assertThat(canLoadResource(parent, "a.txt")).isTrue();
+ assertThat(canLoadResource(parent, "b.txt")).isTrue();
+ assertThat(canLoadResource(parent, "c.txt")).isTrue();
+
+ ClassLoader child = newClassloaders.get("the-child");
+ assertThat(canLoadClass(child, "A")).isFalse();
+ assertThat(canLoadClass(child, "B")).isFalse();
+ assertThat(canLoadClass(child, "C")).isTrue();
+ assertThat(canLoadResource(child, "a.txt")).isFalse();
+ assertThat(canLoadResource(child, "b.txt")).isFalse();
+ assertThat(canLoadResource(child, "c.txt")).isTrue();
+ }
+
+ @Test
+ public void fail_to_create_the_same_classloader_twice() throws Exception {
+ sut.newClassloader("the-cl");
+ try {
+ sut.newClassloader("the-cl");
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("The classloader 'the-cl' already exists. Can not create it twice.");
+ }
+ }
+
+ @Test
+ public void fail_to_create_the_same_previous_classloader_twice() throws Exception {
+ Map classloaders1 = sut.newClassloader("the-cl").build();
+ ClassloaderBuilder classloaderBuilder = new ClassloaderBuilder(classloaders1.values());
+ try {
+ classloaderBuilder.newClassloader("the-cl");
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("The classloader 'the-cl' already exists in the list of previously created classloaders. " +
+ "Can not create it twice.");
+ }
+ }
+
+ @Test
+ public void fail_if_missing_declaration() throws Exception {
+ sut.newClassloader("the-cl");
+ sut.setParent("the-cl", "missing", Mask.ALL);
+ try {
+ sut.build();
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("The classloader 'missing' does not exist");
+ }
+ }
+
+ @Test
+ public void sibling() throws Exception {
+ // sibling1 contains A
+ // sibling2 contains B
+ // child contains C -> see A, B and C
+ Map newClassloaders = sut
+ .newClassloader("sib1")
+ .addURL("sib1", new File("tester/a.jar").toURL())
+
+ .newClassloader("sib2")
+ .addURL("sib2", new File("tester/b.jar").toURL())
+
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/c.jar").toURL())
+ .addSibling("the-child", "sib1", Mask.ALL)
+ .addSibling("the-child", "sib2", Mask.ALL)
+ .build();
+
+ ClassLoader sib1 = newClassloaders.get("sib1");
+ assertThat(canLoadClass(sib1, "A")).isTrue();
+ assertThat(canLoadClass(sib1, "B")).isFalse();
+ assertThat(canLoadClass(sib1, "C")).isFalse();
+ assertThat(canLoadResource(sib1, "a.txt")).isTrue();
+ assertThat(canLoadResource(sib1, "b.txt")).isFalse();
+ assertThat(canLoadResource(sib1, "c.txt")).isFalse();
+
+ ClassLoader sib2 = newClassloaders.get("sib2");
+ assertThat(canLoadClass(sib2, "A")).isFalse();
+ assertThat(canLoadClass(sib2, "B")).isTrue();
+ assertThat(canLoadClass(sib2, "C")).isFalse();
+ assertThat(canLoadResource(sib2, "a.txt")).isFalse();
+ assertThat(canLoadResource(sib2, "b.txt")).isTrue();
+ assertThat(canLoadResource(sib2, "c.txt")).isFalse();
+
+ ClassLoader child = newClassloaders.get("the-child");
+ assertThat(canLoadClass(child, "A")).isTrue();
+ assertThat(canLoadClass(child, "B")).isTrue();
+ assertThat(canLoadClass(child, "C")).isTrue();
+ assertThat(canLoadResource(child, "a.txt")).isTrue();
+ assertThat(canLoadResource(child, "b.txt")).isTrue();
+ assertThat(canLoadResource(child, "c.txt")).isTrue();
+ }
+
+ /**
+ * Sibling classloader can be created outside {@link ClassloaderBuilder}.
+ */
+ @Test
+ public void existing_sibling() throws Exception {
+ // sibling1 contains JUnit
+ // child contains A -> see JUnit and A
+ Map newClassloaders = sut
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/a.jar").toURL())
+ .addSibling("the-child", getClass().getClassLoader(), Mask.ALL)
+ .build();
+
+ ClassLoader child = newClassloaders.get("the-child");
+ assertThat(canLoadClass(child, Test.class.getName())).isTrue();
+ assertThat(canLoadClass(child, "A")).isTrue();
+ }
+
+ /**
+ * - sibling contains A and B
+ * - child contains C and excludes A from sibling -> sees only B and C
+ */
+ @Test
+ public void sibling_mask() throws Exception {
+ Map newClassloaders = sut
+ .newClassloader("sib1")
+ .addURL("sib1", new File("tester/a.jar").toURL())
+ .addURL("sib1", new File("tester/b.jar").toURL())
+
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/c.jar").toURL())
+ .addSibling("the-child", "sib1", Mask.builder().exclude("A.class", "a.txt").build())
+ .build();
+
+ ClassLoader sib1 = newClassloaders.get("sib1");
+ assertThat(canLoadClass(sib1, "A")).isTrue();
+ assertThat(canLoadClass(sib1, "B")).isTrue();
+ assertThat(canLoadResource(sib1, "a.txt")).isTrue();
+ assertThat(canLoadResource(sib1, "b.txt")).isTrue();
+
+ ClassLoader child = newClassloaders.get("the-child");
+ assertThat(canLoadClass(child, "A")).isFalse();
+ assertThat(canLoadClass(child, "B")).isTrue();
+ assertThat(canLoadClass(child, "C")).isTrue();
+ assertThat(canLoadResource(child, "a.txt")).isFalse();
+ assertThat(canLoadResource(child, "b.txt")).isTrue();
+ assertThat(canLoadResource(child, "c.txt")).isTrue();
+ assertThat(Collections.list(child.getResources("a.txt"))).isEmpty();
+ assertThat(Collections.list(child.getResources("b.txt"))).hasSize(1);
+ assertThat(Collections.list(child.getResources("c.txt"))).hasSize(1);
+ }
+
+ /**
+ * - sibling contains A and B but exports only B
+ * - child contains C -> sees only B and C
+ */
+ @Test
+ public void sibling_export_mask() throws Exception {
+ Map newClassloaders = sut
+ .newClassloader("sib1")
+ .addURL("sib1", new File("tester/a.jar").toURL())
+ .addURL("sib1", new File("tester/b.jar").toURL())
+ .setExportMask("sib1", Mask.builder().include("B.class", "b.txt").build())
+
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/c.jar").toURL())
+ .addSibling("the-child", "sib1", Mask.ALL)
+ .build();
+
+ ClassLoader sib1 = newClassloaders.get("sib1");
+ assertThat(canLoadClass(sib1, "A")).isTrue();
+ assertThat(canLoadClass(sib1, "B")).isTrue();
+ assertThat(canLoadResource(sib1, "a.txt")).isTrue();
+ assertThat(canLoadResource(sib1, "b.txt")).isTrue();
+
+ ClassLoader child = newClassloaders.get("the-child");
+ assertThat(canLoadClass(child, "A")).isFalse();
+ assertThat(canLoadClass(child, "B")).isTrue();
+ assertThat(canLoadClass(child, "C")).isTrue();
+ assertThat(canLoadResource(child, "a.txt")).isFalse();
+ assertThat(canLoadResource(child, "b.txt")).isTrue();
+ assertThat(canLoadResource(child, "c.txt")).isTrue();
+ assertThat(Collections.list(child.getResources("a.txt"))).isEmpty();
+ assertThat(Collections.list(child.getResources("b.txt"))).hasSize(1);
+ assertThat(Collections.list(child.getResources("c.txt"))).hasSize(1);
+ }
+
+ /**
+ * Sibling classloader is loaded previously self:
+ * - sibling has version 1 of A
+ * - self has version 2 of A -> sees version 1
+ */
+ @Test
+ public void sibling_prevails_over_self() throws Exception {
+ Map newClassloaders = sut
+ .newClassloader("sib")
+ .addURL("sib", new File("tester/a.jar").toURL())
+
+ .newClassloader("self")
+ .addURL("self", new File("tester/a_v2.jar").toURL())
+ .addSibling("self", "sib", Mask.ALL)
+ .build();
+
+ ClassLoader sib = newClassloaders.get("sib");
+ assertThat(canLoadMethod(sib, "A", "version1")).isTrue();
+ assertThat(canLoadMethod(sib, "A", "version2")).isFalse();
+ assertThat(IOUtils.toString(sib.getResource("a.txt"))).startsWith("version 1 of a.txt");
+
+ ClassLoader self = newClassloaders.get("self");
+ assertThat(canLoadMethod(self, "A", "version1")).isTrue();
+ assertThat(canLoadMethod(self, "A", "version2")).isFalse();
+ assertThat(IOUtils.toString(self.getResource("a.txt"))).startsWith("version 1 of a.txt");
+ }
+
+ /**
+ * Sibling classloader is always loaded previously self, even if self-first strategy:
+ * - sibling has version 1 of A
+ * - self has version 2 of A -> sees version 1
+ */
+ @Test
+ public void sibling_prevails_over_self_even_if_self_first() throws Exception {
+ Map newClassloaders = sut
+ .newClassloader("sib")
+ .addURL("sib", new File("tester/a.jar").toURL())
+
+ .newClassloader("self")
+ .addURL("self", new File("tester/a_v2.jar").toURL())
+ .addSibling("self", "sib", Mask.ALL)
+ .setLoadingOrder("self", ClassloaderBuilder.LoadingOrder.SELF_FIRST)
+ .build();
+
+ ClassLoader sib = newClassloaders.get("sib");
+ assertThat(canLoadMethod(sib, "A", "version1")).isTrue();
+ assertThat(canLoadMethod(sib, "A", "version2")).isFalse();
+ assertThat(IOUtils.toString(sib.getResource("a.txt"))).startsWith("version 1 of a.txt");
+
+ ClassLoader self = newClassloaders.get("self");
+ assertThat(canLoadMethod(self, "A", "version1")).isTrue();
+ assertThat(canLoadMethod(self, "A", "version2")).isFalse();
+ assertThat(IOUtils.toString(self.getResource("a.txt"))).startsWith("version 1 of a.txt");
+ }
+
+ /**
+ * https://github.com/SonarSource/sonar-classloader/issues/1
+ */
+ @Test
+ public void cycle_of_siblings() throws Exception {
+ Map newClassloaders = sut
+ .newClassloader("a")
+ .addURL("a", new File("tester/a.jar").toURL())
+
+ .newClassloader("b")
+ .addURL("b", new File("tester/b.jar").toURL())
+ .addSibling("a", "b", Mask.builder().include("B.class", "b.txt").build())
+ .addSibling("b", "a", Mask.builder().include("A.class", "a.txt").build())
+ .build();
+
+ ClassLoader a = newClassloaders.get("a");
+ assertThat(canLoadClass(a, "A")).isTrue();
+ assertThat(canLoadClass(a, "B")).isTrue();
+ assertThat(IOUtils.toString(a.getResource("a.txt"))).isNotEmpty();
+ assertThat(IOUtils.toString(a.getResource("b.txt"))).isNotEmpty();
+
+ ClassLoader b = newClassloaders.get("b");
+ assertThat(canLoadClass(b, "A")).isTrue();
+ assertThat(canLoadClass(b, "B")).isTrue();
+ assertThat(IOUtils.toString(b.getResource("a.txt"))).isNotEmpty();
+ assertThat(IOUtils.toString(b.getResource("b.txt"))).isNotEmpty();
+ }
+
+ @Test
+ public void getResources_from_parent_and_siblings() throws Exception {
+ Map newClassloaders = sut
+ .newClassloader("the-parent")
+ .addURL("the-parent", new File("tester/a.jar").toURL())
+
+ .newClassloader("the-sib")
+ .addURL("the-sib", new File("tester/b.jar").toURL())
+
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/c.jar").toURL())
+ .setParent("the-child", "the-parent", Mask.ALL)
+ .addSibling("the-child", "the-sib", Mask.ALL)
+ .build();
+
+ ClassLoader parent = newClassloaders.get("the-parent");
+ assertThat(Collections.list(parent.getResources("a.txt"))).hasSize(1);
+ assertThat(Collections.list(parent.getResources("b.txt"))).isEmpty();
+ assertThat(Collections.list(parent.getResources("c.txt"))).isEmpty();
+
+ ClassLoader child = newClassloaders.get("the-child");
+ assertThat(Collections.list(child.getResources("a.txt"))).hasSize(1);
+ assertThat(Collections.list(child.getResources("b.txt"))).hasSize(1);
+ assertThat(Collections.list(child.getResources("c.txt"))).hasSize(1);
+ }
+
+ @Test
+ public void getResources_from_previously_loaded_parent() throws Exception {
+ Map classloaders1 = sut
+ .newClassloader("the-parent")
+ .addURL("the-parent", new File("tester/a.jar").toURL())
+ .build();
+
+
+ Map classloaders2 = new ClassloaderBuilder(classloaders1.values())
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/b.jar").toURL())
+ .setParent("the-child", "the-parent", Mask.ALL)
+ .build();
+
+ ClassLoader parent = classloaders1.get("the-parent");
+ assertThat(Collections.list(parent.getResources("a.txt"))).hasSize(1);
+ assertThat(Collections.list(parent.getResources("b.txt"))).isEmpty();
+
+ ClassLoader child = classloaders2.get("the-child");
+ assertThat(Collections.list(child.getResources("a.txt"))).hasSize(1);
+ assertThat(Collections.list(child.getResources("b.txt"))).hasSize(1);
+ }
+
+ @Test
+ public void getResources_from_previously_loaded_sibling_based_on_export_mask() throws Exception {
+ Map classloaders1 = sut
+ .newClassloader("the-sib")
+ .addURL("the-sib", new File("tester/a.jar").toURL())
+ .setExportMask("the-sib", Mask.builder().include("A.java").build())
+ .build();
+
+ Map classloaders2 = new ClassloaderBuilder(classloaders1.values())
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/b.jar").toURL())
+ .addSibling("the-child", "the-sib", Mask.ALL)
+ .build();
+
+ ClassLoader parent = classloaders1.get("the-sib");
+ assertThat(Collections.list(parent.getResources("a.txt"))).hasSize(1);
+ assertThat(Collections.list(parent.getResources("A.java"))).hasSize(1);
+ assertThat(Collections.list(parent.getResources("b.txt"))).isEmpty();
+
+ ClassLoader child = classloaders2.get("the-child");
+ assertThat(Collections.list(child.getResources("a.txt"))).isEmpty();
+ assertThat(Collections.list(parent.getResources("A.java"))).hasSize(1);
+ assertThat(Collections.list(child.getResources("b.txt"))).hasSize(1);
+ }
+
+ @Test
+ public void getResources_from_previously_loaded_sibling() throws Exception {
+ Map classloaders1 = sut
+ .newClassloader("the-sib")
+ .addURL("the-sib", new File("tester/a.jar").toURL())
+ .build();
+
+ Map classloaders2 = new ClassloaderBuilder(classloaders1.values())
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/b.jar").toURL())
+ .addSibling("the-child", "the-sib", Mask.ALL)
+ .build();
+
+ ClassLoader parent = classloaders1.get("the-sib");
+ assertThat(Collections.list(parent.getResources("a.txt"))).hasSize(1);
+ assertThat(Collections.list(parent.getResources("b.txt"))).isEmpty();
+
+ ClassLoader child = classloaders2.get("the-child");
+ assertThat(Collections.list(child.getResources("a.txt"))).hasSize(1);
+ assertThat(Collections.list(child.getResources("b.txt"))).hasSize(1);
+ }
+
+ @Test
+ public void getResources_multiple_versions_with_parent_first_strategy() throws Exception {
+ Map newClassloaders = sut
+ .newClassloader("the-parent")
+ .addURL("the-parent", new File("tester/a.jar").toURL())
+
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/a_v2.jar").toURL())
+ .setParent("the-child", "the-parent", Mask.ALL)
+ .build();
+
+ ClassLoader parent = newClassloaders.get("the-parent");
+ assertThat(Collections.list(parent.getResources("a.txt"))).hasSize(1);
+
+ ClassLoader child = newClassloaders.get("the-child");
+ List childResources = Collections.list(child.getResources("a.txt"));
+ assertThat(childResources).hasSize(2);
+ assertThat(IOUtils.toString(childResources.get(0))).startsWith("version 1 of a.txt");
+ assertThat(IOUtils.toString(childResources.get(1))).startsWith("version 2 of a.txt");
+ }
+
+ @Test
+ public void resource_not_found_in_parent_first_strategy() throws Exception {
+ Map newClassloaders = sut
+ .newClassloader("the-parent")
+ .addURL("the-parent", new File("tester/a.jar").toURL())
+
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/a_v2.jar").toURL())
+ .setParent("the-child", "the-parent", Mask.ALL)
+ .build();
+
+ ClassLoader parent = newClassloaders.get("the-child");
+ assertThat(parent.getResource("missing")).isNull();
+ try {
+ parent.loadClass("missing");
+ fail();
+ } catch (ClassNotFoundException e) {
+ // ok
+ }
+ }
+
+ @Test
+ public void resource_not_found_in_self_first_strategy() throws Exception {
+ Map newClassloaders = sut
+ .newClassloader("the-parent")
+ .addURL("the-parent", new File("tester/a.jar").toURL())
+
+ .newClassloader("the-child")
+ .addURL("the-child", new File("tester/a_v2.jar").toURL())
+ .setParent("the-child", "the-parent", Mask.ALL)
+ .setLoadingOrder("the-child", ClassloaderBuilder.LoadingOrder.SELF_FIRST)
+ .build();
+
+ ClassLoader parent = newClassloaders.get("the-child");
+ assertThat(parent.getResource("missing")).isNull();
+ try {
+ parent.loadClass("missing");
+ fail();
+ } catch (ClassNotFoundException e) {
+ // ok
+ }
+ }
+
+ private boolean canLoadClass(ClassLoader classloader, String classname) {
+ try {
+ classloader.loadClass(classname);
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+ private boolean canLoadMethod(ClassLoader classloader, String classname, String methodName) {
+ try {
+ Class clazz = classloader.loadClass(classname);
+ return clazz.getMethod(methodName) != null;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private boolean canLoadResource(ClassLoader classloader, String name) {
+ return classloader.getResource(name) != null;
+ }
+}
diff --git a/sonar-core/src/test/java/org/sonar/classloader/MaskTest.java b/sonar-core/src/test/java/org/sonar/classloader/MaskTest.java
new file mode 100644
index 00000000000..a12260ef480
--- /dev/null
+++ b/sonar-core/src/test/java/org/sonar/classloader/MaskTest.java
@@ -0,0 +1,170 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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 org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MaskTest {
+
+ @Test
+ public void ALL_accepts_everything() throws Exception {
+ assertThat(Mask.ALL.acceptClass("org.sonar.Bar")).isTrue();
+ assertThat(Mask.ALL.acceptClass("Bar")).isTrue();
+ }
+
+ @Test
+ public void NONE_accepts_nothing() throws Exception {
+ assertThat(Mask.NONE.acceptClass("org.sonar.Bar")).isFalse();
+ assertThat(Mask.NONE.acceptClass("Bar")).isFalse();
+ }
+
+ @Test
+ public void include_class() throws Exception {
+ Mask mask = Mask.builder().include("org/sonar/Bar.class").build();
+ assertThat(mask.acceptClass("org.sonar.Bar")).isTrue();
+ assertThat(mask.acceptClass("org.sonar.qube.Bar")).isFalse();
+ assertThat(mask.acceptClass("org.sonar.Foo")).isFalse();
+ assertThat(mask.acceptClass("Bar")).isFalse();
+ }
+
+ @Test
+ public void include_class_of_root_package() throws Exception {
+ Mask mask = Mask.builder().include("Bar.class").build();
+ assertThat(mask.acceptClass("Bar")).isTrue();
+ assertThat(mask.acceptClass("Foo")).isFalse();
+ }
+
+ @Test
+ public void include_resource() throws Exception {
+ Mask mask = Mask.builder().include("org/sonar/Bar.class").build();
+ assertThat(mask.acceptResource("org/sonar/Bar.class")).isTrue();
+ assertThat(mask.acceptResource("org/sonar/qube/Bar.class")).isFalse();
+ assertThat(mask.acceptResource("org/sonar/Foo.class")).isFalse();
+ assertThat(mask.acceptResource("Bar.class")).isFalse();
+ }
+
+ @Test
+ public void include_package() throws Exception {
+ Mask mask = Mask.builder().include("org/sonar/", "org/other/").build();
+ assertThat(mask.acceptClass("Foo")).isFalse();
+ assertThat(mask.acceptClass("org.sonar.Bar")).isTrue();
+ assertThat(mask.acceptClass("org.sonarqube.Foo")).isFalse();
+ assertThat(mask.acceptClass("org.sonar.qube.Foo")).isTrue();
+ assertThat(mask.acceptClass("Bar")).isFalse();
+ }
+
+ @Test
+ public void exclude_class() throws Exception {
+ Mask mask = Mask.builder().exclude("org/sonar/Bar.class").build();
+ assertThat(mask.acceptClass("org.sonar.Bar")).isFalse();
+ assertThat(mask.acceptClass("org.sonar.qube.Bar")).isTrue();
+ assertThat(mask.acceptClass("org.sonar.Foo")).isTrue();
+ assertThat(mask.acceptClass("Bar")).isTrue();
+ }
+
+ @Test
+ public void exclude_package() throws Exception {
+ Mask mask = Mask.builder().exclude("org/sonar/", "org/other/").build();
+ assertThat(mask.acceptClass("Foo")).isTrue();
+ assertThat(mask.acceptClass("org.sonar.Bar")).isFalse();
+ assertThat(mask.acceptClass("org.sonarqube.Foo")).isTrue();
+ assertThat(mask.acceptClass("org.sonar.qube.Foo")).isFalse();
+ assertThat(mask.acceptClass("Bar")).isTrue();
+ }
+
+ @Test
+ public void exclusion_is_subset_of_inclusion() throws Exception {
+ Mask mask = Mask.builder()
+ .include("org/sonar/")
+ .exclude("org/sonar/qube/")
+ .build();
+ assertThat(mask.acceptClass("org.sonar.Foo")).isTrue();
+ assertThat(mask.acceptClass("org.sonar.Qube")).isTrue();
+ assertThat(mask.acceptClass("org.sonar.qube.Foo")).isFalse();
+ }
+
+ @Test
+ public void inclusion_is_subset_of_exclusion() throws Exception {
+ Mask mask = Mask.builder()
+ .include("org/sonar/qube/")
+ .exclude("org/sonar/")
+ .build();
+ assertThat(mask.acceptClass("org.sonar.Foo")).isFalse();
+ assertThat(mask.acceptClass("org.sonar.Qube")).isFalse();
+ assertThat(mask.acceptClass("org.sonar.qube.Foo")).isFalse();
+ }
+
+ @Test
+ public void exclude_everything() throws Exception {
+ Mask mask = Mask.builder().exclude("/").build();
+ assertThat(mask.acceptClass("org.sonar.Foo")).isFalse();
+ assertThat(mask.acceptClass("Foo")).isFalse();
+ assertThat(mask.acceptResource("config.xml")).isFalse();
+ assertThat(mask.acceptResource("org/config.xml")).isFalse();
+ }
+
+ @Test
+ public void include_everything() throws Exception {
+ Mask mask = Mask.builder().include("/").build();
+ assertThat(mask.acceptClass("org.sonar.Foo")).isTrue();
+ assertThat(mask.acceptClass("Foo")).isTrue();
+ assertThat(mask.acceptResource("config.xml")).isTrue();
+ assertThat(mask.acceptResource("org/config.xml")).isTrue();
+ }
+
+ @Test
+ public void merge_with_ALL() throws Exception {
+ Mask mask = Mask.builder()
+ .include("org/foo/")
+ .exclude("org/bar/")
+ .merge(Mask.ALL)
+ .build();
+
+ assertThat(mask.getInclusions()).containsOnly("org/foo/");
+ assertThat(mask.getExclusions()).containsOnly("org/bar/");
+ }
+
+ @Test
+ public void merge_exclusions() throws Exception {
+ Mask with = Mask.builder().exclude("bar/").build();
+ Mask mask = Mask.builder().exclude("org/foo/").merge(with).build();
+
+ assertThat(mask.getExclusions()).containsOnly("org/foo/", "bar/");
+ }
+
+ @Test
+ public void should_not_merge_disjoined_inclusions() throws Exception {
+ Mask with = Mask.builder().include("org/bar/").build();
+ Mask mask = Mask.builder().include("org/foo/").merge(with).build();
+
+ assertThat(mask.getInclusions()).isEmpty();
+ // TODO does that mean that merge result accepts everything ?
+ }
+
+ @Test
+ public void merge_inclusions() throws Exception {
+ Mask with = Mask.builder().include("org/foo/sub/", "org/bar/").build();
+ Mask mask = Mask.builder().include("org/foo/", "org/bar/sub/").merge(with).build();
+
+ assertThat(mask.getInclusions()).containsOnly("org/foo/sub/", "org/bar/sub/");
+ }
+}
diff --git a/sonar-core/src/test/java/org/sonar/core/platform/PluginClassloaderFactoryTest.java b/sonar-core/src/test/java/org/sonar/core/platform/PluginClassloaderFactoryTest.java
index d5a74e13704..def7574bdc5 100644
--- a/sonar-core/src/test/java/org/sonar/core/platform/PluginClassloaderFactoryTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/platform/PluginClassloaderFactoryTest.java
@@ -21,12 +21,14 @@ package org.sonar.core.platform;
import com.sonarsource.plugins.license.api.FooBar;
import java.io.File;
+import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.junit.Test;
import org.sonar.api.server.rule.RulesDefinition;
import static java.util.Arrays.asList;
+import static java.util.Collections.emptyMap;
import static org.assertj.core.api.Assertions.assertThat;
public class PluginClassloaderFactoryTest {
@@ -41,7 +43,7 @@ public class PluginClassloaderFactoryTest {
@Test
public void create_isolated_classloader() {
PluginClassLoaderDef def = basePluginDef();
- Map map = factory.create(asList(def));
+ Map map = factory.create(emptyMap(), asList(def));
assertThat(map).containsOnlyKeys(def);
ClassLoader classLoader = map.get(def);
@@ -60,7 +62,7 @@ public class PluginClassloaderFactoryTest {
public void classloader_exports_resources_to_other_classloaders() {
PluginClassLoaderDef baseDef = basePluginDef();
PluginClassLoaderDef dependentDef = dependentPluginDef();
- Map map = factory.create(asList(baseDef, dependentDef));
+ Map map = factory.create(emptyMap(), asList(baseDef, dependentDef));
ClassLoader baseClassloader = map.get(baseDef);
ClassLoader dependentClassloader = map.get(dependentDef);
@@ -74,10 +76,26 @@ public class PluginClassloaderFactoryTest {
assertThat(canLoadClass(baseClassloader, BASE_PLUGIN_CLASSNAME)).isTrue();
}
+ @Test
+ public void classloader_exports_resources_to_other_classloaders_loaded_later() {
+ PluginClassLoaderDef baseDef = basePluginDef();
+ Map map1 = factory.create(emptyMap(), List.of(baseDef));
+
+ PluginClassLoaderDef dependentDef = dependentPluginDef();
+ Map map2 = factory.create(map1, List.of(dependentDef));
+
+ ClassLoader dependentClassloader = map2.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();
+ }
+
@Test
public void classloader_exposes_license_api_from_main_classloader() {
PluginClassLoaderDef def = basePluginDef();
- Map map = factory.create(asList(def));
+ Map map = factory.create(emptyMap(), asList(def));
assertThat(map).containsOnlyKeys(def);
ClassLoader classLoader = map.get(def);
@@ -88,7 +106,7 @@ public class PluginClassloaderFactoryTest {
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.getExportMask().include("org/sonar/plugins/base/api/");
def.addFiles(asList(fakePluginJar("base-plugin/target/base-plugin-0.1-SNAPSHOT.jar")));
return def;
}
@@ -96,7 +114,7 @@ public class PluginClassloaderFactoryTest {
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.getExportMask().include("org/sonar/plugins/dependent/api/");
def.addFiles(asList(fakePluginJar("dependent-plugin/target/dependent-plugin-0.1-SNAPSHOT.jar")));
return def;
}
diff --git a/sonar-core/tester/a.jar b/sonar-core/tester/a.jar
new file mode 100644
index 0000000000000000000000000000000000000000..b2919e07c0e1ca178ef79f2438072274dc6a53d4
GIT binary patch
literal 894
zcmWIWW@Zs#;Nak3cps7I#()Gk8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1gW)3f?%M2`96|Lqm=WL+9_C}qF?TxLC
zQztfFoH)^u;mGy#o@czTfA>G-<*Sz%kdnX@#mVHu)1%PpFp2lXWY$YP4zohDt(hOY
zDKt-Pl=axu$m$p*Q?PVWf*_kus*b}X;T$7zuorD5-gmD9df*X?7vJuPL5UT!eDb4
znM4>+6BI19fD#lcfTs{p@(S=q)ruU)pyY%Awm>FaD^lV@Hvu^aL1_U24gs0al!2@X
z6uQWv0t#IOSjq^}1PW_pJ)rPH4gpYjApkqj^We}6@MZ;@#>&9TAPF?c9~>$G)~(PA
literal 0
HcmV?d00001
diff --git a/sonar-core/tester/a/A.class b/sonar-core/tester/a/A.class
new file mode 100644
index 0000000000000000000000000000000000000000..1b88100eaade08c87837ee20c00057a0f557ae41
GIT binary patch
literal 226
zcmZ8aI|{;35S-257(cCrg`HX$#8!kLScw*j{Y!kv3o(J1Sg$M9Z;wFWKTg>cy
zVBVkS3%~+h7dqNDJR2Q?wN$ApRs?-KIT4I?c9w+RR;6-RUPGB5MVPSTR5HJ*ES(eF
zLssUI+^E{Of>|so;lY4Opn%Y?wV#Oe;_t&)Mg>ODc(OhXJOPu@Vl`wYW^?)g-T5Kd
Xj8;~
literal 0
HcmV?d00001
diff --git a/sonar-core/tester/a/A.java b/sonar-core/tester/a/A.java
new file mode 100644
index 00000000000..a29a8ffbaa4
--- /dev/null
+++ b/sonar-core/tester/a/A.java
@@ -0,0 +1,5 @@
+public class A {
+ public void version1() {
+
+ }
+}
diff --git a/sonar-core/tester/a/a.txt b/sonar-core/tester/a/a.txt
new file mode 100644
index 00000000000..0fdd823d5ea
--- /dev/null
+++ b/sonar-core/tester/a/a.txt
@@ -0,0 +1 @@
+version 1 of a.txt
diff --git a/sonar-core/tester/a_v2.jar b/sonar-core/tester/a_v2.jar
new file mode 100644
index 0000000000000000000000000000000000000000..1e0f89d1a76b9548cbb5ce202064f60530b80b4f
GIT binary patch
literal 892
zcmWIWW@Zs#;Nak3_z;ok#()Gk8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1gy0t*}KSJ^0+Z{0|al{7p
zVOC;UqPB+bsgpio9_KuD^uh{4Ry=zY6rj=2G4*NsGL0!q#YEGeJ(tYPaC(xpGYewD
z!;Qxzbb;DUP%O}4LQMp$K-omSl8O>-NPGnw1fB2?_PCy_T7zQOK1h54%;aZWB7P1MTlz<$9pp<|Bhk#6I
zx69xG#l;TkPz7
z*m-}RF8~X4UFc}r@N9Gl)>3A&ToLr~dP=)kEH2VNXGp}_Fy
z^>dYHOJDz1PhF}u-9W?0fblIO;{vV;P6{X4o}6s@$-}cI>b9hWdDGIHjVjU+0xCxt
zm&}x~U<3u{9G1rUQ-SW>fZ~(8TVqh-f(`7Gti-ZJZ4KX3Cw;;^&UxzSg%t#?c(&>(
z6F5i?{MtTE6sSxX#UM96)JSCo$|mWRRFrr>f@nMP>G(Gmpdctp1H2iTL>N$011zdR
zsR0$hqZgDm(6u5*5GXAmfGv;-*NT)f&`m%N7*Iqaz#$+L8m-8hKw*X)2%s=SfF4GW
nCQt|>>j4E1atMHe2LY}E^?(B_z?&6p8Y=@UgCtOp2OKH@J5s`)
literal 0
HcmV?d00001
diff --git a/sonar-core/tester/b/B.class b/sonar-core/tester/b/B.class
new file mode 100644
index 0000000000000000000000000000000000000000..17df16a46bd5e696b4d4ce5f4b69ab4e94e2dd8b
GIT binary patch
literal 176
zcmX^0Z`VEs1_omWUM>b^1}=66ZgvJ9Mg}&U%)HDJJ4Oa(4b3n{1{UZ1lvG9rexJ;|
zRKL>Pq|~C2#H1Xc2v=}^X;E^jTPBFZ=A@UESeD4cz{0@F$iV2t$RGgX>*plqrR)1A
zWu+#UFeoxG0qp?+pbC&eAjt;g$%6R|46It)85lQ$rP+Zb8&DXclmkdJF>nF^WfUBd
literal 0
HcmV?d00001
diff --git a/sonar-core/tester/b/B.java b/sonar-core/tester/b/B.java
new file mode 100644
index 00000000000..66dd24ce675
--- /dev/null
+++ b/sonar-core/tester/b/B.java
@@ -0,0 +1,2 @@
+public class B {
+}
diff --git a/sonar-core/tester/b/b.txt b/sonar-core/tester/b/b.txt
new file mode 100644
index 00000000000..61780798228
--- /dev/null
+++ b/sonar-core/tester/b/b.txt
@@ -0,0 +1 @@
+b
diff --git a/sonar-core/tester/build.sh b/sonar-core/tester/build.sh
new file mode 100644
index 00000000000..823be7692b2
--- /dev/null
+++ b/sonar-core/tester/build.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+rm *.jar
+
+javac a/*.java
+jar cvf a.jar -C a/ .
+
+javac b/*.java
+jar cvf b.jar -C b/ .
+
+javac c/*.java
+jar cvf c.jar -C c/ .
+
+javac a_v2/*.java
+jar cvf a_v2.jar -C a_v2 .
+
diff --git a/sonar-core/tester/c.jar b/sonar-core/tester/c.jar
new file mode 100644
index 0000000000000000000000000000000000000000..76b546f5e940dd4cf737a7243ff29cda8e9a4ec6
GIT binary patch
literal 826
zcmWIWW@Zs#;Nak3cps7I#()Gk8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1gdP=)kEH2VR^xp}_Fy
z^>dYHOJDz1PhF}u-9W?0fblIO;{vV;P6{X4o}6s@$-}cI>b9hWdDGIHjVjU+0xCxt
zm&}x~U<3u{{hO(YQ-SW>fZ~(K+hS1Sf(`7Gti-ZJZ4KX3Cw;;^&UxzSg%yOYc(&>(
z6F5jxHDAYy0+k7)805r<8mX*6*<`(viV|-&u&(_3JK30lVvJy2j7%a7sEGj<)1btF
z3gEE|N*V#)s9KTZ2b2^Lz!u1aYeh;J=q4bC3n(5D;1G}rja6h#pddpI15l75Ko8J^
o&U&B#MAice9pn%Ig$@E-1?mBZRe(1u*fdrKRt8C+AU8Nv086F5DF6Tf
literal 0
HcmV?d00001
diff --git a/sonar-core/tester/c/C.class b/sonar-core/tester/c/C.class
new file mode 100644
index 0000000000000000000000000000000000000000..a9c2f96622f733e3d24887741e454b1fa5046c29
GIT binary patch
literal 176
zcmX^0Z`VEs1_omWUM>b^1}=66ZgvJ9Mg}&U%)HDJJ4Oa(4b3n{1{UZ1lvG9rexJ;|
zRKL>Pq|~C2#H1Xc2v=}^X;E^jTPBFZ=B$^MSeD4cz{0@F$iV2#$RGgX>*plqrR)1A
zWu+#UFeoxG0qp?+pbC&eAjt;g$%6R|46It)85lQ$rP+Zb8&DXclmkdJF>nF^Wy~Ct
literal 0
HcmV?d00001
diff --git a/sonar-core/tester/c/C.java b/sonar-core/tester/c/C.java
new file mode 100644
index 00000000000..d4053967aab
--- /dev/null
+++ b/sonar-core/tester/c/C.java
@@ -0,0 +1,2 @@
+public class C {
+}
diff --git a/sonar-core/tester/c/c.txt b/sonar-core/tester/c/c.txt
new file mode 100644
index 00000000000..f2ad6c76f01
--- /dev/null
+++ b/sonar-core/tester/c/c.txt
@@ -0,0 +1 @@
+c
--
2.39.5