Skip to content

Commit 242eab9

Browse files
committed
Fixes XML Serialization: Corrects the XML generation for component authors to produce schema-compliant <authors><author>...</author></authors> structure, resolving the issue of nested <authors> tags.
Add backward compatibility to support legecy format: <authors><authors>...</authors></authors>
1 parent fa926d1 commit 242eab9

File tree

7 files changed

+193
-2
lines changed

7 files changed

+193
-2
lines changed

src/main/java/org/cyclonedx/model/Component.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.cyclonedx.model.component.crypto.CryptoProperties;
3030
import org.cyclonedx.model.component.Tags;
3131
import org.cyclonedx.model.component.data.ComponentData;
32+
import org.cyclonedx.util.deserializer.ComponentAuthorsDeserializer;
3233
import org.cyclonedx.util.deserializer.ComponentListDeserializer;
3334
import org.cyclonedx.util.deserializer.ExternalReferencesDeserializer;
3435
import org.cyclonedx.util.deserializer.HashesDeserializer;
@@ -223,7 +224,6 @@ public String getScopeName() {
223224
private Tags tags;
224225

225226
@VersionFilter(Version.VERSION_16)
226-
@JsonProperty("authors")
227227
private List<OrganizationalContact> authors;
228228

229229
@VersionFilter(Version.VERSION_16)
@@ -556,10 +556,13 @@ public void setTags(final Tags tags) {
556556
this.tags = tags;
557557
}
558558

559+
@JacksonXmlElementWrapper(localName = "authors")
560+
@JacksonXmlProperty(localName = "author")
559561
public List<OrganizationalContact> getAuthors() {
560562
return authors;
561563
}
562564

565+
@JsonDeserialize(using = ComponentAuthorsDeserializer.class)
563566
public void setAuthors(final List<OrganizationalContact> authors) {
564567
this.authors = authors;
565568
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* This file is part of CycloneDX Core (Java).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
package org.cyclonedx.util.deserializer;
20+
21+
import com.fasterxml.jackson.core.JsonParser;
22+
import com.fasterxml.jackson.core.JsonToken;
23+
import com.fasterxml.jackson.databind.DeserializationContext;
24+
import com.fasterxml.jackson.databind.JsonDeserializer;
25+
import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser;
26+
import org.cyclonedx.model.OrganizationalContact;
27+
28+
import java.io.IOException;
29+
import java.util.ArrayList;
30+
import java.util.List;
31+
32+
public class ComponentAuthorsDeserializer extends JsonDeserializer<List<OrganizationalContact>> {
33+
34+
@Override
35+
public List<OrganizationalContact> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
36+
List<OrganizationalContact> contacts = new ArrayList<>();
37+
if (p instanceof FromXmlParser) { // Handle XML
38+
while (p.nextToken() != JsonToken.END_OBJECT && p.currentToken() != JsonToken.END_OBJECT) {
39+
if (p.currentToken() == JsonToken.FIELD_NAME) {
40+
// Handles both <author> and <authors> as the item tag
41+
p.nextToken();
42+
OrganizationalContact contact = p.readValueAs(OrganizationalContact.class);
43+
contacts.add(contact);
44+
}
45+
}
46+
} else { // Handle JSON
47+
if (p.isExpectedStartArrayToken()) {
48+
while (p.nextToken() != JsonToken.END_ARRAY) {
49+
// Handles case where author is a JSON object
50+
contacts.add(p.readValueAs(OrganizationalContact.class));
51+
}
52+
}
53+
}
54+
return contacts;
55+
}
56+
}

src/test/java/org/cyclonedx/BomJsonGeneratorTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,18 @@ public void testIssue492() throws Exception {
602602
assertTrue(parser.isValid(loadedFile, version));
603603
}
604604

605+
@Test
606+
public void testComponentAuthorsDeserializationJsonObject16() throws Exception {
607+
Bom bom = createCommonJsonBom("/1.6/valid-component-authors-json-object-1.6.json");
608+
Component component = bom.getComponents().get(0);
609+
assertNotNull(component.getAuthors());
610+
assertEquals(2, component.getAuthors().size());
611+
assertEquals("Test Author 1", component.getAuthors().get(0).getName());
612+
assertEquals("[email protected]", component.getAuthors().get(0).getEmail());
613+
assertEquals("Test Author 2", component.getAuthors().get(1).getName());
614+
assertNull(component.getAuthors().get(1).getEmail());
615+
}
616+
605617
private void assertExternalReferenceInfo(Bom bom) {
606618
assertEquals(3, bom.getExternalReferences().size());
607619
assertEquals(3, bom.getComponents().get(0).getExternalReferences().size());

src/test/java/org/cyclonedx/BomXmlGeneratorTest.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,21 @@
4646
import org.junit.jupiter.params.provider.Arguments;
4747
import org.junit.jupiter.params.provider.MethodSource;
4848
import org.w3c.dom.Document;
49+
import org.w3c.dom.NodeList;
50+
51+
import javax.xml.namespace.NamespaceContext;
52+
import javax.xml.parsers.DocumentBuilderFactory;
53+
import javax.xml.xpath.XPath;
54+
import javax.xml.xpath.XPathConstants;
55+
import javax.xml.xpath.XPathFactory;
4956
import java.io.File;
5057
import java.io.FileWriter;
5158
import java.io.IOException;
5259
import java.nio.file.Files;
5360
import java.nio.file.Path;
5461
import java.util.ArrayList;
55-
import java.util.Arrays;
62+
import java.util.Collections;
63+
import java.util.Iterator;
5664
import java.util.LinkedList;
5765
import java.util.List;
5866
import java.util.UUID;
@@ -786,6 +794,64 @@ public void testIssue408Regression_jsonToXml_externalReferenceBom() throws Excep
786794
assertTrue(parser.isValid(loadedFile, version));
787795
}
788796

797+
@Test
798+
public void testComponentAuthorSerializationOutputAsString() throws Exception {
799+
Version version = Version.VERSION_16;
800+
Bom bom = createCommonBomXml("/1.6/valid-component-authors-1.6.xml");
801+
802+
BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom);
803+
String xmlString = generator.toXmlString();
804+
File loadedFile = writeToFile(xmlString);
805+
806+
XmlParser parser = new XmlParser();
807+
assertTrue(parser.isValid(loadedFile, version));
808+
809+
// Verify the xml content
810+
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
811+
dbf.setNamespaceAware(true);
812+
Document doc = dbf.newDocumentBuilder()
813+
.parse(new java.io.ByteArrayInputStream(xmlString.getBytes()));
814+
815+
XPath xpath = XPathFactory.newInstance().newXPath();
816+
xpath.setNamespaceContext(new NamespaceContext() {
817+
@Override
818+
public String getNamespaceURI(String prefix) {
819+
return "bom".equals(prefix) ? "http://cyclonedx.org/schema/bom/1.6" : null;
820+
}
821+
@Override
822+
public String getPrefix(String namespaceURI) {
823+
return "http://cyclonedx.org/schema/bom/1.6".equals(namespaceURI) ? "bom" : null;
824+
}
825+
@Override
826+
public Iterator<String> getPrefixes(String namespaceURI) {
827+
return Collections.singleton("bom").iterator();
828+
}
829+
});
830+
831+
NodeList authors = (NodeList) xpath.evaluate(
832+
"//bom:component/bom:authors/bom:author",
833+
doc,
834+
XPathConstants.NODESET
835+
);
836+
assertEquals(2, authors.getLength(), "There should be exactly 2 <author> elements");
837+
838+
String author1 = xpath.evaluate("//bom:component/bom:authors/bom:author[1]/bom:name", doc);
839+
String author2 = xpath.evaluate("//bom:component/bom:authors/bom:author[2]/bom:name", doc);
840+
841+
assertEquals("Test Author 1", author1);
842+
assertEquals("Test Author 2", author2);
843+
}
844+
845+
@Test
846+
public void testComponentAuthorsDeserializationLegacy() throws Exception {
847+
Bom bom = createCommonBomXml("/1.6/invalid-component-authors-legacy-1.6.xml");
848+
Component component = bom.getComponents().get(0);
849+
assertNotNull(component.getAuthors());
850+
assertEquals(2, component.getAuthors().size());
851+
assertEquals("Test Author 1", component.getAuthors().get(0).getName());
852+
assertEquals("Test Author 2", component.getAuthors().get(1).getName());
853+
}
854+
789855
private void assertExternalReferenceInfo(Bom bom) {
790856
assertEquals(3, bom.getExternalReferences().size());
791857
assertEquals(3, bom.getComponents().get(0).getExternalReferences().size());
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom serialNumber="urn:uuid:e1acbeda-240f-4ab6-bd4e-749ab4183fec" version="1" xmlns="http://cyclonedx.org/schema/bom/1.6">
3+
<components>
4+
<component type="application">
5+
<authors>
6+
<authors>
7+
<name>Test Author 1</name>
8+
</authors>
9+
<authors>
10+
<name>Test Author 2</name>
11+
</authors>
12+
</authors>
13+
<name>Test Component</name>
14+
</component>
15+
</components>
16+
</bom>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom serialNumber="urn:uuid:e1acbeda-240f-4ab6-bd4e-749ab4183fec" version="1" xmlns="http://cyclonedx.org/schema/bom/1.6">
3+
<components>
4+
<component type="application">
5+
<authors>
6+
<author>
7+
<name>Test Author 1</name>
8+
</author>
9+
<author>
10+
<name>Test Author 2</name>
11+
</author>
12+
</authors>
13+
<name>Test Component</name>
14+
</component>
15+
</components>
16+
</bom>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
3+
"bomFormat": "CycloneDX",
4+
"specVersion": "1.6",
5+
"serialNumber": "urn:uuid:e1acbeda-240f-4ab6-bd4e-749ab4183fec",
6+
"version": 1,
7+
"components": [
8+
{
9+
"type": "application",
10+
"name": "Test Component",
11+
"authors": [
12+
{
13+
"name": "Test Author 1",
14+
"email": "[email protected]"
15+
},
16+
{
17+
"name": "Test Author 2"
18+
}
19+
]
20+
}
21+
]
22+
}

0 commit comments

Comments
 (0)