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 f568de9  add support for license expressions
f568de9 is described below

commit f568de9168144e48be04e744e20ac48ce8f6d97b
Author: Stefan Bodewig <[email protected]>
AuthorDate: Sun Aug 16 17:45:41 2026 +0200

    add support for license expressions
---
 changes.xml                                        |  4 ++
 docs/ivyModule.html                                |  1 -
 docs/license.html                                  |  7 +-
 docs/sbomLink.html                                 |  2 +-
 src/main/org/apache/ant/cyclonedx/Component.java   | 12 ++--
 .../org/apache/ant/cyclonedx/ComponentBomTask.java |  7 +-
 src/main/org/apache/ant/cyclonedx/License.java     | 76 ++++++++++++++++++----
 src/tests/antunit/component-ivymodule-test.xml     |  2 -
 src/tests/antunit/license-test.xml                 | 49 +++++++++++++-
 9 files changed, 129 insertions(+), 31 deletions(-)

diff --git a/changes.xml b/changes.xml
index 37c4e75..f5bd076 100644
--- a/changes.xml
+++ b/changes.xml
@@ -114,6 +114,10 @@
       The license element now tries to guess the license's SPDX id
       if only a name is given.
     </action>
+    <action type="add">
+      The license element now supports a new expression attribute you
+      can use to specify license expressions.
+    </action>
   </release>
 
   <release version="0.1" date="2026-06-03" description="initial release">
diff --git a/docs/ivyModule.html b/docs/ivyModule.html
index dfcd652..e0a2f84 100644
--- a/docs/ivyModule.html
+++ b/docs/ivyModule.html
@@ -243,4 +243,3 @@ <h4>templateComponent</h4>
     </pre>
   </body>
 </html>
-    
diff --git a/docs/license.html b/docs/license.html
index 672e3fc..459a64b 100644
--- a/docs/license.html
+++ b/docs/license.html
@@ -44,12 +44,17 @@ <h3>Attributes</h3>
         <td>The id of the license. Must be a
           valid <a href="https://spdx.org/licenses/";>SPDX
           identifier</a>.</td>
-        <td rowspan="2">One of the two is required</td>
+        <td rowspan="3">One of the three is required</td>
       </tr>
       <tr>
         <td>name</td>
         <td>The name of the license.</td>
       </tr>
+      <tr>
+        <td>expression</td>
+        <td>The license expression.<br/>
+          <em>since CycloneDX Antlib 0.2</em></td>
+      </tr>
     </table>
 
     <p>Since CycloneDX Antlib 0.2 the Antlib will try to guess the
diff --git a/docs/sbomLink.html b/docs/sbomLink.html
index 988d5b3..b0a34e5 100644
--- a/docs/sbomLink.html
+++ b/docs/sbomLink.html
@@ -95,4 +95,4 @@ <h3>Attributes</h3>
     </table>
 
   </body>
-</html>    
+</html>
diff --git a/src/main/org/apache/ant/cyclonedx/Component.java 
b/src/main/org/apache/ant/cyclonedx/Component.java
index f2a8c42..f757189 100644
--- a/src/main/org/apache/ant/cyclonedx/Component.java
+++ b/src/main/org/apache/ant/cyclonedx/Component.java
@@ -71,7 +71,7 @@ public class Component extends DataType {
     private Organization manufacturer = null;
     private Organization supplier = null;
     private boolean supplierIsManufacturer = false;
-    private List<org.cyclonedx.model.License> licenses = new ArrayList<>();
+    private List<LicenseItem> licenses = new ArrayList<>();
     private String purl;
     private String bomRef;
     private List<org.cyclonedx.model.ExternalReference> externalReferences = 
new ArrayList<>();
@@ -380,7 +380,7 @@ public class Component extends DataType {
      */
     public void addConfiguredLicense(License l) {
         checkChildrenAllowed();
-        licenses.add(l.toCycloneDxLicense());
+        licenses.add(l.toCycloneDxLicenseItem());
     }
 
     /**
@@ -1001,8 +1001,7 @@ public class Component extends DataType {
             // would create an empty licenses node otherwise
             LicenseChoice lc = new LicenseChoice();
             lc.setItems(licenses.stream()
-                        .sorted(License.CycloneDxLicenseComparator)
-                        .map(LicenseItem::ofLicense)
+                        .sorted(License.CycloneDxLicenseItemComparator)
                         .collect(Collectors.toList()));
             component.setLicenses(lc);
         }
@@ -1076,10 +1075,7 @@ public class Component extends DataType {
         if (licenses.isEmpty()) {
             LicenseChoice realLicenses = real.getLicenses();
             if (realLicenses != null) {
-                licenses.addAll(realLicenses.getItems().stream()
-                                .filter(i -> 
LicenseItem.LicenseItemType.LICENSE.equals(i.getType()))
-                                .map(LicenseItem::getLicense)
-                                .collect(Collectors.toList()));
+                licenses.addAll(realLicenses.getItems());
             }
         }
         if (externalReferences.isEmpty()) {
diff --git a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java 
b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
index df7e1a5..f74584b 100644
--- a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
+++ b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
@@ -84,7 +84,7 @@ public class ComponentBomTask extends Task {
     private boolean useComponentSupplier = false;
     private boolean useComponentManufacturer = false;
     private Union pureFileComponents = new Union();
-    private List<org.cyclonedx.model.License> licenses = new ArrayList<>();
+    private List<LicenseItem> licenses = new ArrayList<>();
 
     /**
      * Specifies the CycloneDX version to use.
@@ -216,7 +216,7 @@ public class ComponentBomTask extends Task {
      * @param l SBOM's license
      */
     public void addConfiguredLicense(License l) {
-        licenses.add(l.toCycloneDxLicense());
+        licenses.add(l.toCycloneDxLicenseItem());
     }
 
     /**
@@ -410,8 +410,7 @@ public class ComponentBomTask extends Task {
         if (!licenses.isEmpty()) {
             LicenseChoice lc = new LicenseChoice();
             lc.setItems(licenses.stream()
-                        .sorted(License.CycloneDxLicenseComparator)
-                        .map(LicenseItem::ofLicense)
+                        .sorted(License.CycloneDxLicenseItemComparator)
                         .collect(Collectors.toList()));
             meta.setLicenses(lc);
         }
diff --git a/src/main/org/apache/ant/cyclonedx/License.java 
b/src/main/org/apache/ant/cyclonedx/License.java
index e4fac3f..a0adb47 100644
--- a/src/main/org/apache/ant/cyclonedx/License.java
+++ b/src/main/org/apache/ant/cyclonedx/License.java
@@ -20,6 +20,8 @@ package org.apache.ant.cyclonedx;
 import java.util.Comparator;
 
 import org.cyclonedx.model.LicenseChoice;
+import org.cyclonedx.model.LicenseItem;
+import org.cyclonedx.model.license.Expression;
 import org.cyclonedx.util.LicenseResolver;
 
 import org.apache.tools.ant.BuildException;
@@ -45,6 +47,7 @@ public class License extends DataType {
     private String id;
     private String name;
     private String url;
+    private String expression;
 
     /**
      * Comparator for CycloneDX license.
@@ -54,10 +57,31 @@ public class License extends DataType {
      *
      * @since CycloneDX Antlib 0.2
      */
-    public static final Comparator<org.cyclonedx.model.License> 
CycloneDxLicenseComparator =
+    private static final Comparator<org.cyclonedx.model.License> 
CycloneDxLicenseComparator =
         Comparator.comparing(org.cyclonedx.model.License::getId, 
Comparator.nullsLast(Comparator.naturalOrder()))
         .thenComparing(org.cyclonedx.model.License::getName, 
Comparator.nullsLast(Comparator.naturalOrder()));
 
+    /**
+     * Comparator for CycloneDX License Expressions.
+     *
+     * <p>Sorts by expression value.</p>
+     *
+     * @since CycloneDX Antlib 0.2
+     */
+    private static final Comparator<Expression> 
CycloneDxLicenseExpressionComparator =
+        Comparator.comparing(Expression::getValue, Comparator.naturalOrder());
+
+    /**
+     * Comparator for CycloneDX License Items.
+     *
+     * <p>Sorts licenses by id falling back to id before license expressions 
which are sorted by expression value.
+     *
+     * @since CycloneDX Antlib 0.2
+     */
+    public static final Comparator<LicenseItem> CycloneDxLicenseItemComparator 
=
+        Comparator.comparing(LicenseItem::getLicense, 
Comparator.nullsLast(CycloneDxLicenseComparator))
+        .thenComparing(LicenseItem::getExpression, 
Comparator.nullsLast(CycloneDxLicenseExpressionComparator));
+
     /**
      * Sets the {@code id} of the license.
      *
@@ -82,6 +106,17 @@ public class License extends DataType {
         this.name = name;
     }
 
+    /**
+     * Sets the SPDX license expression of the license.
+     *
+     * @param expression license expression
+     * @since CycloneDX Antlib 0.2
+     */
+    public void setExpression(String expression) {
+        checkAttributesAllowed();
+        this.expression = expression;
+    }
+
     /**
      * Sets the url of the license.
      *
@@ -103,13 +138,21 @@ public class License extends DataType {
      *
      * @return CycloneDX version of this instance
      */
-    public org.cyclonedx.model.License toCycloneDxLicense() {
+    public LicenseItem toCycloneDxLicenseItem() {
         if (isReference()) {
-            return getRef().toCycloneDxLicense();
+            return getRef().toCycloneDxLicenseItem();
         }
         dieOnCircularReference();
-        if (name == null && id == null) {
-            throw new BuildException("license name or licenseId is required");
+        if (name == null && id == null && expression == null) {
+            throw new BuildException("license name, licenseId or expression is 
required");
+        }
+
+        if ((name != null || id != null) && expression != null) {
+            throw new BuildException("license name or licenseId prohibit 
expression");
+        }
+
+        if (expression != null) {
+            return LicenseItem.ofExpression(new Expression(expression));
         }
 
         if (id == null) {
@@ -118,7 +161,7 @@ public class License extends DataType {
                 if (url != null) {
                     l.setUrl(url);
                 }
-                return l;
+                return LicenseItem.ofLicense(l);
             }
         }
 
@@ -132,14 +175,25 @@ public class License extends DataType {
         if (url != null) {
             l.setUrl(url);
         }
-        return l;
+        return LicenseItem.ofLicense(l);
     }
 
     /**
      * @since CycloneDX Antlib 0.2
      */
-    public static License from(org.cyclonedx.model.License l) {
+    public static License from(LicenseItem licenseItem) {
         License license = new License();
+        Expression e = licenseItem.getExpression();
+        if (e != null) {
+            license.setExpression(e.getValue());
+            return license;
+        }
+
+        org.cyclonedx.model.License l = licenseItem.getLicense();
+        if (l == null) {
+            throw new BuildException("unsupported LicenseItem " + licenseItem);
+        }
+
         String id = l.getId();
         if (id != null) {
             license.setLicenseId(id);
@@ -170,12 +224,12 @@ public class License extends DataType {
      */
     private org.cyclonedx.model.License guessLicense() {
         LicenseChoice lc = LicenseResolver.resolve(name, false);
-        if ((lc == null || lc.getLicenses() == null || 
lc.getLicenses().isEmpty())
+        if ((lc == null || lc.getItems() == null || lc.getItems().isEmpty())
             && url != null) {
             lc = LicenseResolver.resolve(url, false);
         }
-        if (lc != null && lc.getLicenses() != null && 
!lc.getLicenses().isEmpty()) {
-            return lc.getLicenses().get(0);
+        if (lc != null && lc.getItems() != null && !lc.getItems().isEmpty()) {
+            return lc.getItems().get(0).getLicense();
         }
         return null;
     }
diff --git a/src/tests/antunit/component-ivymodule-test.xml 
b/src/tests/antunit/component-ivymodule-test.xml
index bdb92d0..08f7d6b 100644
--- a/src/tests/antunit/component-ivymodule-test.xml
+++ b/src/tests/antunit/component-ivymodule-test.xml
@@ -272,7 +272,6 @@
         <manufacturer refid="ant-team"/>
       </additionalComponent>
     </cdx:componentbom>
-    <copy todir="/tmp" file="${output}/bom.xml"/>
     <xmlproperty file="${output}/bom.xml"/>
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
@@ -352,7 +351,6 @@
         </cdx:ivyModule>
       </component>
     </cdx:componentbom>
-    <copy todir="/tmp" file="${output}/bom.xml"/>
     <xmlproperty file="${output}/bom.xml"/>
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
diff --git a/src/tests/antunit/license-test.xml 
b/src/tests/antunit/license-test.xml
index 5aec828..3b4740e 100644
--- a/src/tests/antunit/license-test.xml
+++ b/src/tests/antunit/license-test.xml
@@ -19,9 +19,9 @@
 
   <import file="shared.xml" />
 
-  <target name="testLicenseRequiresIdOrName">
+  <target name="testLicenseRequiresIdNameOrExpression">
     <au:expectfailure
-        expectedMessage="license name or licenseId is required"
+        expectedMessage="license name, licenseId or expression is required"
         xmlns:au="antlib:org.apache.ant.antunit">
       <cdx:componentbom
           outputdirectory="${output}" format="xml"
@@ -33,6 +33,34 @@
     </au:expectfailure>
   </target>
 
+  <target name="testLicenseDoesntAllowIdAndExpression">
+    <au:expectfailure
+        expectedMessage="license name or licenseId prohibit expression"
+        xmlns:au="antlib:org.apache.ant.antunit">
+      <cdx:componentbom
+          outputdirectory="${output}" format="xml"
+          xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+        <component name="testname">
+          <license licenseId="Apache-2.0" expression="foo"/>
+        </component>
+      </cdx:componentbom>
+    </au:expectfailure>
+  </target>
+
+  <target name="testLicenseDoesntAllowNameAndExpression">
+    <au:expectfailure
+        expectedMessage="license name or licenseId prohibit expression"
+        xmlns:au="antlib:org.apache.ant.antunit">
+      <cdx:componentbom
+          outputdirectory="${output}" format="xml"
+          xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+        <component name="testname">
+          <license name="bar" expression="foo"/>
+        </component>
+      </cdx:componentbom>
+    </au:expectfailure>
+  </target>
+
   <target name="testLicenseDoesntAllowMultipleUrls">
     <au:expectfailure
         expectedMessage="only one URL is allowed in license"
@@ -50,7 +78,7 @@
     </au:expectfailure>
   </target>
 
-  <target name="testLicenseWorksAsDirectChildOfComponent">
+  <target name="testLicenseNameWorksAsDirectChildOfComponent">
     <cdx:componentbom
         outputdirectory="${output}" format="xml"
         xmlns:cdx="antlib:org.apache.ant.cyclonedx">
@@ -71,6 +99,21 @@
         value="https://example.org/"/>
   </target>
 
+  <target name="testLicenseExpressionWorksAsDirectChildOfComponent">
+    <cdx:componentbom
+        outputdirectory="${output}" format="xml"
+        xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component name="testname">
+        <license expression="foo"/>
+      </component>
+    </cdx:componentbom>
+    <xmlproperty file="${output}/bom.xml"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.licenses.expression"
+        value="foo"/>
+  </target>
+
   <target name="testLicenseGuessesIdFromLicenseName">
     <cdx:componentbom
         outputdirectory="${output}" format="xml"

Reply via email to