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

commit e5a58029d614f105257a795e163d497752913694
Author: Stefan Bodewig <[email protected]>
AuthorDate: Fri May 8 21:55:23 2026 +0200

    add support for nested components
---
 examples/ant-cyclonedx-0.1alpha-cyclonedx.json     | 37 +++++++++++-----------
 examples/ant-cyclonedx-0.1alpha-cyclonedx.xml      | 37 +++++++++++-----------
 src/main/org/apache/ant/cyclonedx/Component.java   | 32 +++++++++++++++++++
 .../org/apache/ant/cyclonedx/ComponentBomTask.java | 12 +++++--
 src/tests/antunit/componentbom-test.xml            | 26 +++++++++++++++
 5 files changed, 105 insertions(+), 39 deletions(-)

diff --git a/examples/ant-cyclonedx-0.1alpha-cyclonedx.json 
b/examples/ant-cyclonedx-0.1alpha-cyclonedx.json
index 6b3e00c..cd77c27 100644
--- a/examples/ant-cyclonedx-0.1alpha-cyclonedx.json
+++ b/examples/ant-cyclonedx-0.1alpha-cyclonedx.json
@@ -1,10 +1,10 @@
 {
   "bomFormat" : "CycloneDX",
   "specVersion" : "1.6",
-  "serialNumber" : "urn:uuid:266aabc2-6812-4036-88b0-1c88d390502c",
+  "serialNumber" : "urn:uuid:b102ed07-8561-4e8d-9124-2fbaeda2af87",
   "version" : 1,
   "metadata" : {
-    "timestamp" : "2026-05-08T16:48:44Z",
+    "timestamp" : "2026-05-08T19:48:59Z",
     "lifecycles" : [
       {
         "phase" : "build"
@@ -34,35 +34,35 @@
           "hashes" : [
             {
               "alg" : "MD5",
-              "content" : "6d0e2adf8c544249288e7ee596c566e9"
+              "content" : "cd60dca84b42bad257fd776c541a937d"
             },
             {
               "alg" : "SHA-1",
-              "content" : "48d86c01594b9ac882d4153bf41e5fc55b620faa"
+              "content" : "f27f9d1e378987673e96868fab2c64ff076310c7"
             },
             {
               "alg" : "SHA-256",
-              "content" : 
"4219d857d3100fd4ec043318f63836bafddedf4181b0767774ee1cf4916f8a2d"
+              "content" : 
"ad7490c23be951353bfdc299269816c87c680f2b08a0461d55bde3689d291234"
             },
             {
               "alg" : "SHA-512",
-              "content" : 
"2347e3843020e10bc1bb5b3d255d105ae4403a832398406c541b7bcf45eef73bfed63bb7181cddae056f386faba9effdd5e83a48692db48df79307b4e39688ba"
+              "content" : 
"80784ae9b36496e7b5a70e91dfe0280d3437d79a211e2a6b8f761b77f0407494752ecbee9e7b6d5a2c014e04b7069b4dfc36a684fb9a11c982c59913d7ff97e6"
             },
             {
               "alg" : "SHA3-256",
-              "content" : 
"6f64a5f69c6a2dc750cef5143c66beaf3aa19600716ed63cbb0afa980c5b46fb"
+              "content" : 
"1c6387b9e32013e79dfb420bc005e6dff736546152a95bbd6673b07a413b78f7"
             },
             {
               "alg" : "SHA3-512",
-              "content" : 
"6859d0d58ea10a43f7704c1617a73609522763d4460ad5c52f067374bc995f7aa3a3d43851015625bcddaf354c528e79b38a5d7c9cb36272afa2a98b9b05c966"
+              "content" : 
"f57f015857b33731781a646b5131037897dc23f2e1a9e202984d5ac9a484d3843bbc9e1216aee46b4a36da213323f2d1f0f79dfa1e69f788d7bc538adfad9bc1"
             },
             {
               "alg" : "SHA-384",
-              "content" : 
"72d5b1dfaa25985a0891d763ef8b65169a58f67ca3b47864f3aa16204649247e8d6f8c0654959553ae12e6d3bb564a81"
+              "content" : 
"e93b7acd21cbb36b41b273582b4c3a58828d0e908261549fdef3da0ebdce4e573cfd65e0406c0186cb6d8e76552347c3"
             },
             {
               "alg" : "SHA3-384",
-              "content" : 
"56afebe15d38d52e2f80580659f0558f618deba550f0139f9a72b6254afe9f3a90bb3cb9837568ca8f93a8495c2ba2f3"
+              "content" : 
"241ac570fb9fc5e5ba1b7362df02167d214be3e7e6e371fe90ce4312bf63c7cd5ba3d988b5bd276040e94a4fdc23464c"
             }
           ],
           "licenses" : [
@@ -102,6 +102,7 @@
           "https://ant.apache.org/";
         ]
       },
+      "publisher" : "The Apache Software Foundation",
       "group" : "org.apache.ant",
       "name" : "ant-cyclonedx",
       "version" : "0.1alpha",
@@ -109,35 +110,35 @@
       "hashes" : [
         {
           "alg" : "MD5",
-          "content" : "6d0e2adf8c544249288e7ee596c566e9"
+          "content" : "cd60dca84b42bad257fd776c541a937d"
         },
         {
           "alg" : "SHA-1",
-          "content" : "48d86c01594b9ac882d4153bf41e5fc55b620faa"
+          "content" : "f27f9d1e378987673e96868fab2c64ff076310c7"
         },
         {
           "alg" : "SHA-256",
-          "content" : 
"4219d857d3100fd4ec043318f63836bafddedf4181b0767774ee1cf4916f8a2d"
+          "content" : 
"ad7490c23be951353bfdc299269816c87c680f2b08a0461d55bde3689d291234"
         },
         {
           "alg" : "SHA-512",
-          "content" : 
"2347e3843020e10bc1bb5b3d255d105ae4403a832398406c541b7bcf45eef73bfed63bb7181cddae056f386faba9effdd5e83a48692db48df79307b4e39688ba"
+          "content" : 
"80784ae9b36496e7b5a70e91dfe0280d3437d79a211e2a6b8f761b77f0407494752ecbee9e7b6d5a2c014e04b7069b4dfc36a684fb9a11c982c59913d7ff97e6"
         },
         {
           "alg" : "SHA3-256",
-          "content" : 
"6f64a5f69c6a2dc750cef5143c66beaf3aa19600716ed63cbb0afa980c5b46fb"
+          "content" : 
"1c6387b9e32013e79dfb420bc005e6dff736546152a95bbd6673b07a413b78f7"
         },
         {
           "alg" : "SHA3-512",
-          "content" : 
"6859d0d58ea10a43f7704c1617a73609522763d4460ad5c52f067374bc995f7aa3a3d43851015625bcddaf354c528e79b38a5d7c9cb36272afa2a98b9b05c966"
+          "content" : 
"f57f015857b33731781a646b5131037897dc23f2e1a9e202984d5ac9a484d3843bbc9e1216aee46b4a36da213323f2d1f0f79dfa1e69f788d7bc538adfad9bc1"
         },
         {
           "alg" : "SHA-384",
-          "content" : 
"72d5b1dfaa25985a0891d763ef8b65169a58f67ca3b47864f3aa16204649247e8d6f8c0654959553ae12e6d3bb564a81"
+          "content" : 
"e93b7acd21cbb36b41b273582b4c3a58828d0e908261549fdef3da0ebdce4e573cfd65e0406c0186cb6d8e76552347c3"
         },
         {
           "alg" : "SHA3-384",
-          "content" : 
"56afebe15d38d52e2f80580659f0558f618deba550f0139f9a72b6254afe9f3a90bb3cb9837568ca8f93a8495c2ba2f3"
+          "content" : 
"241ac570fb9fc5e5ba1b7362df02167d214be3e7e6e371fe90ce4312bf63c7cd5ba3d988b5bd276040e94a4fdc23464c"
         }
       ],
       "licenses" : [
diff --git a/examples/ant-cyclonedx-0.1alpha-cyclonedx.xml 
b/examples/ant-cyclonedx-0.1alpha-cyclonedx.xml
index f9bb86e..18b76f3 100644
--- a/examples/ant-cyclonedx-0.1alpha-cyclonedx.xml
+++ b/examples/ant-cyclonedx-0.1alpha-cyclonedx.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<bom serialNumber="urn:uuid:266aabc2-6812-4036-88b0-1c88d390502c" version="1" 
xmlns="http://cyclonedx.org/schema/bom/1.6";>
+<bom serialNumber="urn:uuid:b102ed07-8561-4e8d-9124-2fbaeda2af87" version="1" 
xmlns="http://cyclonedx.org/schema/bom/1.6";>
   <metadata>
-    <timestamp>2026-05-08T16:48:44Z</timestamp>
+    <timestamp>2026-05-08T19:48:59Z</timestamp>
     <lifecycles>
       <lifecycle>
         <phase>build</phase>
@@ -24,14 +24,14 @@
           <version>0.1alpha</version>
           <description>Apache CycloneDX Antlib</description>
           <hashes>
-            <hash alg="MD5">6d0e2adf8c544249288e7ee596c566e9</hash>
-            <hash alg="SHA-1">48d86c01594b9ac882d4153bf41e5fc55b620faa</hash>
-            <hash 
alg="SHA-256">4219d857d3100fd4ec043318f63836bafddedf4181b0767774ee1cf4916f8a2d</hash>
-            <hash 
alg="SHA-512">2347e3843020e10bc1bb5b3d255d105ae4403a832398406c541b7bcf45eef73bfed63bb7181cddae056f386faba9effdd5e83a48692db48df79307b4e39688ba</hash>
-            <hash 
alg="SHA3-256">6f64a5f69c6a2dc750cef5143c66beaf3aa19600716ed63cbb0afa980c5b46fb</hash>
-            <hash 
alg="SHA3-512">6859d0d58ea10a43f7704c1617a73609522763d4460ad5c52f067374bc995f7aa3a3d43851015625bcddaf354c528e79b38a5d7c9cb36272afa2a98b9b05c966</hash>
-            <hash 
alg="SHA-384">72d5b1dfaa25985a0891d763ef8b65169a58f67ca3b47864f3aa16204649247e8d6f8c0654959553ae12e6d3bb564a81</hash>
-            <hash 
alg="SHA3-384">56afebe15d38d52e2f80580659f0558f618deba550f0139f9a72b6254afe9f3a90bb3cb9837568ca8f93a8495c2ba2f3</hash>
+            <hash alg="MD5">cd60dca84b42bad257fd776c541a937d</hash>
+            <hash alg="SHA-1">f27f9d1e378987673e96868fab2c64ff076310c7</hash>
+            <hash 
alg="SHA-256">ad7490c23be951353bfdc299269816c87c680f2b08a0461d55bde3689d291234</hash>
+            <hash 
alg="SHA-512">80784ae9b36496e7b5a70e91dfe0280d3437d79a211e2a6b8f761b77f0407494752ecbee9e7b6d5a2c014e04b7069b4dfc36a684fb9a11c982c59913d7ff97e6</hash>
+            <hash 
alg="SHA3-256">1c6387b9e32013e79dfb420bc005e6dff736546152a95bbd6673b07a413b78f7</hash>
+            <hash 
alg="SHA3-512">f57f015857b33731781a646b5131037897dc23f2e1a9e202984d5ac9a484d3843bbc9e1216aee46b4a36da213323f2d1f0f79dfa1e69f788d7bc538adfad9bc1</hash>
+            <hash 
alg="SHA-384">e93b7acd21cbb36b41b273582b4c3a58828d0e908261549fdef3da0ebdce4e573cfd65e0406c0186cb6d8e76552347c3</hash>
+            <hash 
alg="SHA3-384">241ac570fb9fc5e5ba1b7362df02167d214be3e7e6e371fe90ce4312bf63c7cd5ba3d988b5bd276040e94a4fdc23464c</hash>
           </hashes>
           <licenses>
             <license>
@@ -60,19 +60,20 @@
         <name>Apache Ant Development Team</name>
         <url>https://ant.apache.org/</url>
       </manufacturer>
+      <publisher>The Apache Software Foundation</publisher>
       <group>org.apache.ant</group>
       <name>ant-cyclonedx</name>
       <version>0.1alpha</version>
       <description>Apache CycloneDX Antlib</description>
       <hashes>
-        <hash alg="MD5">6d0e2adf8c544249288e7ee596c566e9</hash>
-        <hash alg="SHA-1">48d86c01594b9ac882d4153bf41e5fc55b620faa</hash>
-        <hash 
alg="SHA-256">4219d857d3100fd4ec043318f63836bafddedf4181b0767774ee1cf4916f8a2d</hash>
-        <hash 
alg="SHA-512">2347e3843020e10bc1bb5b3d255d105ae4403a832398406c541b7bcf45eef73bfed63bb7181cddae056f386faba9effdd5e83a48692db48df79307b4e39688ba</hash>
-        <hash 
alg="SHA3-256">6f64a5f69c6a2dc750cef5143c66beaf3aa19600716ed63cbb0afa980c5b46fb</hash>
-        <hash 
alg="SHA3-512">6859d0d58ea10a43f7704c1617a73609522763d4460ad5c52f067374bc995f7aa3a3d43851015625bcddaf354c528e79b38a5d7c9cb36272afa2a98b9b05c966</hash>
-        <hash 
alg="SHA-384">72d5b1dfaa25985a0891d763ef8b65169a58f67ca3b47864f3aa16204649247e8d6f8c0654959553ae12e6d3bb564a81</hash>
-        <hash 
alg="SHA3-384">56afebe15d38d52e2f80580659f0558f618deba550f0139f9a72b6254afe9f3a90bb3cb9837568ca8f93a8495c2ba2f3</hash>
+        <hash alg="MD5">cd60dca84b42bad257fd776c541a937d</hash>
+        <hash alg="SHA-1">f27f9d1e378987673e96868fab2c64ff076310c7</hash>
+        <hash 
alg="SHA-256">ad7490c23be951353bfdc299269816c87c680f2b08a0461d55bde3689d291234</hash>
+        <hash 
alg="SHA-512">80784ae9b36496e7b5a70e91dfe0280d3437d79a211e2a6b8f761b77f0407494752ecbee9e7b6d5a2c014e04b7069b4dfc36a684fb9a11c982c59913d7ff97e6</hash>
+        <hash 
alg="SHA3-256">1c6387b9e32013e79dfb420bc005e6dff736546152a95bbd6673b07a413b78f7</hash>
+        <hash 
alg="SHA3-512">f57f015857b33731781a646b5131037897dc23f2e1a9e202984d5ac9a484d3843bbc9e1216aee46b4a36da213323f2d1f0f79dfa1e69f788d7bc538adfad9bc1</hash>
+        <hash 
alg="SHA-384">e93b7acd21cbb36b41b273582b4c3a58828d0e908261549fdef3da0ebdce4e573cfd65e0406c0186cb6d8e76552347c3</hash>
+        <hash 
alg="SHA3-384">241ac570fb9fc5e5ba1b7362df02167d214be3e7e6e371fe90ce4312bf63c7cd5ba3d988b5bd276040e94a4fdc23464c</hash>
       </hashes>
       <licenses>
         <license>
diff --git a/src/main/org/apache/ant/cyclonedx/Component.java 
b/src/main/org/apache/ant/cyclonedx/Component.java
index 6c8f457..c34f820 100644
--- a/src/main/org/apache/ant/cyclonedx/Component.java
+++ b/src/main/org/apache/ant/cyclonedx/Component.java
@@ -48,6 +48,7 @@ public class Component extends DataType {
     private List<org.cyclonedx.model.ExternalReference> externalReferences = 
new ArrayList<>();
     private org.cyclonedx.model.Component.Scope scope;
     private boolean isExternal = false;
+    private List<Component> nestedComponents = new ArrayList<>();
     private List<Dependency> dependencies = new ArrayList<>();
     private boolean unknownDependencies = false;
     private boolean sbomLinkResolved = false;
@@ -221,9 +222,29 @@ public class Component extends DataType {
         if (isReference()) {
             return getRef().getDependencies();
         }
+        dieOnCircularReference();
         return dependencies;
     }
 
+    public void addComponent(Component c) {
+        checkChildrenAllowed();
+        nestedComponents.add(c);
+    }
+
+    public List<Component> getNestedComponents() {
+        if (isReference()) {
+            return getRef().getNestedComponents();
+        }
+        dieOnCircularReference();
+        List<Component> result = new ArrayList<>();
+        result.addAll(nestedComponents);
+        result.addAll(nestedComponents
+                      .stream()
+                      .flatMap(c -> c.getNestedComponents().stream())
+                      .collect(Collectors.toList()));
+        return result;
+    }
+
     public void setUnknownDependencies(boolean unknownDependencies) {
         checkAttributesAllowed();
         this.unknownDependencies = unknownDependencies;
@@ -356,6 +377,7 @@ public class Component extends DataType {
 
     private org.cyclonedx.model.Component toCycloneDxComponent(Version 
bomVersion)
         throws IOException {
+        dieOnCircularReference();
         if (name == null) {
             throw new BuildException("component name is required");
         }
@@ -429,6 +451,9 @@ public class Component extends DataType {
         if (!externalReferences.isEmpty()) {
             component.setExternalReferences(externalReferences);
         }
+        for (Component c : nestedComponents) {
+            
component.addComponent(c.toAdditionalCycloneDxComponent(bomVersion));
+        }
         // add isExternal once VERSION_17 is supported by cyclonedx-java-core
         addHashes(component, bomVersion);
         return component;
@@ -475,6 +500,13 @@ public class Component extends DataType {
             tags.clear();
             tags.addAll(real.getTags().getTags());
         }
+        if (real.getComponents() != null) {
+            nestedComponents.clear();
+            nestedComponents.addAll(real.getComponents()
+                                    .stream()
+                                    .map(Component::from)
+                                    .collect(Collectors.toList()));
+        }
     }
 
     private void addHashes(org.cyclonedx.model.Component component, Version 
bomVersion)
diff --git a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java 
b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
index d811490..ae1c2e2 100644
--- a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
+++ b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
@@ -130,7 +130,7 @@ public class ComponentBomTask extends Task {
             throw new BuildException("nested component element is required");
         }
         Set<String> knownComponents = new HashSet<>();
-        knownComponents.add(component.getGroup() + ":" + component.getName());
+        addToKnownComponents(knownComponents, component);
         
meta.setComponent(component.toMainCycloneDxComponent(specVersion.getVersion()));
         if (useComponentSupplier) {
             OrganizationalEntity componentSupplier = 
meta.getComponent().getSupplier();
@@ -152,14 +152,14 @@ public class ComponentBomTask extends Task {
             List<org.cyclonedx.model.Component> cs = new ArrayList<>();
             List<Component> resolvedComponents = new ArrayList<>();
             for (Component c : additionalComponents) {
-                knownComponents.add(c.getGroup() + ":" + c.getName());
+                addToKnownComponents(knownComponents, c);
                 resolvedComponents.addAll(c.resolve());
                 
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
             }
             for (Component c : resolvedComponents) {
                 String componentKey = c.getGroup() + ":" + c.getName();
                 if (!knownComponents.contains(componentKey)) {
-                    knownComponents.add(componentKey);
+                    addToKnownComponents(knownComponents, c);
                     
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
                 }
             }
@@ -214,6 +214,12 @@ public class ComponentBomTask extends Task {
         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 writeBom(Bom bom, Format format, File bomFile)
         throws IOException, GeneratorException {
         switch (format) {
diff --git a/src/tests/antunit/componentbom-test.xml 
b/src/tests/antunit/componentbom-test.xml
index 1fa123d..0679bdb 100644
--- a/src/tests/antunit/componentbom-test.xml
+++ b/src/tests/antunit/componentbom-test.xml
@@ -272,6 +272,7 @@
         </manufacturer>
         <license licenseId="Apache-2.0"/>
         <externalReference type="WEBSITE" url="https://example.com/"/>
+        <component name="other-test" group="org.example" version="1.1"/>
       </component>
     </cdx:componentbom>
     <xmlproperty file="${output}/bom.xml"/>
@@ -315,6 +316,30 @@
         xmlns:au="antlib:org.apache.ant.antunit"
         resource="${output}/bom.xml"
         value='&lt;hash alg="SHA-256"&gt;${ant.file.sha256}&lt;/hash&gt;'/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.components.component.name"
+        value="other-test"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.components.component(type)"
+        value="library"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.components.component.group"
+        value="org.example"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.components.component.version"
+        value="1.1"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.components.component.purl"
+        value="pkg:maven/org.example/[email protected]?type=jar"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.components.component(bom-ref)"
+        value="pkg:maven/org.example/[email protected]?type=jar"/>
     <au:assertResourceContains
         xmlns:au="antlib:org.apache.ant.antunit"
         resource="${output}/bom.xml"
@@ -393,6 +418,7 @@
           group="org.apache.ant"
           version="${artifact.version}"
           description="Apache CycloneDX Antlib"
+          publisher="The Apache Software Foundation"
           manufacturerIsSupplier="true">
         <file file="${antlib.location}"/>
         <manufacturer refid="ant-team"/>

Reply via email to