Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #307, addresses cyclic dependencies created by self references #308

Merged
merged 1 commit into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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>