Skip to content

Commit 3d16603

Browse files
authored
Merge pull request #308 from knrc/cyclic_dependency
Fixes #307, addresses cyclic dependencies created by self references
2 parents a2754bf + 3a4539e commit 3d16603

File tree

6 files changed

+303
-16
lines changed

6 files changed

+303
-16
lines changed

src/main/java/org/cyclonedx/maven/DefaultModelConverter.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,26 +105,31 @@ private String generatePackageUrl(final Artifact artifact, final boolean include
105105
}
106106

107107
public String generatePackageUrl(final org.eclipse.aether.artifact.Artifact artifact) {
108-
return generatePackageUrl(artifact, true);
108+
return generatePackageUrl(artifact, true, true);
109109
}
110110

111111
public String generateVersionlessPackageUrl(final org.eclipse.aether.artifact.Artifact artifact) {
112-
return generatePackageUrl(artifact, false);
112+
return generatePackageUrl(artifact, false, true);
113+
}
114+
115+
public String generateClassifierlessPackageUrl(final org.eclipse.aether.artifact.Artifact artifact) {
116+
return generatePackageUrl(artifact, true, false);
113117
}
114118

115119
private boolean isEmpty(final String value) {
116120
return (value == null) || (value.length() == 0);
117121
}
118-
private String generatePackageUrl(final org.eclipse.aether.artifact.Artifact artifact, final boolean includeVersion) {
122+
123+
private String generatePackageUrl(final org.eclipse.aether.artifact.Artifact artifact, final boolean includeVersion, final boolean includeClassifier) {
119124
TreeMap<String, String> qualifiers = null;
120125
final String type = artifact.getProperties().get(ArtifactProperties.TYPE);
121126
final String classifier = artifact.getClassifier();
122-
if (!isEmpty(type) || !isEmpty(classifier)) {
127+
if (!isEmpty(type) || (includeClassifier && !isEmpty(classifier))) {
123128
qualifiers = new TreeMap<>();
124129
if (!isEmpty(type)) {
125130
qualifiers.put("type", type);
126131
}
127-
if (!isEmpty(classifier)) {
132+
if (includeClassifier && !isEmpty(classifier)) {
128133
qualifiers.put("classifier", classifier);
129134
}
130135
}

src/main/java/org/cyclonedx/maven/DefaultProjectDependenciesConverter.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ public Set<Dependency> extractBOMDependencies(MavenProject mavenProject, Scopes
8787
// Generate the tree, removing excluded and filtered nodes
8888
final Set<String> loggedReplacementPUrls = new HashSet<>();
8989
final Set<String> loggedFilteredArtifacts = new HashSet<>();
90-
buildDependencyGraphNode(dependencies, root, null, resolvedPUrls, loggedReplacementPUrls, loggedFilteredArtifacts);
90+
91+
buildDependencyGraphNode(dependencies, root, null, null, resolvedPUrls, loggedReplacementPUrls, loggedFilteredArtifacts);
9192
} catch (DependencyCollectorBuilderException e) {
9293
// When executing makeAggregateBom, some projects may not yet be built. Workaround is to warn on this
9394
// rather than throwing an exception https://github.com/CycloneDX/cyclonedx-maven-plugin/issues/55
@@ -142,8 +143,9 @@ private boolean isExcludedNode(final DependencyNode node) {
142143
return ((type == null) || excludeTypesSet.contains(type));
143144
}
144145

145-
private void buildDependencyGraphNode(final Map<Dependency, Dependency> dependencies, DependencyNode node, final Dependency parent,
146-
final Map<String, String> resolvedPUrls, final Set<String> loggedReplacementPUrls, final Set<String> loggedFilteredArtifacts) {
146+
private void buildDependencyGraphNode(final Map<Dependency, Dependency> dependencies, DependencyNode node,
147+
final Dependency parent, final String parentClassifierlessPUrl, final Map<String, String> resolvedPUrls,
148+
final Set<String> loggedReplacementPUrls, final Set<String> loggedFilteredArtifacts) {
147149
String purl = modelConverter.generatePackageUrl(node.getArtifact());
148150

149151
if (isExcludedNode(node) || (parent != null && isFilteredNode(node, loggedFilteredArtifacts))) {
@@ -177,8 +179,12 @@ private void buildDependencyGraphNode(final Map<Dependency, Dependency> dependen
177179
if (parent != null) {
178180
parent.addDependency(new Dependency(purl));
179181
}
180-
for (final DependencyNode childrenNode : node.getChildren()) {
181-
buildDependencyGraphNode(dependencies, childrenNode, topDependency, resolvedPUrls, loggedReplacementPUrls, loggedFilteredArtifacts);
182+
183+
final String nodeClassifierlessPUrl = modelConverter.generateClassifierlessPackageUrl(node.getArtifact());
184+
if (!nodeClassifierlessPUrl.equals(parentClassifierlessPUrl)) {
185+
for (final DependencyNode childrenNode : node.getChildren()) {
186+
buildDependencyGraphNode(dependencies, childrenNode, topDependency, nodeClassifierlessPUrl, resolvedPUrls, loggedReplacementPUrls, loggedFilteredArtifacts);
187+
}
182188
}
183189
}
184190

src/main/java/org/cyclonedx/maven/ModelConverter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public interface ModelConverter {
3737

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

40+
String generateClassifierlessPackageUrl(final org.eclipse.aether.artifact.Artifact artifact);
41+
4042
/**
4143
* Converts a Maven artifact (dependency or transitive dependency) into a
4244
* CycloneDX component.

src/test/java/org/cyclonedx/maven/BaseMavenVerifier.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,32 @@ protected static String fileRead(File file, boolean normalizeEOL) throws IOExcep
5151
}
5252

5353
protected File cleanAndBuild(final String project, final String[] excludeTypes) throws Exception {
54+
return cleanAndBuild(project, excludeTypes, null);
55+
}
56+
57+
protected File cleanAndBuild(final String project, final String[] excludeTypes, final String[] profiles) throws Exception {
58+
return mvnBuild(project, null, excludeTypes, null);
59+
}
60+
61+
protected File mvnBuild(final String project, final String[] goals, final String[] excludeTypes, final String[] profiles) throws Exception {
5462
File projDir = resources.getBasedir(project);
5563

56-
final MavenExecution initExecution = verifier
64+
MavenExecution execution = verifier
5765
.forProject(projDir)
5866
.withCliOption("-Dcurrent.version=" + getCurrentVersion()) // inject cyclonedx-maven-plugin version
5967
.withCliOption("-X") // debug
6068
.withCliOption("-B");
61-
final MavenExecution execution;
6269
if ((excludeTypes != null) && (excludeTypes.length > 0)) {
63-
execution = initExecution.withCliOption("-DexcludeTypes=" + String.join(",", excludeTypes));
70+
execution = execution.withCliOption("-DexcludeTypes=" + String.join(",", excludeTypes));
71+
}
72+
if ((profiles != null) && (profiles.length > 0)) {
73+
execution = execution.withCliOption("-P" + String.join(",", profiles));
74+
}
75+
if (goals != null && goals.length > 0) {
76+
execution.execute(goals).assertErrorFreeLog();
6477
} else {
65-
execution = initExecution;
78+
execution.execute("clean", "package").assertErrorFreeLog();
6679
}
67-
execution.execute("clean", "package")
68-
.assertErrorFreeLog();
6980
return projDir;
7081
}
7182
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package org.cyclonedx.maven;
2+
3+
import java.io.File;
4+
import java.util.Set;
5+
6+
import static org.cyclonedx.maven.TestUtils.getComponentNode;
7+
import static org.cyclonedx.maven.TestUtils.getDependencyNode;
8+
import static org.cyclonedx.maven.TestUtils.getDependencyReferences;
9+
import static org.cyclonedx.maven.TestUtils.readXML;
10+
11+
import static org.junit.Assert.assertEquals;
12+
import static org.junit.Assert.assertNotNull;
13+
import static org.junit.Assert.assertNull;
14+
import static org.junit.Assert.assertTrue;
15+
import static org.junit.jupiter.api.Assertions.fail;
16+
17+
import org.junit.Test;
18+
import org.junit.runner.RunWith;
19+
import org.w3c.dom.Document;
20+
import org.w3c.dom.Node;
21+
import org.w3c.dom.NodeList;
22+
23+
import io.takari.maven.testing.executor.MavenRuntime.MavenRuntimeBuilder;
24+
import io.takari.maven.testing.executor.MavenVersions;
25+
import io.takari.maven.testing.executor.junit.MavenJUnitTestRunner;
26+
27+
/**
28+
* Test for cyclic dependencies on the same project
29+
*/
30+
@RunWith(MavenJUnitTestRunner.class)
31+
@MavenVersions({"3.8.7"})
32+
public class CyclicTest extends BaseMavenVerifier {
33+
private static final String CYCLIC_A_DEPENDENCY = "pkg:maven/com.example.cyclic/cyclic_A@1.0.0?type=jar";
34+
private static final String CYCLIC_A_DEPENDENCY_CLASSIFIER_1 = "pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_1&type=jar";
35+
private static final String CYCLIC_A_DEPENDENCY_CLASSIFIER_2 = "pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_2&type=jar";
36+
private static final String CYCLIC_A_DEPENDENCY_CLASSIFIER_3 = "pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_3&type=jar";
37+
38+
public CyclicTest(MavenRuntimeBuilder runtimeBuilder) throws Exception {
39+
super(runtimeBuilder);
40+
}
41+
42+
@Test
43+
public void testCyclicDependency() throws Exception {
44+
cleanAndBuild("cyclic", null);
45+
File projDir = null;
46+
try {
47+
projDir = mvnBuild("cyclic", new String[]{"package"}, null, new String[] {"profile"});
48+
} catch (final Exception ex) {
49+
fail("Failed to generate SBOM", ex);
50+
}
51+
52+
final Document bom = readXML(new File(projDir, "target/bom.xml"));
53+
54+
final NodeList componentsList = bom.getElementsByTagName("components");
55+
assertEquals("Expected a single components element", 1, componentsList.getLength());
56+
final Node components = componentsList.item(0);
57+
58+
final NodeList dependenciesList = bom.getElementsByTagName("dependencies");
59+
assertEquals("Expected a single dependencies element", 1, dependenciesList.getLength());
60+
final Node dependencies = dependenciesList.item(0);
61+
62+
// BOM should contain pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_1&type=jar
63+
final Node cyclicAClassifier1ComponentNode = getComponentNode(components, CYCLIC_A_DEPENDENCY_CLASSIFIER_1);
64+
assertNotNull("Missing cyclic_A:classifier_1:1.0.0 component", cyclicAClassifier1ComponentNode);
65+
66+
// BOM should contain pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_2&type=jar
67+
final Node cyclicAClassifier2ComponentNode = getComponentNode(components, CYCLIC_A_DEPENDENCY_CLASSIFIER_2);
68+
assertNotNull("Missing cyclic_A:classifier_2:1.0.0 component", cyclicAClassifier2ComponentNode);
69+
70+
// BOM should contain pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_3&type=jar
71+
final Node cyclicAClassifier3ComponentNode = getComponentNode(components, CYCLIC_A_DEPENDENCY_CLASSIFIER_3);
72+
assertNotNull("Missing cyclic_A:classifier_3:1.0.0 component", cyclicAClassifier3ComponentNode);
73+
74+
/*
75+
<dependency ref="pkg:maven/com.example.cyclic/cyclic_A@1.0.0?type=jar">
76+
<dependency ref="pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_1&amp;type=jar"/>
77+
<dependency ref="pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_2&amp;type=jar"/>
78+
<dependency ref="pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_3&amp;type=jar"/>
79+
</dependency>
80+
*/
81+
final Node cyclicADependencyNode = getDependencyNode(dependencies, CYCLIC_A_DEPENDENCY);
82+
assertNotNull("Missing cyclic_A:1.0.0 dependency", cyclicADependencyNode);
83+
Set<String> cyclicADependencies = getDependencyReferences(cyclicADependencyNode);
84+
assertEquals("Invalid dependency count for cyclic_A:1.0.0", 3, cyclicADependencies.size());
85+
assertTrue("Missing cyclic_A:classifier_1:1.0.0 dependency for cyclic_A:1.0.0", cyclicADependencies.contains(CYCLIC_A_DEPENDENCY_CLASSIFIER_1));
86+
assertTrue("Missing cyclic_A:classifier_2:1.0.0 dependency for cyclic_A:1.0.0", cyclicADependencies.contains(CYCLIC_A_DEPENDENCY_CLASSIFIER_2));
87+
assertTrue("Missing cyclic_A:classifier_3:1.0.0 dependency for cyclic_A:1.0.0", cyclicADependencies.contains(CYCLIC_A_DEPENDENCY_CLASSIFIER_3));
88+
89+
/*
90+
<dependency ref="pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_1&amp;type=jar"/>
91+
*/
92+
final Node cyclicAClassifier1DependencyNode = getDependencyNode(dependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_1);
93+
assertNotNull("Missing cyclic_A:classifier_1:1.0.0 dependency", cyclicAClassifier1DependencyNode);
94+
Set<String> cyclicAClassifier1Dependencies = getDependencyReferences(cyclicAClassifier1DependencyNode);
95+
assertEquals("Invalid dependency count for cyclic_A:classifier_1:1.0.0", 0, cyclicAClassifier1Dependencies.size());
96+
97+
/*
98+
<dependency ref="pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_2&amp;type=jar"/>
99+
*/
100+
final Node cyclicAClassifier2DependencyNode = getDependencyNode(dependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_2);
101+
assertNotNull("Missing cyclic_A:classifier_2:1.0.0 dependency", cyclicAClassifier2DependencyNode);
102+
Set<String> cyclicAClassifier2Dependencies = getDependencyReferences(cyclicAClassifier2DependencyNode);
103+
assertEquals("Invalid dependency count for cyclic_A:classifier_2:1.0.0", 0, cyclicAClassifier2Dependencies.size());
104+
105+
/*
106+
<dependency ref="pkg:maven/com.example.cyclic/cyclic_A@1.0.0?classifier=classifier_3&amp;type=jar"/>
107+
*/
108+
final Node cyclicAClassifier3DependencyNode = getDependencyNode(dependencies, CYCLIC_A_DEPENDENCY_CLASSIFIER_3);
109+
assertNotNull("Missing cyclic_A:classifier_3:1.0.0 dependency", cyclicAClassifier3DependencyNode);
110+
Set<String> cyclicAClassifier3Dependencies = getDependencyReferences(cyclicAClassifier3DependencyNode);
111+
assertEquals("Invalid dependency count for cyclic_A:classifier_3:1.0.0", 0, cyclicAClassifier3Dependencies.size());
112+
}
113+
}

src/test/resources/cyclic/pom.xml

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
5+
6+
<modelVersion>4.0.0</modelVersion>
7+
8+
<groupId>com.example.cyclic</groupId>
9+
<artifactId>cyclic_A</artifactId>
10+
<version>1.0.0</version>
11+
12+
<name>Cyclic Dependency A</name>
13+
14+
<profiles>
15+
<profile>
16+
<id>profile</id>
17+
<dependencies>
18+
<dependency>
19+
<groupId>com.example.cyclic</groupId>
20+
<artifactId>cyclic_A</artifactId>
21+
<version>1.0.0</version>
22+
<classifier>classifier_1</classifier>
23+
</dependency>
24+
<dependency>
25+
<groupId>com.example.cyclic</groupId>
26+
<artifactId>cyclic_A</artifactId>
27+
<version>1.0.0</version>
28+
<classifier>classifier_2</classifier>
29+
</dependency>
30+
<dependency>
31+
<groupId>com.example.cyclic</groupId>
32+
<artifactId>cyclic_A</artifactId>
33+
<version>1.0.0</version>
34+
<classifier>classifier_3</classifier>
35+
</dependency>
36+
</dependencies>
37+
<build>
38+
<plugins>
39+
<plugin>
40+
<groupId>org.cyclonedx</groupId>
41+
<artifactId>cyclonedx-maven-plugin</artifactId>
42+
<version>${current.version}</version>
43+
<executions>
44+
<execution>
45+
<phase>package</phase>
46+
<goals>
47+
<goal>makeAggregateBom</goal>
48+
</goals>
49+
</execution>
50+
</executions>
51+
<configuration>
52+
<projectType>library</projectType>
53+
<schemaVersion>1.4</schemaVersion>
54+
<includeBomSerialNumber>true</includeBomSerialNumber>
55+
<includeCompileScope>true</includeCompileScope>
56+
<includeProvidedScope>true</includeProvidedScope>
57+
<includeRuntimeScope>true</includeRuntimeScope>
58+
<includeSystemScope>true</includeSystemScope>
59+
<includeTestScope>false</includeTestScope>
60+
<includeLicenseText>false</includeLicenseText>
61+
<outputFormat>xml</outputFormat>
62+
</configuration>
63+
</plugin>
64+
</plugins>
65+
</build>
66+
</profile>
67+
</profiles>
68+
69+
<build>
70+
<plugins>
71+
<plugin>
72+
<groupId>org.apache.maven.plugins</groupId>
73+
<artifactId>maven-jar-plugin</artifactId>
74+
<version>3.3.0</version>
75+
<executions>
76+
<execution>
77+
<id>classifier_1</id>
78+
<phase>package</phase>
79+
<goals>
80+
<goal>jar</goal>
81+
</goals>
82+
<configuration>
83+
<classifier>classifier_1</classifier>
84+
</configuration>
85+
</execution>
86+
<execution>
87+
<id>classifier_2</id>
88+
<phase>package</phase>
89+
<goals>
90+
<goal>jar</goal>
91+
</goals>
92+
<configuration>
93+
<classifier>classifier_2</classifier>
94+
</configuration>
95+
</execution>
96+
<execution>
97+
<id>classifier_3</id>
98+
<phase>package</phase>
99+
<goals>
100+
<goal>jar</goal>
101+
</goals>
102+
<configuration>
103+
<classifier>classifier_3</classifier>
104+
</configuration>
105+
</execution>
106+
</executions>
107+
</plugin>
108+
<plugin>
109+
<groupId>org.apache.maven.plugins</groupId>
110+
<artifactId>maven-install-plugin</artifactId>
111+
<version>3.1.0</version>
112+
<executions>
113+
<execution>
114+
<id>classifier_1</id>
115+
<phase>package</phase>
116+
<goals>
117+
<goal>install-file</goal>
118+
</goals>
119+
<configuration>
120+
<classifier>classifier_1</classifier>
121+
<file>${project.build.directory}/cyclic_A-1.0.0-classifier_1.jar</file>
122+
</configuration>
123+
</execution>
124+
<execution>
125+
<id>classifier_2</id>
126+
<phase>package</phase>
127+
<goals>
128+
<goal>install-file</goal>
129+
</goals>
130+
<configuration>
131+
<classifier>classifier_2</classifier>
132+
<file>${project.build.directory}/cyclic_A-1.0.0-classifier_2.jar</file>
133+
</configuration>
134+
</execution>
135+
<execution>
136+
<id>classifier_3</id>
137+
<phase>package</phase>
138+
<goals>
139+
<goal>install-file</goal>
140+
</goals>
141+
<configuration>
142+
<classifier>classifier_3</classifier>
143+
<file>${project.build.directory}/cyclic_A-1.0.0-classifier_3.jar</file>
144+
</configuration>
145+
</execution>
146+
</executions>
147+
</plugin>
148+
</plugins>
149+
</build>
150+
</project>

0 commit comments

Comments
 (0)