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<Component> 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+ }
0 commit comments