/*
* 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.api;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Maps.newLinkedHashMap;
import static com.google.common.collect.Sets.newHashSet;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.io.FileUtils.toFile;
import static org.apache.commons.lang.StringUtils.endsWithIgnoreCase;
import static org.eclipse.aether.util.artifact.ArtifactIdUtils.toId;
import static org.eclipse.aether.util.artifact.JavaScopes.COMPILE;
import static org.eclipse.aether.util.artifact.JavaScopes.PROVIDED;
import static org.eclipse.aether.util.artifact.JavaScopes.TEST;
import static org.eclipse.aether.util.filter.DependencyFilterUtils.andFilter;
import static org.eclipse.aether.util.filter.DependencyFilterUtils.classpathFilter;
import static org.eclipse.aether.util.filter.DependencyFilterUtils.orFilter;
import static org.mule.runtime.api.util.Preconditions.checkNotNull;
import static org.mule.runtime.core.util.PropertiesUtils.loadProperties;
import static org.mule.runtime.deployment.model.api.plugin.ArtifactPluginDescriptor.MULE_PLUGIN_CLASSIFIER;
import static org.mule.test.runner.api.ArtifactClassificationType.APPLICATION;
import static org.mule.test.runner.api.ArtifactClassificationType.MODULE;
import static org.mule.test.runner.api.ArtifactClassificationType.PLUGIN;
import org.mule.runtime.extension.api.annotation.Extension;
import org.mule.runtime.module.artifact.classloader.ArtifactClassLoader;
import org.mule.test.runner.classification.PatternExclusionsDependencyFilter;
import org.mule.test.runner.classification.PatternInclusionsDependencyFilter;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.resolution.ArtifactDescriptorException;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Creates the {@link ArtifactUrlClassification} based on the Maven dependencies declared by the rootArtifact using Eclipse
* Aether. Uses a {@link DependencyResolver} to resolve Maven dependencies.
* <p/>
* The classification process classifies the rootArtifact dependencies in three groups: {@code provided}, {@code compile} and
* {@code test} scopes. It resolves dependencies graph for each group applying filters and exclusions and classifies the list of
* {@link URL}s that would define each class loader container, plugins and application.
* <p/>
* Dependencies resolution uses dependencies management declared by these artifacts while resolving the dependency graph.
* <p/>
* Plugins are discovered as {@link Extension} if they do have a annotated a {@link Class}. It generates the {@link Extension}
* metadata in order to later register it to an {@link org.mule.runtime.core.api.extension.ExtensionManager}.
*
* @since 4.0
*/
public class AetherClassPathClassifier implements ClassPathClassifier {
private static final String POM = "pom";
private static final String POM_XML = POM + ".xml";
private static final String POM_EXTENSION = "." + POM;
private static final String ZIP_EXTENSION = ".zip";
private static final String MAVEN_COORDINATES_SEPARATOR = ":";
private static final String JAR_EXTENSION = "jar";
private static final String SNAPSHOT_WILCARD_FILE_FILTER = "*-SNAPSHOT*.*";
private static final String TESTS_CLASSIFIER = "tests";
private static final String TESTS_JAR = "-tests.jar";
private static final String SERVICE_PROPERTIES_FILE_NAME = "service.properties";
private static final String SERVICE_PROVIDER_CLASS_NAME = "service.className";
private static final String MULE_SERVICE_CLASSIFIER = "mule-service";
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private DependencyResolver dependencyResolver;
private ArtifactClassificationTypeResolver artifactClassificationTypeResolver;
private PluginResourcesResolver pluginResourcesResolver = new PluginResourcesResolver();
/**
* Creates an instance of the classifier.
*
* @param dependencyResolver {@link DependencyResolver} to resolve dependencies. Non null.
* @param artifactClassificationTypeResolver {@link ArtifactClassificationTypeResolver} to identify rootArtifact type. Non null.
*/
public AetherClassPathClassifier(DependencyResolver dependencyResolver,
ArtifactClassificationTypeResolver artifactClassificationTypeResolver) {
checkNotNull(dependencyResolver, "dependencyResolver cannot be null");
checkNotNull(artifactClassificationTypeResolver, "artifactClassificationTypeResolver cannot be null");
this.dependencyResolver = dependencyResolver;
this.artifactClassificationTypeResolver = artifactClassificationTypeResolver;
}
/**
* Classifies {@link URL}s and {@link Dependency}s to define how the container, plugins and application class loaders should be
* created.
*
* @param context {@link ClassPathClassifierContext} to be used during the classification. Non null.
* @return {@link ArtifactsUrlClassification} as result with the classification
*/
@Override
public ArtifactsUrlClassification classify(final ClassPathClassifierContext context) {
checkNotNull(context, "context cannot be null");
logger.debug("Building class loaders for rootArtifact: {}", context.getRootArtifact());
List<Dependency> directDependencies;
try {
directDependencies = dependencyResolver.getDirectDependencies(context.getRootArtifact());
} catch (Exception e) {
throw new IllegalStateException("Couldn't get direct dependencies for rootArtifact: '" + context.getRootArtifact() + "'",
e);
}
ArtifactClassificationType rootArtifactType = artifactClassificationTypeResolver
.resolveArtifactClassificationType(context.getRootArtifact());
if (rootArtifactType == null) {
throw new IllegalStateException("Couldn't be identified type for rootArtifact: " + context.getRootArtifact());
}
logger.debug("rootArtifact {} identified as {} type", context.getRootArtifact(), rootArtifactType);
List<URL> pluginSharedLibUrls = buildPluginSharedLibClassification(context, directDependencies);
List<PluginUrlClassification> pluginUrlClassifications =
buildPluginUrlClassifications(context, directDependencies, rootArtifactType);
List<ArtifactUrlClassification> serviceUrlClassifications = buildServicesUrlClassification(context, directDependencies);
List<URL> containerUrls =
buildContainerUrlClassification(context, directDependencies, serviceUrlClassifications, pluginUrlClassifications,
rootArtifactType);
List<URL> applicationUrls = buildApplicationUrlClassification(context, directDependencies, rootArtifactType);
return new ArtifactsUrlClassification(containerUrls, serviceUrlClassifications, pluginSharedLibUrls, pluginUrlClassifications,
applicationUrls);
}
/**
* Finds direct dependencies declared with classifier {@value #MULE_SERVICE_CLASSIFIER}. Creates a
* List of {@link ArtifactUrlClassification} for each service including their {@code compile} scope dependencies.
* <p/>
* {@value #SERVICE_PROVIDER_CLASS_NAME} will be used as {@link ArtifactClassLoader#getArtifactId()}
* <p/>
* Once identified and classified these Maven artifacts will be excluded from container classification.
*
* @param context {@link ClassPathClassifierContext} with settings for the classification process
* @param directDependencies {@link List} of {@link Dependency} with direct dependencies for the rootArtifact
* @return a {@link List} of {@link ArtifactUrlClassification}s that would be the one used for the plugins class loaders.
*/
private List<ArtifactUrlClassification> buildServicesUrlClassification(final ClassPathClassifierContext context,
final List<Dependency> directDependencies) {
Map<String, ArtifactClassificationNode> servicesClassified = newLinkedHashMap();
final Predicate<Dependency> muleServiceClassifiedDependencyFilter =
dependency -> dependency.getArtifact().getClassifier().equals(MULE_SERVICE_CLASSIFIER);
List<Artifact> serviceArtifactsDeclared = filterArtifacts(directDependencies,
muleServiceClassifiedDependencyFilter);
logger.debug("{} services defined to be classified", serviceArtifactsDeclared.size());
serviceArtifactsDeclared.stream()
.forEach(serviceArtifact -> buildPluginUrlClassification(serviceArtifact, context,
muleServiceClassifiedDependencyFilter, servicesClassified));
return toServiceUrlClassification(servicesClassified.values());
}
/**
* Classifies the {@link List} of {@link URL}s from {@value org.eclipse.aether.util.artifact.JavaScopes#TEST} scope direct
* dependencies to be added as plugin runtime shared libraries.
*
* @param context {@link ClassPathClassifierContext} with settings for the classification process
* @param directDependencies {@link List} of {@link Dependency} with direct dependencies for the rootArtifact
* @return {@link List} of {@link URL}s to be added to runtime shared libraries.
*/
private List<URL> buildPluginSharedLibClassification(final ClassPathClassifierContext context,
final List<Dependency> directDependencies) {
List<URL> pluginSharedLibUrls = newArrayList();
List<Dependency> pluginSharedLibDependencies = context.getSharedPluginLibCoordinates().stream()
.map(sharedPluginLibCoords -> findPluginSharedLibArtifact(sharedPluginLibCoords, context.getRootArtifact(),
directDependencies))
.collect(toList());
logger.debug("Plugin sharedLib artifacts matched with versions from direct dependencies declared: {}",
pluginSharedLibDependencies);
pluginSharedLibDependencies.stream()
.map(pluginSharedLibDependency -> {
try {
return dependencyResolver.resolveArtifact(pluginSharedLibDependency.getArtifact())
.getArtifact().getFile().toURI().toURL();
} catch (Exception e) {
throw new IllegalStateException("Error while resolving dependency '" + pluginSharedLibDependency
+ "' as plugin sharedLibs");
}
})
.forEach(pluginSharedLibUrls::add);
logger.debug("Classified URLs as plugin runtime shared libraries: '{}", pluginSharedLibUrls);
return pluginSharedLibUrls;
}
/**
* Container classification is being done by resolving the {@value org.eclipse.aether.util.artifact.JavaScopes#PROVIDED} direct
* dependencies of the rootArtifact. Is uses the exclusions defined in
* {@link ClassPathClassifierContext#getProvidedExclusions()} to filter the dependency graph plus
* {@link ClassPathClassifierContext#getExcludedArtifacts()}.
* <p/>
* In order to resolve correctly the {@value org.eclipse.aether.util.artifact.JavaScopes#PROVIDED} direct dependencies it will
* get for each one the manage dependencies and use that list to resolve the graph.
*
* @param context {@link ClassPathClassifierContext} with settings for the classification process
* @param pluginUrlClassifications {@link PluginUrlClassification}s to check if rootArtifact was classified as plugin
* @param rootArtifactType {@link ArtifactClassificationType} for rootArtifact
* @return {@link List} of {@link URL}s for the container class loader
*/
private List<URL> buildContainerUrlClassification(ClassPathClassifierContext context,
List<Dependency> directDependencies,
List<ArtifactUrlClassification> serviceUrlClassifications,
List<PluginUrlClassification> pluginUrlClassifications,
ArtifactClassificationType rootArtifactType) {
directDependencies = directDependencies.stream()
.filter(getContainerDirectDependenciesFilter(rootArtifactType))
.filter(dependency -> {
Artifact artifact = dependency.getArtifact();
return (!serviceUrlClassifications.stream()
.filter(artifactUrlClassification -> artifactUrlClassification.getArtifactId().equals(toId(artifact))).findAny()
.isPresent()
&& !pluginUrlClassifications.stream()
.filter(artifactUrlClassification -> artifactUrlClassification.getArtifactId().equals(toId(artifact))).findAny()
.isPresent());
})
.map(depToTransform -> depToTransform.setScope(COMPILE))
.collect(toList());
logger.debug("Selected direct dependencies to be used for resolving container dependency graph (changed to compile in " +
"order to resolve the graph): {}", directDependencies);
Set<Dependency> managedDependencies = selectContainerManagedDependencies(context, directDependencies, rootArtifactType);
logger.debug("Collected managed dependencies from direct provided dependencies to be used for resolving container "
+ "dependency graph: {}", managedDependencies);
List<String> excludedFilterPattern = newArrayList(context.getProvidedExclusions());
excludedFilterPattern.addAll(context.getExcludedArtifacts());
if (!pluginUrlClassifications.isEmpty()) {
excludedFilterPattern.addAll(pluginUrlClassifications.stream()
.map(pluginUrlClassification -> pluginUrlClassification.getArtifactId())
.collect(toList()));
}
if (!serviceUrlClassifications.isEmpty()) {
excludedFilterPattern.addAll(serviceUrlClassifications.stream()
.map(serviceUrlClassification -> serviceUrlClassification.getArtifactId())
.collect(toList()));
}
logger.debug("Resolving dependencies for container using exclusion filter patterns: {}", excludedFilterPattern);
final DependencyFilter dependencyFilter = new PatternExclusionsDependencyFilter(excludedFilterPattern);
List<URL> containerUrls;
try {
containerUrls = toUrl(dependencyResolver.resolveDependencies(null, directDependencies, newArrayList(managedDependencies),
dependencyFilter));
} catch (Exception e) {
throw new IllegalStateException("Couldn't resolve dependencies for Container", e);
}
containerUrls = containerUrls.stream().filter(url -> {
String file = toFile(url).getAbsolutePath();
return !(endsWithIgnoreCase(file, POM_XML) || endsWithIgnoreCase(file, POM_EXTENSION) || endsWithIgnoreCase(file,
ZIP_EXTENSION));
}).collect(toList());
if (MODULE.equals(rootArtifactType)) {
File rootArtifactOutputFile = resolveRootArtifactFile(context.getRootArtifact());
if (rootArtifactOutputFile == null) {
throw new IllegalStateException("rootArtifact (" + context.getRootArtifact()
+ ") identified as MODULE but doesn't have an output");
}
containerUrls.add(0, toUrl(rootArtifactOutputFile));
}
resolveSnapshotVersionsToTimestampedFromClassPath(containerUrls, context.getClassPathURLs());
return containerUrls;
}
/**
* Creates the {@link Set} of {@link Dependency} to be used as managed dependencies when resolving Container dependencies. If
* the rootArtifact is a {@link ArtifactClassificationType#MODULE} it will use its managed dependencies, other case it collects
* managed dependencies for each direct dependencies selected for Container.
*
* @param context {@link ClassPathClassifierContext} with settings for the classification process
* @param directDependencies {@link List} of {@link Dependency} with direct dependencies for the rootArtifact
* @param rootArtifactType {@link ArtifactClassificationType} for rootArtifact
* @return {@link Set} of {@link Dependency} to be used as managed dependencies when resolving Container dependencies
*/
private Set<Dependency> selectContainerManagedDependencies(ClassPathClassifierContext context,
List<Dependency> directDependencies,
ArtifactClassificationType rootArtifactType) {
Set<Dependency> managedDependencies;
if (!rootArtifactType.equals(MODULE)) {
managedDependencies = directDependencies.stream()
.map(directDep -> {
try {
return dependencyResolver.readArtifactDescriptor(directDep.getArtifact()).getManagedDependencies();
} catch (ArtifactDescriptorException e) {
throw new IllegalStateException("Couldn't read artifact: '" + directDep.getArtifact() +
"' while collecting managed dependencies for Container", e);
}
})
.flatMap(l -> l.stream())
.collect(toSet());
} else {
try {
managedDependencies = newHashSet(dependencyResolver.readArtifactDescriptor(context.getRootArtifact())
.getManagedDependencies());
} catch (ArtifactDescriptorException e) {
throw new IllegalStateException("Couldn't collect managed dependencies for rootArtifact (" + context.getRootArtifact()
+ ")", e);
}
}
return managedDependencies;
}
/**
* Gets the direct dependencies filter to be used when collecting Container dependencies. If the rootArtifact is a
* {@link ArtifactClassificationType#MODULE} it will include {@value org.eclipse.aether.util.artifact.JavaScopes#COMPILE}
* dependencies too if not just {@value org.eclipse.aether.util.artifact.JavaScopes#PROVIDED}.
*
* @param rootArtifactType the {@link ArtifactClassificationType} for rootArtifact
* @return {@link Predicate} for selecting direct dependencies for the Container.
*/
private Predicate<Dependency> getContainerDirectDependenciesFilter(ArtifactClassificationType rootArtifactType) {
return rootArtifactType.equals(MODULE)
? directDep -> directDep.getScope().equals(PROVIDED) || directDep.getScope().equals(COMPILE)
: directDep -> directDep.getScope().equals(PROVIDED)
|| directDep.getArtifact().getClassifier().equals(MULE_PLUGIN_CLASSIFIER);
}
/**
* Plugin classifications are being done by resolving the dependencies for each plugin coordinates defined by the rootArtifact
* direct dependencies as {@value #MULE_SERVICE_CLASSIFIER}.
* <p/>
* While resolving the dependencies for the plugin artifact, only {@value org.eclipse.aether.util.artifact.JavaScopes#COMPILE}
* dependencies will be selected. {@link ClassPathClassifierContext#getExcludedArtifacts()} will be exluded too.
* <p/>
* The resulting {@link PluginUrlClassification} for each plugin will have as name the Maven artifact id coordinates:
* {@code <groupId>:<artifactId>:<extension>[:<classifier>]:<version>}.
*
* @param context {@link ClassPathClassifierContext} with settings for the classification process
* @param directDependencies {@link List} of {@link Dependency} with direct dependencies for the rootArtifact
* @param rootArtifactType {@link ArtifactClassificationType} for rootArtifact
* @return {@link List} of {@link PluginUrlClassification}s for plugins class loaders
*/
private List<PluginUrlClassification> buildPluginUrlClassifications(ClassPathClassifierContext context,
List<Dependency> directDependencies,
ArtifactClassificationType rootArtifactType) {
Map<String, ArtifactClassificationNode> pluginsClassified = newLinkedHashMap();
Artifact rootArtifact = context.getRootArtifact();
List<Artifact> pluginsArtifacts = directDependencies.stream()
.filter(dependency -> dependency.getArtifact().getClassifier().equals(MULE_PLUGIN_CLASSIFIER))
.map(dependency -> dependency.getArtifact())
.collect(toList());
logger.debug("{} plugins defined to be classified", pluginsArtifacts.size());
Predicate<Dependency> mulePluginDependencyFilter =
dependency -> dependency.getArtifact().getClassifier().equals(MULE_PLUGIN_CLASSIFIER);
if (PLUGIN.equals(rootArtifactType)) {
logger.debug("rootArtifact '{}' identified as Mule plugin", rootArtifact);
buildPluginUrlClassification(rootArtifact, context, mulePluginDependencyFilter, pluginsClassified);
pluginsArtifacts = pluginsArtifacts.stream()
.filter(pluginArtifact -> !(rootArtifact.getGroupId().equals(pluginArtifact.getGroupId())
&& rootArtifact.getArtifactId().equals(pluginArtifact.getArtifactId())))
.collect(toList());
}
pluginsArtifacts.stream()
.forEach(pluginArtifact -> buildPluginUrlClassification(pluginArtifact, context, mulePluginDependencyFilter,
pluginsClassified));
if (context.isExtensionMetadataGenerationEnabled()) {
ExtensionPluginMetadataGenerator extensionPluginMetadataGenerator =
new ExtensionPluginMetadataGenerator(context.getPluginResourcesFolder());
for (ArtifactClassificationNode pluginClassifiedNode : pluginsClassified.values()) {
List<URL> urls = generateExtensionMetadata(pluginClassifiedNode.getArtifact(), context, extensionPluginMetadataGenerator,
pluginClassifiedNode.getUrls());
pluginClassifiedNode.setUrls(urls);
}
extensionPluginMetadataGenerator.generateDslResources();
}
return toPluginUrlClassification(pluginsClassified.values());
}
/**
* Transforms the {@link ArtifactClassificationNode} to {@link ArtifactsUrlClassification}.
*
* @param classificationNodes the fat object classified that needs to be transformed
* @return {@link ArtifactsUrlClassification}
*/
private List<ArtifactUrlClassification> toServiceUrlClassification(Collection<ArtifactClassificationNode> classificationNodes) {
return classificationNodes.stream().map(node -> {
InputStream servicePropertiesStream =
new URLClassLoader(node.getUrls().toArray(new URL[0]), null).getResourceAsStream(SERVICE_PROPERTIES_FILE_NAME);
checkNotNull(servicePropertiesStream,
"Couldn't find " + SERVICE_PROPERTIES_FILE_NAME + " for artifact: " + node.getArtifact());
try {
Properties serviceProperties = loadProperties(servicePropertiesStream);
String serviceProviderClassName = serviceProperties.getProperty(SERVICE_PROVIDER_CLASS_NAME);
logger.debug("Discover serviceProviderClassName: {} for artifact: {}", serviceProviderClassName, node.getArtifact());
if (node.getExportClasses() != null && !node.getExportClasses().isEmpty()) {
logger.warn("exportClasses is not supported for services artifacts, they are going to be ignored");
}
return new ArtifactUrlClassification(toId(node.getArtifact()), serviceProviderClassName, node.getUrls());
} catch (IOException e) {
throw new IllegalArgumentException("Couldn't read " + SERVICE_PROPERTIES_FILE_NAME + " for artifact: "
+ node.getArtifact(), e);
}
}).collect(toList());
}
/**
* Transforms the {@link ArtifactClassificationNode} to {@link PluginUrlClassification}.
*
* @param classificationNodes the fat object classified that needs to be transformed
* @return {@link PluginUrlClassification}
*/
private List<PluginUrlClassification> toPluginUrlClassification(Collection<ArtifactClassificationNode> classificationNodes) {
Map<String, PluginUrlClassification> classifiedPluginUrls = newLinkedHashMap();
for (ArtifactClassificationNode node : classificationNodes) {
final List<String> pluginDependencies = node.getArtifactDependencies().stream()
.map(dependency -> toId(dependency.getArtifact()))
.collect(toList());
final String classifierLessId = toId(node.getArtifact());
final PluginUrlClassification pluginUrlClassification =
pluginResourcesResolver.resolvePluginResourcesFor(
new PluginUrlClassification(classifierLessId, node.getUrls(),
node.getExportClasses(),
pluginDependencies));
classifiedPluginUrls.put(classifierLessId, pluginUrlClassification);
}
for (PluginUrlClassification pluginUrlClassification : classifiedPluginUrls.values()) {
for (String dependency : pluginUrlClassification.getPluginDependencies()) {
final PluginUrlClassification dependencyPlugin = classifiedPluginUrls.get(dependency);
if (dependencyPlugin == null) {
throw new IllegalStateException("Unable to find a plugin dependency: " + dependency);
}
pluginUrlClassification.getExportedPackages().removeAll(dependencyPlugin.getExportedPackages());
}
}
return newArrayList(classifiedPluginUrls.values());
}
/**
* Classifies an {@link Artifact} recursively. {@value org.eclipse.aether.util.artifact.JavaScopes#COMPILE} dependencies will be
* resolved for building the {@link URL}'s for the class loader. Once classified the node is added to {@link Map} of
* artifactsClassified.
*
* @param artifactToClassify {@link Artifact} that represents the artifact to be classified
* @param context {@link ClassPathClassifierContext} with settings for the classification process
* @param artifactsClassified {@link Map} that contains already classified plugins
*/
private void buildPluginUrlClassification(Artifact artifactToClassify, ClassPathClassifierContext context,
Predicate<Dependency> directDependenciesFilter,
Map<String, ArtifactClassificationNode> artifactsClassified) {
List<URL> urls;
try {
List<Dependency> managedDependencies =
dependencyResolver.readArtifactDescriptor(artifactToClassify).getManagedDependencies();
final DependencyFilter dependencyFilter = andFilter(classpathFilter(COMPILE),
new PatternExclusionsDependencyFilter(context.getExcludedArtifacts()),
orFilter(new PatternExclusionsDependencyFilter("*:*:*:"
+ MULE_PLUGIN_CLASSIFIER + ":*"),
new PatternInclusionsDependencyFilter(toId(artifactToClassify))));
urls = toUrl(dependencyResolver.resolveDependencies(new Dependency(artifactToClassify, COMPILE),
Collections.<Dependency>emptyList(), managedDependencies,
dependencyFilter));
} catch (Exception e) {
throw new IllegalStateException("Couldn't resolve dependencies for artifact: '" + artifactToClassify + "' classification",
e);
}
List<Dependency> directDependencies;
List<ArtifactClassificationNode> artifactDependencies = newArrayList();
try {
directDependencies = dependencyResolver.getDirectDependencies(artifactToClassify);
} catch (ArtifactDescriptorException e) {
throw new IllegalStateException("Couldn't get direct dependencies for artifact: '" + artifactToClassify + "'", e);
}
logger.debug("Searching for dependencies on direct dependencies of artifact {}", artifactToClassify);
List<Artifact> pluginArtifactDependencies = filterArtifacts(directDependencies, directDependenciesFilter);
logger.debug("Artifacts {} identified a plugin dependencies for plugin {}", pluginArtifactDependencies, artifactToClassify);
pluginArtifactDependencies.stream()
.map(artifact -> {
String artifactClassifierLessId = toId(artifact);
if (!artifactsClassified.containsKey(artifactClassifierLessId)) {
buildPluginUrlClassification(artifact, context, directDependenciesFilter, artifactsClassified);
}
return artifactsClassified.get(artifactClassifierLessId);
})
.forEach(artifactDependencies::add);
final List<Class> exportClasses = getArtifactExportedClasses(artifactToClassify, context);
ArtifactClassificationNode artifactUrlClassification = new ArtifactClassificationNode(artifactToClassify,
urls,
exportClasses,
artifactDependencies);
artifactsClassified.put(toId(artifactToClassify), artifactUrlClassification);
}
/**
* Resolves the exported plugin classes from the given {@link Artifact}
*
* @param exporterArtifact {@link Artifact} used to resolve the exported classes
* @param context {@link ClassPathClassifierContext} with settings for the classification process
* @return {@link List} of {@link Class} that the given {@link Artifact} exports
*/
private List<Class> getArtifactExportedClasses(Artifact exporterArtifact, ClassPathClassifierContext context) {
final AtomicReference<URL> artifactUrl = new AtomicReference<>();
try {
artifactUrl.set(dependencyResolver.resolveArtifact(exporterArtifact).getArtifact().getFile().toURI().toURL());
} catch (MalformedURLException | ArtifactResolutionException e) {
throw new IllegalStateException("Unable to resolve artifact URL", e);
}
Artifact rootArtifact = context.getRootArtifact();
return context.getExportPluginClasses().stream()
.filter(clazz -> {
boolean isFromCurrentArtifact = clazz.getProtectionDomain().getCodeSource().getLocation().equals(artifactUrl.get());
if (isFromCurrentArtifact && exporterArtifact != rootArtifact) {
logger.warn("Exported class '{}' from plugin '{}' is being used from another artifact, {}", clazz.getSimpleName(),
exporterArtifact, rootArtifact);
}
return isFromCurrentArtifact;
})
.collect(toList());
}
/**
* Collects from the list of directDependencies {@link Dependency} those that are classified with classifier specified.
*
* @param directDependencies {@link List} of direct {@link Dependency}
* @return {@link List} of {@link Artifact}s for those dependencies classified as with the give classifier, can be empty.
*/
private List<Artifact> filterArtifacts(List<Dependency> directDependencies, Predicate<Dependency> filter) {
return directDependencies.stream()
.filter(dependency -> filter.test(dependency))
.map(dependency -> dependency.getArtifact())
.collect(toList());
}
/**
* Checks if the pluginArtifact {@link Artifact} is declared as direct dependency of the rootArtifact or if the pluginArtifact
* is the same rootArtifact. In case if it is a dependency and is not declared it throws an {@link IllegalStateException}.
*
* @param pluginArtifact plugin {@link Artifact} to be checked
* @param context {@link ClassPathClassifierContext} with settings for the classification process
* @param rootArtifactDirectDependencies {@link List} of {@link Dependency} with direct dependencies for the rootArtifact
* @throws {@link IllegalStateException} if the plugin is a dependency not declared in rootArtifact directDependencies
*/
private void checkPluginDeclaredAsDirectDependency(Artifact pluginArtifact, ClassPathClassifierContext context,
List<Dependency> rootArtifactDirectDependencies) {
if (!context.getRootArtifact().equals(pluginArtifact)) {
if (!findDirectDependency(pluginArtifact.getGroupId(), pluginArtifact.getArtifactId(), rootArtifactDirectDependencies)
.isPresent()) {
throw new IllegalStateException("Plugin '" + pluginArtifact
+ "' has to be defined as direct dependency of your Maven project (" + context.getRootArtifact() + ")");
}
}
}
/**
* If enabled generates the Extension metadata and returns the {@link List} of {@link URL}s with the folder were metadata is
* generated as first entry in the list.
*
* @param plugin plugin {@link Artifact} to generate its Extension metadata
* @param context {@link ClassPathClassifierContext} with settings for the classification process
* @param pluginGenerator {@link ExtensionPluginMetadataGenerator} extensions metadata generator
* @param urls current {@link List} of {@link URL}s classified for the plugin
* @return {@link List} of {@link URL}s classified for the plugin
*/
private List<URL> generateExtensionMetadata(Artifact plugin, ClassPathClassifierContext context,
ExtensionPluginMetadataGenerator pluginGenerator,
List<URL> urls) {
Class extensionClass = pluginGenerator.scanForExtensionAnnotatedClasses(plugin, urls);
if (extensionClass != null) {
logger.debug("Plugin '{}' has been discovered as Extension", plugin);
if (context.isExtensionMetadataGenerationEnabled()) {
File generatedMetadataFolder = pluginGenerator.generateExtensionResources(plugin, extensionClass, dependencyResolver);
URL generatedTestResources = toUrl(generatedMetadataFolder);
List<URL> appendedTestResources = newArrayList(generatedTestResources);
appendedTestResources.addAll(urls);
urls = appendedTestResources;
}
}
return urls;
}
/**
* Finds the direct {@link Dependency} from rootArtifact for the given groupId and artifactId.
*
* @param groupId of the artifact to be found
* @param artifactId of the artifact to be found
* @param directDependencies the rootArtifact direct {@link Dependency}s
* @return {@link Optional} {@link Dependency} to the dependency. Could be empty it if not present in the list of direct
* dependencies
*/
private Optional<Dependency> findDirectDependency(String groupId, String artifactId, List<Dependency> directDependencies) {
return directDependencies.isEmpty() ? Optional.<Dependency>empty()
: directDependencies.stream().filter(dependency -> dependency.getArtifact().getGroupId().equals(groupId)
&& dependency.getArtifact().getArtifactId().equals(artifactId)).findFirst();
}
/**
* Finds the plugin shared lib {@link Dependency} from the direct dependencies of the rootArtifact.
*
* @param pluginSharedLibCoords Maven coordinates that define the plugin shared lib artifact
* @param rootArtifact {@link Artifact} that defines the current artifact that requested to build this class loaders
* @param directDependencies {@link List} of {@link Dependency} with direct dependencies for the rootArtifact
* @return {@link Artifact} representing the plugin shared lib artifact
*/
private Dependency findPluginSharedLibArtifact(String pluginSharedLibCoords, Artifact rootArtifact,
List<Dependency> directDependencies) {
Optional<Dependency> pluginSharedLibDependency = discoverDependency(pluginSharedLibCoords, rootArtifact, directDependencies);
if (!pluginSharedLibDependency.isPresent() || !pluginSharedLibDependency.get().getScope().equals(TEST)) {
throw new IllegalStateException("Plugin shared lib artifact '" + pluginSharedLibCoords +
"' in order to be resolved has to be declared as " + TEST + " dependency of your Maven project (" + rootArtifact + ")");
}
return pluginSharedLibDependency.get();
}
/**
* Discovers the {@link Dependency} from the list of directDependencies using the artifact coordiantes in format of:
*
* <pre>
* groupId:artifactId
* </pre>
* <p/>
* If the coordinates matches to the rootArtifact it will return a {@value org.eclipse.aether.util.artifact.JavaScopes#COMPILE}
* {@link Dependency}.
*
* @param artifactCoords Maven coordinates that define the artifact dependency
* @param rootArtifact {@link Artifact} that defines the current artifact that requested to build this class loaders
* @param directDependencies {@link List} of {@link Dependency} with direct dependencies for the rootArtifact
* @return {@link Dependency} representing the artifact if declared as direct dependency or rootArtifact if they match it or
* {@link Optional#EMPTY} if couldn't found the dependency.
* @throws {@link IllegalArgumentException} if artifactCoords are not in the expected format
*/
public Optional<Dependency> discoverDependency(String artifactCoords, Artifact rootArtifact,
List<Dependency> directDependencies) {
final String[] artifactCoordsSplit = artifactCoords.split(MAVEN_COORDINATES_SEPARATOR);
if (artifactCoordsSplit.length != 2) {
throw new IllegalArgumentException("Artifact coordinates should be in format of groupId:artifactId, '" + artifactCoords +
"' is not a valid format");
}
String groupId = artifactCoordsSplit[0];
String artifactId = artifactCoordsSplit[1];
if (rootArtifact.getGroupId().equals(groupId) && rootArtifact.getArtifactId().equals(artifactId)) {
logger.debug("'{}' artifact coordinates matched with rootArtifact '{}', resolving version from rootArtifact",
artifactCoords, rootArtifact);
final DefaultArtifact artifact = new DefaultArtifact(groupId, artifactId, JAR_EXTENSION, rootArtifact.getVersion());
logger.debug("'{}' artifact coordinates resolved to: '{}'", artifactCoords, artifact);
return Optional.of(new Dependency(artifact, COMPILE));
} else {
logger.debug("Resolving version for '{}' from direct dependencies", artifactCoords);
return findDirectDependency(groupId, artifactId, directDependencies);
}
}
/**
* Application classification is being done by resolving the direct dependencies with scope
* {@value org.eclipse.aether.util.artifact.JavaScopes#TEST} for the rootArtifact. Due to Eclipse Aether resolution excludes by
* {@value org.eclipse.aether.util.artifact.JavaScopes#TEST} dependencies an imaginary pom will be created with these
* dependencies as {@value org.eclipse.aether.util.artifact.JavaScopes#COMPILE} so the dependency graph can be resolved (with
* the same results as it will be obtained from Maven).
* <p/>
* If the rootArtifact was classified as plugin its {@value org.eclipse.aether.util.artifact.JavaScopes#COMPILE} will be changed
* to {@value org.eclipse.aether.util.artifact.JavaScopes#PROVIDED} in order to exclude them from the dependency graph.
* <p/>
* Filtering logic includes the following pattern to includes the patterns defined at
* {@link ClassPathClassifierContext#getTestInclusions()}. It also excludes
* {@link ClassPathClassifierContext#getExcludedArtifacts()}, {@link ClassPathClassifierContext#getTestExclusions()}.
* <p/>
* If the application artifact has not been classified as plugin its going to be resolved as {@value #JAR_EXTENSION} in order to
* include this its compiled classes classification.
*
* @param context {@link ClassPathClassifierContext} with settings for the classification process
* @param directDependencies {@link List} of {@link Dependency} with direct dependencies for the rootArtifact
* @param rootArtifactType {@link ArtifactClassificationType} for rootArtifact @return {@link URL}s for application class loader
*/
private List<URL> buildApplicationUrlClassification(ClassPathClassifierContext context,
List<Dependency> directDependencies,
ArtifactClassificationType rootArtifactType) {
logger.debug("Building application classification");
Artifact rootArtifact = context.getRootArtifact();
DependencyFilter dependencyFilter = new PatternInclusionsDependencyFilter(context.getTestInclusions());
logger.debug("Using filter for dependency graph to include: '{}'", context.getTestInclusions());
List<File> appFiles = newArrayList();
List<String> exclusionsPatterns = newArrayList();
if (APPLICATION.equals(rootArtifactType)) {
logger.debug("RootArtifact identified as {} so is going to be added to application classification", APPLICATION);
File rootArtifactOutputFile = resolveRootArtifactFile(rootArtifact);
if (rootArtifactOutputFile != null) {
appFiles.add(rootArtifactOutputFile);
} else {
logger.warn("rootArtifact '{}' identified as {} but doesn't have an output {} file", rootArtifact, rootArtifactType,
JAR_EXTENSION);
}
} else {
logger.debug("RootArtifact already classified as plugin or module, excluding it from application classification");
exclusionsPatterns.add(rootArtifact.getGroupId() + MAVEN_COORDINATES_SEPARATOR + rootArtifact.getArtifactId() +
MAVEN_COORDINATES_SEPARATOR + "*" + MAVEN_COORDINATES_SEPARATOR + "*" + MAVEN_COORDINATES_SEPARATOR
+ rootArtifact.getVersion());
}
directDependencies = directDependencies.stream()
.map(toTransform -> {
if (toTransform.getScope().equals(TEST)) {
// TODO MULE-11332 Review other manifestations of this bug and add unit tests
return toTransform.setScope(COMPILE);
}
if (PLUGIN.equals(rootArtifactType) && toTransform.getScope().equals(COMPILE)) {
// TODO MULE-11332 Review other manifestations of this bug and add unit tests
return toTransform.setScope(PROVIDED);
}
return toTransform;
})
.collect(toList());
logger.debug("OR exclude: {}", context.getExcludedArtifacts());
exclusionsPatterns.addAll(context.getExcludedArtifacts());
if (!context.getTestExclusions().isEmpty()) {
logger.debug("OR exclude application specific artifacts: {}", context.getTestExclusions());
exclusionsPatterns.addAll(context.getTestExclusions());
}
try {
List<Dependency> managedDependencies =
newArrayList(dependencyResolver.readArtifactDescriptor(rootArtifact).getManagedDependencies());
managedDependencies.addAll(directDependencies.stream()
.filter(directDependency -> !directDependency.getScope().equals(TEST))
.collect(toList()));
logger.debug("Resolving dependency graph for '{}' scope direct dependencies: {} and managed dependencies {}",
TEST, directDependencies, managedDependencies);
final Dependency rootTestDependency = new Dependency(new DefaultArtifact(rootArtifact.getGroupId(),
rootArtifact.getArtifactId(), TESTS_CLASSIFIER,
JAR_EXTENSION,
rootArtifact.getVersion()),
TEST);
List<File> urls = dependencyResolver
.resolveDependencies(rootTestDependency, directDependencies, managedDependencies,
orFilter(dependencyFilter,
new PatternExclusionsDependencyFilter(exclusionsPatterns)));
appFiles
.addAll(urls);
} catch (Exception e) {
throw new IllegalStateException("Couldn't resolve dependencies for application '" + context.getRootArtifact()
+ "' classification", e);
}
List<URL> appUrls = newArrayList(toUrl(appFiles));
logger.debug("Appending URLs to application: {}", context.getApplicationUrls());
appUrls.addAll(context.getApplicationUrls());
return appUrls;
}
/**
* Resolves the rootArtifact {@value #JAR_EXTENSION} output {@link File}s to be added to class loader.
*
* @param rootArtifact {@link Artifact} being classified
* @return {@link File} to be added to class loader
*/
private File resolveRootArtifactFile(Artifact rootArtifact) {
final DefaultArtifact jarArtifact = new DefaultArtifact(rootArtifact.getGroupId(), rootArtifact.getArtifactId(),
JAR_EXTENSION, JAR_EXTENSION, rootArtifact.getVersion());
try {
return dependencyResolver.resolveArtifact(jarArtifact).getArtifact().getFile();
} catch (ArtifactResolutionException e) {
logger.warn("'{}' rootArtifact output {} file couldn't be resolved", rootArtifact, JAR_EXTENSION);
return null;
}
}
/**
* Converts the {@link List} of {@link File}s to {@link URL}s
*
* @param files {@link File} to get {@link URL}s
* @return {@link List} of {@link URL}s for the files
*/
private List<URL> toUrl(Collection<File> files) {
return files.stream().map(this::toUrl).collect(toList());
}
/**
* Converts the {@link File} to {@link URL}
*
* @param file {@link File} to get its {@link URL}
* @return {@link URL} for the file
*/
private URL toUrl(File file) {
try {
return file.toURI().toURL();
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Couldn't get URL", e);
}
}
/**
* Eclipse Aether is set to work in {@code offline} mode and to ignore artifact descriptors repositories the metadata for
* SNAPSHOTs versions cannot be read from remote repositories. So, it will always resolve SNAPSHOT dependencies as normalized,
* meaning that the resolved URL/File will have the SNAPSHOT format instead of timestamped one.
* <p/>
* At the same time IDEs or even Maven when running tests will resolve to timestamped versions instead, so we must do this
* "resolve" operation that matches SNAPSHOTs resolved artifacts to timestamped SNAPSHOT versions from classpath.
*
* @param resolvedURLs {@link URL}s resolved from the dependency graph
* @param classpathURLs {@link URL}s already provided in class path by IDE or Maven
*/
private void resolveSnapshotVersionsToTimestampedFromClassPath(List<URL> resolvedURLs, List<URL> classpathURLs) {
logger.debug("Checking if resolved SNAPSHOT URLs had a timestamped version already included in class path URLs");
Map<File, List<URL>> classpathFolders = groupArtifactUrlsByFolder(classpathURLs);
FileFilter snapshotFileFilter = new WildcardFileFilter(SNAPSHOT_WILCARD_FILE_FILTER);
ListIterator<URL> listIterator = resolvedURLs.listIterator();
while (listIterator.hasNext()) {
final URL urlResolved = listIterator.next();
File artifactResolvedFile = toFile(urlResolved);
if (snapshotFileFilter.accept(artifactResolvedFile)) {
File artifactResolvedFileParentFile = artifactResolvedFile.getParentFile();
logger.debug("Checking if resolved SNAPSHOT artifact: '{}' has a timestamped version already in class path",
artifactResolvedFile);
URL urlFromClassPath = null;
if (classpathFolders.containsKey(artifactResolvedFileParentFile)) {
urlFromClassPath = findArtifactUrlFromClassPath(classpathFolders, artifactResolvedFile);
}
if (urlFromClassPath != null) {
logger.debug("Replacing resolved URL '{}' from class path URL '{}'", urlResolved, urlFromClassPath);
listIterator.set(urlFromClassPath);
} else {
logger.error("'{}' resolved SNAPSHOT version couldn't be matched to a class path URL: '{}'", artifactResolvedFile,
classpathURLs);
throw new IllegalStateException(artifactResolvedFile
+ " resolved SNAPSHOT version couldn't be matched to a class path URL");
}
}
}
}
/**
* Creates a {@link Map} that has as key the folder that holds the artifact and value a {@link List} of {@link URL}s. For
* instance, an artifact in class path that only has its jar packaged output:
*
* <pre>
* key=/Users/jdoe/.m2/repository/org/mule/extensions/mule-extensions-api-xml-dsl/1.0.0-SNAPSHOT/
* value=[file:/Users/jdoe/.m2/repository/org/mule/extensions/mule-extensions-api-xml-dsl/1.0.0-SNAPSHOT/mule-extensions-api-xml-dsl-1.0.0-20160823.170911-32.jar]
* </pre>
* <p/>
* Another case is for those artifacts that have both packaged versions, the jar and the -tests.jar. For instance:
*
* <pre>
* key=/Users/jdoe/Development/mule/extensions/file/target
* value=[file:/Users/jdoe/.m2/repository/org/mule/modules/mule-module-file-extension-common/4.0-SNAPSHOT/mule-module-file-extension-common-4.0-SNAPSHOT.jar,
* file:/Users/jdoe/.m2/repository/org/mule/modules/mule-module-file-extension-common/4.0-SNAPSHOT/mule-module-file-extension-common-4.0-SNAPSHOT-tests.jar]
* </pre>
*
* @param classpathURLs the class path {@link List} of {@link URL}s to be grouped by folder
* @return {@link Map} that has as key the folder that holds the artifact and value a {@link List} of {@link URL}s.
*/
private Map<File, List<URL>> groupArtifactUrlsByFolder(List<URL> classpathURLs) {
Map<File, List<URL>> classpathFolders = newHashMap();
classpathURLs.forEach(url -> {
File folder = toFile(url).getParentFile();
if (classpathFolders.containsKey(folder)) {
classpathFolders.get(folder).add(url);
} else {
classpathFolders.put(folder, newArrayList(url));
}
});
return classpathFolders;
}
/**
* Finds the corresponding {@link URL} in class path grouped by folder {@link Map} for the given artifact {@link File}.
*
* @param classpathFolders a {@link Map} that has as entry the folder of the artifacts from class path and value a {@link List}
* with the artifacts (jar, tests.jar, etc).
* @param artifactResolvedFile the {@link Artifact} resolved from the Maven dependencies and resolved as SNAPSHOT
* @return {@link URL} for the artifact found in the class path or {@code null}
*/
private URL findArtifactUrlFromClassPath(Map<File, List<URL>> classpathFolders, File artifactResolvedFile) {
List<URL> urls = classpathFolders.get(artifactResolvedFile.getParentFile());
logger.debug("URLs found for '{}' in class path are: {}", artifactResolvedFile, urls);
if (urls.size() == 1) {
return urls.get(0);
}
// If more than one is found, we have to check for the case of a test-jar...
Optional<URL> urlOpt;
if (endsWithIgnoreCase(artifactResolvedFile.getName(), TESTS_JAR)) {
urlOpt = urls.stream().filter(url -> toFile(url).getAbsolutePath().endsWith(TESTS_JAR)).findFirst();
} else {
urlOpt = urls.stream()
.filter(url -> {
String filePath = toFile(url).getAbsolutePath();
return !filePath.endsWith(TESTS_JAR) && filePath.endsWith(JAR_EXTENSION);
}).findFirst();
}
return urlOpt.orElse(null);
}
}