This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-cyclonedx.git
The following commit(s) were added to refs/heads/main by this push:
new 7775f3f document and test nested components (and handle their
dependencies)
7775f3f is described below
commit 7775f3f03e06136a2193789bb8366e19d0dacfb5
Author: Stefan Bodewig <[email protected]>
AuthorDate: Fri May 15 15:51:38 2026 +0200
document and test nested components (and handle their dependencies)
---
docs/component.html | 15 ++-
src/main/org/apache/ant/cyclonedx/Component.java | 6 ++
.../org/apache/ant/cyclonedx/ComponentBomTask.java | 109 +++++++++++++--------
src/tests/antunit/componentbom-test.xml | 58 +++++++++++
4 files changed, 144 insertions(+), 44 deletions(-)
diff --git a/docs/component.html b/docs/component.html
index 56654dd..b929173 100644
--- a/docs/component.html
+++ b/docs/component.html
@@ -250,7 +250,7 @@ <h4 id="dependency">dependency</h4>
<p>Inside the SBOM both the dependee and the dependency side are
identified by their bom-ref. Therefore nested dependency children
are only allowed in components that provide a bom-ref - either via
- an explicit <code>bomRef</code> attribute or an explicit r
+ an explicit <code>bomRef</code> attribute or an explicit or
calculated <code>purl</code>.</p>
<h5>Attributes</h5>
@@ -275,6 +275,19 @@ <h5>Attributes</h5>
</tr>
</table>
+ <h4 id="component">component</h4>
+
+ <p>Adds a nested component to the component.</p>
+
+ <p>Nested components can be used to represent a hierarchy of
+ components into sub-components and so on.</p>
+
+ <p>Components can be added as full elements or via
+ the <code>refid</code> attribute as references to components
+ defined elsewhere in the build file. Ant verifies this doesn't
+ cause a circular dependency between a component and one higher
+ up in the hierarchy.</p>
+
<h3>Examples</h3>
<p>Below is a component that could describe this Antlib.</p>
diff --git a/src/main/org/apache/ant/cyclonedx/Component.java
b/src/main/org/apache/ant/cyclonedx/Component.java
index 7a10ade..6157154 100644
--- a/src/main/org/apache/ant/cyclonedx/Component.java
+++ b/src/main/org/apache/ant/cyclonedx/Component.java
@@ -300,6 +300,9 @@ public class Component extends DataType {
this.unknownDependencies = unknownDependencies;
}
+ /**
+ * Adds a nested component.
+ */
public void addComponent(Component c) {
checkChildrenAllowed();
nestedComponents.add(c);
@@ -393,6 +396,9 @@ public class Component extends DataType {
return unknownDependencies;
}
+ /**
+ * Recursively returns the nested components of this component.
+ */
public List<Component> getNestedComponents() {
if (isReference()) {
return getRef().getNestedComponents();
diff --git a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
index 7d41bee..98806e1 100644
--- a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
+++ b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
@@ -13,6 +13,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
+import java.util.function.Consumer;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
@@ -141,7 +142,7 @@ public class ComponentBomTask extends Task {
throw new BuildException("nested component element is required");
}
Set<String> knownComponents = new HashSet<>();
- addToKnownComponents(knownComponents, component);
+ visitAllComponents(c ->
knownComponents.add(getUnversionedCoordinates(c)));
meta.setComponent(component.toMainCycloneDxComponent(specVersion.getVersion()));
if (useComponentSupplier) {
OrganizationalEntity componentSupplier =
meta.getComponent().getSupplier();
@@ -163,14 +164,13 @@ public class ComponentBomTask extends Task {
List<org.cyclonedx.model.Component> cs = new ArrayList<>();
List<Component> resolvedComponents = new ArrayList<>();
for (Component c : additionalComponents) {
- addToKnownComponents(knownComponents, c);
resolvedComponents.addAll(c.resolve());
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
}
for (Component c : resolvedComponents) {
- String componentKey = c.getGroup() + ":" + c.getName();
+ String componentKey = getUnversionedCoordinates(c);
if (!knownComponents.contains(componentKey)) {
- addToKnownComponents(knownComponents, c);
+ knownComponents.add(componentKey);
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
}
}
@@ -191,52 +191,71 @@ public class ComponentBomTask extends Task {
}
private void addDependencies(Bom bom) {
- List<Dependency> dependencies = new ArrayList<>();
- Set<String> bomRefs = new HashSet<>();
- if (component.getBomRef() != null) {
- bomRefs.add(component.getBomRef());
- }
- List<org.cyclonedx.model.Component> components = bom.getComponents();
- if (components != null) {
- for (org.cyclonedx.model.Component c : components) {
- if (c.getBomRef() != null) {
- bomRefs.add(c.getBomRef());
- }
- }
- }
-
- if (component.getBomRef() != null &&
!component.areDependenciesUnknown()) {
- Dependency dep = new Dependency(component.getBomRef());
- for (Component.Dependency d : component.getDependencies()) {
- String br = d.getBomRef();
- if (!bomRefs.contains(br)) {
- throw new BuildException("dependency '" + br + "' is
unknown");
+ final Set<String> bomRefs = new HashSet<>();
+ visitAllBomComponents(bom, c -> {
+ String bomRef = c.getBomRef();
+ if (bomRef != null) {
+ bomRefs.add(bomRef);
}
- dep.addDependency(new Dependency(br));
- }
- dependencies.add(dep);
- }
- for (Component c : additionalComponents) {
- if (!c.areDependenciesUnknown() && c.getBomRef() != null) {
- Dependency dep = new Dependency(c.getBomRef());
- for (Component.Dependency d : c.getDependencies()) {
- String br = d.getBomRef();
- if (!bomRefs.contains(br)) {
- throw new BuildException("dependency '" + br + "' is
unknown");
+ });
+
+ final List<Dependency> dependencies = new ArrayList<>();
+ visitAllComponents(c -> {
+ String bomRef = c.getBomRef();
+ if (bomRef != null && !c.areDependenciesUnknown()) {
+ Dependency dep = new Dependency(bomRef);
+ for (Component.Dependency d : c.getDependencies()) {
+ String br = d.getBomRef();
+ if (!bomRefs.contains(br)) {
+ throw new BuildException("dependency '" + br + "'
is unknown");
+ }
+ dep.addDependency(new Dependency(br));
}
- dep.addDependency(new Dependency(br));
+ dependencies.add(dep);
}
- dependencies.add(dep);
- }
- }
+ });
bom.setDependencies(dependencies);
}
- private void addToKnownComponents(Set<String> knownComponents, Component
component) {
- knownComponents.add(component.getGroup() + ":" + component.getName());
- component.getNestedComponents().stream()
- .forEach(c -> addToKnownComponents(knownComponents, c));
+ private void visitAllComponents(Consumer<Component> visitor) {
+ visitAllComponents(component, visitor);
+ visitAllComponents(additionalComponents, visitor);
+ }
+
+ private void visitAllComponents(Component c,
+ Consumer<Component> visitor) {
+ visitor.accept(c);
+ List<Component> cs = c.getNestedComponents();
+ if (cs != null) {
+ // getNestedComponents() has already traversed the whole hierarchy
recursively
+ cs.forEach(visitor);
+ }
+ }
+
+ private void visitAllComponents(List<Component> cs,
+ Consumer<Component> visitor) {
+ if (cs != null) {
+ cs.forEach(c -> visitAllComponents(c, visitor));
+ }
+ }
+
+ private void visitAllBomComponents(Bom bom,
Consumer<org.cyclonedx.model.Component> visitor) {
+ visitAllBomComponents(bom.getMetadata().getComponent(), visitor);
+ visitAllBomComponents(bom.getComponents(), visitor);
+ }
+
+ private void visitAllBomComponents(org.cyclonedx.model.Component c,
+ Consumer<org.cyclonedx.model.Component>
visitor) {
+ visitor.accept(c);
+ visitAllBomComponents(c.getComponents(), visitor);
+ }
+
+ private void visitAllBomComponents(List<org.cyclonedx.model.Component> cs,
+ Consumer<org.cyclonedx.model.Component>
visitor) {
+ if (cs != null) {
+ cs.forEach(c -> visitAllBomComponents(c, visitor));
+ }
}
private void writeBom(Bom bom, Format format, File bomFile)
@@ -266,4 +285,8 @@ public class ComponentBomTask extends Task {
writer.write(generator.toXmlString());
}
}
+
+ private static String getUnversionedCoordinates(Component c) {
+ return c.getGroup() + ":" + c.getName();
+ }
}
diff --git a/src/tests/antunit/componentbom-test.xml
b/src/tests/antunit/componentbom-test.xml
index 2ffe899..4340914 100644
--- a/src/tests/antunit/componentbom-test.xml
+++ b/src/tests/antunit/componentbom-test.xml
@@ -294,6 +294,64 @@
value="pkg:maven/org.example/[email protected]?type=jar,pkg:maven/org.example/[email protected]?type=jar"/>
</target>
+ <target name="testNestedComponents">
+ <cdx:componentbom outputdirectory="${output}" format="xml"
+ xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+ <component name="testname" group="org.example" version="1.0">
+ <component name="testname-child" group="org.example" version="1.0">
+ <component name="testname-grand-child" group="org.example"
version="1.0"/>
+ </component>
+ </component>
+ <additionalComponent name="testname2" group="org.example" version="1.0">
+ <component name="testname2-child" group="org.example" version="1.0">
+ <component name="testname2-grand-child" group="org.example"
version="1.0"/>
+ </component>
+ </additionalComponent>
+ </cdx:componentbom>
+ <xmlproperty file="${output}/bom.xml"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component.name"
+ value="testname-child"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+
name="bom.metadata.component.components.component.components.component.name"
+ value="testname-grand-child"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+
name="bom.components.component.components.component.components.component.name"
+ value="testname2-grand-child"/>
+ </target>
+
+ <target name="testNestedComponentDependencies">
+ <cdx:componentbom outputdirectory="${output}" format="xml"
+ xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+ <component name="testname" group="org.example" version="1.0"
unknownDependencies="true">
+ <component name="testname-child" group="org.example" version="1.0">
+ <component name="testname-grand-child" group="org.example"
version="1.0" unknownDependencies="true"/>
+ <dependency
bomref="pkg:maven/org.example/[email protected]?type=jar"/>
+ </component>
+ </component>
+ <additionalComponent name="testname2" group="org.example" version="1.0"
unknownDependencies="true">
+ <component name="testname2-child" group="org.example" version="1.0"
unknownDependencies="true">
+ <component name="testname2-grand-child" group="org.example"
version="1.0">
+ <dependency
bomref="pkg:maven/org.example/[email protected]?type=jar"/>
+ </component>
+ </component>
+ </additionalComponent>
+ </cdx:componentbom>
+ <xmlproperty file="${output}/bom.xml"/>
+ <copy todir="/tmp" file="${output}/bom.xml"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.dependencies.dependency(ref)"
+
value="pkg:maven/org.example/[email protected]?type=jar,pkg:maven/org.example/[email protected]?type=jar"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.dependencies.dependency.dependency(ref)"
+
value="pkg:maven/org.example/[email protected]?type=jar,pkg:maven/org.example/[email protected]?type=jar"/>
+ </target>
+
<target name="testAntlibsOwnBom" depends="commonReferences">
<cdx:componentbom
bomName="ant-cyclonedx-${artifact.version}-cyclonedx"