Skip to content

Commit 9f85e39

Browse files
committed
Support XmlSeeAlso in Jaxb2XmlDecoder
This commit adds support for the @XmlSeeAlso annotation in the Jaxb2XmlDecoder. This includes - Finding the set of possible qualified names given a class name, rather than a single name. - Splitting the XMLEvent stream when coming across one of the names in this set. Closes gh-30167
1 parent 7df2e2a commit 9f85e39

12 files changed

+411
-177
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
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+
* https://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+
17+
package org.springframework.http.codec.xml;
18+
19+
import java.util.ArrayList;
20+
import java.util.HashSet;
21+
import java.util.List;
22+
import java.util.Set;
23+
import java.util.function.BiConsumer;
24+
25+
import javax.xml.XMLConstants;
26+
import javax.xml.namespace.QName;
27+
import javax.xml.stream.events.XMLEvent;
28+
29+
import jakarta.xml.bind.annotation.XmlRootElement;
30+
import jakarta.xml.bind.annotation.XmlSchema;
31+
import jakarta.xml.bind.annotation.XmlSeeAlso;
32+
import jakarta.xml.bind.annotation.XmlType;
33+
import reactor.core.publisher.Flux;
34+
import reactor.core.publisher.SynchronousSink;
35+
36+
import org.springframework.lang.Nullable;
37+
import org.springframework.util.Assert;
38+
import org.springframework.util.ClassUtils;
39+
40+
/**
41+
* Helper class for JAXB2.
42+
*
43+
* @author Arjen Poutsma
44+
* @since 6.1
45+
*/
46+
abstract class Jaxb2Helper {
47+
48+
/**
49+
* The default value for JAXB annotations.
50+
* @see XmlRootElement#name()
51+
* @see XmlRootElement#namespace()
52+
* @see XmlType#name()
53+
* @see XmlType#namespace()
54+
*/
55+
private static final String JAXB_DEFAULT_ANNOTATION_VALUE = "##default";
56+
57+
58+
/**
59+
* Returns the set of qualified names for the given class, according to the
60+
* mapping rules in the JAXB specification.
61+
*/
62+
public static Set<QName> toQNames(Class<?> clazz) {
63+
Set<QName> result = new HashSet<>(1);
64+
findQNames(clazz, result, new HashSet<>());
65+
return result;
66+
}
67+
68+
private static void findQNames(Class<?> clazz, Set<QName> qNames, Set<Class<?>> completedClasses) {
69+
// safety against circular XmlSeeAlso references
70+
if (completedClasses.contains(clazz)) {
71+
return;
72+
}
73+
if (clazz.isAnnotationPresent(XmlRootElement.class)) {
74+
XmlRootElement annotation = clazz.getAnnotation(XmlRootElement.class);
75+
qNames.add(new QName(namespace(annotation.namespace(), clazz),
76+
localPart(annotation.name(), clazz)));
77+
}
78+
else if (clazz.isAnnotationPresent(XmlType.class)) {
79+
XmlType annotation = clazz.getAnnotation(XmlType.class);
80+
qNames.add(new QName(namespace(annotation.namespace(), clazz),
81+
localPart(annotation.name(), clazz)));
82+
}
83+
else {
84+
throw new IllegalArgumentException("Output class [" + clazz.getName() +
85+
"] is neither annotated with @XmlRootElement nor @XmlType");
86+
}
87+
completedClasses.add(clazz);
88+
if (clazz.isAnnotationPresent(XmlSeeAlso.class)) {
89+
XmlSeeAlso annotation = clazz.getAnnotation(XmlSeeAlso.class);
90+
for (Class<?> seeAlso : annotation.value()) {
91+
findQNames(seeAlso, qNames, completedClasses);
92+
}
93+
}
94+
}
95+
96+
private static String localPart(String value, Class<?> outputClass) {
97+
if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(value)) {
98+
return ClassUtils.getShortNameAsProperty(outputClass);
99+
}
100+
else {
101+
return value;
102+
}
103+
}
104+
105+
private static String namespace(String value, Class<?> outputClass) {
106+
if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(value)) {
107+
Package outputClassPackage = outputClass.getPackage();
108+
if (outputClassPackage != null && outputClassPackage.isAnnotationPresent(XmlSchema.class)) {
109+
XmlSchema annotation = outputClassPackage.getAnnotation(XmlSchema.class);
110+
return annotation.namespace();
111+
}
112+
else {
113+
return XMLConstants.NULL_NS_URI;
114+
}
115+
}
116+
else {
117+
return value;
118+
}
119+
}
120+
121+
/**
122+
* Split a flux of {@link XMLEvent XMLEvents} into a flux of XMLEvent lists, one list
123+
* for each branch of the tree that starts with one of the given qualified names.
124+
* That is, given the XMLEvents shown {@linkplain XmlEventDecoder here},
125+
* and the name "{@code child}", this method returns a flux
126+
* of two lists, each of which containing the events of a particular branch
127+
* of the tree that starts with "{@code child}".
128+
* <ol>
129+
* <li>The first list, dealing with the first branch of the tree:
130+
* <ol>
131+
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
132+
* <li>{@link javax.xml.stream.events.Characters} {@code foo}</li>
133+
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
134+
* </ol>
135+
* <li>The second list, dealing with the second branch of the tree:
136+
* <ol>
137+
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
138+
* <li>{@link javax.xml.stream.events.Characters} {@code bar}</li>
139+
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
140+
* </ol>
141+
* </li>
142+
* </ol>
143+
*/
144+
public static Flux<List<XMLEvent>> split(Flux<XMLEvent> xmlEventFlux, Set<QName> names) {
145+
return xmlEventFlux.handle(new SplitHandler(names));
146+
}
147+
148+
149+
private static class SplitHandler implements BiConsumer<XMLEvent, SynchronousSink<List<XMLEvent>>> {
150+
151+
private final Set<QName> names;
152+
153+
@Nullable
154+
private List<XMLEvent> events;
155+
156+
private int elementDepth = 0;
157+
158+
private int barrier = Integer.MAX_VALUE;
159+
160+
public SplitHandler(Set<QName> names) {
161+
this.names = names;
162+
}
163+
164+
@Override
165+
public void accept(XMLEvent event, SynchronousSink<List<XMLEvent>> sink) {
166+
if (event.isStartElement()) {
167+
if (this.barrier == Integer.MAX_VALUE) {
168+
QName startElementName = event.asStartElement().getName();
169+
if (this.names.contains(startElementName)) {
170+
this.events = new ArrayList<>();
171+
this.barrier = this.elementDepth;
172+
}
173+
}
174+
this.elementDepth++;
175+
}
176+
if (this.elementDepth > this.barrier) {
177+
Assert.state(this.events != null, "No XMLEvent List");
178+
this.events.add(event);
179+
}
180+
if (event.isEndElement()) {
181+
this.elementDepth--;
182+
if (this.elementDepth == this.barrier) {
183+
this.barrier = Integer.MAX_VALUE;
184+
Assert.state(this.events != null, "No XMLEvent List");
185+
sink.next(this.events);
186+
}
187+
}
188+
}
189+
}
190+
191+
192+
}

spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java

+9-128
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,10 +21,9 @@
2121
import java.util.Iterator;
2222
import java.util.List;
2323
import java.util.Map;
24-
import java.util.function.BiConsumer;
24+
import java.util.Set;
2525
import java.util.function.Function;
2626

27-
import javax.xml.XMLConstants;
2827
import javax.xml.namespace.QName;
2928
import javax.xml.stream.XMLEventReader;
3029
import javax.xml.stream.XMLInputFactory;
@@ -36,13 +35,12 @@
3635
import jakarta.xml.bind.UnmarshalException;
3736
import jakarta.xml.bind.Unmarshaller;
3837
import jakarta.xml.bind.annotation.XmlRootElement;
39-
import jakarta.xml.bind.annotation.XmlSchema;
38+
import jakarta.xml.bind.annotation.XmlSeeAlso;
4039
import jakarta.xml.bind.annotation.XmlType;
4140
import org.reactivestreams.Publisher;
4241
import reactor.core.Exceptions;
4342
import reactor.core.publisher.Flux;
4443
import reactor.core.publisher.Mono;
45-
import reactor.core.publisher.SynchronousSink;
4644

4745
import org.springframework.core.ResolvableType;
4846
import org.springframework.core.codec.AbstractDecoder;
@@ -54,9 +52,8 @@
5452
import org.springframework.core.io.buffer.DataBufferUtils;
5553
import org.springframework.core.log.LogFormatUtils;
5654
import org.springframework.http.MediaType;
55+
import org.springframework.lang.NonNull;
5756
import org.springframework.lang.Nullable;
58-
import org.springframework.util.Assert;
59-
import org.springframework.util.ClassUtils;
6057
import org.springframework.util.MimeType;
6158
import org.springframework.util.MimeTypeUtils;
6259
import org.springframework.util.xml.StaxUtils;
@@ -72,15 +69,6 @@
7269
*/
7370
public class Jaxb2XmlDecoder extends AbstractDecoder<Object> {
7471

75-
/**
76-
* The default value for JAXB annotations.
77-
* @see XmlRootElement#name()
78-
* @see XmlRootElement#namespace()
79-
* @see XmlType#name()
80-
* @see XmlType#namespace()
81-
*/
82-
private static final String JAXB_DEFAULT_ANNOTATION_VALUE = "##default";
83-
8472
private static final XMLInputFactory inputFactory = StaxUtils.createDefensiveInputFactory();
8573

8674

@@ -162,8 +150,8 @@ public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType ele
162150
inputStream, ResolvableType.forClass(XMLEvent.class), mimeType, hints);
163151

164152
Class<?> outputClass = elementType.toClass();
165-
QName typeName = toQName(outputClass);
166-
Flux<List<XMLEvent>> splitEvents = split(xmlEventFlux, typeName);
153+
Set<QName> typeNames = Jaxb2Helper.toQNames(outputClass);
154+
Flux<List<XMLEvent>> splitEvents = Jaxb2Helper.split(xmlEventFlux, typeNames);
167155

168156
return splitEvents.map(events -> {
169157
Object value = unmarshal(events, outputClass);
@@ -184,6 +172,7 @@ public Mono<Object> decodeToMono(Publisher<DataBuffer> input, ResolvableType ele
184172
}
185173

186174
@Override
175+
@NonNull
187176
public Object decode(DataBuffer dataBuffer, ResolvableType targetType,
188177
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) throws DecodingException {
189178

@@ -229,7 +218,8 @@ private Object unmarshal(List<XMLEvent> events, Class<?> outputClass) {
229218
try {
230219
Unmarshaller unmarshaller = initUnmarshaller(outputClass);
231220
XMLEventReader eventReader = StaxUtils.createXMLEventReader(events);
232-
if (outputClass.isAnnotationPresent(XmlRootElement.class)) {
221+
if (outputClass.isAnnotationPresent(XmlRootElement.class) ||
222+
outputClass.isAnnotationPresent(XmlSeeAlso.class)) {
233223
return unmarshaller.unmarshal(eventReader);
234224
}
235225
else {
@@ -250,113 +240,4 @@ private Unmarshaller initUnmarshaller(Class<?> outputClass) throws CodecExceptio
250240
return this.unmarshallerProcessor.apply(unmarshaller);
251241
}
252242

253-
/**
254-
* Returns the qualified name for the given class, according to the mapping rules
255-
* in the JAXB specification.
256-
*/
257-
QName toQName(Class<?> outputClass) {
258-
String localPart;
259-
String namespaceUri;
260-
261-
if (outputClass.isAnnotationPresent(XmlRootElement.class)) {
262-
XmlRootElement annotation = outputClass.getAnnotation(XmlRootElement.class);
263-
localPart = annotation.name();
264-
namespaceUri = annotation.namespace();
265-
}
266-
else if (outputClass.isAnnotationPresent(XmlType.class)) {
267-
XmlType annotation = outputClass.getAnnotation(XmlType.class);
268-
localPart = annotation.name();
269-
namespaceUri = annotation.namespace();
270-
}
271-
else {
272-
throw new IllegalArgumentException("Output class [" + outputClass.getName() +
273-
"] is neither annotated with @XmlRootElement nor @XmlType");
274-
}
275-
276-
if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(localPart)) {
277-
localPart = ClassUtils.getShortNameAsProperty(outputClass);
278-
}
279-
if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(namespaceUri)) {
280-
Package outputClassPackage = outputClass.getPackage();
281-
if (outputClassPackage != null && outputClassPackage.isAnnotationPresent(XmlSchema.class)) {
282-
XmlSchema annotation = outputClassPackage.getAnnotation(XmlSchema.class);
283-
namespaceUri = annotation.namespace();
284-
}
285-
else {
286-
namespaceUri = XMLConstants.NULL_NS_URI;
287-
}
288-
}
289-
return new QName(namespaceUri, localPart);
290-
}
291-
292-
/**
293-
* Split a flux of {@link XMLEvent XMLEvents} into a flux of XMLEvent lists, one list
294-
* for each branch of the tree that starts with the given qualified name.
295-
* That is, given the XMLEvents shown {@linkplain XmlEventDecoder here},
296-
* and the {@code desiredName} "{@code child}", this method returns a flux
297-
* of two lists, each of which containing the events of a particular branch
298-
* of the tree that starts with "{@code child}".
299-
* <ol>
300-
* <li>The first list, dealing with the first branch of the tree:
301-
* <ol>
302-
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
303-
* <li>{@link javax.xml.stream.events.Characters} {@code foo}</li>
304-
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
305-
* </ol>
306-
* <li>The second list, dealing with the second branch of the tree:
307-
* <ol>
308-
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
309-
* <li>{@link javax.xml.stream.events.Characters} {@code bar}</li>
310-
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
311-
* </ol>
312-
* </li>
313-
* </ol>
314-
*/
315-
Flux<List<XMLEvent>> split(Flux<XMLEvent> xmlEventFlux, QName desiredName) {
316-
return xmlEventFlux.handle(new SplitHandler(desiredName));
317-
}
318-
319-
320-
private static class SplitHandler implements BiConsumer<XMLEvent, SynchronousSink<List<XMLEvent>>> {
321-
322-
private final QName desiredName;
323-
324-
@Nullable
325-
private List<XMLEvent> events;
326-
327-
private int elementDepth = 0;
328-
329-
private int barrier = Integer.MAX_VALUE;
330-
331-
public SplitHandler(QName desiredName) {
332-
this.desiredName = desiredName;
333-
}
334-
335-
@Override
336-
public void accept(XMLEvent event, SynchronousSink<List<XMLEvent>> sink) {
337-
if (event.isStartElement()) {
338-
if (this.barrier == Integer.MAX_VALUE) {
339-
QName startElementName = event.asStartElement().getName();
340-
if (this.desiredName.equals(startElementName)) {
341-
this.events = new ArrayList<>();
342-
this.barrier = this.elementDepth;
343-
}
344-
}
345-
this.elementDepth++;
346-
}
347-
if (this.elementDepth > this.barrier) {
348-
Assert.state(this.events != null, "No XMLEvent List");
349-
this.events.add(event);
350-
}
351-
if (event.isEndElement()) {
352-
this.elementDepth--;
353-
if (this.elementDepth == this.barrier) {
354-
this.barrier = Integer.MAX_VALUE;
355-
Assert.state(this.events != null, "No XMLEvent List");
356-
sink.next(this.events);
357-
}
358-
}
359-
}
360-
}
361-
362243
}

0 commit comments

Comments
 (0)