Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/main/java/org/cyclonedx/model/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
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.data.ComponentData;
import org.cyclonedx.util.deserializer.ComponentAuthorsDeserializer;
import org.cyclonedx.util.deserializer.ComponentListDeserializer;
import org.cyclonedx.util.deserializer.ExternalReferencesDeserializer;
import org.cyclonedx.util.deserializer.HashesDeserializer;
Expand All @@ -43,6 +45,7 @@
import com.github.packageurl.PackageURL;
import org.cyclonedx.util.deserializer.LicenseDeserializer;
import org.cyclonedx.util.deserializer.PropertiesDeserializer;
import org.cyclonedx.util.serializer.ComponentAuthorsSerializer;

@SuppressWarnings("unused")
@JacksonXmlRootElement(localName = "component")
Expand Down Expand Up @@ -224,6 +227,9 @@ public String getScopeName() {

@VersionFilter(Version.VERSION_16)
@JsonProperty("authors")
@JacksonXmlElementWrapper(localName = "authors")
@JsonSerialize(using = ComponentAuthorsSerializer.class)
@JsonDeserialize(using = ComponentAuthorsDeserializer.class)
private List<OrganizationalContact> authors;

@VersionFilter(Version.VERSION_16)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* This file is part of CycloneDX Core (Java).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/
package org.cyclonedx.util.deserializer;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser;
import org.cyclonedx.model.OrganizationalContact;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class ComponentAuthorsDeserializer extends JsonDeserializer<List<OrganizationalContact>> {

@Override
public List<OrganizationalContact> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
List<OrganizationalContact> contacts = new ArrayList<>();
if (p instanceof FromXmlParser) { // Handle XML
while (p.nextToken() != JsonToken.END_OBJECT && p.currentToken() != JsonToken.END_OBJECT) {
if (p.currentToken() == JsonToken.FIELD_NAME) {
String fieldName = p.currentName();
if ("author".equals(fieldName) || "authors".equals(fieldName)) {
// Handles both <author> and <authors> as the item tag
p.nextToken();
contacts.add(p.readValueAs(OrganizationalContact.class));
} else {
ctxt.reportInputMismatch(
List.class,
"Unexpected field '%s' in %s",
fieldName,
getClass().getSimpleName()
);
}
}
}
} else { // Handle JSON
if (p.isExpectedStartArrayToken()) {
while (p.nextToken() != JsonToken.END_ARRAY) {
// Handles case where author is a JSON object
contacts.add(p.readValueAs(OrganizationalContact.class));
}
}
}
return contacts;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* This file is part of CycloneDX Core (Java).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/

package org.cyclonedx.util.serializer;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
import org.cyclonedx.model.OrganizationalContact;

import javax.xml.namespace.QName;
import java.io.IOException;
import java.util.List;

public class ComponentAuthorsSerializer extends StdSerializer<List<OrganizationalContact>> {


public ComponentAuthorsSerializer() {
super(List.class, false);
}


@Override
public void serialize(List<OrganizationalContact> authors, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (jsonGenerator instanceof ToXmlGenerator) { // Handle XML
ToXmlGenerator xmlGenerator = (ToXmlGenerator) jsonGenerator;
xmlGenerator.writeStartArray();

for (OrganizationalContact author : authors) {
xmlGenerator.setNextName(new QName("author"));
xmlGenerator.writeObject(author);
}

xmlGenerator.writeEndArray();
} else { // Handle JSON, as default.
JsonSerializer<Object> defaultSerializer =
serializerProvider.findValueSerializer(List.class, null);
defaultSerializer.serialize(authors, jsonGenerator, serializerProvider);
}
}

}
72 changes: 66 additions & 6 deletions src/test/java/org/cyclonedx/BomJsonGeneratorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,14 @@
import com.fasterxml.jackson.databind.JsonNode;

import java.nio.charset.StandardCharsets;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import org.cyclonedx.generators.BomGeneratorFactory;
import org.cyclonedx.generators.json.BomJsonGenerator;
import org.cyclonedx.generators.xml.BomXmlGenerator;
import org.cyclonedx.model.Bom;
import org.cyclonedx.model.Component;
import org.cyclonedx.model.*;
import org.cyclonedx.model.Component.Type;
import org.cyclonedx.model.License;
import org.cyclonedx.model.LicenseChoice;
import org.cyclonedx.model.Metadata;
import org.cyclonedx.model.Service;
import org.cyclonedx.model.license.Expression;
import org.cyclonedx.parsers.JsonParser;
import org.cyclonedx.parsers.XmlParser;
Expand All @@ -48,6 +45,8 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;
import java.util.Objects;

Expand Down Expand Up @@ -602,6 +601,67 @@ public void testIssue492() throws Exception {
assertTrue(parser.isValid(loadedFile, version));
}

@Test
public void testComponentAuthorsSerializationAndDeserialization() throws Exception {
Version version = Version.VERSION_16;
Bom bom = createCommonJsonBom("/1.6/valid-component-authors-1.6.json");

assertNotNull(bom.getComponents());
assertEquals(1, bom.getComponents().size());

Component bomComponent = bom.getComponents().get(0);
assertEquals("Outer Author with String value", bomComponent.getAuthor());

List<OrganizationalContact> bomAuthors = bomComponent.getAuthors();
assertNotNull(bomAuthors);
assertEquals(2, bomAuthors.size());

OrganizationalContact bomAuthor1 = bomAuthors.get(0);
OrganizationalContact bomAuthor2 = bomAuthors.get(1);

assertNotNull(bomAuthor1);
assertEquals("Test Author 1", bomAuthor1.getName());
assertEquals("[email protected]", bomAuthor1.getEmail());
assertEquals("123", bomAuthor1.getPhone());

assertNotNull(bomAuthor2);
assertEquals("Test Author 2", bomAuthor2.getName());
assertEquals("[email protected]", bomAuthor2.getEmail());
assertEquals("456", bomAuthor2.getPhone());

BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom);
String jsonString = generator.toJsonString();

File loadedFile = writeToFile(jsonString);
JsonParser parser = new JsonParser();
assertTrue(parser.isValid(loadedFile, version));

// Verify the json content
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(jsonString);

JsonNode component = rootNode.path("components").get(0);
assertNotNull(component);

String outerAuthor = component.path("author").asText();
assertEquals("Outer Author with String value", outerAuthor, "Outer author value mismatch");

JsonNode authorsNode = component.path("authors");
assertTrue(authorsNode.isArray());
assertEquals(2, authorsNode.size(), "Authors list size mismatch");

Iterator<JsonNode> elements = authorsNode.elements();
JsonNode author1 = elements.next();
assertEquals("Test Author 1", author1.path("name").asText());
assertEquals("[email protected]", author1.path("email").asText());
assertEquals("123", author1.path("phone").asText());

JsonNode author2 = elements.next();
assertEquals("Test Author 2", author2.path("name").asText());
assertEquals("[email protected]", author2.path("email").asText());
assertEquals("456", author2.path("phone").asText());
}

private void assertExternalReferenceInfo(Bom bom) {
assertEquals(3, bom.getExternalReferences().size());
assertEquals(3, bom.getComponents().get(0).getExternalReferences().size());
Expand Down
110 changes: 109 additions & 1 deletion src/test/java/org/cyclonedx/BomXmlGeneratorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,23 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
Expand Down Expand Up @@ -786,6 +795,105 @@ public void testIssue408Regression_jsonToXml_externalReferenceBom() throws Excep
assertTrue(parser.isValid(loadedFile, version));
}

@ParameterizedTest
@ValueSource(strings = {
"/1.6/valid-component-authors-1.6.xml",
"/1.6/invalid-component-authors-legacy-1.6.xml"
})
public void testComponentAuthorsSerializationAndDeserialization(String xmlFilePath) throws Exception {
Version version = Version.VERSION_16;
Bom bom = createCommonBomXml(xmlFilePath);

assertNotNull(bom.getComponents());
assertEquals(1, bom.getComponents().size());

Component bomComponent = bom.getComponents().get(0);
assertEquals("Outer Author with String value", bomComponent.getAuthor());

List<OrganizationalContact> bomAuthors = bomComponent.getAuthors();
assertNotNull(bomAuthors);
assertEquals(2, bomAuthors.size());

OrganizationalContact bomAuthor1 = bomAuthors.get(0);
OrganizationalContact bomAuthor2 = bomAuthors.get(1);

assertNotNull(bomAuthor1);
assertEquals("Test Author 1", bomAuthor1.getName());
assertEquals("[email protected]", bomAuthor1.getEmail());
assertEquals("123", bomAuthor1.getPhone());

assertNotNull(bomAuthor2);
assertEquals("Test Author 2", bomAuthor2.getName());
assertEquals("[email protected]", bomAuthor2.getEmail());
assertEquals("456", bomAuthor2.getPhone());

BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom);
String xmlString = generator.toXmlString();
File loadedFile = writeToFile(xmlString);

XmlParser parser = new XmlParser();
assertTrue(parser.isValid(loadedFile, version));

// Verify the xml content
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
Document doc = dbf.newDocumentBuilder()
.parse(new java.io.ByteArrayInputStream(xmlString.getBytes()));

XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setNamespaceContext(new NamespaceContext() {
@Override
public String getNamespaceURI(String prefix) {
return "bom".equals(prefix) ? "http://cyclonedx.org/schema/bom/1.6" : null;
}
@Override
public String getPrefix(String namespaceURI) {
return "http://cyclonedx.org/schema/bom/1.6".equals(namespaceURI) ? "bom" : null;
}
@Override
public Iterator<String> getPrefixes(String namespaceURI) {
return Collections.singleton("bom").iterator();
}
});

NodeList authors = (NodeList) xpath.evaluate(
"//bom:component/bom:authors/bom:author",
doc,
XPathConstants.NODESET
);
assertEquals(2, authors.getLength(), "There should be exactly 2 <author> elements");

String author1 = xpath.evaluate("//bom:component/bom:authors/bom:author[1]/bom:name", doc);
String author2 = xpath.evaluate("//bom:component/bom:authors/bom:author[2]/bom:name", doc);

assertEquals("Test Author 1", author1);
assertEquals("Test Author 2", author2);

String email1 = xpath.evaluate("//bom:component/bom:authors/bom:author[1]/bom:email", doc);
String email2 = xpath.evaluate("//bom:component/bom:authors/bom:author[2]/bom:email", doc);

assertEquals("[email protected]", email1);
assertEquals("[email protected]", email2);

String phone1 = xpath.evaluate("//bom:component/bom:authors/bom:author[1]/bom:phone", doc);
String phone2 = xpath.evaluate("//bom:component/bom:authors/bom:author[2]/bom:phone", doc);

assertEquals("123", phone1);
assertEquals("456", phone2);

String outerAuthorStr = (String) xpath.evaluate("//bom:component/bom:author", doc, XPathConstants.STRING);
assertEquals("Outer Author with String value", outerAuthorStr);
}

@Test
public void testComponentAuthorsWithInvalidItemTag(){
assertThrows(ParseException.class, () -> createCommonBomXml("/1.6/invalid-component-authors-bad-item-name-1.6.xml"));
}

private void assertExternalReferenceInfo(Bom bom) {
assertEquals(3, bom.getExternalReferences().size());
assertEquals(3, bom.getComponents().get(0).getExternalReferences().size());
Expand Down
Loading