/*
* Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com
* The software in this package is published under the terms of the CPAL v1.0
* license, a copy of which has been included with this distribution in the
* LICENSE.txt file.
*/
package org.mule.runtime.module.artifact.classloader;
import static java.lang.Integer.toHexString;
import static java.lang.String.format;
import static java.lang.System.identityHashCode;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import static org.mule.runtime.api.util.Preconditions.checkArgument;
import org.mule.runtime.core.util.ClassUtils;
import org.mule.runtime.module.artifact.classloader.exception.ClassNotFoundInRegionException;
import org.mule.runtime.module.artifact.descriptor.ArtifactDescriptor;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import sun.misc.CompoundEnumeration;
/**
* Defines a classloader for a Mule artifact composed of other artifacts.
* <p/>
* Each artifact in the region can provide classes and resources to other artifacts. What is shared is defined on each artifact by
* providing a {@link ArtifactClassLoaderFilter}. Lookup policy for each classloader added to the region must be aware of any
* dependency between members of the region and updated accordingly.
* <p/>
* For any member X of the region, if it has a dependency against another region member Y, then X must add all the exported
* packages from Y as PARENT_FIRST. (to indicate that X wants to load those Y's packages)
* <p/>
* For any member X of the region, if there is another region member Y that is not a dependency, then X must add all the exported
* packages from Y as CHILD_ONLY. (to indicate that X does not want to load those Y's packages)
* <p/>
* Only a region member can export a given package, but same resources can be exported by many members. The order in which the
* resources are found will depend on the order in which the class loaders were added to the region.
*/
public class RegionClassLoader extends MuleDeployableArtifactClassLoader {
protected static final String REGION_OWNER_CANNOT_BE_REMOVED_ERROR = "Region owner cannot be removed";
static {
registerAsParallelCapable();
}
private final List<RegionMemberClassLoader> registeredClassLoaders = new ArrayList<>();
private final Map<String, ArtifactClassLoader> packageMapping = new HashMap<>();
private final Map<String, List<ArtifactClassLoader>> resourceMapping = new HashMap<>();
private ArtifactClassLoader ownerClassLoader;
/**
* Creates a new region.
*
* @param artifactId artifact unique ID for the artifact owning the created class loader instance. Non empty.
* @param artifactDescriptor descriptor for the artifact owning the created class loader instance. Non null.
* @param parent parent classloader for the region. Non null
* @param lookupPolicy lookup policy to use on the region
*/
public RegionClassLoader(String artifactId, ArtifactDescriptor artifactDescriptor, ClassLoader parent,
ClassLoaderLookupPolicy lookupPolicy) {
super(artifactId, artifactDescriptor, new URL[0], parent, lookupPolicy, emptyList());
}
@Override
public List<ArtifactClassLoader> getArtifactPluginClassLoaders() {
return registeredClassLoaders.stream().map(r -> r.unfilteredClassLoader).collect(toList());
}
/**
* Adds a class loader to the region.
*
* @param artifactClassLoader classloader to add. Non null.
* @param filter filter used to provide access to the added classloader. Non null
* @throws IllegalArgumentException if the class loader is already a region member.
*/
public synchronized void addClassLoader(ArtifactClassLoader artifactClassLoader, ArtifactClassLoaderFilter filter) {
checkArgument(artifactClassLoader != null, "artifactClassLoader cannot be null");
checkArgument(filter != null, "filter cannot be null");
RegionMemberClassLoader registeredClassLoader = findRegisteredClassLoader(artifactClassLoader);
if (artifactClassLoader == ownerClassLoader || registeredClassLoader != null) {
throw new IllegalArgumentException(createClassLoaderAlreadyInRegionError(artifactClassLoader.getArtifactId()));
}
if (ownerClassLoader == null) {
ownerClassLoader = artifactClassLoader;
} else {
registeredClassLoaders.add(new RegionMemberClassLoader(artifactClassLoader, filter));
}
filter.getExportedClassPackages().forEach(p -> {
LookupStrategy packageLookupStrategy = getClassLoaderLookupPolicy().getPackageLookupStrategy(p);
if (!(packageLookupStrategy instanceof ChildFirstLookupStrategy)) {
throw new IllegalStateException(illegalPackageMappingError(p, packageLookupStrategy));
} else if (packageMapping.containsKey(p)) {
throw new IllegalStateException(duplicatePackageMappingError(p));
} else {
packageMapping.put(p, artifactClassLoader);
}
});
for (String exportedResource : filter.getExportedResources()) {
List<ArtifactClassLoader> classLoaders = resourceMapping.get(exportedResource);
if (classLoaders == null) {
classLoaders = new ArrayList<>();
resourceMapping.put(exportedResource, classLoaders);
}
classLoaders.add(artifactClassLoader);
}
}
static String illegalPackageMappingError(String p, LookupStrategy packageLookupStrategy) {
return format("Attempt to map package '%s' which was already defined on the region lookup policy with '%s'",
p, packageLookupStrategy.getClass().getName());
}
static String duplicatePackageMappingError(String packageName) {
return "Attempt to redefine mapping for package: " + packageName;
}
private RegionMemberClassLoader findRegisteredClassLoader(ArtifactClassLoader artifactClassLoader) {
for (RegionMemberClassLoader registeredClassLoader : registeredClassLoaders) {
if (registeredClassLoader.unfilteredClassLoader == artifactClassLoader) {
return registeredClassLoader;
}
}
return null;
}
/**
* Removes a class loader member from the region.
* <p/>
* Only region members that do not export any package or resoruce can be removed from the region as they are not visible to
* other members.
*
* @param artifactClassLoader class loader to remove. Non null
* @return true if the class loader is a region member and was removed, false if it is not a region member.
* @throws IllegalArgumentException if the class loader is the region owner or is a regiion member that exports packages or
* resources.
*/
public synchronized boolean removeClassLoader(ArtifactClassLoader artifactClassLoader) {
checkArgument(artifactClassLoader != null, "artifactClassLoader cannot be null");
if (ownerClassLoader == artifactClassLoader) {
throw new IllegalArgumentException(REGION_OWNER_CANNOT_BE_REMOVED_ERROR);
}
RegionMemberClassLoader registeredClassLoader = findRegisteredClassLoader(artifactClassLoader);
int index = registeredClassLoaders.indexOf(registeredClassLoader);
if (index < 0) {
return false;
}
if (!registeredClassLoader.filter.getExportedClassPackages().isEmpty()
|| !registeredClassLoader.filter.getExportedResources().isEmpty()) {
throw new IllegalArgumentException(createCannotRemoveClassLoaderError(artifactClassLoader.getArtifactId()));
}
registeredClassLoaders.remove(index);
return true;
}
@Override
public Class<?> findLocalClass(String name) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
final String packageName = ClassUtils.getPackageName(name);
final ArtifactClassLoader artifactClassLoader = packageMapping.get(packageName);
if (artifactClassLoader != null) {
try {
return artifactClassLoader.findLocalClass(name);
} catch (ClassNotFoundException e) {
throw new ClassNotFoundInRegionException(name, getArtifactId(), artifactClassLoader.getArtifactId(), e);
}
} else {
throw new ClassNotFoundInRegionException(name, getArtifactId());
}
}
}
@Override
public final URL findResource(final String name) {
URL resource = null;
final List<ArtifactClassLoader> artifactClassLoaders = resourceMapping.get(name);
if (artifactClassLoaders != null) {
for (ArtifactClassLoader artifactClassLoader : artifactClassLoaders) {
resource = artifactClassLoader.getClassLoader().getResource(name);
if (resource != null) {
break;
}
}
}
return resource;
}
@Override
public final Enumeration<URL> findResources(final String name) throws IOException {
final List<ArtifactClassLoader> artifactClassLoaders = resourceMapping.get(name);
List<Enumeration<URL>> enumerations = new ArrayList<>(registeredClassLoaders.size());
if (artifactClassLoaders != null) {
for (ArtifactClassLoader artifactClassLoader : artifactClassLoaders) {
final Enumeration<URL> partialResources = artifactClassLoader.findResources(name);
if (partialResources.hasMoreElements()) {
enumerations.add(partialResources);
}
}
}
return new CompoundEnumeration<>(enumerations.toArray(new Enumeration[0]));
}
@Override
public void dispose() {
registeredClassLoaders.stream().map(c -> c.unfilteredClassLoader).forEach(classLoader -> {
disposeClassLoader(classLoader);
});
registeredClassLoaders.clear();
disposeClassLoader(ownerClassLoader);
super.dispose();
}
private void disposeClassLoader(ArtifactClassLoader classLoader) {
try {
classLoader.dispose();
} catch (Exception e) {
final String message = "Error disposing classloader for '{}'. This can cause a memory leak";
if (logger.isDebugEnabled()) {
logger.debug(message, classLoader.getArtifactId(), e);
} else {
logger.error(message, classLoader.getArtifactId());
}
}
}
@Override
public URL findLocalResource(String resourceName) {
URL resource = getOwnerClassLoader().findLocalResource(resourceName);
if (resource == null && getParent() instanceof LocalResourceLocator) {
resource = ((LocalResourceLocator) getParent()).findLocalResource(resourceName);
}
return resource;
}
private ArtifactClassLoader getOwnerClassLoader() {
return ownerClassLoader;
}
@Override
public String toString() {
return format("%s[%s] -> %s@%s", getClass().getName(), getArtifactId(), packageMapping.toString(),
toHexString(identityHashCode(this)));
}
static String createCannotRemoveClassLoaderError(String artifactId) {
return format("Cannot remove classloader '%s' as it exports at least a package or resource", artifactId);
}
static String createClassLoaderAlreadyInRegionError(String artifactId) {
return "Region already contains classloader for artifact:" + artifactId;
}
private static class RegionMemberClassLoader {
final ArtifactClassLoader unfilteredClassLoader;
final ArtifactClassLoaderFilter filter;
private RegionMemberClassLoader(ArtifactClassLoader unfilteredClassLoader, ArtifactClassLoaderFilter filter) {
this.unfilteredClassLoader = unfilteredClassLoader;
this.filter = filter;
}
}
}