Repository: CycloneDX/cyclonedx-core-java (java)

Issue:
Error in ExternalReferenceSerializer
When writing an ExternalReference to xml or json, you check the url string by using "new URL(url).toURI()".
If this call throws an Exception the ExternalReference is skipped.
This is not correct. Java only accepts urls, for which it has a protocol handler. On every other url Java throws a MalformedURLException.
So you may have a correct url, but Java does not know the protocol and throws a exception.
Because of this test, ExternalReferences with valid urls are skipped.
So you should just write out the ExternalRefererence as it is.

Pull request:
Correcting bomlink generation for XML and JSON. Added unit tests.
Resolves #237

Patch:
diff --git a/README.md b/README.md
index 1549bca84..38a2f7f9c 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ Maven Usage
 <dependency>
     <groupId>org.cyclonedx</groupId>
     <artifactId>cyclonedx-core-java</artifactId>
-    <version>7.2.1</version>
+    <version>7.2.2</version>
 </dependency>
 ```
 
diff --git a/src/main/java/org/cyclonedx/util/BomUtils.java b/src/main/java/org/cyclonedx/util/BomUtils.java
index 58b3072ed..820ffac34 100644
--- a/src/main/java/org/cyclonedx/util/BomUtils.java
+++ b/src/main/java/org/cyclonedx/util/BomUtils.java
@@ -27,6 +27,7 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
+import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.file.Files;
@@ -95,6 +96,7 @@ public final class BomUtils {
         throw new IllegalArgumentException("Unable to find algorithm matching '"+digest.getAlgorithm()+"'. Known algorithms: "+ Arrays.stream(Hash.Algorithm.values()).map(Hash.Algorithm::getSpec).sorted().collect(Collectors.joining()));
     }
 
+    @Deprecated
     public static boolean validateUrlString(String url) {
         try {
             new URL(url).toURI();
@@ -103,4 +105,13 @@ public final class BomUtils {
             return false;
         }
     }
+
+    public static boolean validateUriString(String uri) {
+        try {
+            new URI(uri);
+            return true;
+        } catch (URISyntaxException e) {
+            return false;
+        }
+    }
 }
diff --git a/src/main/java/org/cyclonedx/util/ExternalReferenceSerializer.java b/src/main/java/org/cyclonedx/util/ExternalReferenceSerializer.java
index 4e2686301..119a43956 100644
--- a/src/main/java/org/cyclonedx/util/ExternalReferenceSerializer.java
+++ b/src/main/java/org/cyclonedx/util/ExternalReferenceSerializer.java
@@ -46,7 +46,7 @@ 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.validateUrlString(url));
+    final BiPredicate<Type, String> validateExternalReference = (type, url) -> (type != null && url != null && BomUtils.validateUriString(url));
     if (gen instanceof ToXmlGenerator) {
       final ToXmlGenerator toXmlGenerator = (ToXmlGenerator) gen;
       final XMLStreamWriter staxWriter = toXmlGenerator.getStaxWriter();
diff --git a/src/main/resources/licenses/licenses.json b/src/main/resources/licenses/licenses.json
index 1413eb324..6737ebd2b 100644
--- a/src/main/resources/licenses/licenses.json
+++ b/src/main/resources/licenses/licenses.json
@@ -5238,8 +5238,7 @@
       "seeAlso": [
         "https://www.gnu.org/licenses/lgpl-3.0-standalone.html",
         "https://www.gnu.org/licenses/lgpl+gpl-3.0.txt",
-        "https://opensource.org/licenses/LGPL-3.0",
-        "https://www.gnu.org/licenses/lgpl-3.0.html"
+        "https://opensource.org/licenses/LGPL-3.0"
       ],
       "isOsiApproved": true
     },
@@ -5397,4 +5396,4 @@
     }
   ],
   "releaseDate": "2022-05-08"
-}
+}
\ No newline at end of file


Test patch:
diff --git a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java
index 1ccd8a7a3..c5f9038ed 100644
--- a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java
+++ b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java
@@ -164,6 +164,19 @@ public class BomJsonGeneratorTest {
         assertTrue(jsonParser.isValid(file, CycloneDxSchema.Version.VERSION_14));
     }
 
+    @Test
+    public void schema14JBomLinkGenerationTest() throws Exception {
+        Bom bom = createCommonBom("/bom-1.4-bomlink.xml");
+        BomJsonGenerator generator = BomGeneratorFactory.createJson(Version.VERSION_14, bom);
+        File file = writeToFile(generator.toJsonString());
+        JsonParser parser = new JsonParser();
+        assertTrue(parser.isValid(file, CycloneDxSchema.Version.VERSION_14));
+        Bom bom2 = parser.parse(file);
+        assertNotNull(bom2.getComponents().get(0).getExternalReferences());
+        assertEquals("bom", bom2.getComponents().get(0).getExternalReferences().get(0).getType().getTypeName());
+        assertEquals("urn:cdx:f08a6ccd-4dce-4759-bd84-c626675d60a7/1", bom2.getComponents().get(0).getExternalReferences().get(0).getUrl());
+    }
+
     private File writeToFile(String jsonString) throws Exception {
         try (FileWriter writer = new FileWriter(tempFile.getAbsolutePath())) {
             writer.write(jsonString);
diff --git a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java
index 8c10f3ddf..541106533 100644
--- a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java
+++ b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java
@@ -308,6 +308,19 @@ public class BomXmlGeneratorTest {
         assertTrue(parser.isValid(file, CycloneDxSchema.Version.VERSION_13));
     }
 
+    @Test
+    public void schema14JBomLinkGenerationTest() throws Exception {
+        Bom bom = createCommonBom("/bom-1.4-bomlink.xml");
+        BomXmlGenerator generator = BomGeneratorFactory.createXml(Version.VERSION_14, bom);
+        File file = writeToFile(generator.toXmlString());
+        XmlParser parser = new XmlParser();
+        assertTrue(parser.isValid(file, CycloneDxSchema.Version.VERSION_14));
+        Bom bom2 = parser.parse(file);
+        assertNotNull(bom2.getComponents().get(0).getExternalReferences());
+        assertEquals("bom", bom2.getComponents().get(0).getExternalReferences().get(0).getType().getTypeName());
+        assertEquals("urn:cdx:f08a6ccd-4dce-4759-bd84-c626675d60a7/1", bom2.getComponents().get(0).getExternalReferences().get(0).getUrl());
+    }
+
     private File writeToFile(String xmlString) throws Exception {
         try (FileWriter writer = new FileWriter(tempFile.getAbsolutePath())) {
             writer.write(xmlString);
diff --git a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
index 4641cf38f..c907f2f1a 100644
--- a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
+++ b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
@@ -312,6 +312,17 @@ public class JsonParserTest {
         assertEquals("pkg:npm/acme/common@2.2.0", d22.getRef());
     }
 
+    @Test
+    public void testValidBomLink() throws Exception {
+        final File file = new File(this.getClass().getResource("/bom-1.4-bomlink.json").getFile());
+        final JsonParser parser = new JsonParser();
+        Bom bom = parser.parse(file);
+        assertTrue(parser.isValid(file, CycloneDxSchema.Version.VERSION_14));
+        ExternalReference ref = bom.getComponents().get(0).getExternalReferences().get(0);
+        assertEquals("bom", ref.getType().getTypeName());
+        assertEquals("urn:cdx:f08a6ccd-4dce-4759-bd84-c626675d60a7/1", ref.getUrl());
+    }
+
     @Test
     public void testParsedObjects14Bom() throws Exception {
         final byte[] bomBytes = IOUtils.toByteArray(this.getClass().getResourceAsStream("/bom-1.4.json"));
diff --git a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
index 8b55c1020..cb035776a 100644
--- a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
+++ b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
@@ -92,6 +92,17 @@ public class XmlParserTest {
         assertTrue(valid);
     }
 
+    @Test
+    public void testValidBomLink() throws Exception {
+        final File file = new File(this.getClass().getResource("/bom-1.4-bomlink.xml").getFile());
+        final XmlParser parser = new XmlParser();
+        Bom bom = parser.parse(file);
+        assertTrue(parser.isValid(file, CycloneDxSchema.Version.VERSION_14));
+        ExternalReference ref = bom.getComponents().get(0).getExternalReferences().get(0);
+        assertEquals("bom", ref.getType().getTypeName());
+        assertEquals("urn:cdx:f08a6ccd-4dce-4759-bd84-c626675d60a7/1", ref.getUrl());
+    }
+
     @Test
     public void testValid12BomWithPedigree() throws Exception {
         final File file = new File(this.getClass().getResource("/bom-1.2-pedigree.xml").getFile());
diff --git a/src/test/resources/bom-1.4-bomlink.json b/src/test/resources/bom-1.4-bomlink.json
new file mode 100644
index 000000000..a3aa1c732
--- /dev/null
+++ b/src/test/resources/bom-1.4-bomlink.json
@@ -0,0 +1,28 @@
+{
+  "bomFormat": "CycloneDX",
+  "specVersion": "1.4",
+  "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
+  "version": 1,
+  "components": [
+    {
+      "type": "library",
+      "publisher": "Acme Inc",
+      "group": "org.example",
+      "name": "mylibrary",
+      "version": "1.0.0",
+      "externalReferences": [
+        {
+          "type": "bom",
+          "url": "urn:cdx:f08a6ccd-4dce-4759-bd84-c626675d60a7/1",
+          "comment": "An external SBOM that describes what this component includes",
+          "hashes": [
+            {
+              "alg": "SHA-256",
+              "content": "708f1f53b41f11f02d12a11b1a38d2905d47b099afc71a0f1124ef8582ec7313"
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
diff --git a/src/test/resources/bom-1.4-bomlink.xml b/src/test/resources/bom-1.4-bomlink.xml
new file mode 100644
index 000000000..d62225c04
--- /dev/null
+++ b/src/test/resources/bom-1.4-bomlink.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<bom serialNumber="urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79" version="1" xmlns="http://cyclonedx.org/schema/bom/1.4">
+    <components>
+        <component type="library">
+            <group>org.example</group>
+            <name>mylibrary</name>
+            <version>1.0.0</version>
+            <externalReferences>
+                <reference type="bom">
+                    <url>urn:cdx:f08a6ccd-4dce-4759-bd84-c626675d60a7/1</url>
+                    <comment>An external SBOM that describes what this component includes</comment>
+                    <hashes>
+                        <hash alg="SHA-256">f498a8ff2dd007e29c2074f5e4b01a9a01775c3ff3aeaf6906ea503bc5791b7b</hash>
+                    </hashes>
+                </reference>
+            </externalReferences>
+        </component>
+    </components>
+</bom>