Skip to content

Commit

Permalink
Fixes #307, addresses cyclic dependencies created by self references
Browse files Browse the repository at this point in the history
Signed-off-by: Kevin Conner <[email protected]>
  • Loading branch information
knrc committed Mar 14, 2023
1 parent a2754bf commit 3a4539e
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 16 deletions.
15 changes: 10 additions & 5 deletions src/main/java/org/cyclonedx/maven/DefaultModelConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,26 +105,31 @@ private String generatePackageUrl(final Artifact artifact, final boolean include
}

public String generatePackageUrl(final org.eclipse.aether.artifact.Artifact artifact) {
return generatePackageUrl(artifact, true);
return generatePackageUrl(artifact, true, true);
}

public String generateVersionlessPackageUrl(final org.eclipse.aether.artifact.Artifact artifact) {
return generatePackageUrl(artifact, false);
return generatePackageUrl(artifact, false, true);
}

public String generateClassifierlessPackageUrl(final org.eclipse.aether.artifact.Artifact artifact) {
return generatePackageUrl(artifact, true, false);
}

private boolean isEmpty(final String value) {
return (value == null) || (value.length() == 0);
}
private String generatePackageUrl(final org.eclipse.aether.artifact.Artifact artifact, final boolean includeVersion) {

private String generatePackageUrl(final org.eclipse.aether.artifact.Artifact artifact, final boolean includeVersion, final boolean includeClassifier) {
TreeMap<String, String> qualifiers = null;
final String type = artifact.getProperties().get(ArtifactProperties.TYPE);
final String classifier = artifact.getClassifier();
if (!isEmpty(type) || !isEmpty(classifier)) {
if (!isEmpty(type) || (includeClassifier && !isEmpty(classifier))) {
qualifiers = new TreeMap<>();
if (!isEmpty(type)) {
qualifiers.put("type", type);
}
if (!isEmpty(classifier)) {
if (includeClassifier && !isEmpty(classifier)) {
qualifiers.put("classifier", classifier);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ public Set<Dependency> extractBOMDependencies(MavenProject mavenProject, Scopes
// Generate the tree, removing excluded and filtered nodes
final Set<String> loggedReplacementPUrls = new HashSet<>();
final Set<String> loggedFilteredArtifacts = new HashSet<>();
buildDependencyGraphNode(dependencies, root, null, resolvedPUrls, loggedReplacementPUrls, loggedFilteredArtifacts);

buildDependencyGraphNode(dependencies, root, null, null, resolvedPUrls, loggedReplacementPUrls, loggedFilteredArtifacts);
} catch (DependencyCollectorBuilderException e) {
// When executing makeAggregateBom, some projects may not yet be built. Workaround is to warn on this
// rather than throwing an exception https://github.com/CycloneDX/cyclonedx-maven-plugin/issues/55
Expand Down Expand Up @@ -142,8 +143,9 @@ private boolean isExcludedNode(final DependencyNode node) {
return ((type == null) || excludeTypesSet.contains(type));
}

private void buildDependencyGraphNode(final Map<Dependency, Dependency> dependencies, DependencyNode node, final Dependency parent,
final Map<String, String> resolvedPUrls, final Set<String> loggedReplacementPUrls, final Set<String> loggedFilteredArtifacts) {
private void buildDependencyGraphNode(final Map<Dependency, Dependency> dependencies, DependencyNode node,
final Dependency parent, final String parentClassifierlessPUrl, final Map<String, String> resolvedPUrls,
final Set<String> loggedReplacementPUrls, final Set<String> loggedFilteredArtifacts) {
String purl = modelConverter.generatePackageUrl(node.getArtifact());

if (isExcludedNode(node) || (parent != null && isFilteredNode(node, loggedFilteredArtifacts))) {
Expand Down Expand Up @@ -177,8 +179,12 @@ private void buildDependencyGraphNode(final Map<Dependency, Dependency> dependen
if (parent != null) {
parent.addDependency(new Dependency(purl));
}
for (final DependencyNode childrenNode : node.getChildren()) {
buildDependencyGraphNode(dependencies, childrenNode, topDependency, resolvedPUrls, loggedReplacementPUrls, loggedFilteredArtifacts);

final String nodeClassifierlessPUrl = modelConverter.generateClassifierlessPackageUrl(node.getArtifact());
if (!nodeClassifierlessPUrl.equals(parentClassifierlessPUrl)) {
for (final DependencyNode childrenNode : node.getChildren()) {
buildDependencyGraphNode(dependencies, childrenNode, topDependency, nodeClassifierlessPUrl, resolvedPUrls, loggedReplacementPUrls, loggedFilteredArtifacts);
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/cyclonedx/maven/ModelConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public interface ModelConverter {

String generateVersionlessPackageUrl(final org.eclipse.aether.artifact.Artifact artifact);

String generateClassifierlessPackageUrl(final org.eclipse.aether.artifact.Artifact artifact);

/**
* Converts a Maven artifact (dependency or transitive dependency) into a
* CycloneDX component.
Expand Down
23 changes: 17 additions & 6 deletions src/test/java/org/cyclonedx/maven/BaseMavenVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,32 @@ protected static String fileRead(File file, boolean normalizeEOL) throws IOExcep
}

protected File cleanAndBuild(final String project, final String[] excludeTypes) throws Exception {
return cleanAndBuild(project, excludeTypes, null);
}

protected File cleanAndBuild(final String project, final String[] excludeTypes, final String[] profiles) throws Exception {
return mvnBuild(project, null, excludeTypes, null);
}

protected File mvnBuild(final String project, final String[] goals, final String[] excludeTypes, final String[] profiles) throws Exception {
File projDir = resources.getBasedir(project);

final MavenExecution initExecution = verifier
MavenExecution execution = verifier
.forProject(projDir)
.withCliOption("-Dcurrent.version=" + getCurrentVersion()) // inject cyclonedx-maven-plugin version
.withCliOption("-X") // debug
.withCliOption("-B");
final MavenExecution execution;
if ((excludeTypes != null) && (excludeTypes.length > 0)) {
execution = initExecution.withCliOption("-DexcludeTypes=" + String.join(",", excludeTypes));
execution = execution.withCliOption("-DexcludeTypes=" + String.join(",", excludeTypes));
}
if ((profiles != null) && (profiles.length > 0)) {
execution = execution.withCliOption("-P" + String.join(",", profiles));
}
if (goals != null && goals.length > 0) {
execution.execute(goals).assertErrorFreeLog();
} else {
execution = initExecution;
execution.execute("clean", "package").assertErrorFreeLog();
}
execution.execute("clean", "package")
.assertErrorFreeLog();
return projDir;
}
}
113 changes: 113 additions & 0 deletions src/test/java/org/cyclonedx/maven/CyclicTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package org.cyclonedx.maven;

import java.io.File;
import java.util.Set;

import static org.cyclonedx.maven.TestUtils.getComponentNode;
import static org.cyclonedx.maven.TestUtils.getDependencyNode;
import static org.cyclonedx.maven.TestUtils.getDependencyReferences;
import static org.cyclonedx.maven.TestUtils.readXML;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import io.takari.maven.testing.executor.MavenRuntime.MavenRuntimeBuilder;
import io.takari.maven.testing.executor.MavenVersions;
import io.takari.maven.testing.executor.junit.MavenJUnitTestRunner;

/**
* Test for cyclic dependencies on the same project
*/
@RunWith(MavenJUnitTestRunner.class)
@MavenVersions({"3.8.7"})
public class CyclicTest extends BaseMavenVerifier {
private static final String CYCLIC_A_DEPENDENCY = "pkg:maven/com.example.cyclic/[email protected]?type=jar";
private static final String CYCLIC_A_DEPENDENCY_CLASSIFIER_1 = "pkg:maven/com.example.cyclic/[email protected]?classifier=classifier_1&type=jar";
private static final String CYCLIC_A_DEPENDENCY_CLASSIFIER_2 = "pkg:maven/com.example.cyclic/[email protected]?classifier=classifier_2&type=jar";
private static final String CYCLIC_A_DEPENDENCY_CLASSIFIER_3 = "pkg:maven/com.example.cyclic/[email protected]?classifier=classifier_3&type=jar";

public CyclicTest(MavenRuntimeBuilder runtimeBuilder) throws Exception {
super(runtimeBuilder);
}

@Test
public void testCyclicDependency() throws Exception {
cleanAndBuild("cyclic", null);
File projDir = null;
try {
projDir = mvnBuild("cyclic", new String[]{"package"}, null, new String[] {"profile"});
} catch (final Exception ex) {
fail("Failed to generate SBOM", ex);
}

final Document bom = readXML(new File(projDir, "target/bom.xml"));

final NodeList componentsList = bom.getElementsByTagName("components");
assertEquals("Expected a single components element", 1, componentsList.getLength());
final Node components = componentsList.item(0);

final NodeList dependenciesList = bom.getElementsByTagName("dependencies");
assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength());
final Node dependencies = dependenciesList.item(0);

// BOM should contain pkg:maven/com.example.cyclic/[email protected]?classifier=classifier_1&type=jar
final Node cyclicAClassifier1ComponentNode = getComponentNode(components, CYCLIC_A_DEPENDENCY_CLASSIFIER_1);
assertNotNull("Missing cyclic_A:classifier_1:1.0.0 component", cyclicAClassifier1ComponentNode);

// BOM should contain pkg:maven/com.example.cyclic/[email protected]?classifier=classifier_2&type=jar
final Node cyclicAClassifier2ComponentNode = getComponentNode(components, CYCLIC_A_DEPENDENCY_CLASSIFIER_2);
assertNotNull("Missing cyclic_A:classifier_2:1.0.0 component", cyclicAClassifier2ComponentNode);

// BOM should contain pkg:maven/com.example.cyclic/[email protected]?classifier=classifier_3&type=jar
final Node cyclicAClassifier3ComponentNode = getComponentNode(components, CYCLIC_A_DEPENDENCY_CLASSIFIER_3);
assertNotNull("Missing cyclic_A:classifier_3:1.0.0 component", cyclicAClassifier3ComponentNode);

/*
<dependency ref="pkg:maven/com.example.cyclic/[email protected]?type=jar">
<dependency ref="pkg:maven/com.example.cyclic/[email protected]?classifier=classifier_1&amp;type=jar"/>
<dependency ref="pkg:maven/com.example.cyclic/[email protected]?classifier=classifier_2&amp;type=jar"/>
<dependency ref="pkg:maven/com.example.cyclic/[email protected]?classifier=classifier_3&amp;type=jar"/>
</dependency>
*/
final Node cyclicADependencyNode = getDependencyNode(dependencies, CYCLIC_A_DEPENDENCY);
assertNotNull("Missing cyclic_A:1.0.0 dependency", cyclicADependencyNode);
Set<String> cyclicADependencies = getDependencyReferences(cyclicADependencyNode);
assertEquals("Invalid dependency count for cyclic_A:1.0.0", 3, cyclicADependencies.size());
assertTrue("Missing cyclic_A:classifier_1:1.0.0 dependency for cyclic_A:1.0.0", cyclicADependencies.contains(CYCLIC_A_DEPENDENCY_CLASSIFIER_1));
assertTrue("Missing cyclic_A:classifier_2:1.0.0 dependency for cyclic_A:1.0.0", cyclicADependencies.contains(CYCLIC_A_DEPENDENCY_CLASSIFIER_2));
assertTrue("Missing cyclic_A:classifier_3:1.0.0 dependency for cyclic_A:1.0.0", cyclicADependencies.contains(CYCLIC_A_DEPENDENCY_CLASSIFIER_3));

/*
<dependency ref="pkg:maven/com.example.cyclic/[email protected]?classifier=classifier_1&amp;type=jar"/>
*/
final Node cyclicAClassifier1DependencyNode = getDependencyNode(dependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_1);
assertNotNull("Missing cyclic_A:classifier_1:1.0.0 dependency", cyclicAClassifier1DependencyNode);
Set<String> cyclicAClassifier1Dependencies = getDependencyReferences(cyclicAClassifier1DependencyNode);
assertEquals("Invalid dependency count for cyclic_A:classifier_1:1.0.0", 0, cyclicAClassifier1Dependencies.size());

/*
<dependency ref="pkg:maven/com.example.cyclic/[email protected]?classifier=classifier_2&amp;type=jar"/>
*/
final Node cyclicAClassifier2DependencyNode = getDependencyNode(dependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_2);
assertNotNull("Missing cyclic_A:classifier_2:1.0.0 dependency", cyclicAClassifier2DependencyNode);
Set<String> cyclicAClassifier2Dependencies = getDependencyReferences(cyclicAClassifier2DependencyNode);
assertEquals("Invalid dependency count for cyclic_A:classifier_2:1.0.0", 0, cyclicAClassifier2Dependencies.size());

/*
<dependency ref="pkg:maven/com.example.cyclic/[email protected]?classifier=classifier_3&amp;type=jar"/>
*/
final Node cyclicAClassifier3DependencyNode = getDependencyNode(dependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_3);
assertNotNull("Missing cyclic_A:classifier_3:1.0.0 dependency", cyclicAClassifier3DependencyNode);
Set<String> cyclicAClassifier3Dependencies = getDependencyReferences(cyclicAClassifier3DependencyNode);
assertEquals("Invalid dependency count for cyclic_A:classifier_3:1.0.0", 0, cyclicAClassifier3Dependencies.size());
}
}
150 changes: 150 additions & 0 deletions src/test/resources/cyclic/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>com.example.cyclic</groupId>
<artifactId>cyclic_A</artifactId>
<version>1.0.0</version>

<name>Cyclic Dependency A</name>

<profiles>
<profile>
<id>profile</id>
<dependencies>
<dependency>
<groupId>com.example.cyclic</groupId>
<artifactId>cyclic_A</artifactId>
<version>1.0.0</version>
<classifier>classifier_1</classifier>
</dependency>
<dependency>
<groupId>com.example.cyclic</groupId>
<artifactId>cyclic_A</artifactId>
<version>1.0.0</version>
<classifier>classifier_2</classifier>
</dependency>
<dependency>
<groupId>com.example.cyclic</groupId>
<artifactId>cyclic_A</artifactId>
<version>1.0.0</version>
<classifier>classifier_3</classifier>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>${current.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
<configuration>
<projectType>library</projectType>
<schemaVersion>1.4</schemaVersion>
<includeBomSerialNumber>true</includeBomSerialNumber>
<includeCompileScope>true</includeCompileScope>
<includeProvidedScope>true</includeProvidedScope>
<includeRuntimeScope>true</includeRuntimeScope>
<includeSystemScope>true</includeSystemScope>
<includeTestScope>false</includeTestScope>
<includeLicenseText>false</includeLicenseText>
<outputFormat>xml</outputFormat>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>classifier_1</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<classifier>classifier_1</classifier>
</configuration>
</execution>
<execution>
<id>classifier_2</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<classifier>classifier_2</classifier>
</configuration>
</execution>
<execution>
<id>classifier_3</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<classifier>classifier_3</classifier>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>classifier_1</id>
<phase>package</phase>
<goals>
<goal>install-file</goal>
</goals>
<configuration>
<classifier>classifier_1</classifier>
<file>${project.build.directory}/cyclic_A-1.0.0-classifier_1.jar</file>
</configuration>
</execution>
<execution>
<id>classifier_2</id>
<phase>package</phase>
<goals>
<goal>install-file</goal>
</goals>
<configuration>
<classifier>classifier_2</classifier>
<file>${project.build.directory}/cyclic_A-1.0.0-classifier_2.jar</file>
</configuration>
</execution>
<execution>
<id>classifier_3</id>
<phase>package</phase>
<goals>
<goal>install-file</goal>
</goals>
<configuration>
<classifier>classifier_3</classifier>
<file>${project.build.directory}/cyclic_A-1.0.0-classifier_3.jar</file>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

0 comments on commit 3a4539e

Please sign in to comment.