Repository: CycloneDX/cyclonedx-core-java
Pull request #445 (2024-07-03 08:21:12)

Teach versions / generators about their supported file formats
This allows things like iterating over the supported file formats for a given version, and generating all BOM file formats for it.

codacy-production[bot]: ## Coverage summary from Codacy
### [See diff coverage on Codacy](https://app.codacy.com/gh/CycloneDX/cyclonedx-core-java/coverage/pull-requests/445?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummaryPullRequest)

 | Coverage variation | Diff coverage | 
 | ------------- | ------------- |
 | :white_check_mark: **-0.03%** | :white_check_mark: **76.67%** |

<details>
<summary>Coverage variation details</summary>

| | Coverable lines | Covered lines | Coverage |
| ------------- | ------------- | ------------- | ------------- |
| Common ancestor commit (47cc29c8609e571d56b8cecb9c9ce90dd740f288) | 5928 | 4332 | 73.08% | |
| Head commit (a6bd10d0e8f24f186bc89554ae6e9304a3ae785d) | 5947 (+19) | 4344 (+12) | 73.05% (**-0.03%**) |

**Coverage variation** is the difference between the coverage for the head and common ancestor commits of the pull request branch: `<coverage of head commit> - <coverage of common ancestor commit>`
</details>

<details>
<summary>Diff coverage details</summary>

| | Coverable lines | Covered lines | Diff coverage |
| ------------- | ------------- | ------------- | ------------- |
| Pull request (#445) | 30 | 23 | **76.67%** |

**Diff coverage** is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: `<covered lines added or modified>/<coverable lines added or modified> * 100%`
</details>

#### [See your quality gate settings](https://app.codacy.com/gh/CycloneDX/cyclonedx-core-java/settings/quality?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummaryGates)&nbsp;&nbsp;&nbsp;&nbsp;[Change summary preferences](https://app.codacy.com/gh/CycloneDX/cyclonedx-core-java/settings/integrations?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummarySettings)
---
<sub>:rocket: <strong>Don’t miss a bit, [follow what’s new on Codacy](https://app.codacy.com/to/launched)</strong>.</sub>

<sub>Codacy stopped sending the deprecated coverage status on June 5th, 2024. [Learn more](https://docs.codacy.com/release-notes/cloud/cloud-2023-11-23-new-coverage-engine-status-checks/#deprecation-and-removal-of-the-old-coverage-engine-status-checks)</sub>

<!-- 108f945d-e553-4918-8ccf-027c6d4acc2c -->
sschuberth: > For consistency, please consider adding similar checks to the validation side of things

Done as well in a separate commit.

Patch:
diff --git a/src/main/java/org/cyclonedx/CycloneDxMediaType.java b/src/main/java/org/cyclonedx/CycloneDxMediaType.java
index d8eb7075d..445865401 100644
--- a/src/main/java/org/cyclonedx/CycloneDxMediaType.java
+++ b/src/main/java/org/cyclonedx/CycloneDxMediaType.java
@@ -18,6 +18,7 @@
  */
 package org.cyclonedx;
 
+@Deprecated
 public class CycloneDxMediaType {
 
     private CycloneDxMediaType() { }
diff --git a/src/main/java/org/cyclonedx/Format.java b/src/main/java/org/cyclonedx/Format.java
new file mode 100644
index 000000000..9fdc513b6
--- /dev/null
+++ b/src/main/java/org/cyclonedx/Format.java
@@ -0,0 +1,33 @@
+package org.cyclonedx;
+
+public enum Format {
+    XML("xml", "application/vnd.cyclonedx+xml"),
+    JSON("json", "application/vnd.cyclonedx+json");
+
+    private final String extension;
+    private final String mediaType;
+
+    Format(String extension, String mediaType) {
+        this.extension = extension;
+        this.mediaType = mediaType;
+    }
+
+    /**
+     * The file extension associated to this format.
+     *
+     * @return The file extension, excluding the dot.
+     */
+    public String getExtension() {
+        return extension;
+    }
+
+    /**
+     * The official CycloneDX media type assigned by IANA for this format, see
+     * https://cyclonedx.org/specification/overview/#registered-media-types.
+     *
+     * @return The identifier for the media type.
+     */
+    public String getMediaType() {
+        return mediaType;
+    }
+}
diff --git a/src/main/java/org/cyclonedx/Version.java b/src/main/java/org/cyclonedx/Version.java
index 973f91562..211bfc32e 100644
--- a/src/main/java/org/cyclonedx/Version.java
+++ b/src/main/java/org/cyclonedx/Version.java
@@ -1,14 +1,18 @@
 package org.cyclonedx;
 
+import java.util.EnumSet;
+
+import static org.cyclonedx.Format.*;
+
 public enum Version
 {
-  VERSION_10(CycloneDxSchema.NS_BOM_10, "1.0", 1.0),
-  VERSION_11(CycloneDxSchema.NS_BOM_11, "1.1", 1.1),
-  VERSION_12(CycloneDxSchema.NS_BOM_12, "1.2", 1.2),
-  VERSION_13(CycloneDxSchema.NS_BOM_13, "1.3", 1.3),
-  VERSION_14(CycloneDxSchema.NS_BOM_14, "1.4", 1.4),
-  VERSION_15(CycloneDxSchema.NS_BOM_15, "1.5", 1.5),
-  VERSION_16(CycloneDxSchema.NS_BOM_16, "1.6", 1.6);
+  VERSION_10(CycloneDxSchema.NS_BOM_10, "1.0", 1.0, EnumSet.of(XML)),
+  VERSION_11(CycloneDxSchema.NS_BOM_11, "1.1", 1.1, EnumSet.of(XML)),
+  VERSION_12(CycloneDxSchema.NS_BOM_12, "1.2", 1.2, EnumSet.of(XML, JSON)),
+  VERSION_13(CycloneDxSchema.NS_BOM_13, "1.3", 1.3, EnumSet.of(XML, JSON)),
+  VERSION_14(CycloneDxSchema.NS_BOM_14, "1.4", 1.4, EnumSet.of(XML, JSON)),
+  VERSION_15(CycloneDxSchema.NS_BOM_15, "1.5", 1.5, EnumSet.of(XML, JSON)),
+  VERSION_16(CycloneDxSchema.NS_BOM_16, "1.6", 1.6, EnumSet.of(XML, JSON));
 
   private final String namespace;
 
@@ -16,6 +20,8 @@ public enum Version
 
   private final double version;
 
+  private final EnumSet<Format> formats;
+
   public String getNamespace() {
     return this.namespace;
   }
@@ -28,9 +34,14 @@ public enum Version
     return version;
   }
 
-  Version(String namespace, String versionString, double version) {
+  public EnumSet<Format> getFormats() {
+    return formats;
+  }
+
+  Version(String namespace, String versionString, double version, EnumSet<Format> formats) {
     this.namespace = namespace;
     this.versionString = versionString;
     this.version = version;
+    this.formats = formats;
   }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java b/src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java
index 5ea066642..2fcc550c2 100644
--- a/src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java
+++ b/src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java
@@ -3,6 +3,7 @@ package org.cyclonedx.generators;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.module.SimpleModule;
 import org.cyclonedx.CycloneDxSchema;
+import org.cyclonedx.Format;
 import org.cyclonedx.Version;
 import org.cyclonedx.model.Bom;
 import org.cyclonedx.util.serializer.CustomSerializerModifier;
@@ -24,10 +25,19 @@ public abstract class AbstractBomGenerator extends CycloneDxSchema
 
   protected Bom bom;
 
-  public AbstractBomGenerator(final Version version, final Bom bom) {
+  protected Format format;
+
+  public AbstractBomGenerator(final Version version, final Bom bom, final Format format) {
     this.mapper = new ObjectMapper();
     this.version = version;
     this.bom = bom;
+    this.format = format;
+
+    if (!version.getFormats().contains(format)) {
+      throw new IllegalArgumentException(
+              "CycloneDX version " + version.getVersionString() + " does not support the " + format + " format"
+      );
+    }
   }
 
   /**
@@ -38,6 +48,14 @@ public abstract class AbstractBomGenerator extends CycloneDxSchema
     return version;
   }
 
+  /**
+   * Returns the format that this generator creates.
+   * @return a Format enum
+   */
+  public Format getFormat() {
+    return format;
+  }
+
   protected void setupObjectMapper(boolean isXml) {
     SimpleModule licenseModule = new SimpleModule();
     licenseModule.addSerializer(new LicenseChoiceSerializer(isXml, version));
diff --git a/src/main/java/org/cyclonedx/generators/BomGeneratorFactory.java b/src/main/java/org/cyclonedx/generators/BomGeneratorFactory.java
index 60fc8e94a..0fdd456a7 100644
--- a/src/main/java/org/cyclonedx/generators/BomGeneratorFactory.java
+++ b/src/main/java/org/cyclonedx/generators/BomGeneratorFactory.java
@@ -18,12 +18,24 @@
  */
 package org.cyclonedx.generators;
 
+import org.cyclonedx.Format;
 import org.cyclonedx.Version;
 import org.cyclonedx.generators.xml.BomXmlGenerator;
 import org.cyclonedx.model.Bom;
 import org.cyclonedx.generators.json.BomJsonGenerator;
 
 public class BomGeneratorFactory {
+    public static AbstractBomGenerator create(Version version, Bom bom, Format format) {
+        AbstractBomGenerator generator;
+
+        switch (format) {
+            case XML: generator = createXml(version, bom); break;
+            case JSON: generator = createJson(version, bom); break;
+            default: throw new IllegalArgumentException("Unsupported format " + format);
+        }
+
+        return generator;
+    }
 
     public static BomXmlGenerator createXml(Version version, Bom bom) {
         return new BomXmlGenerator(bom, version);
diff --git a/src/main/java/org/cyclonedx/generators/json/BomJsonGenerator.java b/src/main/java/org/cyclonedx/generators/json/BomJsonGenerator.java
index a92c63ff3..b8c7f4f6e 100644
--- a/src/main/java/org/cyclonedx/generators/json/BomJsonGenerator.java
+++ b/src/main/java/org/cyclonedx/generators/json/BomJsonGenerator.java
@@ -26,6 +26,7 @@ import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.module.SimpleModule;
+import org.cyclonedx.Format;
 import org.cyclonedx.Version;
 import org.cyclonedx.exception.GeneratorException;
 import org.cyclonedx.generators.AbstractBomGenerator;
@@ -46,7 +47,7 @@ public class BomJsonGenerator extends AbstractBomGenerator
    * @param bom the BOM to generate
    */
   public BomJsonGenerator(Bom bom, final Version version) {
-    super(version, bom);
+    super(version, bom, Format.JSON);
     Bom modifiedBom = null;
     try {
       modifiedBom = injectBomFormatAndSpecVersion(bom);
diff --git a/src/main/java/org/cyclonedx/generators/xml/BomXmlGenerator.java b/src/main/java/org/cyclonedx/generators/xml/BomXmlGenerator.java
index fb9bd2fe6..8f4653ae6 100644
--- a/src/main/java/org/cyclonedx/generators/xml/BomXmlGenerator.java
+++ b/src/main/java/org/cyclonedx/generators/xml/BomXmlGenerator.java
@@ -30,6 +30,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.module.SimpleModule;
 import com.fasterxml.jackson.dataformat.xml.XmlMapper;
 import com.fasterxml.jackson.dataformat.xml.util.DefaultXmlPrettyPrinter;
+import org.cyclonedx.Format;
 import org.cyclonedx.generators.AbstractBomGenerator;
 import org.cyclonedx.Version;
 import org.cyclonedx.exception.GeneratorException;
@@ -49,7 +50,7 @@ public class BomXmlGenerator extends AbstractBomGenerator
      * @param bom the BOM to generate
      */
     public BomXmlGenerator(final Bom bom, final Version version) {
-        super(version, bom);
+        super(version, bom, Format.XML);
 
         mapper = new XmlMapper();
         prettyPrinter = new DefaultXmlPrettyPrinter();
diff --git a/src/main/java/org/cyclonedx/parsers/JsonParser.java b/src/main/java/org/cyclonedx/parsers/JsonParser.java
index 7445503a0..5684d9533 100644
--- a/src/main/java/org/cyclonedx/parsers/JsonParser.java
+++ b/src/main/java/org/cyclonedx/parsers/JsonParser.java
@@ -24,6 +24,7 @@ import com.networknt.schema.ValidationMessage;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.IOUtils;
 import org.cyclonedx.CycloneDxSchema;
+import org.cyclonedx.Format;
 import org.cyclonedx.Version;
 import org.cyclonedx.exception.ParseException;
 import org.cyclonedx.model.Bom;
@@ -171,10 +172,19 @@ public class JsonParser extends CycloneDxSchema implements Parser {
      */
     public List<ParseException> validate(final JsonNode bomJson, final Version schemaVersion) throws IOException {
         final List<ParseException> exceptions = new ArrayList<>();
+
+        if (!schemaVersion.getFormats().contains(Format.JSON)) {
+            exceptions.add(
+                    new ParseException("CycloneDX version " + schemaVersion.getVersionString() +
+                            " does not support the JSON format")
+            );
+        }
+
         Set<ValidationMessage> errors = getJsonSchema(schemaVersion, mapper).validate(mapper.readTree(bomJson.toString()));
         for (ValidationMessage message: errors) {
             exceptions.add(new ParseException(message.getMessage()));
         }
+
         return exceptions;
     }
 


Test patch:
diff --git a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java
index 10fac4626..723c14092 100644
--- a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java
+++ b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java
@@ -49,11 +49,7 @@ import java.util.ArrayList;
 import java.util.stream.Stream;
 import java.util.Objects;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class BomJsonGeneratorTest {
 
@@ -71,6 +67,16 @@ public class BomJsonGeneratorTest {
         tempFile.getParentFile().delete();
     }
 
+    @Test
+    public void testGenerateBomPrior12() {
+        Throwable exception = assertThrowsExactly(
+                IllegalArgumentException.class,
+                () -> new BomJsonGenerator(new Bom(), Version.VERSION_11)
+        );
+
+        assertEquals("CycloneDX version 1.1 does not support the JSON format", exception.getMessage());
+    }
+
     @Test
     public void schema12GenerationTest() throws Exception {
         Bom bom =  createCommonXmlBom("/bom-1.2.xml");
diff --git a/src/test/java/org/cyclonedx/parse/BaseParseTest.java b/src/test/java/org/cyclonedx/parse/BaseParseTest.java
index 15d31c3f3..97034bdae 100644
--- a/src/test/java/org/cyclonedx/parse/BaseParseTest.java
+++ b/src/test/java/org/cyclonedx/parse/BaseParseTest.java
@@ -18,6 +18,7 @@
  */
 package org.cyclonedx.parse;
 
+import org.cyclonedx.Format;
 import org.cyclonedx.generators.BomGeneratorFactory;
 import org.cyclonedx.parsers.BomParserFactory;
 import org.cyclonedx.CycloneDxSchema;
@@ -37,6 +38,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 public abstract class BaseParseTest {
 
@@ -61,7 +63,11 @@ public abstract class BaseParseTest {
     }
 
     void generateBomXml(final String testName, final Bom bom) throws ParserConfigurationException {
-        for (Version version : CycloneDxSchema.ALL_VERSIONS) {
+        List<Version> xmlVersions = Arrays.stream(Version.values())
+                .filter(v -> v.getFormats().contains(Format.XML))
+                .collect(Collectors.toList());
+
+        for (Version version : xmlVersions) {
             System.out.println("Generating CycloneDX " + version.getVersionString() + " XML for " + testName);
             BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom);
             Document doc = generator.generate();
@@ -70,7 +76,11 @@ public abstract class BaseParseTest {
     }
 
     void generateBomJson(final String testName, final Bom bom) {
-        for (Version version : CycloneDxSchema.ALL_VERSIONS) {
+        List<Version> jsonVersions = Arrays.stream(Version.values())
+                .filter(v -> v.getFormats().contains(Format.JSON))
+                .collect(Collectors.toList());
+
+        for (Version version : jsonVersions) {
             System.out.println("Generating CycloneDX " + version.getVersionString() + " JSON for " + testName);
             BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom);
             Assertions.assertNotNull(generator.toJsonString());
diff --git a/src/test/java/org/cyclonedx/parse/JsonParseTest.java b/src/test/java/org/cyclonedx/parse/JsonParseTest.java
index e1625223f..1bc8dd065 100644
--- a/src/test/java/org/cyclonedx/parse/JsonParseTest.java
+++ b/src/test/java/org/cyclonedx/parse/JsonParseTest.java
@@ -18,14 +18,18 @@
  */
 package org.cyclonedx.parse;
 
+import org.cyclonedx.Version;
+import org.cyclonedx.exception.ParseException;
 import org.cyclonedx.model.Bom;
+import org.cyclonedx.parsers.JsonParser;
 import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestFactory;
 import java.io.File;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
+import java.io.IOException;
+import java.util.*;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 public class JsonParseTest extends BaseParseTest {
@@ -51,4 +55,15 @@ public class JsonParseTest extends BaseParseTest {
         return dynamicTests;
     }
 
+    @Test
+    public void testValidateBomPrior12() throws IOException {
+        final JsonParser parser = new JsonParser();
+
+        final List<ParseException> exceptions = parser.validate("", Version.VERSION_11);
+
+        assertThat(exceptions.stream().map(ParseException::getMessage)).containsExactly(
+                "CycloneDX version 1.1 does not support the JSON format",
+                "$: unknown found, object expected"
+        );
+    }
 }