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 fe902d2  add ability to state completeness of relationship description
fe902d2 is described below

commit fe902d2cb49ed6aa72e5a3c3fcbe4a029dade182
Author: Stefan Bodewig <[email protected]>
AuthorDate: Thu Aug 27 22:26:59 2026 +0200

    add ability to state completeness of relationship description
---
 build.xml                                          | 38 +++++++++++++++-
 changes.xml                                        |  4 ++
 docs/component.html                                | 12 +++++
 src/main/org/apache/ant/cyclonedx/Component.java   | 26 +++++++++++
 .../org/apache/ant/cyclonedx/ComponentBomTask.java | 30 ++++++++++++
 .../apache/ant/cyclonedx/CompositionAggregate.java | 53 ++++++++++++++++++++++
 src/tests/antunit/componentbom-test.xml            | 48 ++++++++++++++++++++
 7 files changed, 209 insertions(+), 2 deletions(-)

diff --git a/build.xml b/build.xml
index a13aa19..5ca6312 100644
--- a/build.xml
+++ b/build.xml
@@ -111,6 +111,38 @@ under the License.
     </cdx:externalreferenceset>
   </target>
 
+  <!-- overridden until common/cyclonedx.xml supports compositionAggregate -->
+  <target name="define-core-ant-components"
+          if="add.ant.core.component"
+          xmlns:cdx="antlib:org.apache.ant.cyclonedx"
+          depends="define-common-cyclonedx-references">
+    <cdx:component
+        name="ant-launcher"
+        group="org.apache.ant"
+        version="${ant.core.version}"
+        isExternal="true"
+        compositionAggregate="complete"
+        id="ant-launcher">
+      <supplier refid="ant-pmc"/>
+      <license refid="apache-2"/>
+      <externalReferenceSet refid="ant-common-refs"/>
+      <externalReferenceSet refid="ant-ext-refs"/>
+    </cdx:component>
+    <cdx:component
+        name="ant"
+        group="org.apache.ant"
+        version="${ant.core.version}"
+        isExternal="true"
+        compositionAggregate="complete"
+        id="ant">
+      <supplier refid="ant-pmc"/>
+      <license refid="apache-2"/>
+      <externalReferenceSet refid="ant-common-refs"/>
+      <externalReferenceSet refid="ant-ext-refs"/>
+      <dependency componentRef="ant-launcher"/>
+    </cdx:component>
+  </target>
+
   <target name="create-antlib-sbom"
           depends="define-cyclonedx-components"
           if="can.use.cyclonedx"
@@ -134,7 +166,8 @@ under the License.
       <component
           description="Apache CycloneDX Antlib"
           publisher="The Apache Software Foundation"
-          supplierIsManufacturer="true">
+          supplierIsManufacturer="true"
+          compositionAggregate="complete">
         <cdx:ivyModule conf="default,provided" externalConf="provided">
           <templateComponent refid="ant"/>
           <templateComponent refid="ant-launcher"/>
@@ -143,7 +176,8 @@ under the License.
               group="org.apache.ivy"
               publisher="The Apache Software Foundation"
               description="Apache Ivy"
-              supplierIsManufacturer="true">
+              supplierIsManufacturer="true"
+              compositionAggregate="complete">
             <supplier refid="ant-pmc"/>
             <license refid="apache-2"/>
             <externalReferenceSet refid="ant-common-refs"/>
diff --git a/changes.xml b/changes.xml
index a34da27..c1b147f 100644
--- a/changes.xml
+++ b/changes.xml
@@ -118,6 +118,10 @@
       The license element now supports a new expression attribute you
       can use to specify license expressions.
     </action>
+    <action type="add">
+      A new attribute compositionAggregate on component allows you to
+      specify the completeness of the component's description.
+    </action>
   </release>
 
   <release version="0.1" date="2026-06-03" description="initial release">
diff --git a/docs/component.html b/docs/component.html
index 1716483..bf8403a 100644
--- a/docs/component.html
+++ b/docs/component.html
@@ -140,6 +140,18 @@ <h3>Attributes</h3>
           having no dependencies at all. </td>
         <td>No - defaults to <code>false</code>.</td>
       </tr>
+      <tr>
+        <td>compositionAggregate</td>
+        <td>The completeness of the component's description specified
+          as a CylcloneDX composition aggregate. Valid
+          compositionAggregate are defined by the
+          <a 
href="https://cyclonedx.org/docs/1.7/json/#compositions_items_aggregate";>CycloneDX
+          specification</a>.</td>
+        <td>No - by default no composition including this component
+          will be created.<br/>
+          <em>since CylconeDX Antlib 0.2</em>
+        </td>
+      </tr>
     </table>
 
     <h3>Nested elements</h3>
diff --git a/src/main/org/apache/ant/cyclonedx/Component.java 
b/src/main/org/apache/ant/cyclonedx/Component.java
index cd7a6c7..406bea4 100644
--- a/src/main/org/apache/ant/cyclonedx/Component.java
+++ b/src/main/org/apache/ant/cyclonedx/Component.java
@@ -85,6 +85,7 @@ public class Component extends DataType {
     private Set<String> tags = new HashSet<>();
     private List<Property> properties = new ArrayList<>();
     private String mimeType;
+    private CompositionAggregate compositionAggregate;
     private ComponentResolver resolver;
 
     /**
@@ -164,6 +165,7 @@ public class Component extends DataType {
         this.tags = new HashSet<>(other.tags);
         this.properties = new ArrayList<>(other.properties);
         this.mimeType = other.mimeType;
+        this.compositionAggregate = other.compositionAggregate;
         this.resolver = other.resolver;
     }
 
@@ -287,6 +289,16 @@ public class Component extends DataType {
         this.mimeType = mimeType;
     }
 
+    /**
+     * Sets the composition aggregate of this component.
+     *
+     * @param compositionAggregate composition aggregate
+     * @since CycloneDX Antlib 0.2
+     */
+    public void setCompositionAggregate(CompositionAggregate 
compositionAggregate) {
+        this.compositionAggregate = compositionAggregate;
+    }
+
     /**
      * Sets the manufacturer of the component.
      *
@@ -644,6 +656,20 @@ public class Component extends DataType {
         return mimeType;
     }
 
+    /**
+     * Gets the composition aggregate of the component.
+     *
+     * @return composition aggregate - may be null
+     * @since CycloneDX Antlib 0.2
+     */
+    public CompositionAggregate getCompositionAggregate() {
+        if (isReference()) {
+            return getRef().getCompositionAggregate();
+        }
+        dieOnCircularReference();
+        return compositionAggregate;
+    }
+
     /**
      * Gets the manufacturer of the component.
      *
diff --git a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java 
b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
index 179b994..9e04701 100644
--- a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
+++ b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
@@ -35,6 +35,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.UUID;
 import java.util.function.Consumer;
 import java.util.regex.Matcher;
@@ -53,6 +54,8 @@ import org.cyclonedx.generators.BomGeneratorFactory;
 import org.cyclonedx.generators.json.BomJsonGenerator;
 import org.cyclonedx.generators.xml.BomXmlGenerator;
 import org.cyclonedx.model.Bom;
+import org.cyclonedx.model.BomReference;
+import org.cyclonedx.model.Composition;
 import org.cyclonedx.model.Dependency;
 import org.cyclonedx.model.LicenseChoice;
 import org.cyclonedx.model.LicenseItem;
@@ -346,6 +349,7 @@ public class ComponentBomTask extends Task {
         cs.sort(Component.CycloneDxComponentComparator);
         bom.setComponents(cs);
         addDependencies(bom, knownComponents, resolvedComponentsAdded);
+        addCompositions(bom, resolvedComponentsAdded);
 
         return bom;
     }
@@ -484,6 +488,32 @@ public class ComponentBomTask extends Task {
         bom.setDependencies(dependencies);
     }
 
+    private void addCompositions(Bom bom, List<Component> 
resolvedComponentsAdded) {
+        final Map<Composition.Aggregate, List<String>> componentsByAggregate = 
new TreeMap<>();
+        visitAllComponentsWithExtra(resolvedComponentsAdded, c -> {
+                String bomRef = c.getBomRef();
+                CompositionAggregate a = c.getCompositionAggregate();
+                if (bomRef != null && a != null) {
+                    Composition.Aggregate key = a.getAggregate();
+                    componentsByAggregate.putIfAbsent(key, new ArrayList<>());
+                    List<String> dependencies = componentsByAggregate.get(key);
+                    dependencies.add(bomRef);
+                }
+            });
+
+        List<Composition> compositions = new ArrayList<>();
+        for (Map.Entry<Composition.Aggregate, List<String>> compositionEntry : 
componentsByAggregate.entrySet()) {
+            Composition composition = new Composition();
+            composition.setAggregate(compositionEntry.getKey());
+            compositionEntry.getValue().stream().sorted().forEach(s -> 
composition.addDependency(new BomReference(s)));
+            compositions.add(composition);
+        }
+
+        if (!compositions.isEmpty()) {
+            bom.setCompositions(compositions);
+        }
+    }
+
     private void visitAllComponents(Consumer<Component> visitor) {
         visitAllComponentsWithExtra(Collections.emptyList(), visitor);
     }
diff --git a/src/main/org/apache/ant/cyclonedx/CompositionAggregate.java 
b/src/main/org/apache/ant/cyclonedx/CompositionAggregate.java
new file mode 100644
index 0000000..002e66c
--- /dev/null
+++ b/src/main/org/apache/ant/cyclonedx/CompositionAggregate.java
@@ -0,0 +1,53 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package org.apache.ant.cyclonedx;
+
+import org.apache.tools.ant.BuildException;
+import org.apache.tools.ant.types.EnumeratedAttribute;
+
+import org.cyclonedx.model.Composition.Aggregate;
+
+/**
+ * CycloneDX composition's aggregate type.
+ *
+ * <p>Specifies an aggregate type that describes how complete a
+ * relationship of a composition is.</p>
+ *
+ * <p>Accepts the enum constants like {@code COMPLETE} as well as the
+ * lowercase version {@code complete}. The values are directly
+ * provided by CycloneDX Core's enum.</p>
+ *
+ * @since CycloneDX Antlib 0.2
+ */
+public class CompositionAggregate extends EnumeratedAttribute {
+
+    @Override
+    public String[] getValues() {
+        return EnumUtils.valuesPlus(Aggregate.class, 
Aggregate::getAggregateName);
+    }
+
+    /**
+     * Translates this instance to a {@link Aggregate}.
+     *
+     * @return translated aggregate
+     * @throws BuildException if the value can not be translated.
+     */
+    public Aggregate getAggregate() {
+        return EnumUtils.valueOf(Aggregate.class, getValue(), 
Aggregate::getAggregateName);
+    }
+}
diff --git a/src/tests/antunit/componentbom-test.xml 
b/src/tests/antunit/componentbom-test.xml
index 2c8b99b..fff9d57 100644
--- a/src/tests/antunit/componentbom-test.xml
+++ b/src/tests/antunit/componentbom-test.xml
@@ -538,4 +538,52 @@
         resource="${output}/bom.xml"
         value='&lt;hash alg="SHA-256"&gt;${ant.file.sha256}&lt;/hash&gt;'/>
   </target>
+
+  <target name="testNoCompositionsByDefault">
+    <cdx:componentbom outputdirectory="${output}" format="xml"
+                      xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component name="testname">
+        <file file="${antlib.location}"/>
+      </component>
+    </cdx:componentbom>
+    <au:assertResourceDoesntContain
+        xmlns:au="antlib:org.apache.ant.antunit"
+        resource="${output}/bom.xml"
+        value='&lt;compositions&gt;'/>
+  </target>
+
+  <target name="testAddsCompositionsWhenAggregateIsSet">
+    <cdx:componentbom outputdirectory="${output}" format="xml"
+                      xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component bomRef="[email protected]" name="testname" 
compositionAggregate="complete"/>
+    </cdx:componentbom>
+    <xmlproperty file="${output}/bom.xml"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.compositions.composition.aggregate"
+        value="complete"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.compositions.composition.dependencies.dependency(ref)"
+        value="[email protected]"/>
+  </target>
+
+  <target name="testGroupsCompositionsByAggregate">
+    <cdx:componentbom outputdirectory="${output}" format="xml"
+                      xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component bomRef="[email protected]" name="testname1" 
compositionAggregate="complete"/>
+      <additionalComponent bomRef="[email protected]" name="testname2" 
compositionAggregate="unknown"/>
+      <additionalComponent bomRef="[email protected]" name="testname4" 
compositionAggregate="complete"/>
+      <additionalComponent bomRef="[email protected]" name="testname3" 
compositionAggregate="complete"/>
+    </cdx:componentbom>
+    <xmlproperty file="${output}/bom.xml"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.compositions.composition.aggregate"
+        value="complete,unknown"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.compositions.composition.dependencies.dependency(ref)"
+        value="[email protected],[email protected],[email protected],[email protected]"/>
+  </target>
 </project>

Reply via email to