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 2323b40  configure the component the bom is about
2323b40 is described below

commit 2323b403d7e3948dbd60e54b041b1c10c009baef
Author: Stefan Bodewig <[email protected]>
AuthorDate: Fri May 1 15:23:24 2026 +0200

    configure the component the bom is about
---
 src/main/org/apache/ant/cyclonedx/Component.java   | 205 +++++++++++++++++++++
 .../org/apache/ant/cyclonedx/ComponentBomTask.java |  14 ++
 src/main/org/apache/ant/cyclonedx/ToolData.java    |  29 ++-
 src/tests/antunit/componentbom-test.xml            | 168 ++++++++++++++++-
 4 files changed, 396 insertions(+), 20 deletions(-)

diff --git a/src/main/org/apache/ant/cyclonedx/Component.java 
b/src/main/org/apache/ant/cyclonedx/Component.java
new file mode 100644
index 0000000..39242b3
--- /dev/null
+++ b/src/main/org/apache/ant/cyclonedx/Component.java
@@ -0,0 +1,205 @@
+package org.apache.ant.cyclonedx;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.types.Resource;
+import org.apache.tools.ant.types.resources.FileProvider;
+import org.apache.tools.ant.types.resources.URLResource;
+
+import org.cyclonedx.Version;
+import org.cyclonedx.model.LicenseChoice;
+import org.cyclonedx.model.OrganizationalEntity;
+import org.cyclonedx.util.BomUtils;
+
+public class Component {
+    private Resource resource;
+    private org.cyclonedx.model.Component.Type type = 
org.cyclonedx.model.Component.Type.LIBRARY;
+    private String name;
+    private String group;
+    private String version;
+    private String description;
+    private Manufacturer manufacturer = null;
+    private List<org.cyclonedx.model.License> licenses = new ArrayList<>();
+    private String purl;
+    private String bomRef;
+
+    public void add(Resource resource) {
+        if (this.resource != null) {
+            throw new BuildException("component can only be defined for a 
single asset");
+        }
+        this.resource = resource;
+    }
+
+    public void setType(org.cyclonedx.model.Component.Type type) {
+        this.type = type;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public void setGroup(String group) {
+        this.group = group;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public Manufacturer createManufacturer() {
+        if (manufacturer != null) {
+            throw new BuildException("component can only have one 
manufacturer");
+        }
+        manufacturer = new Manufacturer();
+        return manufacturer;
+    }
+
+    public void addConfiguredLicense(License l) {
+        licenses.add(l.toCycloneDxLicense());
+    }
+
+    public void setPurl(String purl) {
+        this.purl = purl;
+    }
+
+    public String getPurl() {
+        if (purl != null) {
+            return purl;
+        }
+        if (group != null && name != null && version != null) {
+            return "pkg:maven/" + group + "/" + name + "@" + version + 
"?type=jar";
+        }
+        return null;
+    }
+
+    public void setBomRef(String bomRef) {
+        this.bomRef = bomRef;
+    }
+
+    public String getBomRef() {
+        if (bomRef == null) {
+            return getPurl();
+        }
+        return bomRef;
+    }
+
+    public org.cyclonedx.model.Component toCycloneDxComponent(Version 
bomVersion)
+        throws IOException {
+        if (name == null) {
+            throw new BuildException("component name is required");
+        }
+
+        org.cyclonedx.model.Component component = new 
org.cyclonedx.model.Component();
+
+        component.setType(type);
+        component.setName(name);
+        if (group != null) {
+            component.setGroup(group);
+        }
+        if (version != null) {
+            component.setVersion(version);
+        }
+        if (description != null) {
+            component.setDescription(description);
+        }
+        if (manufacturer != null) {
+            component.setManufacturer(manufacturer.toOrganizationalEntity());
+        }
+        if (!licenses.isEmpty()) {
+            LicenseChoice lc = new LicenseChoice();
+            lc.setLicenses(licenses);
+            component.setLicenses(lc);
+        }
+        String purl = getPurl();
+        if (purl != null) {
+            component.setPurl(purl);
+        }
+        String bomRef = getBomRef();
+        if (bomRef != null) {
+            component.setBomRef(bomRef);
+        }
+        addHashes(component, bomVersion);
+        return component;
+    }
+
+    private void addHashes(org.cyclonedx.model.Component component, Version 
bomVersion)
+        throws IOException {
+        if (resource == null) {
+            return;
+        }
+
+        if (!resource.isFilesystemOnly()) {
+            throw new BuildException("component resource must be a file system 
resource");
+        }
+
+        File file = null;
+        FileProvider fp = resource.as(FileProvider.class);
+        if (fp != null) {
+            file = fp.getFile();
+        }
+        if (file == null || !file.isFile()) {
+            throw new BuildException("component resource doesn't provide a 
file");
+        }
+
+        component.setHashes(BomUtils.calculateHashes(file, bomVersion));
+    }
+
+    public static class Manufacturer {
+        private String name;
+        private List<String> urls = new ArrayList<>();
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public void addConfiguredUrl(URLResource url) {
+            urls.add(url.getURL().toExternalForm());
+        }
+
+        public OrganizationalEntity toOrganizationalEntity() {
+            OrganizationalEntity oe = new OrganizationalEntity();
+            if (name != null) {
+                oe.setName(name);
+            }
+            if (!urls.isEmpty()) {
+                oe.setUrls(urls);
+            }
+            return oe;
+        }
+    }
+
+    public static class License {
+        private String id;
+        private String name;
+
+        public void setLicenseId(String id) {
+            this.id = id;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public org.cyclonedx.model.License toCycloneDxLicense() {
+            if (name == null && id == null) {
+                throw new BuildException("license name or id is required");
+            }
+            org.cyclonedx.model.License l = new org.cyclonedx.model.License();
+            if (name != null) {
+                l.setName(name);
+            }
+            if (id != null) {
+                l.setId(id);
+            }
+            return l;
+        }
+    }
+}
diff --git a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java 
b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
index a081757..0615bf0 100644
--- a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
+++ b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
@@ -30,6 +30,7 @@ public class ComponentBomTask extends Task {
 
     private File bomFile;
     private Format format = Format.JSON;
+    private Component component;
 
     public void setBomFile(File f) {
         bomFile = f;
@@ -39,6 +40,14 @@ public class ComponentBomTask extends Task {
         this.format = format;
     }
 
+    public Component createComponent() {
+        if (component != null) {
+            throw new BuildException("only one nested component element is 
permitted");
+        }
+        component = new Component();
+        return component;
+    }
+
     public void execute() {
         try {
             Bom bom = createBom();
@@ -62,6 +71,11 @@ public class ComponentBomTask extends Task {
         l.setLifecycleChoice(Collections.singletonList(lc));
         meta.setLifecycles(l);
 
+        if (component == null) {
+            throw new BuildException("nested component element is required");
+        }
+        meta.setComponent(component.toCycloneDxComponent(Version.VERSION_16));
+
         bom.setMetadata(meta);
         return bom;
     }
diff --git a/src/main/org/apache/ant/cyclonedx/ToolData.java 
b/src/main/org/apache/ant/cyclonedx/ToolData.java
index 6cdcd7e..5ac2b40 100644
--- a/src/main/org/apache/ant/cyclonedx/ToolData.java
+++ b/src/main/org/apache/ant/cyclonedx/ToolData.java
@@ -8,13 +8,11 @@ import java.security.CodeSource;
 import java.util.Collections;
 import java.util.Properties;
 
+import org.apache.tools.ant.types.resources.FileResource;
+import org.apache.tools.ant.types.resources.URLResource;
+
 import org.cyclonedx.Version;
-import org.cyclonedx.model.Component;
-import org.cyclonedx.model.License;
-import org.cyclonedx.model.LicenseChoice;
-import org.cyclonedx.model.OrganizationalEntity;
 import org.cyclonedx.model.metadata.ToolInformation;
-import org.cyclonedx.util.BomUtils;
 
 /**
  * Provides tool information for BOM's metadata section.
@@ -34,29 +32,28 @@ public class ToolData {
         ToolInformation tool = new ToolInformation();
         Component antlibComponent = new Component();
 
-        antlibComponent.setType(Component.Type.LIBRARY);
         antlibComponent.setGroup("org.apache.ant");
         antlibComponent.setName("ant-cyclonedx");
         antlibComponent.setVersion(getVersion());
         antlibComponent.setDescription("Apache CycloneDX Antlib");
 
-        OrganizationalEntity manufacturer = new OrganizationalEntity();
+        Component.Manufacturer manufacturer = 
antlibComponent.createManufacturer();
         manufacturer.setName("Apache Ant Development Team");
-        
manufacturer.setUrls(Collections.singletonList("https://ant.apache.org/";));
-        antlibComponent.setManufacturer(manufacturer);
+        manufacturer.addConfiguredUrl(new 
URLResource("https://ant.apache.org/";));
 
-        LicenseChoice lc = new LicenseChoice();
-        License license = new License();
-        license.setId("Apache-2.0");
-        lc.setLicenses(Collections.singletonList(license));
-        antlibComponent.setLicenses(lc);
+        Component.License license = new Component.License();
+        license.setLicenseId("Apache-2.0");
+        antlibComponent.addConfiguredLicense(license);
 
         File antlib = findAntlib();
         if (antlib != null) {
-            antlibComponent.setHashes(BomUtils.calculateHashes(antlib, 
Version.VERSION_16));
+            antlibComponent.add(new FileResource(antlib));
         }
 
-        tool.setComponents(Collections.singletonList(antlibComponent));
+        org.cyclonedx.model.Component cdxComponent =
+            antlibComponent.toCycloneDxComponent(Version.VERSION_16);
+        cdxComponent.setBomRef(null);
+        tool.setComponents(Collections.singletonList(cdxComponent));
         return tool;
     }
 
diff --git a/src/tests/antunit/componentbom-test.xml 
b/src/tests/antunit/componentbom-test.xml
index c3c0ac4..7d1e1f2 100644
--- a/src/tests/antunit/componentbom-test.xml
+++ b/src/tests/antunit/componentbom-test.xml
@@ -19,11 +19,16 @@
 
   <import file="shared.xml" />
 
+  <checksum property="sha256hash" file="${antlib.location}" 
algorithm="SHA-256"/>
+
   <target name="testToolMetadataInJsonFormat">
-    <checksum property="sha256hash" file="${antlib.location}" 
algorithm="SHA-256"/>
     <mkdir dir="${output}"/>
     <cdx:componentbom bomfile="${output}/bom.json"
-                      xmlns:cdx="antlib:org.apache.ant.cyclonedx"/>
+                      xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component name="testname">
+        <file file="${antlib.location}"/>
+      </component>
+    </cdx:componentbom>
     <au:assertResourceContains
         xmlns:au="antlib:org.apache.ant.antunit"
         resource="${output}/bom.json"
@@ -47,10 +52,13 @@
   </target>
 
   <target name="testToolMetadataInXmlFormat">
-    <checksum property="sha256hash" file="${antlib.location}" 
algorithm="SHA-256"/>
     <mkdir dir="${output}"/>
     <cdx:componentbom bomfile="${output}/bom.xml" format="XML"
-                      xmlns:cdx="antlib:org.apache.ant.cyclonedx"/>
+                      xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component name="testname">
+        <file file="${antlib.location}"/>
+      </component>
+    </cdx:componentbom>
     <xmlproperty file="${output}/bom.xml"/>
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
@@ -93,4 +101,156 @@
         resource="${output}/bom.xml"
         value='&lt;hash alg="SHA-256"&gt;${sha256hash}&lt;/hash&gt;'/>
   </target>
+
+  <target name="testComponentIsRequired">
+    <au:expectfailure expectedMessage="nested component element is required"
+        xmlns:au="antlib:org.apache.ant.antunit">
+      <cdx:componentbom bomfile="${output}/bom.xml" format="XML"
+                        xmlns:cdx="antlib:org.apache.ant.cyclonedx"/>
+    </au:expectfailure>
+  </target>
+
+  <target name="testOnlyOneComponentIsPermitted">
+    <au:expectfailure expectedMessage="only one nested component element is 
permitted"
+        xmlns:au="antlib:org.apache.ant.antunit">
+      <cdx:componentbom bomfile="${output}/bom.xml" format="XML"
+                        xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+        <component/>
+        <component/>
+      </cdx:componentbom>
+    </au:expectfailure>
+  </target>
+
+  <target name="testComponentNameIsRequired">
+    <au:expectfailure expectedMessage="component name is required"
+        xmlns:au="antlib:org.apache.ant.antunit">
+      <cdx:componentbom bomfile="${output}/bom.xml" format="XML"
+                        xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+        <component/>
+      </cdx:componentbom>
+    </au:expectfailure>
+  </target>
+
+  <target name="testMinimalComponentData">
+    <mkdir dir="${output}"/>
+    <cdx:componentbom bomfile="${output}/bom.xml" format="XML"
+                      xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component name="testname"/>
+    </cdx:componentbom>
+    <xmlproperty file="${output}/bom.xml"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.name"
+        value="testname"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component(type)"
+        value="library"/>
+  </target>
+
+  <target name="testComponentTypeCanBeSet">
+    <mkdir dir="${output}"/>
+    <cdx:componentbom bomfile="${output}/bom.xml" format="XML"
+                      xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component name="testname" type="APPLICATION"/>
+    </cdx:componentbom>
+    <xmlproperty file="${output}/bom.xml"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component(type)"
+        value="application"/>
+  </target>
+
+  <target name="testCalculatesPurlAndBomRef">
+    <mkdir dir="${output}"/>
+    <cdx:componentbom bomfile="${output}/bom.xml" format="XML"
+                      xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component name="testname" group="org.example" version="1.0"/>
+    </cdx:componentbom>
+    <xmlproperty file="${output}/bom.xml"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.purl"
+        value="pkg:maven/org.example/[email protected]?type=jar"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component(bom-ref)"
+        value="pkg:maven/org.example/[email protected]?type=jar"/>
+  </target>
+
+  <target name="testPurlAndBomRefCanBeSet">
+    <mkdir dir="${output}"/>
+    <cdx:componentbom bomfile="${output}/bom.xml" format="XML"
+                      xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component name="testname" group="org.example" version="1.0"
+                 purl="foo" bomRef="bar"/>
+    </cdx:componentbom>
+    <xmlproperty file="${output}/bom.xml"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.purl"
+        value="foo"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component(bom-ref)"
+        value="bar"/>
+  </target>
+
+  <target name="testMaximalComponentData">
+    <mkdir dir="${output}"/>
+    <checksum property="ant.file.sha256" file="${ant.file}" 
algorithm="SHA-256"/>
+    <cdx:componentbom bomfile="${output}/bom.xml" format="XML"
+                      xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component name="testname" group="org.example" version="1.0"
+                 description="My Test Library">
+        <file file="${ant.file}"/>
+        <manufacturer name="Example">
+          <url url="https://example.org/"/>
+        </manufacturer>
+        <license licenseId="Apache-2.0"/>
+      </component>
+    </cdx:componentbom>
+    <xmlproperty file="${output}/bom.xml"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.name"
+        value="testname"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component(type)"
+        value="library"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.group"
+        value="org.example"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.version"
+        value="1.0"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.purl"
+        value="pkg:maven/org.example/[email protected]?type=jar"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component(bom-ref)"
+        value="pkg:maven/org.example/[email protected]?type=jar"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.manufacturer.name"
+        value="Example"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.manufacturer.url"
+        value="https://example.org/"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.licenses.license.id"
+        value="Apache-2.0"/>
+    <au:assertResourceContains
+        xmlns:au="antlib:org.apache.ant.antunit"
+        resource="${output}/bom.xml"
+        value='&lt;hash alg="SHA-256"&gt;${ant.file.sha256}&lt;/hash&gt;'/>
+  </target>
+
 </project>

Reply via email to