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