@@ -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' |
@@ -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' |
@@ -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); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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); | |||
} | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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); | |||
} |
@@ -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; | |||
@@ -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()); | |||
} | |||
} |
@@ -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; | |||
} | |||
@@ -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(); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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/"); | |||
} | |||
} |
@@ -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; | |||
} |
@@ -0,0 +1,5 @@ | |||
public class A { | |||
public void version1() { | |||
} | |||
} |
@@ -0,0 +1 @@ | |||
version 1 of a.txt |
@@ -0,0 +1,6 @@ | |||
public class A { | |||
public void version2() { | |||
} | |||
} |
@@ -0,0 +1 @@ | |||
version 2 of a.txt |
@@ -0,0 +1,2 @@ | |||
public class B { | |||
} |
@@ -0,0 +1 @@ | |||
b |
@@ -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 . | |||
@@ -0,0 +1,2 @@ | |||
public class C { | |||
} |
@@ -0,0 +1 @@ | |||
c |