Browse Source

SONAR-21195 allow plugins loaded in different containers to access classLoader resources. Integrate sonarsource-classeloader library into sonar-core source.

tags/10.4.0.87286
Steve Marion 4 months ago
parent
commit
aec0c95e90
36 changed files with 2133 additions and 44 deletions
  1. 0
    1
      build.gradle
  2. 0
    1
      sonar-core/build.gradle
  3. 206
    0
      sonar-core/src/main/java/org/sonar/classloader/ClassRealm.java
  4. 246
    0
      sonar-core/src/main/java/org/sonar/classloader/ClassloaderBuilder.java
  5. 53
    0
      sonar-core/src/main/java/org/sonar/classloader/ClassloaderRef.java
  6. 69
    0
      sonar-core/src/main/java/org/sonar/classloader/DefaultClassloaderRef.java
  7. 208
    0
      sonar-core/src/main/java/org/sonar/classloader/Mask.java
  8. 46
    0
      sonar-core/src/main/java/org/sonar/classloader/NullClassloaderRef.java
  9. 64
    0
      sonar-core/src/main/java/org/sonar/classloader/ParentFirstStrategy.java
  10. 66
    0
      sonar-core/src/main/java/org/sonar/classloader/SelfFirstStrategy.java
  11. 35
    0
      sonar-core/src/main/java/org/sonar/classloader/Strategy.java
  12. 52
    0
      sonar-core/src/main/java/org/sonar/classloader/StrategyContext.java
  13. 24
    0
      sonar-core/src/main/java/org/sonar/classloader/package-info.java
  14. 9
    5
      sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoader.java
  15. 3
    3
      sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoaderDef.java
  16. 37
    29
      sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderFactory.java
  17. 787
    0
      sonar-core/src/test/java/org/sonar/classloader/ClassloaderBuilderTest.java
  18. 170
    0
      sonar-core/src/test/java/org/sonar/classloader/MaskTest.java
  19. 23
    5
      sonar-core/src/test/java/org/sonar/core/platform/PluginClassloaderFactoryTest.java
  20. BIN
      sonar-core/tester/a.jar
  21. BIN
      sonar-core/tester/a/A.class
  22. 5
    0
      sonar-core/tester/a/A.java
  23. 1
    0
      sonar-core/tester/a/a.txt
  24. BIN
      sonar-core/tester/a_v2.jar
  25. BIN
      sonar-core/tester/a_v2/A.class
  26. 6
    0
      sonar-core/tester/a_v2/A.java
  27. 1
    0
      sonar-core/tester/a_v2/a.txt
  28. BIN
      sonar-core/tester/b.jar
  29. BIN
      sonar-core/tester/b/B.class
  30. 2
    0
      sonar-core/tester/b/B.java
  31. 1
    0
      sonar-core/tester/b/b.txt
  32. 16
    0
      sonar-core/tester/build.sh
  33. BIN
      sonar-core/tester/c.jar
  34. BIN
      sonar-core/tester/c/C.class
  35. 2
    0
      sonar-core/tester/c/C.java
  36. 1
    0
      sonar-core/tester/c/c.txt

+ 0
- 1
build.gradle View File

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

+ 0
- 1
sonar-core/build.gradle View File

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

+ 206
- 0
sonar-core/src/main/java/org/sonar/classloader/ClassRealm.java View File

@@ -0,0 +1,206 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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<ClassloaderRef> 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<URL> 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<URL> 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<URL> 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<URL> appendTo) {
for (ClassloaderRef siblingRef : siblingRefs) {
siblingRef.loadResources(name, appendTo);
}
}

@Override
public void loadResourcesFromParent(String name, Collection<URL> appendTo) {
parentRef.loadResources(name, appendTo);
}

@Override
public String toString() {
return String.format("ClassRealm{%s}", key);
}
}

+ 246
- 0
sonar-core/src/main/java/org/sonar/classloader/ClassloaderBuilder.java View File

@@ -0,0 +1,246 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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<String, ClassRealm> previouslyCreatedClassLoaders;

private final Map<String, NewRealm> 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<ClassLoader> 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<String> siblingKeys = new ArrayList<>();
private final Map<String, Mask> 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.
* <p/>
* 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.<PrivilegedAction<ClassRealm>>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<String, ClassLoader> build() {
Map<String, ClassLoader> result = new HashMap<>();

// all the classloaders are created. Associations can now be resolved.
for (Map.Entry<String, NewRealm> 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;
}
}

+ 53
- 0
sonar-core/src/main/java/org/sonar/classloader/ClassloaderRef.java View File

@@ -0,0 +1,53 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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<URL> appendTo);
}

+ 69
- 0
sonar-core/src/main/java/org/sonar/classloader/DefaultClassloaderRef.java View File

@@ -0,0 +1,69 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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<URL> appendTo) {
if (mask.acceptResource(name)) {
try {
Enumeration<URL> 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);
}
}
}
}

+ 208
- 0
sonar-core/src/main/java/org/sonar/classloader/Mask.java View File

@@ -0,0 +1,208 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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.
* <p/>
* 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<String> inclusions;
private final Set<String> 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<String> getInclusions() {
return inclusions;
}

public Set<String> 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<String> inclusions = new HashSet<>();
private final Set<String> 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<String> 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<String> 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;
}
}
}

+ 46
- 0
sonar-core/src/main/java/org/sonar/classloader/NullClassloaderRef.java View File

@@ -0,0 +1,46 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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<URL> appendTo) {
// do nothing
}
}

+ 64
- 0
sonar-core/src/main/java/org/sonar/classloader/ParentFirstStrategy.java View File

@@ -0,0 +1,64 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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<URL> appendTo) {
context.loadResourcesFromSiblings(name, appendTo);
context.loadResourcesFromParent(name, appendTo);
context.loadResourcesFromSelf(name, appendTo);
}
}

+ 66
- 0
sonar-core/src/main/java/org/sonar/classloader/SelfFirstStrategy.java View File

@@ -0,0 +1,66 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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<URL> appendTo) {
context.loadResourcesFromSiblings(name, appendTo);
context.loadResourcesFromSelf(name, appendTo);
context.loadResourcesFromParent(name, appendTo);
}
}

+ 35
- 0
sonar-core/src/main/java/org/sonar/classloader/Strategy.java View File

@@ -0,0 +1,35 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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<URL> urls);

}

+ 52
- 0
sonar-core/src/main/java/org/sonar/classloader/StrategyContext.java View File

@@ -0,0 +1,52 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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<URL> appendTo);

void loadResourcesFromSelf(String name, Collection<URL> appendTo);

void loadResourcesFromParent(String name, Collection<URL> appendTo);

}

+ 24
- 0
sonar-core/src/main/java/org/sonar/classloader/package-info.java View File

@@ -0,0 +1,24 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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;


+ 9
- 5
sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoader.java View File

@@ -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<PluginClassLoaderDef, ClassLoader> classLoaders = new HashMap<>();

public PluginClassLoader(PluginClassloaderFactory classloaderFactory) {
this.classloaderFactory = classloaderFactory;
@@ -63,8 +64,9 @@ public class PluginClassLoader {

public Map<String, Plugin> load(Map<String, ExplodedPlugin> pluginsByKey) {
Collection<PluginClassLoaderDef> defs = defineClassloaders(pluginsByKey);
Map<PluginClassLoaderDef, ClassLoader> classloaders = classloaderFactory.create(defs);
return instantiatePluginClasses(classloaders);
Map<PluginClassLoaderDef, ClassLoader> 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());
}
}

+ 3
- 3
sonar-core/src/main/java/org/sonar/core/platform/PluginClassLoaderDef.java View File

@@ -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<String, String> mainClassesByPluginKey = new HashMap<>();
private final List<File> 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;
}


+ 37
- 29
sonar-core/src/main/java/org/sonar/core/platform/PluginClassloaderFactory.java View File

@@ -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<PluginClassLoaderDef, ClassLoader> create(Collection<PluginClassLoaderDef> defs) {
public Map<PluginClassLoaderDef, ClassLoader> create(Map<PluginClassLoaderDef, ClassLoader> previouslyCreatedClassloaders,
Collection<PluginClassLoaderDef> newDefs) {
ClassLoader baseClassLoader = baseClassLoader();

ClassloaderBuilder builder = new ClassloaderBuilder();
Collection<PluginClassLoaderDef> 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<PluginClassLoaderDef> allPlugins) {
private static void exportResources(PluginClassLoaderDef newDef, ClassloaderBuilder builder,
Collection<PluginClassLoaderDef> 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</p>
*/
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();
}
}

+ 787
- 0
sonar-core/src/test/java/org/sonar/classloader/ClassloaderBuilderTest.java View File

@@ -0,0 +1,787 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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<String, ClassLoader> 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<String, ClassLoader> classloaders1 = sut.newClassloader("example1").build();
Map<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<URL> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> classloaders1 = sut
.newClassloader("the-parent")
.addURL("the-parent", new File("tester/a.jar").toURL())
.build();


Map<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> 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<String, ClassLoader> classloaders1 = sut
.newClassloader("the-sib")
.addURL("the-sib", new File("tester/a.jar").toURL())
.build();

Map<String, ClassLoader> 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<String, ClassLoader> 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<URL> 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<String, ClassLoader> 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<String, ClassLoader> 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;
}
}

+ 170
- 0
sonar-core/src/test/java/org/sonar/classloader/MaskTest.java View File

@@ -0,0 +1,170 @@
/*
* SonarQube
* Copyright (C) 2009-2024 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/");
}
}

+ 23
- 5
sonar-core/src/test/java/org/sonar/core/platform/PluginClassloaderFactoryTest.java View File

@@ -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<PluginClassLoaderDef, ClassLoader> map = factory.create(asList(def));
Map<PluginClassLoaderDef, ClassLoader> 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<PluginClassLoaderDef, ClassLoader> map = factory.create(asList(baseDef, dependentDef));
Map<PluginClassLoaderDef, ClassLoader> 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<PluginClassLoaderDef, ClassLoader> map1 = factory.create(emptyMap(), List.of(baseDef));

PluginClassLoaderDef dependentDef = dependentPluginDef();
Map<PluginClassLoaderDef, ClassLoader> 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<PluginClassLoaderDef, ClassLoader> map = factory.create(asList(def));
Map<PluginClassLoaderDef, ClassLoader> 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;
}

BIN
sonar-core/tester/a.jar View File


BIN
sonar-core/tester/a/A.class View File


+ 5
- 0
sonar-core/tester/a/A.java View File

@@ -0,0 +1,5 @@
public class A {
public void version1() {

}
}

+ 1
- 0
sonar-core/tester/a/a.txt View File

@@ -0,0 +1 @@
version 1 of a.txt

BIN
sonar-core/tester/a_v2.jar View File


BIN
sonar-core/tester/a_v2/A.class View File


+ 6
- 0
sonar-core/tester/a_v2/A.java View File

@@ -0,0 +1,6 @@
public class A {

public void version2() {

}
}

+ 1
- 0
sonar-core/tester/a_v2/a.txt View File

@@ -0,0 +1 @@
version 2 of a.txt

BIN
sonar-core/tester/b.jar View File


BIN
sonar-core/tester/b/B.class View File


+ 2
- 0
sonar-core/tester/b/B.java View File

@@ -0,0 +1,2 @@
public class B {
}

+ 1
- 0
sonar-core/tester/b/b.txt View File

@@ -0,0 +1 @@
b

+ 16
- 0
sonar-core/tester/build.sh View File

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


BIN
sonar-core/tester/c.jar View File


BIN
sonar-core/tester/c/C.class View File


+ 2
- 0
sonar-core/tester/c/C.java View File

@@ -0,0 +1,2 @@
public class C {
}

+ 1
- 0
sonar-core/tester/c/c.txt View File

@@ -0,0 +1 @@
c

Loading…
Cancel
Save