Skip to content

Commit 5e81df6

Browse files
add functionality to include (grant)parent poms into the dependency hierarchy
Signed-off-by: Jan van de Pol <jan.van.de.pol@telenetgroup.be>
1 parent 583ffc8 commit 5e81df6

File tree

16 files changed

+983
-1
lines changed

16 files changed

+983
-1
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ Default Values
5858
<outputName>bom</outputName>
5959
<outputDirectory>${project.build.directory}</outputDirectory><!-- usually target, if not redefined in pom.xml -->
6060
<verbose>false</verbose><!-- = ${cyclonedx.verbose} -->
61+
<preserveParentReferences>false</preserveParentReferences>
62+
<includeParentsAsComponents>true</includeParentsAsComponents>
6163
</configuration>
6264
</plugin>
6365
</plugins>
@@ -67,6 +69,33 @@ Default Values
6769

6870
See also [External References](https://cyclonedx.github.io/cyclonedx-maven-plugin/external-references.html) documentation for details on this topic.
6971

72+
Parent POM Preservation
73+
-------------------
74+
By default, the plugin flattens the Maven effective POM model, merging all dependencies from parent POMs into a single list. When `preserveParentReferences` is enabled, the plugin preserves the parent POM hierarchy:
75+
76+
* **`preserveParentReferences`**: Enable parent POM preservation (default: `false`)
77+
* When `true`, parent POMs become direct dependencies of the component that references them
78+
* Walks the entire parent chain recursively (child ? parent ? grandparent ? ...)
79+
* Dependencies inherited from parent POMs are reorganized to depend on their introducing parent
80+
81+
* **`includeParentsAsComponents`**: Include parent POMs in the components section (default: `true`)
82+
* When `true`, parent POMs appear as components in the BOM
83+
* When `false`, parent POMs are referenced only in the dependency graph but not listed as components
84+
85+
Example configuration to preserve parent POM references:
86+
87+
```xml
88+
<configuration>
89+
<preserveParentReferences>true</preserveParentReferences>
90+
<includeParentsAsComponents>true</includeParentsAsComponents>
91+
</configuration>
92+
```
93+
94+
This is useful for:
95+
* Understanding the complete project structure including parent POMs
96+
* Tracking which parent POM introduces specific dependencies
97+
* Maintaining visibility of the full Maven inheritance hierarchy
98+
7099
Excluding Projects
71100
-------------------
72101
With `makeAggregateBom` goal, it is possible to exclude certain Maven reactor projects (aka modules) from getting included in the aggregate BOM:

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

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,25 @@ public abstract class BaseCycloneDxMojo extends AbstractMojo {
183183
@Parameter(property = "detectUnusedForOptionalScope", defaultValue = "false")
184184
protected boolean detectUnusedForOptionalScope;
185185

186+
/**
187+
* Should parent POM references be preserved as dependencies instead of being flattened in the effective POM?
188+
* When enabled, if a project has a parent POM, the parent will be added as a direct dependency.
189+
*
190+
* @since 3.0.0
191+
*/
192+
@Parameter(property = "preserveParentReferences", defaultValue = "false", required = false)
193+
protected boolean preserveParentReferences;
194+
195+
/**
196+
* When preserveParentReferences is enabled, this controls whether parent POMs should be included
197+
* as components in the BOM or only as dependency relationships.
198+
* Only takes effect when preserveParentReferences is true.
199+
*
200+
* @since 3.0.0
201+
*/
202+
@Parameter(property = "includeParentsAsComponents", defaultValue = "true", required = false)
203+
protected boolean includeParentsAsComponents;
204+
186205
/**
187206
* Skip CycloneDX execution.
188207
*
@@ -472,7 +491,7 @@ private void saveBomToFile(String bomString, String extension, Parser bomParser)
472491

473492
protected BomDependencies extractBOMDependencies(MavenProject mavenProject) throws MojoExecutionException {
474493
ProjectDependenciesConverter.MavenDependencyScopes include = new ProjectDependenciesConverter.MavenDependencyScopes(includeCompileScope, includeProvidedScope, includeRuntimeScope, includeTestScope, includeSystemScope);
475-
return projectDependenciesConverter.extractBOMDependencies(mavenProject, include, excludeTypes);
494+
return projectDependenciesConverter.extractBOMDependencies(mavenProject, include, excludeTypes, preserveParentReferences);
476495
}
477496

478497
/**
@@ -527,6 +546,16 @@ protected void populateComponents(final Set<String> topLevelComponents, final Ma
527546
for (Map.Entry<String, Artifact> entry: artifacts.entrySet()) {
528547
final String purl = entry.getKey();
529548
final Artifact artifact = entry.getValue();
549+
550+
// Skip parent POMs if includeParentsAsComponents is false
551+
if (preserveParentReferences && !includeParentsAsComponents && "pom".equals(artifact.getType())) {
552+
// Check if this artifact is a parent POM by seeing if it's referenced as a parent
553+
if (isParentArtifact(artifact, artifacts)) {
554+
getLog().debug("Skipping parent POM component (includeParentsAsComponents=false): " + purl);
555+
continue;
556+
}
557+
}
558+
530559
final Component.Scope artifactScope = getComponentScope(artifact, dependencyAnalysis);
531560
final Component component = components.get(purl);
532561
if (component == null) {
@@ -634,4 +663,48 @@ private static boolean isDeployable(final MavenProject project,
634663
}
635664
return false;
636665
}
666+
667+
/**
668+
* Checks if an artifact is a parent POM by determining if any other artifact references it as a parent.
669+
*
670+
* @param candidate the artifact to check
671+
* @param artifacts all artifacts in the project
672+
* @return true if the candidate is a parent of any artifact
673+
*/
674+
private boolean isParentArtifact(Artifact candidate, Map<String, Artifact> artifacts) {
675+
if (!"pom".equals(candidate.getType())) {
676+
return false;
677+
}
678+
679+
// First check if the main project has this as a parent
680+
if (getProject().hasParent() && getProject().getParent() != null) {
681+
MavenProject parent = getProject().getParent();
682+
if (parent.getGroupId().equals(candidate.getGroupId()) &&
683+
parent.getArtifactId().equals(candidate.getArtifactId()) &&
684+
parent.getVersion().equals(candidate.getVersion())) {
685+
return true;
686+
}
687+
}
688+
689+
// Check each artifact to see if this candidate is its parent
690+
for (Artifact artifact : artifacts.values()) {
691+
// Skip comparing the artifact with itself (by coordinates, not object identity)
692+
if (artifact.getGroupId().equals(candidate.getGroupId()) &&
693+
artifact.getArtifactId().equals(candidate.getArtifactId()) &&
694+
artifact.getVersion().equals(candidate.getVersion())) {
695+
continue;
696+
}
697+
698+
if (modelConverter.hasParentPom(artifact)) {
699+
Artifact parent = modelConverter.getParentArtifact(artifact);
700+
if (parent != null &&
701+
parent.getGroupId().equals(candidate.getGroupId()) &&
702+
parent.getArtifactId().equals(candidate.getArtifactId()) &&
703+
parent.getVersion().equals(candidate.getVersion())) {
704+
return true;
705+
}
706+
}
707+
}
708+
return false;
709+
}
637710
}

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

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
import org.apache.maven.execution.MavenSession;
2828
import org.apache.maven.model.MailingList;
2929
import org.apache.maven.model.building.ModelBuildingRequest;
30+
import org.apache.maven.project.DefaultProjectBuildingRequest;
3031
import org.apache.maven.project.MavenProject;
3132
import org.apache.maven.project.ProjectBuilder;
3233
import org.apache.maven.project.ProjectBuildingException;
34+
import org.apache.maven.project.ProjectBuildingRequest;
3335
import org.apache.maven.project.ProjectBuildingResult;
3436
import org.apache.maven.repository.RepositorySystem;
3537
import org.cyclonedx.Version;
@@ -56,8 +58,10 @@
5658
import java.net.URISyntaxException;
5759
import java.util.Arrays;
5860
import java.util.Collections;
61+
import java.util.HashSet;
5962
import java.util.List;
6063
import java.util.Properties;
64+
import java.util.Set;
6165
import java.util.TreeMap;
6266
import java.util.stream.Collectors;
6367
import org.apache.maven.model.Plugin;
@@ -443,4 +447,130 @@ private static boolean isBlank(String s) {
443447
private static boolean isLicenseBlank(org.apache.maven.model.License license) {
444448
return isBlank(license.getName()) && isBlank(license.getUrl());
445449
}
450+
451+
@Override
452+
public boolean hasParentPom(Artifact artifact) {
453+
try {
454+
final Artifact pomArtifact = repositorySystem.createProjectArtifact(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion());
455+
final ProjectBuildingResult build = mavenProjectBuilder.build(pomArtifact,
456+
session.getProjectBuildingRequest()
457+
.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL)
458+
.setProcessPlugins(false)
459+
.setResolveDependencies(false)
460+
);
461+
final MavenProject project = build.getProject();
462+
return project.hasParent() && project.getParent() != null;
463+
} catch (ProjectBuildingException e) {
464+
logger.debug("Unable to check parent for " + artifact.getId(), e);
465+
return false;
466+
}
467+
}
468+
469+
@Override
470+
public Artifact getParentArtifact(Artifact artifact) {
471+
try {
472+
final Artifact pomArtifact = repositorySystem.createProjectArtifact(artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion());
473+
final ProjectBuildingResult build = mavenProjectBuilder.build(pomArtifact,
474+
session.getProjectBuildingRequest()
475+
.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL)
476+
.setProcessPlugins(false)
477+
.setResolveDependencies(false)
478+
);
479+
final MavenProject project = build.getProject();
480+
if (project.hasParent() && project.getParent() != null) {
481+
final MavenProject parent = project.getParent();
482+
// Create an artifact for the parent POM
483+
return new DefaultArtifact(
484+
parent.getGroupId(),
485+
parent.getArtifactId(),
486+
parent.getVersion(),
487+
null, // scope
488+
"pom", // type
489+
null, // classifier
490+
new DefaultArtifactHandler("pom")
491+
);
492+
}
493+
} catch (ProjectBuildingException e) {
494+
logger.debug("Unable to get parent for " + artifact.getId(), e);
495+
}
496+
return null;
497+
}
498+
499+
@Override
500+
public Set<String> getDirectDependencyPurls(Artifact artifact) {
501+
final Set<String> directDeps = new HashSet<>();
502+
503+
try {
504+
// Build the artifact's POM to get its model
505+
final DefaultArtifact pomArtifact = new DefaultArtifact(
506+
artifact.getGroupId(),
507+
artifact.getArtifactId(),
508+
artifact.getVersion(),
509+
null, // scope
510+
"pom",
511+
null, // classifier
512+
new DefaultArtifactHandler("pom")
513+
);
514+
515+
final ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest(session.getProjectBuildingRequest());
516+
buildingRequest.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL);
517+
buildingRequest.setResolveDependencies(false);
518+
buildingRequest.setProcessPlugins(false);
519+
520+
final ProjectBuildingResult build = mavenProjectBuilder.build(pomArtifact, buildingRequest);
521+
final MavenProject project = build.getProject();
522+
523+
// Get dependencies from the original model
524+
if (project.getOriginalModel() != null && project.getOriginalModel().getDependencies() != null) {
525+
for (org.apache.maven.model.Dependency dep : project.getOriginalModel().getDependencies()) {
526+
try {
527+
// Skip if essential coordinates are missing
528+
if (dep.getGroupId() == null || dep.getArtifactId() == null) {
529+
continue;
530+
}
531+
532+
// Version might be null if inherited from dependencyManagement
533+
String version = dep.getVersion();
534+
if (version == null) {
535+
// Try to find the version from the effective dependencies
536+
for (org.apache.maven.model.Dependency effectiveDep : project.getDependencies()) {
537+
if (dep.getGroupId().equals(effectiveDep.getGroupId()) &&
538+
dep.getArtifactId().equals(effectiveDep.getArtifactId())) {
539+
version = effectiveDep.getVersion();
540+
break;
541+
}
542+
}
543+
}
544+
545+
// If we still don't have a version, skip this dependency
546+
if (version == null) {
547+
logger.debug("Skipping dependency without version: " + dep.getGroupId() + ":" + dep.getArtifactId());
548+
continue;
549+
}
550+
551+
// Create an artifact to generate the pURL
552+
final DefaultArtifact depArtifact = new DefaultArtifact(
553+
dep.getGroupId(),
554+
dep.getArtifactId(),
555+
version,
556+
dep.getScope(),
557+
dep.getType() != null ? dep.getType() : "jar",
558+
dep.getClassifier(),
559+
new DefaultArtifactHandler(dep.getType() != null ? dep.getType() : "jar")
560+
);
561+
final String purl = generatePackageUrl(depArtifact);
562+
if (purl != null) {
563+
directDeps.add(purl);
564+
}
565+
} catch (Exception e) {
566+
logger.warn("Error processing dependency " + dep.getGroupId() + ":" + dep.getArtifactId(), e);
567+
}
568+
}
569+
}
570+
} catch (Exception e) {
571+
logger.warn("Error loading POM for artifact " + artifact.getId(), e);
572+
}
573+
574+
return directDeps;
575+
}
446576
}

0 commit comments

Comments
 (0)