From d42b84839dc8182b0117465e013719201ffdb00e Mon Sep 17 00:00:00 2001
From: Alex Alzate <aalzate@sonatype.com>
Date: Fri, 1 Mar 2024 08:48:30 -0500
Subject: [PATCH] Add Tags Support and others

Signed-off-by: Alex Alzate <aalzate@sonatype.com>
---
 .../java/org/cyclonedx/model/Component.java   | 39 ++++++++++++++++
 .../java/org/cyclonedx/model/Metadata.java    | 13 ++++++
 .../org/cyclonedx/model/ReleaseNotes.java     | 12 ++---
 .../java/org/cyclonedx/model/Service.java     |  6 +++
 .../org/cyclonedx/model/component/Tags.java   | 35 +++++++++++++++
 .../util/deserializer/TagsDeserializer.java   | 44 +++++++++++++++++++
 .../cyclonedx/parsers/AbstractParserTest.java |  2 +-
 7 files changed, 144 insertions(+), 7 deletions(-)
 create mode 100644 src/main/java/org/cyclonedx/model/component/Tags.java
 create mode 100644 src/main/java/org/cyclonedx/util/deserializer/TagsDeserializer.java

diff --git a/src/main/java/org/cyclonedx/model/Component.java b/src/main/java/org/cyclonedx/model/Component.java
index 5774b64bbf..6fde7019a5 100644
--- a/src/main/java/org/cyclonedx/model/Component.java
+++ b/src/main/java/org/cyclonedx/model/Component.java
@@ -22,9 +22,11 @@
 import java.util.List;
 import java.util.Objects;
 
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
 import org.cyclonedx.Version;
 import org.cyclonedx.model.component.ModelCard;
 import org.cyclonedx.model.component.crypto.CryptoProperties;
+import org.cyclonedx.model.component.Tags;
 import org.cyclonedx.model.component.modelCard.ComponentData;
 import org.cyclonedx.util.deserializer.ExternalReferencesDeserializer;
 import org.cyclonedx.util.deserializer.HashesDeserializer;
@@ -142,6 +144,7 @@ public String getScopeName() {
     private String mimeType;
     @VersionFilter(Version.VERSION_12)
     private OrganizationalEntity supplier;
+    @Deprecated
     @VersionFilter(Version.VERSION_12)
     private String author;
     @VersionFilter(Version.VERSION_11)
@@ -193,6 +196,18 @@ public String getScopeName() {
     @JsonProperty("provides")
     private List<String> provides;
 
+    @VersionFilter(Version.VERSION_16)
+    @JsonUnwrapped
+    private Tags tags;
+
+    @VersionFilter(Version.VERSION_16)
+    @JsonProperty("authors")
+    private List<OrganizationalContact> authors;
+
+    @VersionFilter(Version.VERSION_16)
+    @JsonProperty("manufacturer")
+    private OrganizationalEntity manufacturer;
+
     @JsonOnly
     @VersionFilter(Version.VERSION_14)
     private Signature signature;
@@ -493,6 +508,30 @@ public void setProvides(final List<String> provides) {
         this.provides = provides;
     }
 
+    public Tags getTags() {
+        return tags;
+    }
+
+    public void setTags(final Tags tags) {
+        this.tags = tags;
+    }
+
+    public List<OrganizationalContact> getAuthors() {
+        return authors;
+    }
+
+    public void setAuthors(final List<OrganizationalContact> authors) {
+        this.authors = authors;
+    }
+
+    public OrganizationalEntity getManufacturer() {
+        return manufacturer;
+    }
+
+    public void setManufacturer(final OrganizationalEntity manufacturer) {
+        this.manufacturer = manufacturer;
+    }
+
     @Override
     public int hashCode() {
         return Objects.hash(author, publisher, group, name, version, description, scope, hashes, license, copyright,
diff --git a/src/main/java/org/cyclonedx/model/Metadata.java b/src/main/java/org/cyclonedx/model/Metadata.java
index 2ddc208497..a79ae8aa51 100644
--- a/src/main/java/org/cyclonedx/model/Metadata.java
+++ b/src/main/java/org/cyclonedx/model/Metadata.java
@@ -73,6 +73,11 @@ public class Metadata
     @VersionFilter(Version.VERSION_12)
     private Component component;
 
+    @VersionFilter(Version.VERSION_16)
+    @JsonProperty("manufacturer")
+    private OrganizationalEntity manufacturer;
+
+    @Deprecated
     @VersionFilter(Version.VERSION_12)
     private OrganizationalEntity manufacture;
 
@@ -197,6 +202,14 @@ public void setToolChoice(final ToolInformation toolInformation) {
         this.toolInformation = toolInformation;
     }
 
+    public OrganizationalEntity getManufacturer() {
+        return manufacturer;
+    }
+
+    public void setManufacturer(final OrganizationalEntity manufacturer) {
+        this.manufacturer = manufacturer;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/src/main/java/org/cyclonedx/model/ReleaseNotes.java b/src/main/java/org/cyclonedx/model/ReleaseNotes.java
index c9a777976e..34239e892d 100644
--- a/src/main/java/org/cyclonedx/model/ReleaseNotes.java
+++ b/src/main/java/org/cyclonedx/model/ReleaseNotes.java
@@ -24,12 +24,14 @@
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
 import org.cyclonedx.Version;
+import org.cyclonedx.model.component.Tags;
 import org.cyclonedx.util.deserializer.NotesDeserializer;
 import org.cyclonedx.util.deserializer.PropertiesDeserializer;
 import org.cyclonedx.util.deserializer.ResolvesDeserializer;
@@ -67,7 +69,8 @@ public ReleaseNotes() {}
   @VersionFilter(Version.VERSION_14)
   private Date timestamp;
   private List<String> aliases;
-  private List<String> tags;
+  @JsonUnwrapped
+  private Tags tags;
   private List<Resolves> resolves;
   private List<Notes> notes;
   private List<Property> properties;
@@ -131,14 +134,11 @@ public void setAliases(final List<String> aliases) {
     this.aliases = aliases;
   }
 
-  @JacksonXmlElementWrapper(localName = "tags")
-  @JacksonXmlProperty(localName = "tag")
-  @JsonDeserialize(using = StringListDeserializer.class)
-  public List<String> getTags() {
+  public Tags getTags() {
     return tags;
   }
 
-  public void setTags(final List<String> tags) {
+  public void setTags(final Tags tags) {
     this.tags = tags;
   }
 
diff --git a/src/main/java/org/cyclonedx/model/Service.java b/src/main/java/org/cyclonedx/model/Service.java
index 14b087c727..b692f996d8 100644
--- a/src/main/java/org/cyclonedx/model/Service.java
+++ b/src/main/java/org/cyclonedx/model/Service.java
@@ -22,10 +22,12 @@
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
 import org.cyclonedx.Version;
+import org.cyclonedx.model.component.Tags;
 import org.cyclonedx.util.deserializer.ExternalReferencesDeserializer;
 import org.cyclonedx.util.deserializer.StringListDeserializer;
 
@@ -48,6 +50,7 @@
         "licenses",
         "externalReferences",
         "properties",
+        "tags",
         "services",
         "releaseNotes",
         "signature"
@@ -72,6 +75,9 @@ public class Service extends ExtensibleElement {
     private List<ExternalReference> externalReferences;
     @VersionFilter(Version.VERSION_13)
     private List<Property> properties;
+    @VersionFilter(value = Version.VERSION_16)
+    @JsonUnwrapped
+    private Tags tags;
     private List<Service> services;
     private ReleaseNotes releaseNotes;
     @JsonOnly
diff --git a/src/main/java/org/cyclonedx/model/component/Tags.java b/src/main/java/org/cyclonedx/model/component/Tags.java
new file mode 100644
index 0000000000..e180e3d592
--- /dev/null
+++ b/src/main/java/org/cyclonedx/model/component/Tags.java
@@ -0,0 +1,35 @@
+package org.cyclonedx.model.component;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import org.cyclonedx.util.deserializer.TagsDeserializer;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonDeserialize(using = TagsDeserializer.class)
+public class Tags
+{
+  private List<String> tags;
+
+  public Tags() {
+  }
+
+  public Tags(List<String> tags){
+    this.tags = tags;
+  }
+
+  @JacksonXmlElementWrapper(localName = "tags")
+  @JacksonXmlProperty(localName = "tag")
+  @JsonProperty("tags")
+  public List<String> getTags() {
+    return tags;
+  }
+
+  public void setTags(final List<String> tags) {
+    this.tags = tags;
+  }
+}
diff --git a/src/main/java/org/cyclonedx/util/deserializer/TagsDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/TagsDeserializer.java
new file mode 100644
index 0000000000..ebf288eba7
--- /dev/null
+++ b/src/main/java/org/cyclonedx/util/deserializer/TagsDeserializer.java
@@ -0,0 +1,44 @@
+package org.cyclonedx.util.deserializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import org.cyclonedx.model.component.Tags;
+
+public class TagsDeserializer
+    extends JsonDeserializer<Tags>
+{
+
+  private final ObjectMapper mapper = new ObjectMapper();
+  @Override
+  public Tags deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+    JsonNode node = parser.getCodec().readTree(parser);
+
+    if(node.has("tag")) {
+      return parseNode(node.get("tag"));
+    } else {
+      return parseNode(node);
+    }
+  }
+
+  private Tags parseNode(JsonNode node) {
+    List<String> list = new ArrayList<>();
+
+    ArrayNode nodes = (node.isArray() ? (ArrayNode) node : new ArrayNode(null).add(node));
+    for (JsonNode tagNode : nodes) {
+      list.add(tagNode.asText());
+    }
+
+    if(!list.isEmpty()) {
+      return new Tags(list);
+    }
+    return null;
+  }
+}
\ No newline at end of file
diff --git a/src/test/java/org/cyclonedx/parsers/AbstractParserTest.java b/src/test/java/org/cyclonedx/parsers/AbstractParserTest.java
index 279e923d87..bf34ef7558 100644
--- a/src/test/java/org/cyclonedx/parsers/AbstractParserTest.java
+++ b/src/test/java/org/cyclonedx/parsers/AbstractParserTest.java
@@ -837,7 +837,7 @@ void assertMetadata(final Metadata metadata, final Version version) {
     assertNotNull(releaseNotes.getSocialImage());
     assertNotNull(releaseNotes.getTimestamp());
     assertEquals(1, releaseNotes.getAliases().size());
-    assertEquals(1, releaseNotes.getTags().size());
+    assertEquals(1, releaseNotes.getTags().getTags().size());
 
     //Resolves
     assertEquals(1, releaseNotes.getResolves().size());
