/*
* 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.maven;
import static com.google.common.collect.Lists.newArrayList;
import static java.lang.System.getProperty;
import static java.nio.file.FileVisitResult.CONTINUE;
import static java.nio.file.FileVisitResult.SKIP_SUBTREE;
import static java.nio.file.Files.walkFileTree;
import static java.nio.file.Paths.get;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.io.FileUtils.toFile;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.mule.runtime.api.util.Preconditions.checkNotNull;
import static org.mule.runtime.core.util.StringMessageUtils.DEFAULT_MESSAGE_WIDTH;
import static org.mule.runtime.core.util.StringMessageUtils.getBoilerPlate;
import org.mule.test.runner.api.WorkspaceLocationResolver;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.maven.model.Model;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Discovers Maven projects from the rootArtifactClassesFolder folder and Maven variable
* {@value #MAVEN_MULTI_MODULE_PROJECT_DIRECTORY} (if present) to define the root project directory.
* <p/>
* Matches each Maven project found with the class path in order to check if it is part of the build session. When
* {@value #MAVEN_MULTI_MODULE_PROJECT_DIRECTORY} is not present, meaning that this not running under a Maven build session,
* Workspace references are resolved by filtering class path {@link URL}s if they reference to a Maven project (a target/classes
* or target/test-classes/ folders).
* <p/>
* If Maven surefire plugin is used to run test in Maven and the plugin has been configured with {@code forkMode=always} the
* following Maven system property has to be propagated on surefire configuration:
*
* <pre>
* <systemPropertyVariables>
* <maven.multiModuleProjectDirectory>${maven.multiModuleProjectDirectory}</maven.multiModuleProjectDirectory>
* </systemPropertyVariables>
* </pre>
* <p/>
*
* @since 4.0
*/
public class AutoDiscoverWorkspaceLocationResolver implements WorkspaceLocationResolver {
public static final String POM_XML_FILE = "pom.xml";
public static final String MAVEN_MULTI_MODULE_PROJECT_DIRECTORY = "maven.multiModuleProjectDirectory";
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
private Map<String, File> filePathByArtifactId = new HashMap<>();
/**
* Creates an instance of this class.
*
* @param classPath {@link URL}'s defined in class path
* @throws IllegalArgumentException if the rootArtifactClassesFolder doesn't point to a Maven project.
*/
public AutoDiscoverWorkspaceLocationResolver(List<URL> classPath, File rootArtifactClassesFolder) {
checkNotNull(classPath, "classPath cannot be null");
checkNotNull(rootArtifactClassesFolder, "rootArtifactClassesFolder cannot be null");
File rootArtifactFolder = rootArtifactClassesFolder.getParentFile().getParentFile();
logger.debug("Discovering workspace artifacts locations from '{}'", rootArtifactFolder);
if (!containsMavenProject(rootArtifactFolder)) {
logger.warn("Couldn't find any workspace reference for artifacts due to '{}' is not a Maven project", rootArtifactFolder);
}
String rootProjectDirectoryProperty = getProperty(MAVEN_MULTI_MODULE_PROJECT_DIRECTORY);
if (isNotBlank(rootProjectDirectoryProperty)) {
logger.debug("Using Maven System.property['{}']='{}' to find out project root directory for discovering poms",
MAVEN_MULTI_MODULE_PROJECT_DIRECTORY, rootProjectDirectoryProperty);
discoverMavenReactorProjects(rootProjectDirectoryProperty, classPath,
rootArtifactClassesFolder.getParentFile().getParentFile());
} else {
logger.debug("Filtering class path entries to find out Maven projects");
discoverMavenProjectsFromClassPath(classPath);
}
logger.debug("Workspace location discover process completed");
List<String> messages = newArrayList("Workspace:");
messages.add(" ");
messages.addAll(filePathByArtifactId.keySet());
logger.debug(getBoilerPlate(newArrayList(messages), '*', DEFAULT_MESSAGE_WIDTH));
}
/**
* Traverses the directory tree from the {@value #MAVEN_MULTI_MODULE_PROJECT_DIRECTORY} property to look for Maven projects that
* are also listed as entries in class path.
*
* @param rootProjectDirectoryProperty the root directory of the multi-module project build session
* @param classPath the whole class path built by IDE or Maven (surefire Maven plugin)
* @param rootArtifactClassesFolder the current rootArtifact directory
*/
private void discoverMavenReactorProjects(String rootProjectDirectoryProperty, List<URL> classPath,
File rootArtifactClassesFolder) {
Path rootProjectDirectory = get(rootProjectDirectoryProperty);
logger.debug("Defined rootProjectDirectory='{}'", rootProjectDirectory);
File currentDir = rootArtifactClassesFolder;
File lastMavenProjectDir = currentDir;
while (containsMavenProject(currentDir) && !currentDir.toPath().equals(rootProjectDirectory.getParent())) {
lastMavenProjectDir = currentDir;
currentDir = currentDir.getParentFile();
}
logger.debug("Top folder found, parent pom found at: '{}'", lastMavenProjectDir);
try {
walkFileTree(lastMavenProjectDir.toPath(), new MavenDiscovererFileVisitor(classPath));
} catch (IOException e) {
throw new RuntimeException("Error while discovering Maven projects from path: " + currentDir.toPath());
}
}
/**
* Discovers Maven projects by searching in class path provided by IDE or Maven (surefire Maven plugin) by looking at those
* {@link URL}s that have a {@value #POM_XML_FILE} in its {@code url.toFile.getParent.getParent}, because reference between
* modules in IDE should be like the following:
*
* <pre>
* /Users/jdoe/Development/mule/extensions/file/target/test-classes
* /Users/jdoe/Development/mule/extensions/file/target/classes
* /Users/jdoe/Development/mule/core/target/classes
* </pre>
*
* @param classPath
*/
private void discoverMavenProjectsFromClassPath(List<URL> classPath) {
List<Path> classPaths = classPath.stream().map(url -> toFile(url).toPath()).collect(toList());
List<File> mavenProjects = classPaths.stream()
.filter(path -> containsMavenProject(path.getParent().getParent().toFile()))
.map(path -> path.getParent().getParent().toFile()).collect(toList());
logger.debug("Filtered from class path Maven projects: {}", mavenProjects);
mavenProjects.stream().forEach(file -> resolvedArtifact(readMavenPomFile(getPomFile(file)).getArtifactId(), file.toPath()));
}
/**
* {@inheritDoc}
*/
@Override
public File resolvePath(String artifactId) {
return filePathByArtifactId.get(artifactId);
}
/**
* Reads the Maven pom file to get build the {@link Model}.
*
* @param pomFile to be read
* @return {@link Model} represeting the Maven project
*/
private Model readMavenPomFile(File pomFile) {
MavenXpp3Reader mavenReader = new MavenXpp3Reader();
try (FileReader reader = new FileReader(pomFile)) {
return mavenReader.read(reader);
} catch (Exception e) {
throw new RuntimeException("Error while reading Maven model from " + pomFile, e);
}
}
/**
* Creates a {@link File} for the {@value #POM_XML_FILE} in the given directory
*
* @param currentDir to create a {@value #POM_XML_FILE}
* @return {@link File} to the {@value #POM_XML_FILE} in the give directory
*/
private File getPomFile(File currentDir) {
return new File(currentDir, POM_XML_FILE);
}
/**
* @param dir {@link File} directory to check if it has a {@value #POM_XML_FILE}
* @return true if the directory contains a {@value #POM_XML_FILE}
*/
private boolean containsMavenProject(File dir) {
return dir.isDirectory() && getPomFile(dir).exists();
}
/**
* @param file {@link File} to check if it a {@value #POM_XML_FILE}
* @return true if the file is a {@value #POM_XML_FILE}
*/
private boolean isPomFile(File file) {
return file.getName().equalsIgnoreCase(POM_XML_FILE);
}
/**
* Adds the resolved artifact with its path.
*
* @param artifactId the Maven artifactId found in workspace
* @param path the {@link Path} location to the artifactId
*/
private void resolvedArtifact(String artifactId, Path path) {
logger.trace("Resolved artifactId from workspace at {}={}", artifactId, path);
filePathByArtifactId.put(artifactId, path.toFile());
}
/**
* Looks for directories that contain a {@value #POM_XML_FILE} file so it will be added to the resolved artifacts locations.
*/
private class MavenDiscovererFileVisitor implements FileVisitor<Path> {
private List<Path> classPath;
public MavenDiscovererFileVisitor(List<URL> urlClassPath) {
this.classPath =
urlClassPath.stream().map(url -> toFile(url).getParentFile().getParentFile().toPath()).collect(toList());
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return getPomFile(dir.toFile()).exists() ? CONTINUE : SKIP_SUBTREE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (isPomFile(file.toFile())) {
Model model = readMavenPomFile(file.toFile());
Path location = file.getParent();
logger.debug("Checking if location {} is already present in class path", location);
if (this.classPath.contains(location)) {
resolvedArtifact(model.getArtifactId(), location);
}
}
return CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return CONTINUE;
}
}
}