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"

Reply via email to