Skip to content

Commit d748b57

Browse files
authored
Merge pull request #703 from CycloneDX/issue_663
Fixes #663
2 parents b5fedbf + ead5ae1 commit d748b57

File tree

6 files changed

+521
-1
lines changed

6 files changed

+521
-1
lines changed

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

Lines changed: 2 additions & 0 deletions
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.ComponentListDeserializer;
3233
import org.cyclonedx.util.deserializer.ExternalReferencesDeserializer;
3334
import org.cyclonedx.util.deserializer.HashesDeserializer;
3435
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@@ -452,6 +453,7 @@ public void addProperty(Property property) {
452453

453454
@JacksonXmlElementWrapper(localName = "components")
454455
@JacksonXmlProperty(localName = "component")
456+
@JsonDeserialize(using = ComponentListDeserializer.class)
455457
public List<Component> getComponents() {
456458
return components;
457459
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.databind.JsonNode;
26+
import com.fasterxml.jackson.databind.ObjectMapper;
27+
import com.fasterxml.jackson.databind.node.ObjectNode;
28+
import org.cyclonedx.model.Component;
29+
30+
import java.io.IOException;
31+
import java.util.ArrayList;
32+
import java.util.Arrays;
33+
import java.util.Collections;
34+
import java.util.List;
35+
36+
/**
37+
* Custom deserializer for List&lt;Component&gt; that handles XML parsing issues where
38+
* single nested components might be represented as objects instead of arrays.
39+
*
40+
* This addresses GitHub issue #663 where nested components in metadata fail to parse
41+
* due to XML-to-JSON mapping inconsistencies.
42+
*/
43+
public class ComponentListDeserializer extends JsonDeserializer<List<Component>> {
44+
45+
@Override
46+
public List<Component> deserialize(JsonParser parser, DeserializationContext context) throws IOException {
47+
JsonToken currentToken = parser.getCurrentToken();
48+
49+
if (currentToken == JsonToken.START_ARRAY) {
50+
// Handle normal array case
51+
return Arrays.asList(parser.readValueAs(Component[].class));
52+
} else if (currentToken == JsonToken.START_OBJECT) {
53+
// Handle single object case (common in XML parsing)
54+
ObjectMapper mapper = getMapper(parser);
55+
ObjectNode node = parser.readValueAs(ObjectNode.class);
56+
57+
if (node.has("component")) {
58+
JsonNode componentNode = node.get("component");
59+
return deserializeComponentNode(componentNode, parser, mapper);
60+
} else {
61+
// If the object doesn't have a "component" field, treat the whole object as a single component
62+
Component component = mapper.convertValue(node, Component.class);
63+
return Collections.singletonList(component);
64+
}
65+
} else if (currentToken == JsonToken.VALUE_NULL) {
66+
return null;
67+
} else {
68+
// Try to deserialize as a single component
69+
ObjectMapper mapper = getMapper(parser);
70+
Component component = parser.readValueAs(Component.class);
71+
return Collections.singletonList(component);
72+
}
73+
}
74+
75+
/**
76+
* Deserializes a component node that might be either a single component or an array of components
77+
*/
78+
private List<Component> deserializeComponentNode(JsonNode componentNode, JsonParser originalParser, ObjectMapper mapper) throws IOException {
79+
try (JsonParser componentParser = componentNode.traverse(originalParser.getCodec())) {
80+
componentParser.nextToken(); // Advance to the first token
81+
82+
if (componentNode.isArray()) {
83+
return Arrays.asList(componentParser.readValueAs(Component[].class));
84+
} else {
85+
Component component = componentParser.readValueAs(Component.class);
86+
return Collections.singletonList(component);
87+
}
88+
}
89+
}
90+
91+
/**
92+
* Gets the ObjectMapper from the JsonParser codec or creates a new one
93+
*/
94+
private ObjectMapper getMapper(JsonParser parser) {
95+
if (parser.getCodec() instanceof ObjectMapper) {
96+
return (ObjectMapper) parser.getCodec();
97+
} else {
98+
return new ObjectMapper();
99+
}
100+
}
101+
}

src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public Metadata deserialize(JsonParser jsonParser, DeserializationContext ctxt)
4242
}
4343

4444
if(node.has("component")) {
45-
Component component = mapper.convertValue(node.get("component"), Component.class);
45+
Component component = deserializeComponent(node.get("component"), jsonParser, mapper);
4646
metadata.setComponent(component);
4747
}
4848

@@ -136,4 +136,16 @@ private void setTimestamp(JsonNode node, Metadata metadata) {
136136
metadata.setTimestamp(TimestampUtils.parseTimestamp(timestampNode.textValue()));
137137
}
138138
}
139+
140+
/**
141+
* Deserializes a Component from a JsonNode, handling both simple and complex nested structures.
142+
* This method properly handles XML parsing where nested components might be represented
143+
* as single objects or arrays depending on the XML structure.
144+
*/
145+
private Component deserializeComponent(JsonNode componentNode, JsonParser originalParser, ObjectMapper mapper) throws IOException {
146+
try (JsonParser componentParser = componentNode.traverse(originalParser.getCodec())) {
147+
componentParser.nextToken(); // Advance to the first token
148+
return componentParser.readValueAs(Component.class);
149+
}
150+
}
139151
}

0 commit comments

Comments
 (0)