Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 110 additions & 7 deletions src/main/java/org/codehaus/mojo/flatten/FlattenMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Queue;
import java.util.Set;
Expand Down Expand Up @@ -84,15 +87,17 @@
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositoryException;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.RequestTrace;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.collection.CollectResult;
import org.eclipse.aether.collection.DependencyCollectionException;
import org.eclipse.aether.collection.DependencyGraphTransformationContext;
import org.eclipse.aether.collection.DependencyGraphTransformer;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.graph.DependencyVisitor;
import org.eclipse.aether.resolution.ArtifactDescriptorException;
import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
import org.eclipse.aether.resolution.ArtifactDescriptorResult;
import org.eclipse.aether.util.artifact.JavaScopes;
Expand Down Expand Up @@ -1103,14 +1108,13 @@ private void createFlattenedDependenciesDirect(
*
* @param projectDependencies is the effective POM {@link Model}'s current dependencies
* @param flattenedDependencies is the {@link List} where to add the collected {@link Dependency dependencies}.
* @throws DependencyCollectionException
* @throws ArtifactDescriptorException
* @throws RepositoryException throws on conflict
*/
private void createFlattenedDependenciesAll(
List<Dependency> projectDependencies,
List<Dependency> managedDependencies,
List<Dependency> flattenedDependencies)
throws ArtifactDescriptorException, DependencyCollectionException {
throws RepositoryException {
final Queue<DependencyNode> dependencyNodeLinkedList = new LinkedList<>();
final Set<String> processedDependencies = new HashSet<>();
final Artifact projectArtifact = this.project.getArtifact();
Expand All @@ -1123,8 +1127,9 @@ private void createFlattenedDependenciesAll(
dependency, session.getRepositorySession().getArtifactTypeRegistry()));
}

for (Artifact artifact : project.getArtifacts()) {
collectRequest.addDependency(RepositoryUtils.toDependency(artifact, null));
for (Dependency dependency : project.getDependencies()) {
collectRequest.addDependency(RepositoryUtils.toDependency(
dependency, session.getRepositorySession().getArtifactTypeRegistry()));
}

for (Dependency dependency : managedDependencies) {
Expand All @@ -1139,6 +1144,10 @@ private void createFlattenedDependenciesAll(
CollectResult collectResult = repositorySystem.collectDependencies(derived, collectRequest);

final DependencyNode root = collectResult.getRoot();

removeTestDependencies(root);
resolveConflicts(derived, root);

final Set<String> directDependencyKeys = Stream.concat(
projectDependencies.stream().map(this::getKey),
project.getArtifacts().stream().map(this::getKey))
Expand All @@ -1150,6 +1159,12 @@ public boolean visitEnter(DependencyNode node) {
if (root == node) {
return true;
}

if (node.getData().containsKey(ConflictResolver.NODE_DATA_WINNER)) {
// if this node has a conflict winner in data, it means this node lost in conflict
return false; // skip lost conflicts
}

if (JavaScopes.PROVIDED.equals(node.getDependency().getScope())) {
String dependencyKey = getKey(node.getDependency());
if (!directDependencyKeys.contains(dependencyKey)) {
Expand Down Expand Up @@ -1323,6 +1338,56 @@ public boolean isUpdatePomFile() {
}
}

/**
* Recursively removes all test-scoped dependencies from the given dependency node.
*
* @param node the root dependency node
*/
private void removeTestDependencies(DependencyNode node) {
Iterator<DependencyNode> it = node.getChildren().iterator();
while (it.hasNext()) {
DependencyNode child = it.next();
org.eclipse.aether.graph.Dependency dep = child.getDependency();
if (dep != null && "test".equals(dep.getScope())) {
it.remove();
} else {
removeTestDependencies(child);
}
}
}

/**
* Recursively clears previous conflict resolution data from the dependency tree.
*
* @param node the root dependency node
*/
private void clearPreviousConflictResolution(DependencyNode node) {
Iterator<DependencyNode> it = node.getChildren().iterator();
while (it.hasNext()) {
DependencyNode child = it.next();
child.getData().clear();
clearPreviousConflictResolution(child);
}
}

/**
* Resolves version conflicts in the dependency tree rooted at the given node.
*
* @param derived the repository system session
* @param root the root dependency node
* @throws RepositoryException if an error occurs during conflict resolution
*/
private void resolveConflicts(RepositorySystemSession derived, final DependencyNode root)
throws RepositoryException {
clearPreviousConflictResolution(root);

DefaultDependencyGraphTransformationContext transformationContext =
new DefaultDependencyGraphTransformationContext(derived);

DependencyGraphTransformer transformer = derived.getDependencyGraphTransformer();
transformer.transformGraph(root, transformationContext);
}

private class ModelsFactory {
private final File pomFile;
private Model effectivePom;
Expand Down Expand Up @@ -1468,4 +1533,42 @@ public void startElement(String uri, String localName, String qName, Attributes
this.rootTagSeen = true;
}
}

/**
* Default implementation of {@link DependencyGraphTransformationContext}.
* As maven libraries do not expose an implementation, we need to provide our own.
*/
class DefaultDependencyGraphTransformationContext implements DependencyGraphTransformationContext {

private final RepositorySystemSession session;

private final Map<Object, Object> map;

DefaultDependencyGraphTransformationContext(RepositorySystemSession session) {
this.session = session;
this.map = new HashMap<>();
}

public RepositorySystemSession getSession() {
return session;
}

public Object get(Object key) {
return map.get(Objects.requireNonNull(key, "key cannot be null"));
}

public Object put(Object key, Object value) {
Objects.requireNonNull(key, "key cannot be null");
if (value != null) {
return map.put(key, value);
} else {
return map.remove(key);
}
}

@Override
public String toString() {
return String.valueOf(map);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package org.codehaus.mojo.flatten;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.maven.execution.DefaultMavenExecutionRequest;
import org.apache.maven.execution.DefaultMavenExecutionResult;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenExecutionResult;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.testing.MojoRule;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.project.ProjectBuildingResult;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.LocalRepositoryManager;
import org.eclipse.aether.repository.NoLocalRepositoryManagerException;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Test-Case for {@link FlattenMojo}.
*/
public class FlattenMojoConflictWinnerTest {

private static final String PATH = "src/test/resources/conflict-winner/";
private static final String POM = PATH + "pom.xml";
private static final String FLATTENED_POM = PATH + ".flattened-pom.xml";

private static final String REPO_PATH = "src/test/resources/conflict-winner/repository/";

@Rule
public MojoRule rule = new MojoRule();

/**
* Test method to check that version conflict are correctly honored. Two levels
* of dependencies are defined for this test:
* <ul>
* <li>A depends on B 0.0.1</li>
* <li>B 0.0.2</li>
* </ul>
*
* The tested pom depends on A and B. It is then expected that B use version
* 0.0.2 in the flattened pom because B 0.0.2 is closer to the root.
*
* 1.7.3 of the plugin was handling this case correctly but since solving #408
* it is not the case anymore. By removing resolved transitive dependencies from
* the collect request parameters, the conflict resolution is not applied anymore
* as resolved transitive dependencies does not appear first anymore. This test ensure
* that the conflicting dependencies are correctly filtered from collect
* results and the winner version is kept.
*
* @see <a href=
* "https://github.com/mojohaus/flatten-maven-plugin/issues/408">Issue
* #408</a>
*
* @since 1.7.3+
* @throws Exception if something goes wrong.
*/
@Test
public void testConflictResolutionIsEnforced() throws Exception {

MavenSession session = newMavenSession(REPO_PATH);
MavenProject project = loadResolvedProject(session, new File(POM));
FlattenMojo flattenMojo = (FlattenMojo) rule.lookupConfiguredMojo(project, "flatten");
rule.setVariableValueToObject(flattenMojo, "session", session);

flattenMojo.execute();

MavenProject flattenedProject = loadResolvedProject(session, new File(FLATTENED_POM));

List<Dependency> bDependencies = flattenedProject.getDependencies().stream()
.filter(dep -> dep.getArtifactId().equals("b"))
.collect(Collectors.toList());

assertThat(bDependencies)
.hasSize(1)
.withFailMessage("There must be only one B dependency in flattened POM.")
.allMatch(
dep -> dep.getVersion().equals("0.0.2"),
"B dependency version 0.0.2 must win the conflicting version match in flattened POM.");
}

/**
* Load a maven project with resolved dependencies (same as standard maven execution).
* By default dependencies are not resolved by MojoRule and project.getArtifacts is empty
* @param session the Maven session to use for building the project
* @param pomFile the POM file to load and resolve dependencies for
* @return the resolved MavenProject instance with dependencies
* @throws ComponentLookupException if the ProjectBuilder component cannot be found
* @throws ProjectBuildingException if an error occurs while building the project
*/
private MavenProject loadResolvedProject(MavenSession session, File pomFile)
throws ComponentLookupException, ProjectBuildingException {
ProjectBuilder projectBuilder = rule.lookup(ProjectBuilder.class);

ProjectBuildingRequest buildingRequest = session.getProjectBuildingRequest();
buildingRequest.setResolveDependencies(true);

ProjectBuildingResult result = projectBuilder.build(pomFile, buildingRequest);
return result.getProject();
}

/**
* Create a new maven session with a local repository at the given path.
* @param repoPath the path to the local repository to use for the session
* @return a new MavenSession instance configured with the specified local repository
* @throws NoLocalRepositoryManagerException if the local repository manager cannot be created
*/
protected MavenSession newMavenSession(String repoPath) throws NoLocalRepositoryManagerException {
MavenExecutionRequest request = new DefaultMavenExecutionRequest();
MavenExecutionResult result = new DefaultMavenExecutionResult();

MavenSession session =
new MavenSession(rule.getContainer(), MavenRepositorySystemUtils.newSession(), request, result);
LocalRepository testRepo = new LocalRepository(repoPath);
LocalRepositoryManager lrm =
new SimpleLocalRepositoryManagerFactory().newInstance(session.getRepositorySession(), testRepo);

((DefaultRepositorySystemSession) session.getRepositorySession()).setLocalRepositoryManager(lrm);
return session;
}

/**
* After test method. Removes flattened-pom.xml file which is created during test.
*
* @throws IOException if can't remove file.
*/
@After
public void removeFlattenedPom() throws IOException {
File flattenedPom = new File(FLATTENED_POM);
if (flattenedPom.exists()) {
if (!flattenedPom.delete()) {
throw new IOException("Can't delete " + flattenedPom);
}
}
}
}
Loading
Loading