/* * 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.test.runner.classloader; import static com.google.common.collect.Lists.newArrayList; import static java.lang.Boolean.valueOf; import static java.lang.System.getProperty; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toSet; import static org.mule.runtime.container.internal.ContainerClassLoaderFactory.SYSTEM_PACKAGES; import static org.mule.runtime.core.api.config.MuleProperties.MULE_LOG_VERBOSE_CLASSLOADING; import static org.mule.runtime.deployment.model.internal.AbstractArtifactClassLoaderBuilder.getArtifactPluginId; import static org.mule.runtime.module.artifact.classloader.ParentFirstLookupStrategy.PARENT_FIRST; import org.mule.runtime.container.api.MuleModule; import org.mule.runtime.container.internal.ContainerClassLoaderFactory; import org.mule.runtime.container.internal.ContainerClassLoaderFilterFactory; import org.mule.runtime.container.internal.ContainerModuleDiscoverer; import org.mule.runtime.container.internal.ContainerOnlyLookupStrategy; import org.mule.runtime.container.internal.DefaultModuleRepository; import org.mule.runtime.container.internal.MuleClassLoaderLookupPolicy; import org.mule.runtime.deployment.model.internal.application.MuleApplicationClassLoader; import org.mule.runtime.deployment.model.internal.nativelib.DefaultNativeLibraryFinderFactory; import org.mule.runtime.module.artifact.classloader.ArtifactClassLoader; import org.mule.runtime.module.artifact.classloader.ArtifactClassLoaderFilter; import org.mule.runtime.module.artifact.classloader.ArtifactClassLoaderFilterFactory; import org.mule.runtime.module.artifact.classloader.ChildFirstLookupStrategy; import org.mule.runtime.module.artifact.classloader.ClassLoaderFilter; import org.mule.runtime.module.artifact.classloader.ClassLoaderFilterFactory; import org.mule.runtime.module.artifact.classloader.ClassLoaderLookupPolicy; import org.mule.runtime.module.artifact.classloader.DefaultArtifactClassLoaderFilter; import org.mule.runtime.module.artifact.classloader.FilteringArtifactClassLoader; import org.mule.runtime.module.artifact.classloader.LookupStrategy; import org.mule.runtime.module.artifact.classloader.MuleArtifactClassLoader; import org.mule.runtime.module.artifact.classloader.RegionClassLoader; import org.mule.runtime.module.artifact.descriptor.ArtifactDescriptor; import org.mule.runtime.module.artifact.util.FileJarExplorer; import org.mule.runtime.module.artifact.util.JarExplorer; import org.mule.runtime.module.artifact.util.JarInfo; import org.mule.test.runner.api.ArtifactClassLoaderHolder; import org.mule.test.runner.api.ArtifactUrlClassification; import org.mule.test.runner.api.ArtifactsUrlClassification; import org.mule.test.runner.api.PluginUrlClassification; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Factory that creates a class loader hierarchy to emulate the one used in a mule standalone container. * <p/> * The class loaders created have the following hierarchy: * <ul> * <li>Container: all the provided scope dependencies plus their dependencies (non test dependencies) and java</li> * <li>Plugins (optional): for each plugin a class loader will be created with all the compile scope dependencies and their * transitive dependencies (only the ones with scope compile)</li> * <li>Application: all the test scope dependencies and their dependencies if they are not defined to be excluded, plus their * transitive dependencies (again if they are not excluded).</li> * </ul> * * @since 4.0 */ public class IsolatedClassLoaderFactory { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); private ClassLoaderFilterFactory classLoaderFilterFactory = new ArtifactClassLoaderFilterFactory(); private PluginLookPolicyFactory pluginLookupPolicyGenerator = new PluginLookPolicyFactory(); /** * Creates a {@link ArtifactClassLoaderHolder} containing the container, plugins and application {@link ArtifactClassLoader}s * * @param extraBootPackages {@link List} of {@link String}s of extra boot packages to be appended to the container * {@link ClassLoader} * @param artifactsUrlClassification the {@link ArtifactsUrlClassification} that defines the different {@link URL}s for each * {@link ClassLoader} * @return a {@link ArtifactClassLoaderHolder} that would be used to run the test */ public ArtifactClassLoaderHolder createArtifactClassLoader(List<String> extraBootPackages, ArtifactsUrlClassification artifactsUrlClassification) { JarInfo testJarInfo = getTestJarInfo(artifactsUrlClassification); ArtifactClassLoader containerClassLoader; ClassLoaderLookupPolicy childClassLoaderLookupPolicy; RegionClassLoader regionClassLoader; final List<ArtifactClassLoader> filteredPluginsArtifactClassLoaders = new ArrayList<>(); final List<ArtifactClassLoader> pluginsArtifactClassLoaders = new ArrayList<>(); final List<ArtifactClassLoaderFilter> pluginArtifactClassLoaderFilters = new ArrayList<>(); List<ArtifactClassLoader> serviceArtifactClassLoaders; DefaultModuleRepository moduleRepository = new DefaultModuleRepository(new ContainerModuleDiscoverer(ContainerClassLoaderFactory.class.getClassLoader())); try (final TestContainerClassLoaderFactory testContainerClassLoaderFactory = new TestContainerClassLoaderFactory(extraBootPackages, artifactsUrlClassification.getContainerUrls().toArray(new URL[0]), moduleRepository)) { containerClassLoader = createContainerArtifactClassLoader(testContainerClassLoaderFactory, artifactsUrlClassification); childClassLoaderLookupPolicy = testContainerClassLoaderFactory.getContainerClassLoaderLookupPolicy(containerClassLoader.getClassLoader()); serviceArtifactClassLoaders = createServiceClassLoaders(containerClassLoader.getClassLoader(), childClassLoaderLookupPolicy, artifactsUrlClassification); regionClassLoader = new RegionClassLoader("Region", new ArtifactDescriptor("Region"), containerClassLoader.getClassLoader(), childClassLoaderLookupPolicy); if (!artifactsUrlClassification.getPluginUrlClassifications().isEmpty()) { for (PluginUrlClassification pluginUrlClassification : artifactsUrlClassification.getPluginUrlClassifications()) { logClassLoaderUrls("PLUGIN (" + pluginUrlClassification.getName() + ")", pluginUrlClassification.getUrls()); String artifactId = getArtifactPluginId(regionClassLoader.getArtifactId(), pluginUrlClassification.getName()); ClassLoaderLookupPolicy pluginLookupPolicy = extendLookupPolicyForPrivilegedAccess(childClassLoaderLookupPolicy, moduleRepository, testContainerClassLoaderFactory, pluginUrlClassification); MuleArtifactClassLoader pluginCL = new MuleArtifactClassLoader(artifactId, new ArtifactDescriptor(pluginUrlClassification.getName()), pluginUrlClassification.getUrls().toArray(new URL[0]), regionClassLoader, pluginLookupPolicyGenerator.createLookupPolicy(pluginUrlClassification, artifactsUrlClassification .getPluginUrlClassifications(), pluginLookupPolicy)); pluginsArtifactClassLoaders.add(pluginCL); ArtifactClassLoaderFilter filter = createArtifactClassLoaderFilter(pluginUrlClassification, testJarInfo.getPackages(), childClassLoaderLookupPolicy); pluginArtifactClassLoaderFilters.add(filter); filteredPluginsArtifactClassLoaders.add(new FilteringArtifactClassLoader(pluginCL, filter, emptyList())); } } } final Map<String, LookupStrategy> pluginsLookupStrategies = new HashMap<>(); for (int i = 0; i < filteredPluginsArtifactClassLoaders.size(); i++) { final ArtifactClassLoaderFilter classLoaderFilter = pluginArtifactClassLoaderFilters.get(i); classLoaderFilter.getExportedClassPackages() .forEach(p -> pluginsLookupStrategies.put(p, PARENT_FIRST)); } final ClassLoaderLookupPolicy appLookupPolicy = childClassLoaderLookupPolicy.extend(pluginsLookupStrategies); ArtifactClassLoader appClassLoader = createApplicationArtifactClassLoader(regionClassLoader, appLookupPolicy, artifactsUrlClassification, pluginsArtifactClassLoaders); regionClassLoader.addClassLoader(appClassLoader, new DefaultArtifactClassLoaderFilter(testJarInfo.getPackages(), testJarInfo.getResources())); for (int i = 0; i < filteredPluginsArtifactClassLoaders.size(); i++) { final ArtifactClassLoaderFilter classLoaderFilter = pluginArtifactClassLoaderFilters.get(i); regionClassLoader.addClassLoader(filteredPluginsArtifactClassLoaders.get(i), classLoaderFilter); } return new ArtifactClassLoaderHolder(containerClassLoader, serviceArtifactClassLoaders, pluginsArtifactClassLoaders, appClassLoader); } private ClassLoaderLookupPolicy extendLookupPolicyForPrivilegedAccess(ClassLoaderLookupPolicy childClassLoaderLookupPolicy, DefaultModuleRepository moduleRepository, TestContainerClassLoaderFactory testContainerClassLoaderFactory, PluginUrlClassification pluginUrlClassification) { ContainerOnlyLookupStrategy containerOnlyLookupStrategy = new ContainerOnlyLookupStrategy(testContainerClassLoaderFactory.getContainerClassLoader().getClassLoader()); Map<String, LookupStrategy> privilegedLookupStrategies = new HashMap<>(); for (MuleModule module : moduleRepository.getModules()) { if (hasPrivilegedApiAccess(pluginUrlClassification, module)) { for (String packageName : module.getPrivilegedExportedPackages()) { privilegedLookupStrategies.put(packageName, containerOnlyLookupStrategy); } } } if (privilegedLookupStrategies.isEmpty()) { return childClassLoaderLookupPolicy; } else { return childClassLoaderLookupPolicy.extend(privilegedLookupStrategies); } } private boolean hasPrivilegedApiAccess(PluginUrlClassification pluginUrlClassification, MuleModule module) { return module.getPrivilegedArtifacts().stream() .filter(artifact -> pluginUrlClassification.getName().contains(":" + artifact + ":")).findFirst().isPresent(); } /** * For each service defined in the classification it creates an {@link ArtifactClassLoader} wit the name defined in * classification. * * @param parent the parent class loader to be assigned to the new one created here * @param childClassLoaderLookupPolicy look policy to be used * @param artifactsUrlClassification the url classifications to get service {@link URL}s * @return a list of {@link ArtifactClassLoader} for service class loaders */ protected List<ArtifactClassLoader> createServiceClassLoaders(ClassLoader parent, ClassLoaderLookupPolicy childClassLoaderLookupPolicy, ArtifactsUrlClassification artifactsUrlClassification) { List<ArtifactClassLoader> servicesArtifactClassLoaders = newArrayList(); for (ArtifactUrlClassification serviceUrlClassification : artifactsUrlClassification.getServiceUrlClassifications()) { logClassLoaderUrls("SERVICE (" + serviceUrlClassification.getArtifactId() + ")", serviceUrlClassification.getUrls()); MuleArtifactClassLoader artifactClassLoader = new MuleArtifactClassLoader(serviceUrlClassification.getName(), new ArtifactDescriptor(serviceUrlClassification.getName()), serviceUrlClassification.getUrls().toArray(new URL[0]), parent, childClassLoaderLookupPolicy); servicesArtifactClassLoaders.add(artifactClassLoader); } return servicesArtifactClassLoaders; } /** * Creates the {@link JarInfo} for the {@link ArtifactsUrlClassification}. * * @param artifactsUrlClassification the {@link ArtifactsUrlClassification} that defines the different {@link URL}s for each * {@link ClassLoader} * @return {@link JarInfo} for the classification */ private JarInfo getTestJarInfo(ArtifactsUrlClassification artifactsUrlClassification) { URL testCodeUrl = artifactsUrlClassification.getApplicationUrls().get(0); // sometimes the test-classes URL is the second one. if (!testCodeUrl.getFile().contains("test-classes") && artifactsUrlClassification.getApplicationUrls().size() > 1) { testCodeUrl = artifactsUrlClassification.getApplicationUrls().get(1); } Set<String> productionPackages = getProductionCodePackages(testCodeUrl); JarInfo testJarInfo = getTestCodePackages(artifactsUrlClassification, testCodeUrl); Set<String> testPackages = sanitizeTestExportedPackages(productionPackages, testJarInfo.getPackages()); return new JarInfo(testPackages, testJarInfo.getResources()); } /** * Sanitizes packages exported on the test class loader. * <p/> * Test runner exports test packages to the plugins used during the test, to enable the usage of test classes to configure them. * A problem is that test code usually contains a mix of unit and integration tests. This causes that packages from the * production code are also used to write unit tests for them. The test runner cannot export those production packages as that * will cause an error when creating the class loader for the test. To avoid this, every production code package will not be * exported on the test. * <p/> * A similar sanitization is done for packages that are system packages, as child artifacts cannot redefine them. * * @param productionPackages all packages from the module under test's production code. * @param testPackages all packages from the module under test's test code * @return sanitized packages to export on the test class loader. */ private Set<String> sanitizeTestExportedPackages(Set<String> productionPackages, Set<String> testPackages) { Set<String> sanitizedTestPackages = new HashSet<>(testPackages); removePackagesFromTestClassLoader(sanitizedTestPackages, SYSTEM_PACKAGES); removePackagesFromTestClassLoader(sanitizedTestPackages, productionPackages); return sanitizedTestPackages; } private JarInfo getTestCodePackages(ArtifactsUrlClassification artifactsUrlClassification, URL testCodeUrl) { List<URL> libraries = newArrayList(testCodeUrl); libraries.addAll(artifactsUrlClassification.getPluginSharedLibUrls()); Set<String> packages = new HashSet<>(); Set<String> resources = new HashSet<>(); final JarExplorer jarExplorer = new FileJarExplorer(); for (URL library : libraries) { JarInfo jarInfo = jarExplorer.explore(library); packages.addAll(jarInfo.getPackages()); resources.addAll(jarInfo.getResources()); } return new JarInfo(packages, resources); } private Set<String> getProductionCodePackages(URL testCodeUrl) { int index = testCodeUrl.toString().lastIndexOf("test-classes"); try { URL productionCodeUrl = new URL(testCodeUrl.toString().substring(0, index) + "classes"); if (new File(productionCodeUrl.getFile()).exists()) { final JarExplorer jarExplorer = new FileJarExplorer(); return jarExplorer.explore(productionCodeUrl).getPackages(); } else { return emptySet(); } } catch (MalformedURLException e) { throw new IllegalArgumentException(e); } } private void removePackagesFromTestClassLoader(Set<String> packages, Collection<String> systemPackages) { Set<String> packagesToRemove = new HashSet<>(); systemPackages.stream().forEach(systemPackage -> packages.stream().filter(p -> p.startsWith(systemPackage)) .forEach(p -> packagesToRemove.add(p))); packages.removeAll(packagesToRemove); } /** * Creates an {@link ArtifactClassLoader} for the container. The difference between a mule container {@link ArtifactClassLoader} * in standalone mode and this one is that it has to be aware that the parent class loader has all the URLs loaded in launcher * app class loader so it has to create a particular look policy to resolve classes as CHILD_FIRST. * <p/> * In order to do that a {@link FilteringArtifactClassLoader} resolve is created with and empty look policy (meaning that * CHILD_FIRST strategy will be used) for the {@link URL}s that are going to be exposed from the container class loader. This * would be the parent class loader for the container so instead of going directly the launcher application class loader that * has access to the whole classpath this filtering class loader will resolve only the classes for the {@link URL}s defined to * be in the container. * * @param testContainerClassLoaderFactory {@link TestContainerClassLoaderFactory} that has the logic to create a container class * loader * @param artifactsUrlClassification the classifications to get plugins {@link URL}s * @return an {@link ArtifactClassLoader} for the container */ protected ArtifactClassLoader createContainerArtifactClassLoader( TestContainerClassLoaderFactory testContainerClassLoaderFactory, ArtifactsUrlClassification artifactsUrlClassification) { MuleArtifactClassLoader launcherArtifact = createLauncherArtifactClassLoader(); final List<MuleModule> muleModules = Collections.<MuleModule>emptyList(); ClassLoaderFilter filteredClassLoaderLauncher = new ContainerClassLoaderFilterFactory() .create(testContainerClassLoaderFactory.getBootPackages(), muleModules); logClassLoaderUrls("CONTAINER", artifactsUrlClassification.getContainerUrls()); ArtifactClassLoader containerClassLoader = testContainerClassLoaderFactory .createContainerClassLoader(new FilteringArtifactClassLoader(launcherArtifact, filteredClassLoaderLauncher, emptyList())); return containerClassLoader; } /** * Creates the launcher application class loader to delegate from container class loader. * * @return an {@link ArtifactClassLoader} for the launcher, parent of container */ protected MuleArtifactClassLoader createLauncherArtifactClassLoader() { ClassLoader launcherClassLoader = IsolatedClassLoaderFactory.class.getClassLoader(); return new MuleArtifactClassLoader("launcher", new ArtifactDescriptor("launcher"), new URL[0], launcherClassLoader, new MuleClassLoaderLookupPolicy(Collections.emptyMap(), Collections.emptySet())) { @Override public URL findResource(String name) { URL url = super.findResource(name); if (url == null && getParent() != null) { url = getParent().getResource(name); } return url; } }; } private ArtifactClassLoaderFilter createArtifactClassLoaderFilter(PluginUrlClassification pluginUrlClassification, Set<String> parentExportedPackages, ClassLoaderLookupPolicy childClassLoaderLookupPolicy) { Set<String> sanitizedExportedPackages = sanitizePluginExportedPackages(pluginUrlClassification, parentExportedPackages, childClassLoaderLookupPolicy); String exportedPackages = sanitizedExportedPackages.stream().collect(joining(", ")); final String exportedResources = pluginUrlClassification.getExportedResources().stream().collect(joining(", ")); ArtifactClassLoaderFilter artifactClassLoaderFilter = classLoaderFilterFactory.create(exportedPackages, exportedResources); if (!pluginUrlClassification.getExportClasses().isEmpty()) { artifactClassLoaderFilter = new TestArtifactClassLoaderFilter(artifactClassLoaderFilter, pluginUrlClassification.getExportClasses()); } return artifactClassLoaderFilter; } private Set<String> sanitizePluginExportedPackages(PluginUrlClassification pluginUrlClassification, Set<String> parentExportedPackages, ClassLoaderLookupPolicy childClassLoaderLookupPolicy) { Set<String> exportedPackages = new HashSet<>(pluginUrlClassification.getExportedPackages()); Set<String> containerProvidedPackages = exportedPackages.stream().filter(p -> { LookupStrategy lookupStrategy = childClassLoaderLookupPolicy.getPackageLookupStrategy(p); return !(lookupStrategy instanceof ChildFirstLookupStrategy); }).collect(toSet()); if (!containerProvidedPackages.isEmpty()) { exportedPackages.removeAll(containerProvidedPackages); logger .warn("Exported packages from plugin '" + pluginUrlClassification.getName() + "' are provided by parent class loader: " + containerProvidedPackages); } Set<String> appProvidedPackages = parentExportedPackages.stream().filter(p -> exportedPackages.contains(p)).collect(toSet()); if (!appProvidedPackages.isEmpty()) { exportedPackages.removeAll(appProvidedPackages); logger.warn("Exported packages from plugin '" + pluginUrlClassification.getName() + "' are provided by the artifact owner: " + appProvidedPackages); } return exportedPackages; } /** * Creates an {@link ArtifactClassLoader} for the application. * * @param parent the parent class loader to be assigned to the new one created here * @param childClassLoaderLookupPolicy look policy to be used * @param artifactsUrlClassification the url classifications to get plugins urls * @param pluginsArtifactClassLoaders the classloaders of the plugins used by the application * @return the {@link ArtifactClassLoader} to be used for running the test */ protected ArtifactClassLoader createApplicationArtifactClassLoader(ClassLoader parent, ClassLoaderLookupPolicy childClassLoaderLookupPolicy, ArtifactsUrlClassification artifactsUrlClassification, List<ArtifactClassLoader> pluginsArtifactClassLoaders) { logClassLoaderUrls("APP", artifactsUrlClassification.getApplicationUrls()); return new MuleApplicationClassLoader("app", new ArtifactDescriptor("app"), parent, new DefaultNativeLibraryFinderFactory().create("app"), artifactsUrlClassification.getApplicationUrls(), childClassLoaderLookupPolicy, pluginsArtifactClassLoaders); } /** * Logs the {@link List} of {@link URL}s for the classLoaderName * * @param classLoaderName the name of the {@link ClassLoader} to be logged * @param urls {@link List} of {@link URL}s that are going to be used for the {@link ClassLoader} */ protected void logClassLoaderUrls(final String classLoaderName, final List<URL> urls) { StringBuilder builder = new StringBuilder(classLoaderName).append(" classloader urls: ["); urls.stream().forEach(e -> builder.append("\n").append(" ").append(e)); builder.append("\n]"); logClassLoadingTrace(builder.toString()); } /** * Logs the message with info severity if {@link org.mule.runtime.core.api.config.MuleProperties#MULE_LOG_VERBOSE_CLASSLOADING} * is set or trace severity * * @param message the message to be logged */ private void logClassLoadingTrace(String message) { if (isVerboseClassLoading()) { logger.info(message); } else { logger.debug(message); } } /** * @return true if {@link org.mule.runtime.core.api.config.MuleProperties#MULE_LOG_VERBOSE_CLASSLOADING} is set to true */ private Boolean isVerboseClassLoading() { return valueOf(getProperty(MULE_LOG_VERBOSE_CLASSLOADING)); } }