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 eb3e778  properly describe what happens when linking external SBOMs
eb3e778 is described below

commit eb3e7788473156a4091eb89632b4d25b19d42e6c
Author: Stefan Bodewig <[email protected]>
AuthorDate: Sat May 16 12:18:59 2026 +0200

    properly describe what happens when linking external SBOMs
    
    and make the implementation agree with what I just came up with :-)
---
 docs/component.html                                |  33 ++
 src/main/org/apache/ant/cyclonedx/Component.java   | 264 +++++++++-------
 .../org/apache/ant/cyclonedx/ComponentBomTask.java |  24 +-
 src/tests/antunit/component-test.xml               | 334 ++++++++++++++++++++-
 4 files changed, 532 insertions(+), 123 deletions(-)

diff --git a/docs/component.html b/docs/component.html
index b929173..c5ad4d9 100644
--- a/docs/component.html
+++ b/docs/component.html
@@ -153,6 +153,39 @@ <h4>any
       describes. This is required if you want to include hashes for
       the component in your SBOM.</p>
 
+    <h4 id="sbomLink">sbomLink</h4>
+
+    <p>At most one resource can be specified
+      as <code>sbomLink</code>. When present the referenced resource is
+      read as CycloneDX SBOM and:</p>
+
+    <ul>
+      <li><code>type</code>, <code>name</code>, <code>group</code>, 
<code>version</code>,
+        <code>purl</code>, <code>bomRef</code>, <code>scope</code>,
+        <code>decription</code>, <code>publisher</code>, <code>copright</code>,
+        <code>mimeType</code> and <code>manufacturer</code> are taken
+        from the SBOM's metadata component unless they are explicitly
+        specified on the component element itself.</li>
+      <li><code>supplier</code> is taken from the SBOM's metadata
+        component unless it is explicitly specified on the component
+        element itself or <code>manufacturerissupplier</code>
+        is <code>true</code>.</li>
+      <li>Tags are merged wiht those of the SBOM's metadata
+        component.</li>
+      <li><code>author</code>s, <code>license</code>s, 
<code>exteranlReference</code>s,
+        <code>dependency</code>s and nested <code>components</code>
+        are taken from the SBOM's metadata component if and only if
+        there is no corresponding element in this component
+        element.</li>
+      <li>Hashes of the linked SBOM are ignored completely.</li>
+      <li>Other components specified in the linked SBOM are also added
+        to the SBOM created by the compomentbom task if they are
+        direct dependencies of the current component element. And
+        their dependencies are set to "unknown" as handling of
+        transitive dependencies is beyond the scope of this
+        library.</li>
+    </ul>
+
     <h4 id="manufacturer">manufacturer</h4>
 
     <p>At most one nested <a href="organization.html">organization</a>
diff --git a/src/main/org/apache/ant/cyclonedx/Component.java 
b/src/main/org/apache/ant/cyclonedx/Component.java
index 6157154..0e123f2 100644
--- a/src/main/org/apache/ant/cyclonedx/Component.java
+++ b/src/main/org/apache/ant/cyclonedx/Component.java
@@ -7,8 +7,10 @@ import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.Stack;
 import java.util.stream.Collectors;
 
@@ -45,7 +47,7 @@ import org.cyclonedx.util.BomUtils;
  */
 public class Component extends DataType {
     private Resource resource;
-    private org.cyclonedx.model.Component.Type type = 
org.cyclonedx.model.Component.Type.LIBRARY;
+    private org.cyclonedx.model.Component.Type type;
     private String name;
     private String group;
     private String publisher;
@@ -66,7 +68,7 @@ public class Component extends DataType {
     private boolean unknownDependencies = false;
     private boolean sbomLinkResolved = false;
     private List<OrganizationalContact> authors = new ArrayList<>();
-    private List<Tag> tags = new ArrayList<>();
+    private Set<String> tags = new HashSet<>();
     private List<Property> properties = new ArrayList<>();
     private String mimeType;
     private Union sbomLink;
@@ -205,9 +207,9 @@ public class Component extends DataType {
     /**
      * Adds a tag to the component.
      */
-    public void addTag(Tag tag) {
+    public void addConfiguredTag(Tag tag) {
         checkChildrenAllowed();
-        tags.add(tag);
+        tags.add(tag.getTag());
     }
 
     /**
@@ -428,67 +430,30 @@ public class Component extends DataType {
         dieOnCircularReference();
 
         if (sbomLink != null && !sbomLinkResolved) {
-            if (sbomLink.size() != 1) {
-                throw new BuildException("sbomLink requires exactly one nested 
resource");
+            Bom bom = readLinkedSbom();
+            if (bom.getMetadata() == null) {
+                throw new BuildException("referenced SBOM file lacks 
metadata");
             }
-            Resource sbom = sbomLink.iterator().next();
-            try (InputStream data = sbom.getInputStream();
-                 ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
-                byte[] buf = new byte[4096];
-                int count = data.read(buf, 0, buf.length);
-                while (count >= 0) {
-                    baos.write(buf, 0, count);
-                    count = data.read(buf, 0, buf.length);
-                }
-                byte[] content = baos.toByteArray();
-                try {
-                    Parser parser = BomParserFactory.createParser(content);
-                    Bom bom = parser.parse(content);
-                    if (bom.getMetadata() == null) {
-                        throw new BuildException("referenced SBOM file lacks 
metadata");
-                    }
-                    org.cyclonedx.model.Component real = 
bom.getMetadata().getComponent();
-                    if (real == null) {
-                        throw new BuildException("referenced SBOM file lacks 
component");
-                    }
-                    fillFrom(real);
-
-                    List<org.cyclonedx.model.Dependency> allDependencies = 
bom.getDependencies();
-                    if (allDependencies != null) {
-                        setUnknownDependencies(true);
-                        org.cyclonedx.model.Dependency myDependencies = 
allDependencies
-                            .stream()
-                            .filter(d -> Objects.equals(d.getRef(), 
getBomRef()))
-                            .findAny()
-                            .orElse(null);
-                        if (myDependencies != null && 
myDependencies.getDependencies() != null) {
-                            setUnknownDependencies(false);
-                            dependencies.clear();
-                            dependencies
-                                .addAll(myDependencies.getDependencies()
-                                        .stream()
-                                        .map(Dependency::from)
-                                        .collect(Collectors.toList()));
-                        }
-                    }
-
-                    List<org.cyclonedx.model.Component> additionalComponents = 
bom.getComponents();
-                    if (additionalComponents != null && 
!areDependenciesUnknown()) {
-                        List<Component> toReturn = new ArrayList<>();
-                        for (org.cyclonedx.model.Component c : 
additionalComponents) {
-                            Component dep = from(c);
-                            if (dependencies.stream().anyMatch(d -> 
Objects.equals(dep.getBomRef(), d.getBomRef()))) {
-                                // we don't want to resolve transitive 
dependencies automatically
-                                dep.setUnknownDependencies(true);
-                                toReturn.add(dep);
-                            }
-                        }
-                        return toReturn;
+            org.cyclonedx.model.Component real = 
bom.getMetadata().getComponent();
+            if (real == null) {
+                throw new BuildException("referenced SBOM file lacks 
component");
+            }
+            List<org.cyclonedx.model.Dependency> allDependencies = 
bom.getDependencies();
+            fillFrom(real, allDependencies);
+
+            List<org.cyclonedx.model.Component> additionalComponents = 
bom.getComponents();
+            if (additionalComponents != null && !areDependenciesUnknown()) {
+                List<Component> toReturn = new ArrayList<>();
+                for (org.cyclonedx.model.Component c : additionalComponents) {
+                    Component dep = from(c, Collections.emptyList());
+                    if (dependencies.stream().anyMatch(d -> 
Objects.equals(dep.getBomRef(), d.getBomRef()))) {
+                        // only include "additional components" this component 
depends on directly.
+                        // we don't want to resolve transitive dependencies 
automatically
+                        dep.setUnknownDependencies(true);
+                        toReturn.add(dep);
                     }
-
-                } catch (ParseException ex) {
-                    throw new BuildException("failed to parse sbomlink " + 
sbom.getName());
                 }
+                return toReturn;
             }
             sbomLinkResolved = true;
         }
@@ -522,9 +487,10 @@ public class Component extends DataType {
         return component;
     }
 
-    private static Component from(org.cyclonedx.model.Component real) {
+    private static Component from(org.cyclonedx.model.Component real,
+                                  List<org.cyclonedx.model.Dependency> 
dependencies) {
         Component c = new Component();
-        c.fillFrom(real);
+        c.fillFrom(real, dependencies);
         return c;
     }
 
@@ -548,7 +514,11 @@ public class Component extends DataType {
 
         org.cyclonedx.model.Component component = new 
org.cyclonedx.model.Component();
 
-        component.setType(type);
+        if (type != null) {
+            component.setType(type);
+        } else {
+            component.setType(org.cyclonedx.model.Component.Type.LIBRARY);
+        }
         component.setName(name);
         if (group != null) {
             component.setGroup(group);
@@ -585,9 +555,7 @@ public class Component extends DataType {
             component.setProperties(properties);
         }
         if (!tags.isEmpty()) {
-            Tags ts = new Tags();
-            ts.setTags(tags.stream().map(t -> 
t.getTag()).collect(Collectors.toList()));
-            component.setTags(ts);
+            component.setTags(new 
Tags(tags.stream().sorted().collect(Collectors.toList())));
         }
         if (!licenses.isEmpty()) {
             LicenseChoice lc = new LicenseChoice();
@@ -615,63 +583,110 @@ public class Component extends DataType {
         return component;
     }
 
-    private void fillFrom(org.cyclonedx.model.Component real) {
-        setType(ComponentType.from(real.getType()));
-        setName(real.getName());
-        setGroup(real.getGroup());
-        setVersion(real.getVersion());
-        setDescription(real.getDescription());
-        setPublisher(real.getPublisher());
-        setCopyright(real.getCopyright());
-        setMimeType(real.getMimeType());
-        setPurl(real.getPurl());
-        setBomRef(real.getBomRef());
-        if (real.getScope() != null) {
+    private void fillFrom(org.cyclonedx.model.Component real,
+                          List<org.cyclonedx.model.Dependency> 
allDependencies) {
+        if (type == null) {
+            setType(ComponentType.from(real.getType()));
+        }
+        if (getBomRef() == null) {
+            setBomRef(real.getBomRef());
+        }
+        if (getPurl() == null) {
+            setPurl(real.getPurl());
+        }
+        if (name == null) {
+            setName(real.getName());
+        }
+        if (group == null) {
+            setGroup(real.getGroup());
+        }
+        if (version == null) {
+            setVersion(real.getVersion());
+        }
+        if (scope == null && real.getScope() != null) {
             setScope(ComponentScope.from(real.getScope()));
         }
-        OrganizationalEntity manufacturer = real.getManufacturer();
-        if (manufacturer != null) {
-            this.manufacturer = Organization.from(manufacturer);
+        // copy isExternal once CycloneDX Core supports it
+        if (description == null) {
+            setDescription(real.getDescription());
         }
-        OrganizationalEntity supplier = real.getSupplier();
-        if (supplier != null) {
-            this.supplier = Organization.from(supplier);
+        if (publisher == null) {
+            setPublisher(real.getPublisher());
         }
-        LicenseChoice licenses = real.getLicenses();
-        if (licenses != null) {
-            this.licenses.clear();
-            this.licenses.addAll(licenses.getLicenses());
+        if (copyright == null) {
+            setCopyright(real.getCopyright());
         }
-        if (real.getExternalReferences() != null) {
-            this.externalReferences.clear();
-            this.externalReferences.addAll(real.getExternalReferences());
+        if (mimeType == null) {
+            setMimeType(real.getMimeType());
         }
-        if (real.getAuthors() != null) {
-            authors.clear();
-            authors.addAll(real.getAuthors());
+        if (manufacturer == null) {
+            OrganizationalEntity realManufacturer = real.getManufacturer();
+            if (realManufacturer != null) {
+                manufacturer = Organization.from(realManufacturer);
+            }
         }
-        if (real.getProperties() != null) {
-            properties.clear();
-            properties.addAll(real.getProperties());
+        if (supplier == null && !manufacturerIsSupplier) {
+            OrganizationalEntity realSupplier = real.getSupplier();
+            if (realSupplier != null) {
+                supplier = Organization.from(realSupplier);
+            }
+        }
+        if (licenses.isEmpty()) {
+            LicenseChoice realLicenses = real.getLicenses();
+            if (realLicenses != null) {
+                licenses.addAll(realLicenses.getLicenses());
+            }
+        }
+        if (externalReferences.isEmpty()) {
+            List<org.cyclonedx.model.ExternalReference> realExternalReferences 
=
+                real.getExternalReferences();
+            if (realExternalReferences != null) {
+                externalReferences.addAll(realExternalReferences);
+            }
+        }
+        if (authors.isEmpty()) {
+            List<OrganizationalContact> realAuthors = real.getAuthors();
+            if (realAuthors != null) {
+                authors.addAll(realAuthors);
+            }
+        }
+        if (properties.isEmpty()) {
+            List<Property> realProperties = real.getProperties();
+            if (realProperties != null) {
+                properties.addAll(realProperties);
+            }
         }
         if (real.getTags() != null && real.getTags().getTags() != null) {
-            tags.clear();
-            tags.addAll(real.getTags().getTags()
+            tags.addAll(real.getTags().getTags());
+        }
+        if (nestedComponents.isEmpty()) {
+            List<org.cyclonedx.model.Component> realComponents = 
real.getComponents();
+            if (realComponents != null) {
+                nestedComponents.addAll(realComponents.stream()
+                                        .map(c -> Component.from(c, 
allDependencies))
+                                        .collect(Collectors.toList()));
+            }
+        }
+        if (dependencies.isEmpty() && allDependencies != null) {
+            fillDependencies(allDependencies);
+        }
+    }
+
+    private void fillDependencies(List<org.cyclonedx.model.Dependency> 
allDependencies) {
+        setUnknownDependencies(true);
+        org.cyclonedx.model.Dependency myDependencies = allDependencies
+            .stream()
+            .filter(d -> Objects.equals(d.getRef(), getBomRef()))
+            .findAny()
+            .orElse(null);
+        if (myDependencies != null && myDependencies.getDependencies() != 
null) {
+            setUnknownDependencies(false);
+            dependencies
+                .addAll(myDependencies.getDependencies()
                         .stream()
-                        .map(t -> {
-                                Tag tag = new Tag();
-                                tag.addText(t);
-                                return tag;
-                            })
+                        .map(Dependency::from)
                         .collect(Collectors.toList()));
         }
-        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)
@@ -696,6 +711,29 @@ public class Component extends DataType {
         component.setHashes(BomUtils.calculateHashes(file, bomVersion));
     }
 
+    private Bom readLinkedSbom() throws IOException {
+        if (sbomLink.size() != 1) {
+            throw new BuildException("sbomLink requires exactly one nested 
resource");
+        }
+        Resource sbom = sbomLink.iterator().next();
+        try (InputStream data = sbom.getInputStream();
+             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            byte[] buf = new byte[4096];
+            int count = data.read(buf, 0, buf.length);
+            while (count >= 0) {
+                baos.write(buf, 0, count);
+                count = data.read(buf, 0, buf.length);
+            }
+            byte[] content = baos.toByteArray();
+            try {
+                Parser parser = BomParserFactory.createParser(content);
+                return parser.parse(content);
+            } catch (ParseException ex) {
+                throw new BuildException("failed to parse sbomlink " + 
sbom.getName(), ex);
+            }
+        }
+    }
+
     /**
      * Represents a dependency of a component.
      */
diff --git a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java 
b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
index 98806e1..91d4d85 100644
--- a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
+++ b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
@@ -142,7 +142,15 @@ public class ComponentBomTask extends Task {
             throw new BuildException("nested component element is required");
         }
         Set<String> knownComponents = new HashSet<>();
-        visitAllComponents(c -> 
knownComponents.add(getUnversionedCoordinates(c)));
+        List<Component> resolvedComponents = new ArrayList<>();
+        visitAllComponents(c -> {
+                try {
+                    resolvedComponents.addAll(c.resolve());
+                } catch (IOException ex) {
+                    throw new BuildException("failed to resolve component", 
ex);
+                }
+                knownComponents.add(getUnversionedCoordinates(c));
+            });
         
meta.setComponent(component.toMainCycloneDxComponent(specVersion.getVersion()));
         if (useComponentSupplier) {
             OrganizationalEntity componentSupplier = 
meta.getComponent().getSupplier();
@@ -160,13 +168,14 @@ public class ComponentBomTask extends Task {
 
         bom.setMetadata(meta);
 
-        if (!additionalComponents.isEmpty() || pureFileComponents.size() > 0) {
-            List<org.cyclonedx.model.Component> cs = new ArrayList<>();
-            List<Component> resolvedComponents = new ArrayList<>();
+        List<org.cyclonedx.model.Component> cs = new ArrayList<>();
+        if (!additionalComponents.isEmpty()) {
             for (Component c : additionalComponents) {
-                resolvedComponents.addAll(c.resolve());
                 
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
             }
+        }
+
+        if (!resolvedComponents.isEmpty()) {
             for (Component c : resolvedComponents) {
                 String componentKey = getUnversionedCoordinates(c);
                 if (!knownComponents.contains(componentKey)) {
@@ -174,6 +183,9 @@ public class ComponentBomTask extends Task {
                     
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
                 }
             }
+        }
+
+        if (pureFileComponents.size() > 0) {
             for (Resource r : pureFileComponents) {
                 Component c = new Component();
                 c.setProject(getProject());
@@ -182,9 +194,9 @@ public class ComponentBomTask extends Task {
                 
c.setType(ComponentType.from(org.cyclonedx.model.Component.Type.FILE));
                 
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
             }
-            bom.setComponents(cs);
         }
 
+        bom.setComponents(cs);
         addDependencies(bom);
 
         return bom;
diff --git a/src/tests/antunit/component-test.xml 
b/src/tests/antunit/component-test.xml
index c4413d9..be612d6 100644
--- a/src/tests/antunit/component-test.xml
+++ b/src/tests/antunit/component-test.xml
@@ -177,9 +177,9 @@
     </cdx:componentbom>
   </target>
 
-  <target name="testMaximalComponentData">
+  <target name="createMaximalComponentData" depends="setUp">
     <checksum property="ant.file.sha256" file="${ant.file}" 
algorithm="SHA-256"/>
-    <cdx:componentbom outputdirectory="${output}" format="xml"
+    <cdx:componentbom outputdirectory="${output}" format="all"
                       xmlns:cdx="antlib:org.apache.ant.cyclonedx">
       <component
           name="testname"
@@ -199,12 +199,18 @@
         </supplier>
         <license name="My License"/>
         <externalReference type="WEBSITE" url="https://example.com/"/>
-        <component name="other-test" group="org.example" version="1.1"/>
+        <component name="other-test" group="org.example" version="1.1"
+                   unknownDependencies="true"/>
         <author name="Author" email="[email protected]"/>
         <tag>label</tag>
         <property name="foo" value="bar"/>
+        <dependency bomRef="some-dependency"/>
       </component>
+      <additionalComponent name="dep" bomRef="some-dependency"/>
     </cdx:componentbom>
+  </target>
+
+  <target name="testMaximalComponentData" depends="createMaximalComponentData">
     <xmlproperty file="${output}/bom.xml"/>
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
@@ -242,6 +248,10 @@
         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.description"
+        value="My Test Library"/>
     <au:assertPropertyEquals
         xmlns:au="antlib:org.apache.ant.antunit"
         name="bom.metadata.component.authors.author.name"
@@ -314,6 +324,14 @@
         xmlns:au="antlib:org.apache.ant.antunit"
         resource="${output}/bom.xml"
         value='&lt;url&gt;https://example.com/&lt;/url&gt;'/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.dependencies.dependency(ref)"
+        value="pkg:maven/org.example/[email protected]?type=jar,some-dependency"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.dependencies.dependency.dependency(ref)"
+        value="some-dependency"/>
   </target>
 
   <target name="testManufacturerIsNotCopiedToSupplierByDefault">
@@ -474,7 +492,6 @@
                       xmlns:cdx="antlib:org.apache.ant.cyclonedx">
       <component name="foo" bomRef="foo" unknownDependencies="true"/>
     </cdx:componentbom>
-    <copy todir="/tmp" file="${output}/bom.json"/>
     <au:assertResourceContains
         xmlns:au="antlib:org.apache.ant.antunit"
         resource="${output}/bom.json"
@@ -747,5 +764,314 @@
         <component/>
       </cdx:component>
     </au:expectfailure>
+    <au:expectfailure
+        expectedMessage='You must not specify nested elements when using refid'
+        xmlns:au="antlib:org.apache.ant.antunit">
+      <cdx:component refid="foo"
+                     xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+        <sbomLink/>
+      </cdx:component>
+    </au:expectfailure>
+  </target>
+
+  <target name="testSbomLinkUsesDataFromLinkedSbom" 
depends="createMaximalComponentData">
+    <cdx:componentbom
+        bomName="merged"
+        outputdirectory="${output}"
+        format="xml"
+        xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component>
+        <sbomLink>
+          <file file="${output}/bom.json"/>
+        </sbomLink>
+      </component>
+    </cdx:componentbom>
+    <xmlproperty file="${output}/merged.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.publisher"
+        value="test publisher"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.copyright"
+        value="Copyright 2026 ACME Com"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component(mime-type)"
+        value="text/plain"/>
+    <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.description"
+        value="My Test Library"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.authors.author.name"
+        value="Author"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.authors.author.email"
+        value="[email protected]"/>
+    <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.supplier.name"
+        value="Example 2"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.supplier.url"
+        value="https://example.com/"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.licenses.license.name"
+        value="My License"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.tags.tag"
+        value="label"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.properties.property(name)"
+        value="foo"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.properties.property"
+        value="bar"/>
+    <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"
+        value='&lt;url&gt;https://example.com/&lt;/url&gt;'/>
+    <!-- some dependency is not here as we explicitly set the its
+         dependencies to unknown. We don't want top open the can of
+         transitive dependency worms -->
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.dependencies.dependency(ref)"
+        value="pkg:maven/org.example/[email protected]?type=jar"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.dependencies.dependency.dependency(ref)"
+        value="some-dependency"/>
+    <!-- ensure hashes are not taken from the linked SBOM -->
+    <au:assertResourceDoesntContain
+        xmlns:au="antlib:org.apache.ant.antunit"
+        resource="${output}/merged.xml"
+        value='&lt;hash alg="SHA-256"&gt;${ant.file.sha256}&lt;/hash&gt;'/>
+  </target>
+
+  <target
+      name="testDataFromLinkedSbomCanBeOverwerittenOrMerged"
+      depends="createMaximalComponentData">
+    <cdx:componentbom
+        bomName="merged"
+        outputdirectory="${output}"
+        format="xml"
+        xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+      <component
+          type="APPLICATION"
+          name="testname2"
+          group="org.example2"
+          version="2.0"
+          description="My Second Test Library"
+          publisher="test2 publisher"
+          copyright="Copyright 2026 ACME Corp"
+          mimeType="text/xml"
+          >
+        <file file="${ant.file}"/>
+        <manufacturer name="Example 3">
+          <url url="https://example.org/3"/>
+        </manufacturer>
+        <supplier name="Example 4">
+          <url url="https://example.com/4"/>
+        </supplier>
+        <license name="My Other License"/>
+        <externalReference type="WEBSITE" url="https://example.org/site"/>
+        <component name="yet-another-test" group="org.example2"
+                   version="1.2" unknownDependencies="true"/>
+        <author name="Author2" email="[email protected]"/>
+        <tag>label2</tag>
+        <property name="xyzzy" value="baz"/>
+        <dependency bomRef="my-own-dependency"/>
+        <sbomLink>
+          <file file="${output}/bom.json"/>
+        </sbomLink>
+      </component>
+      <additionalComponent name="my-own-dependency" 
bomRef="my-own-dependency"/>
+    </cdx:componentbom>
+    <xmlproperty file="${output}/merged.xml"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.name"
+        value="testname2"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component(type)"
+        value="application"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.group"
+        value="org.example2"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.version"
+        value="2.0"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.publisher"
+        value="test2 publisher"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.copyright"
+        value="Copyright 2026 ACME Corp"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component(mime-type)"
+        value="text/xml"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.purl"
+        value="pkg:maven/org.example2/[email protected]?type=jar"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component(bom-ref)"
+        value="pkg:maven/org.example2/[email protected]?type=jar"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.description"
+        value="My Second Test Library"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.authors.author.name"
+        value="Author2"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.authors.author.email"
+        value="[email protected]"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.manufacturer.name"
+        value="Example 3"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.manufacturer.url"
+        value="https://example.org/3"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.supplier.name"
+        value="Example 4"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.supplier.url"
+        value="https://example.com/4"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.licenses.license.name"
+        value="My Other License"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.tags.tag"
+        value="label,label2"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.properties.property(name)"
+        value="xyzzy"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.properties.property"
+        value="baz"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.components.component.name"
+        value="yet-another-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.example2"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.components.component.version"
+        value="1.2"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.metadata.component.components.component.purl"
+        value="pkg:maven/org.example2/[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.example2/[email protected]?type=jar"/>
+    <au:assertResourceContains
+        xmlns:au="antlib:org.apache.ant.antunit"
+        resource="${output}/merged.xml"
+        value='&lt;url&gt;https://example.org/site&lt;/url&gt;'/>
+    <au:assertResourceContains
+        xmlns:au="antlib:org.apache.ant.antunit"
+        resource="${output}/merged.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.dependencies.dependency(ref)"
+        
value="pkg:maven/org.example2/[email protected]?type=jar,my-own-dependency"/>
+    <au:assertPropertyEquals
+        xmlns:au="antlib:org.apache.ant.antunit"
+        name="bom.dependencies.dependency.dependency(ref)"
+        value="my-own-dependency"/>
   </target>
 </project>


Reply via email to