Repository: CycloneDX/cyclonedx-core-java
Pull request #262 (2023-02-02 20:01:20)

Add properties on license
This adds properties to `License`

Implements: https://github.com/CycloneDX/specification/issues/152

References: https://github.com/CycloneDX/specification/pull/177

mr-zepol: Merging this into the feature branch, tests added and passed

Patch:
diff --git a/src/main/java/org/cyclonedx/model/License.java b/src/main/java/org/cyclonedx/model/License.java
index 8b715113f..1f9157ed8 100644
--- a/src/main/java/org/cyclonedx/model/License.java
+++ b/src/main/java/org/cyclonedx/model/License.java
@@ -18,18 +18,20 @@
  */
 package org.cyclonedx.model;
 
+import java.util.List;
 import java.util.Objects;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
 import com.fasterxml.jackson.annotation.JsonRootName;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
 
 @SuppressWarnings("unused")
 @JsonIgnoreProperties(ignoreUnknown = true)
 @JsonInclude(JsonInclude.Include.NON_NULL)
-@JsonPropertyOrder({"id", "name", "text", "url"})
+@JsonPropertyOrder({"id", "name", "text", "url", "properties"})
 @JsonRootName("license")
 public class License extends ExtensibleElement {
 
@@ -43,6 +45,9 @@ public class License extends ExtensibleElement {
     private AttachmentText attachmentText;
     private String url;
 
+    @VersionFilter(versions = {"1.1", "1.2", "1.3", "1.4"})
+    private List<Property> properties;
+
     public String getId() {
         return id;
     }
@@ -67,6 +72,16 @@ public class License extends ExtensibleElement {
         this.url = url;
     }
 
+    @JacksonXmlElementWrapper(localName = "properties")
+    @JacksonXmlProperty(localName = "property")
+    public List<Property> getProperties() {
+        return properties;
+    }
+
+    public void setProperties(final List<Property> properties) {
+        this.properties = properties;
+    }
+
     public AttachmentText getAttachmentText() {
         return attachmentText;
     }
@@ -83,11 +98,12 @@ public class License extends ExtensibleElement {
         return Objects.equals(id, license.id) &&
                 Objects.equals(name, license.name) &&
                 Objects.equals(url, license.url) &&
-                Objects.equals(attachmentText, license.attachmentText);
+                Objects.equals(attachmentText, license.attachmentText) &&
+                Objects.equals(properties, license.properties);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(id, name, url, attachmentText);
+        return Objects.hash(id, name, url, attachmentText, properties);
     }
 }
diff --git a/src/main/resources/bom-1.5.proto b/src/main/resources/bom-1.5.proto
index b36b2983c..111217429 100644
--- a/src/main/resources/bom-1.5.proto
+++ b/src/main/resources/bom-1.5.proto
@@ -277,6 +277,8 @@ message License {
   optional string bom_ref = 5;
   // Licensing details describing the licensor/licensee, license type, renewal and expiration dates, and other important metadata
   optional Licensing licensing = 6;
+  // Specifies optional, custom, properties
+  repeated Property properties = 7;
 }
 
 message Licensing {
diff --git a/src/main/resources/bom-1.5.schema.json b/src/main/resources/bom-1.5.schema.json
index 43a09c02d..2d3b30050 100644
--- a/src/main/resources/bom-1.5.schema.json
+++ b/src/main/resources/bom-1.5.schema.json
@@ -789,6 +789,15 @@
               "description": "The timestamp indicating when the current license expires (if applicable)."
             }
           }
+        },
+        "properties": {
+          "type": "array",
+          "title": "Properties",
+          "description": "Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Property names of interest to the general public are encouraged to be registered in the [CycloneDX Property Taxonomy](https://github.com/CycloneDX/cyclonedx-property-taxonomy). Formal registration is OPTIONAL.",
+          "additionalItems": false,
+          "items": {
+            "$ref": "#/definitions/property"
+          }
         }
       }
     },


Test patch:
diff --git a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
index 7cdb3600c..901b7f127 100644
--- a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
+++ b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
@@ -25,6 +25,8 @@ import org.cyclonedx.model.Bom;
 import org.cyclonedx.model.Component;
 import org.cyclonedx.model.Dependency;
 import org.cyclonedx.model.ExternalReference;
+import org.cyclonedx.model.License;
+import org.cyclonedx.model.LicenseChoice;
 import org.cyclonedx.model.Metadata;
 import org.cyclonedx.model.OrganizationalContact;
 import org.cyclonedx.model.OrganizationalEntity;
@@ -523,7 +525,10 @@ public class JsonParserTest {
         assertEquals(Component.Type.LIBRARY, component.getType());
         assertEquals("pkg:maven/com.acme/jackson-databind@2.9.4", component.getPurl());
         assertEquals(12, component.getHashes().size());
-        assertNotNull(component.getLicenseChoice().getExpression());
+
+        //Licenses
+        assertLicense(component.getLicenseChoice(), version);
+
         assertEquals("Copyright Example Inc. All rights reserved.", component.getCopyright());
         assertEquals("Acme Application", component.getSwid().getName());
         assertEquals("9.1.1", component.getSwid().getVersion());
@@ -547,12 +552,27 @@ public class JsonParserTest {
             component.getEvidence().getLicenseChoice().getLicenses().get(0).getUrl());
     }
 
-    private void assertSecurityContact(ExternalReference externalReference){
+    private void assertSecurityContact(ExternalReference externalReference) {
         assertEquals(externalReference.getType(), ExternalReference.Type.SECURITY_CONTACT);
         assertEquals(externalReference.getComment(), "test");
         assertEquals(externalReference.getUrl(), "http://example.org/docs");
     }
 
+    private void assertLicense(LicenseChoice licenseChoice, final Version version) {
+        assertNotNull(licenseChoice);
+        assertNull(licenseChoice.getExpression());
+
+        assertEquals(licenseChoice.getLicenses().size(), 1);
+        License license = licenseChoice.getLicenses().get(0);
+
+        if (version == Version.VERSION_15) {
+            assertEquals(license.getProperties().size(), 1);
+        }
+        else {
+            assertNull(license.getProperties());
+        }
+    }
+
     private void assertMetadata(final Metadata metadata) {
         assertNotNull(metadata);
         assertNotNull(metadata.getTimestamp());
diff --git a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
index beffabdad..d29a59739 100644
--- a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
+++ b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
@@ -25,6 +25,8 @@ import org.cyclonedx.model.Bom;
 import org.cyclonedx.model.Component;
 import org.cyclonedx.model.Dependency;
 import org.cyclonedx.model.ExternalReference;
+import org.cyclonedx.model.License;
+import org.cyclonedx.model.LicenseChoice;
 import org.cyclonedx.model.Metadata;
 import org.cyclonedx.model.OrganizationalContact;
 import org.cyclonedx.model.OrganizationalEntity;
@@ -666,7 +668,10 @@ public class XmlParserTest {
         assertEquals(Component.Type.LIBRARY, component.getType());
         assertEquals("pkg:maven/com.acme/jackson-databind@2.9.4", component.getPurl());
         assertEquals(12, component.getHashes().size());
-        assertNotNull(component.getLicenseChoice().getExpression());
+
+        //Licenses
+        assertLicense(component.getLicenseChoice(), version);
+
         assertEquals("Copyright Example Inc. All rights reserved.", component.getCopyright());
         assertEquals("Acme Application", component.getSwid().getName());
         assertEquals("9.1.1", component.getSwid().getVersion());
@@ -690,12 +695,27 @@ public class XmlParserTest {
             component.getEvidence().getLicenseChoice().getLicenses().get(0).getUrl());
     }
 
-    private void assertSecurityContact(ExternalReference externalReference){
+    private void assertSecurityContact(ExternalReference externalReference) {
         assertEquals(externalReference.getType(), ExternalReference.Type.SECURITY_CONTACT);
         assertEquals(externalReference.getComment(), "test");
         assertEquals(externalReference.getUrl(), "http://example.org/docs");
     }
 
+    private void assertLicense(LicenseChoice licenseChoice, final Version version) {
+        assertNotNull(licenseChoice);
+        assertNull(licenseChoice.getExpression());
+
+        assertEquals(licenseChoice.getLicenses().size(), 1);
+        License license = licenseChoice.getLicenses().get(0);
+
+        if (version == Version.VERSION_15) {
+            assertEquals(license.getProperties().size(), 1);
+        }
+        else {
+            assertNull(license.getProperties());
+        }
+    }
+
     private void assertMetadata(final Metadata metadata) {
         assertNotNull(metadata);
         assertNotNull(metadata.getTimestamp());
diff --git a/src/test/resources/bom-1.4.json b/src/test/resources/bom-1.4.json
index 3ccf48006..7857f464f 100644
--- a/src/test/resources/bom-1.4.json
+++ b/src/test/resources/bom-1.4.json
@@ -164,7 +164,9 @@
       ],
       "licenses": [
         {
-          "expression": "EPL-2.0 OR GPL-2.0-with-classpath-exception"
+          "license": {
+            "id": "Apache-2.0"
+          }
         }
       ],
       "copyright": "Copyright Example Inc. All rights reserved.",
diff --git a/src/test/resources/bom-1.4.xml b/src/test/resources/bom-1.4.xml
index 25095bef2..0fe4b725b 100644
--- a/src/test/resources/bom-1.4.xml
+++ b/src/test/resources/bom-1.4.xml
@@ -98,7 +98,9 @@
                 <hash alg="BLAKE3">26cdc7fb3fd65fc3b621a4ef70bc7d2489d5c19e70c76cf7ec20e538df0047cf</hash>
             </hashes>
             <licenses>
-                <expression>EPL-2.0 OR GPL-2.0-with-classpath-exception</expression>
+                <license>
+                    <id>Apache-2.0</id>
+                </license>
             </licenses>
             <copyright>Copyright Example Inc. All rights reserved.</copyright>
             <cpe>cpe:/a:example:myapplication:1.0.0</cpe>
diff --git a/src/test/resources/bom-1.5.json b/src/test/resources/bom-1.5.json
index 2ee6f2834..c90ac3497 100644
--- a/src/test/resources/bom-1.5.json
+++ b/src/test/resources/bom-1.5.json
@@ -164,7 +164,15 @@
       ],
       "licenses": [
         {
-          "expression": "EPL-2.0 OR GPL-2.0-with-classpath-exception"
+          "license": {
+            "id": "Apache-2.0",
+            "properties": [
+              {
+                "name": "Foo",
+                "value": "Bar"
+              }
+            ]
+          }
         }
       ],
       "copyright": "Copyright Example Inc. All rights reserved.",
diff --git a/src/test/resources/bom-1.5.xml b/src/test/resources/bom-1.5.xml
index 97e810bbc..13d6093c9 100644
--- a/src/test/resources/bom-1.5.xml
+++ b/src/test/resources/bom-1.5.xml
@@ -98,7 +98,12 @@
                 <hash alg="BLAKE3">26cdc7fb3fd65fc3b621a4ef70bc7d2489d5c19e70c76cf7ec20e538df0047cf</hash>
             </hashes>
             <licenses>
-                <expression>EPL-2.0 OR GPL-2.0-with-classpath-exception</expression>
+                <license>
+                    <id>Apache-2.0</id>
+                    <properties>
+                        <property name="Foo">Bar</property>
+                    </properties>
+                </license>
             </licenses>
             <copyright>Copyright Example Inc. All rights reserved.</copyright>
             <cpe>cpe:/a:example:myapplication:1.0.0</cpe>