Repository: CycloneDX/cyclonedx-core-java
Pull request #380 (2024-03-14 19:36:12)

Add values to External References for 1.6
It implements:

https://github.com/CycloneDX/specification/pull/269
https://github.com/CycloneDX/specification/pull/381

NOTE: failing test are unrelated to the current changes


Patch:
diff --git a/pom.xml b/pom.xml
index ba43578e7..9dbda9771 100644
--- a/pom.xml
+++ b/pom.xml
@@ -161,6 +161,13 @@
             <artifactId>commons-lang3</artifactId>
             <version>3.14.0</version>
         </dependency>
+        
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-collections4</artifactId>
+            <version>4.4</version>
+        </dependency>
+
 
         <!-- Package URL -->
 
diff --git a/src/main/java/org/cyclonedx/model/ExternalReference.java b/src/main/java/org/cyclonedx/model/ExternalReference.java
index 9f908192f..31c32a13b 100644
--- a/src/main/java/org/cyclonedx/model/ExternalReference.java
+++ b/src/main/java/org/cyclonedx/model/ExternalReference.java
@@ -35,7 +35,7 @@ import org.cyclonedx.util.serializer.ExternalReferenceSerializer;
 @JsonIgnoreProperties(ignoreUnknown = true)
 @JsonInclude(JsonInclude.Include.NON_NULL)
 @JsonSerialize(using = ExternalReferenceSerializer.class)
-@JsonPropertyOrder({"url", "comment", "hashes"})
+@JsonPropertyOrder({"bom", "url", "comment", "hashes"})
 public class ExternalReference {
 
     public enum Type {
@@ -59,6 +59,8 @@ public class ExternalReference {
         DOCUMENTATION("documentation"),
         @JsonProperty("support")
         SUPPORT("support"),
+        @JsonProperty("source-distribution")
+        SOURCE_DISTRIBUTION("source-distribution"),
         @JsonProperty("distribution")
         DISTRIBUTION("distribution"),
         @JsonProperty("distribution-intake")
@@ -113,6 +115,8 @@ public class ExternalReference {
         EVIDENCE("evidence"),
         @JsonProperty("formulation")
         FORMULATION("formulation"),
+        @JsonProperty("rfc-9116")
+        RFC_9116("rfc-9116"),
         @JsonProperty("other")
         OTHER("other");
 
diff --git a/src/main/java/org/cyclonedx/util/serializer/ExternalReferenceSerializer.java b/src/main/java/org/cyclonedx/util/serializer/ExternalReferenceSerializer.java
index fbac4e9ee..2102f3c3e 100644
--- a/src/main/java/org/cyclonedx/util/serializer/ExternalReferenceSerializer.java
+++ b/src/main/java/org/cyclonedx/util/serializer/ExternalReferenceSerializer.java
@@ -20,19 +20,19 @@ package org.cyclonedx.util.serializer;
 
 import java.io.IOException;
 import java.util.function.BiPredicate;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.XMLStreamWriter;
 
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.databind.SerializerProvider;
 import com.fasterxml.jackson.databind.ser.std.StdSerializer;
 import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
+import org.apache.commons.collections4.CollectionUtils;
 import org.cyclonedx.model.ExternalReference;
 import org.cyclonedx.model.ExternalReference.Type;
 import org.cyclonedx.model.Hash;
 import org.cyclonedx.util.BomUtils;
 
-public class ExternalReferenceSerializer extends StdSerializer<ExternalReference>
+public class ExternalReferenceSerializer
+    extends StdSerializer<ExternalReference>
 {
   public ExternalReferenceSerializer() {
     this(null);
@@ -46,52 +46,72 @@ public class ExternalReferenceSerializer extends StdSerializer<ExternalReference
   public void serialize(
       final ExternalReference extRef, final JsonGenerator gen, final SerializerProvider provider) throws IOException
   {
-    final BiPredicate<Type, String> validateExternalReference = (type, url) -> (type != null && url != null && BomUtils.validateUriString(url));
+    final BiPredicate<Type, String> validateExternalReference =
+        (type, url) -> (type != null && url != null && BomUtils.validateUriString(url));
+
+    if (!validateExternalReference.test(extRef.getType(), extRef.getUrl())) {
+      return;
+    }
+
     if (gen instanceof ToXmlGenerator) {
-      final ToXmlGenerator toXmlGenerator = (ToXmlGenerator) gen;
-      final XMLStreamWriter staxWriter = toXmlGenerator.getStaxWriter();
+      serializeXml((ToXmlGenerator) gen, extRef);
+    }
+    else {
+      serializeJson(gen, extRef);
+    }
+  }
 
-      if (validateExternalReference.test(extRef.getType(), extRef.getUrl())) {
-        try {
-          staxWriter.writeStartElement("reference");
-          staxWriter.writeAttribute("type", extRef.getType().getTypeName());
-          staxWriter.writeStartElement("url");
-          staxWriter.writeCharacters(extRef.getUrl());
-          staxWriter.writeEndElement();
-          if (extRef.getComment() != null) {
-            staxWriter.writeStartElement("comment");
-            staxWriter.writeCharacters(extRef.getComment());
-            staxWriter.writeEndElement();
-          }
-          if (extRef.getHashes() != null && !extRef.getHashes().isEmpty()) {
-            staxWriter.writeStartElement("hashes");
-            for (Hash hash : extRef.getHashes()) {
-              if (hash != null) {
-                staxWriter.writeStartElement("hash");
-                staxWriter.writeAttribute("alg", hash.getAlgorithm());
-                staxWriter.writeCharacters(hash.getValue());
-                staxWriter.writeEndElement();
-              }
-            }
-            staxWriter.writeEndElement();
-          }
-          staxWriter.writeEndElement();
-        }
-        catch (XMLStreamException ex) {
-          throw new IOException(ex);
-        }
-      }
-    } else if (validateExternalReference.test(extRef.getType(), extRef.getUrl())) {
-      gen.writeStartObject();
-      gen.writeStringField("type", extRef.getType().getTypeName());
-      gen.writeStringField("url", extRef.getUrl());
-      if (extRef.getComment() != null) {
-        gen.writeStringField("comment", extRef.getComment());
+  private void serializeXml(final ToXmlGenerator toXmlGenerator, final ExternalReference extRef) throws IOException {
+    toXmlGenerator.writeStartObject();
+
+    toXmlGenerator.setNextIsAttribute(true);
+    toXmlGenerator.writeFieldName("type");
+    toXmlGenerator.writeString(extRef.getType().getTypeName());
+    toXmlGenerator.setNextIsAttribute(false);
+
+    toXmlGenerator.writeStringField("url", extRef.getUrl());
+    if (extRef.getComment() != null) {
+      toXmlGenerator.writeStringField("comment", extRef.getComment());
+    }
+    if (CollectionUtils.isNotEmpty(extRef.getHashes())) {
+      toXmlGenerator.writeFieldName("hashes");
+      toXmlGenerator.writeStartObject();
+      for (Hash hash : extRef.getHashes()) {
+        toXmlGenerator.writeFieldName("hash");
+        toXmlGenerator.writeStartObject();
+        toXmlGenerator.setNextIsAttribute(true);
+        toXmlGenerator.writeFieldName("alg");
+        toXmlGenerator.writeString(hash.getAlgorithm());
+        toXmlGenerator.setNextIsAttribute(false);
+
+        toXmlGenerator.setNextIsUnwrapped(true);
+        toXmlGenerator.writeStringField("", hash.getValue());
+
+        toXmlGenerator.writeEndObject();
       }
-      if (extRef.getHashes() != null && !extRef.getHashes().isEmpty()) {
-        gen.writePOJOField("hashes", extRef.getHashes());
+      toXmlGenerator.writeEndObject();
+    }
+    toXmlGenerator.writeEndObject();
+  }
+
+  private void serializeJson(final JsonGenerator gen, final ExternalReference extRef) throws IOException {
+    gen.writeStartObject();
+    gen.writeStringField("type", extRef.getType().getTypeName());
+    gen.writeStringField("url", extRef.getUrl());
+    if (extRef.getComment() != null) {
+      gen.writeStringField("comment", extRef.getComment());
+    }
+    if (CollectionUtils.isNotEmpty(extRef.getHashes())) {
+      gen.writeFieldName("hashes");
+      gen.writeStartArray();
+      for (Hash hash : extRef.getHashes()) {
+        gen.writeStartObject();
+        gen.writeStringField("alg", hash.getAlgorithm());
+        gen.writeStringField("content", hash.getValue());
+        gen.writeEndObject();
       }
-      gen.writeEndObject();
+      gen.writeEndArray();
     }
+    gen.writeEndObject();
   }
 }


Test patch:
diff --git a/src/test/java/org/cyclonedx/Issue214RegressionTest.java b/src/test/java/org/cyclonedx/Issue214RegressionTest.java
index adf81b398..235d540ad 100644
--- a/src/test/java/org/cyclonedx/Issue214RegressionTest.java
+++ b/src/test/java/org/cyclonedx/Issue214RegressionTest.java
@@ -4,21 +4,14 @@ import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.regex.Pattern;
-import javax.xml.XMLConstants;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.transform.OutputKeys;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
+import java.util.stream.Stream;
 
 import org.apache.commons.io.IOUtils;
 import org.cyclonedx.generators.BomGeneratorFactory;
+import org.cyclonedx.exception.GeneratorException;
 import org.cyclonedx.generators.json.BomJsonGenerator;
 import org.cyclonedx.generators.xml.BomXmlGenerator;
 import org.cyclonedx.model.Bom;
@@ -29,58 +22,39 @@ import org.cyclonedx.parsers.JsonParser;
 import org.cyclonedx.parsers.Parser;
 import org.cyclonedx.parsers.XmlParser;
 import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.w3c.dom.Document;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 
 public class Issue214RegressionTest
 {
-    @Test
-    public void schema13JsonObjectGenerationTest()
-        throws IOException, ReflectiveOperationException
-    {
-        performJsonTest(Version.VERSION_13);
-    }
-
-    @Test
-    public void schema14JsonObjectGenerationTest()
-        throws IOException, ReflectiveOperationException
-    {
-        performJsonTest(Version.VERSION_14);
-    }
-
-    @Test
-    public void schema13XmlObjectGenerationTest()
-        throws ParserConfigurationException, IOException, ReflectiveOperationException
-    {
-        performXmlTest(Version.VERSION_13);
-    }
-
-    @Test
-    public void schema14XmlObjectGenerationTest()
-        throws ParserConfigurationException, IOException, ReflectiveOperationException
-    {
-        performXmlTest(Version.VERSION_14);
+    static Stream<Arguments> testData() {
+        return Stream.of(
+            Arguments.of(Version.VERSION_16),
+            Arguments.of(Version.VERSION_15),
+            Arguments.of(Version.VERSION_14),
+            Arguments.of(Version.VERSION_13)
+        );
     }
 
-    @Test
-    public void schema15XmlObjectGenerationTest()
-        throws ParserConfigurationException, IOException, ReflectiveOperationException
-    {
-        performXmlTest(Version.VERSION_15);
+    @ParameterizedTest
+    @MethodSource("testData")
+    public void testObjectGeneration(Version version) throws IOException, ReflectiveOperationException, GeneratorException {
+        performJsonTest(version);
+        performXmlTest(version);
     }
 
     private void performXmlTest(final Version pSpecVersion)
-        throws ParserConfigurationException, IOException, ReflectiveOperationException
+        throws GeneratorException, ReflectiveOperationException, IOException
     {
         final Bom inputBom = createIssue214Bom();
         BomXmlGenerator generator = BomGeneratorFactory.createXml(pSpecVersion, inputBom);
-        Document doc = generator.generate();
 
         Assertions.assertTrue(BomXmlGenerator.class.isAssignableFrom(generator.getClass()));
         Assertions.assertEquals(pSpecVersion, generator.getSchemaVersion());
 
-        final String actual = xmlDocumentToString(doc);
+        final String actual = generator.toXmlString();
         final String expected = readFixture("/regression/issue214-expected-output.xml", pSpecVersion);
         Assertions.assertEquals(expected, actual);
         validate(actual, XmlParser.class, pSpecVersion);
@@ -101,37 +75,13 @@ public class Issue214RegressionTest
         validate(actual, JsonParser.class, pSpecVersion);
     }
 
-    private String xmlDocumentToString(final Document doc)
-    {
-        Assertions.assertNotNull(doc);
-        TransformerFactory tf = TransformerFactory.newInstance();
-        Transformer transformer;
-        try {
-            tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
-            transformer = tf.newTransformer();
-
-            transformer.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name());
-            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
-            transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes");
-            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
-
-            StringWriter sw = new StringWriter();
-            transformer.transform(new DOMSource(doc), new StreamResult(sw));
-            return sw.getBuffer().toString().trim();
-        }
-        catch (TransformerException ex) {
-            Assertions.fail("Failed to serialize XML document", ex);
-        }
-        return null;
-    }
-
     private String readFixture(final String pPath, final Version pSpecVersion)
     {
         try (InputStream is = getClass().getResourceAsStream(pPath)) {
             if (is != null) {
                 String result = IOUtils.toString(is, StandardCharsets.UTF_8);
                 result = result.replaceAll(Pattern.quote("${specVersion}"), pSpecVersion.getVersionString());
-                return result.trim();
+                return result;
             }
             else {
                 Assertions.fail("failed to read expected data file: " + pPath);
diff --git a/src/test/resources/regression/issue214-expected-output.json b/src/test/resources/regression/issue214-expected-output.json
index 86474430c..f8a7008cb 100644
--- a/src/test/resources/regression/issue214-expected-output.json
+++ b/src/test/resources/regression/issue214-expected-output.json
@@ -31,4 +31,4 @@
       "type" : "library"
     }
   ]
-}
+}
\ No newline at end of file
diff --git a/src/test/resources/regression/issue214-expected-output.xml b/src/test/resources/regression/issue214-expected-output.xml
index 66affdde3..7d542a5d9 100644
--- a/src/test/resources/regression/issue214-expected-output.xml
+++ b/src/test/resources/regression/issue214-expected-output.xml
@@ -1,21 +1,21 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<bom xmlns="http://cyclonedx.org/schema/bom/${specVersion}" version="1">
-    <components>
-        <component type="library">
-            <group>org.example</group>
-            <name>mylibrary</name>
-            <version>1.0.0</version>
-            <externalReferences>
-                <reference type="bom">
-                    <url>https://example.org/support/sbom/portal-server/1.0.0</url>
-                    <comment>An external SBOM that describes what this component includes</comment>
-                    <hashes>
-                        <hash alg="MD5">2cd42512b65500dc7ba0ff13490b0b73</hash>
-                        <hash alg="SHA-1">226247b40160f2892fa4c7851b5b913d5d10912d</hash>
-                        <hash alg="SHA-256">09a72795a920c1a9c0209cfb8395f8d97089832d249cba8c0938a3423b3ed1d1</hash>
-                    </hashes>
-                </reference>
-            </externalReferences>
-        </component>
-    </components>
+<bom version="1" xmlns="http://cyclonedx.org/schema/bom/${specVersion}">
+  <components>
+    <component type="library">
+      <group>org.example</group>
+      <name>mylibrary</name>
+      <version>1.0.0</version>
+      <externalReferences>
+        <reference type="bom">
+          <url>https://example.org/support/sbom/portal-server/1.0.0</url>
+          <comment>An external SBOM that describes what this component includes</comment>
+          <hashes>
+            <hash alg="MD5">2cd42512b65500dc7ba0ff13490b0b73</hash>
+            <hash alg="SHA-1">226247b40160f2892fa4c7851b5b913d5d10912d</hash>
+            <hash alg="SHA-256">09a72795a920c1a9c0209cfb8395f8d97089832d249cba8c0938a3423b3ed1d1</hash>
+          </hashes>
+        </reference>
+      </externalReferences>
+    </component>
+  </components>
 </bom>