1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.bremersee.xml.http.codec;
18
19 import static java.util.Objects.isNull;
20
21 import java.util.ArrayList;
22 import java.util.Iterator;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.function.BiConsumer;
27 import javax.xml.XMLConstants;
28 import jakarta.xml.bind.JAXBElement;
29 import jakarta.xml.bind.JAXBException;
30 import jakarta.xml.bind.UnmarshalException;
31 import jakarta.xml.bind.Unmarshaller;
32 import jakarta.xml.bind.annotation.XmlRootElement;
33 import jakarta.xml.bind.annotation.XmlSchema;
34 import jakarta.xml.bind.annotation.XmlType;
35 import javax.xml.namespace.QName;
36 import javax.xml.stream.XMLEventReader;
37 import javax.xml.stream.XMLInputFactory;
38 import javax.xml.stream.XMLStreamException;
39 import javax.xml.stream.events.XMLEvent;
40 import org.bremersee.xml.JaxbContextBuilder;
41 import org.reactivestreams.Publisher;
42 import org.springframework.core.ResolvableType;
43 import org.springframework.core.codec.AbstractDecoder;
44 import org.springframework.core.codec.CodecException;
45 import org.springframework.core.codec.DecodingException;
46 import org.springframework.core.codec.Hints;
47 import org.springframework.core.io.buffer.DataBuffer;
48 import org.springframework.core.io.buffer.DataBufferLimitException;
49 import org.springframework.core.io.buffer.DataBufferUtils;
50 import org.springframework.core.log.LogFormatUtils;
51 import org.springframework.http.MediaType;
52 import org.springframework.http.codec.xml.XmlEventDecoder;
53 import org.springframework.lang.NonNull;
54 import org.springframework.lang.Nullable;
55 import org.springframework.util.Assert;
56 import org.springframework.util.ClassUtils;
57 import org.springframework.util.MimeType;
58 import org.springframework.util.MimeTypeUtils;
59 import org.springframework.util.xml.StaxUtils;
60 import reactor.core.Exceptions;
61 import reactor.core.publisher.Flux;
62 import reactor.core.publisher.Mono;
63 import reactor.core.publisher.SynchronousSink;
64
65
66
67
68
69
70
71
72
73
74 public class ReactiveJaxbDecoder extends AbstractDecoder<Object> {
75
76
77
78
79
80
81
82
83
84 private static final String JAXB_DEFAULT_ANNOTATION_VALUE = "##default";
85
86 private static final XMLInputFactory inputFactory = StaxUtils.createDefensiveInputFactory();
87
88
89 private final XmlEventDecoder xmlEventDecoder = new XmlEventDecoder();
90
91 private final JaxbContextBuilder jaxbContextBuilder;
92
93 private final Set<Class<?>> ignoreReadingClasses;
94
95 private int maxInMemorySize = 256 * 1024;
96
97
98
99
100
101
102 public ReactiveJaxbDecoder(JaxbContextBuilder jaxbContextBuilder) {
103 this(jaxbContextBuilder, null);
104 }
105
106
107
108
109
110
111
112 public ReactiveJaxbDecoder(
113 JaxbContextBuilder jaxbContextBuilder,
114 Set<Class<?>> ignoreReadingClasses) {
115
116 super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML,
117 new MediaType("application", "*+xml"));
118 Assert.notNull(jaxbContextBuilder, "JaxbContextBuilder must be present.");
119 this.jaxbContextBuilder = jaxbContextBuilder;
120 this.ignoreReadingClasses = isNull(ignoreReadingClasses) ? Set.of() : ignoreReadingClasses;
121 }
122
123
124
125
126
127
128
129
130
131
132
133 public void setMaxInMemorySize(int byteCount) {
134 this.maxInMemorySize = byteCount;
135 this.xmlEventDecoder.setMaxInMemorySize(byteCount);
136 }
137
138
139
140
141
142
143 public int getMaxInMemorySize() {
144 return this.maxInMemorySize;
145 }
146
147
148 @Override
149 public boolean canDecode(
150 @NonNull ResolvableType elementType,
151 @Nullable MimeType mimeType) {
152
153 if (super.canDecode(elementType, mimeType)) {
154 final Class<?> outputClass = elementType.getRawClass();
155 return !ignoreReadingClasses.contains(outputClass)
156 && jaxbContextBuilder.canUnmarshal(outputClass);
157 } else {
158 return false;
159 }
160 }
161
162 @NonNull
163 @Override
164 public Flux<Object> decode(
165 @NonNull Publisher<DataBuffer> inputStream,
166 ResolvableType elementType,
167 @Nullable MimeType mimeType,
168 @Nullable Map<String, Object> hints) {
169
170 Flux<XMLEvent> xmlEventFlux = this.xmlEventDecoder.decode(
171 inputStream, ResolvableType.forClass(XMLEvent.class), mimeType, hints);
172
173 Class<?> outputClass = elementType.toClass();
174 QName typeName = toQName(outputClass);
175 Flux<List<XMLEvent>> splitEvents = split(xmlEventFlux, typeName);
176
177 return splitEvents.map(events -> {
178 Object value = unmarshal(events, outputClass);
179 LogFormatUtils.traceDebug(logger, traceOn -> {
180 String formatted = LogFormatUtils.formatValue(value, !traceOn);
181 return Hints.getLogPrefix(hints) + "Decoded [" + formatted + "]";
182 });
183 return value;
184 });
185 }
186
187 @NonNull
188 @Override
189 @SuppressWarnings({"rawtypes", "unchecked", "cast"})
190
191 public Object decode(
192 DataBuffer dataBuffer,
193 ResolvableType targetType,
194 @Nullable MimeType mimeType,
195 @Nullable Map<String, Object> hints) throws DecodingException {
196
197 try {
198 Iterator eventReader = inputFactory.createXMLEventReader(dataBuffer.asInputStream());
199 List<XMLEvent> events = new ArrayList<>();
200 eventReader.forEachRemaining(event -> events.add((XMLEvent) event));
201 return unmarshal(events, targetType.toClass());
202 } catch (XMLStreamException ex) {
203 throw Exceptions.propagate(ex);
204 } finally {
205 DataBufferUtils.release(dataBuffer);
206 }
207 }
208
209 @NonNull
210 @Override
211 public Mono<Object> decodeToMono(
212 @NonNull Publisher<DataBuffer> input,
213 @NonNull ResolvableType elementType,
214 @Nullable MimeType mimeType,
215 @Nullable Map<String, Object> hints) {
216
217
218 return DataBufferUtils.join(input, this.maxInMemorySize)
219 .map(dataBuffer -> decode(dataBuffer, elementType, mimeType, hints));
220 }
221
222 private Object unmarshal(List<XMLEvent> events, Class<?> outputClass) {
223 try {
224 Unmarshaller unmarshaller = jaxbContextBuilder.buildUnmarshaller(outputClass);
225 XMLEventReader eventReader = StaxUtils.createXMLEventReader(events);
226 if (outputClass.isAnnotationPresent(XmlRootElement.class)) {
227 return unmarshaller.unmarshal(eventReader);
228 } else {
229 JAXBElement<?> jaxbElement = unmarshaller.unmarshal(eventReader, outputClass);
230 return jaxbElement.getValue();
231 }
232 } catch (UnmarshalException ex) {
233 throw new DecodingException("Could not unmarshal XML to " + outputClass, ex);
234 } catch (JAXBException ex) {
235 throw new CodecException("Invalid JAXB configuration", ex);
236 }
237 }
238
239
240
241
242
243
244
245
246 QName toQName(Class<?> outputClass) {
247 String localPart;
248 String namespaceUri;
249
250 if (outputClass.isAnnotationPresent(XmlRootElement.class)) {
251 XmlRootElement annotation = outputClass.getAnnotation(XmlRootElement.class);
252 localPart = annotation.name();
253 namespaceUri = annotation.namespace();
254 } else if (outputClass.isAnnotationPresent(XmlType.class)) {
255 XmlType annotation = outputClass.getAnnotation(XmlType.class);
256 localPart = annotation.name();
257 namespaceUri = annotation.namespace();
258 } else {
259 throw new IllegalArgumentException("Output class [" + outputClass.getName()
260 + "] is neither annotated with @XmlRootElement nor @XmlType");
261 }
262
263 if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(localPart)) {
264 localPart = ClassUtils.getShortNameAsProperty(outputClass);
265 }
266 if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(namespaceUri)) {
267 Package outputClassPackage = outputClass.getPackage();
268 if (outputClassPackage != null && outputClassPackage.isAnnotationPresent(XmlSchema.class)) {
269 XmlSchema annotation = outputClassPackage.getAnnotation(XmlSchema.class);
270 namespaceUri = annotation.namespace();
271 } else {
272 namespaceUri = XMLConstants.NULL_NS_URI;
273 }
274 }
275 return new QName(namespaceUri, localPart);
276 }
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304 Flux<List<XMLEvent>> split(Flux<XMLEvent> xmlEventFlux, QName desiredName) {
305 return xmlEventFlux.handle(new SplitHandler(desiredName));
306 }
307
308 private static class SplitHandler implements
309 BiConsumer<XMLEvent, SynchronousSink<List<XMLEvent>>> {
310
311 private final QName desiredName;
312
313 private List<XMLEvent> events;
314
315 private int elementDepth = 0;
316
317 private int barrier = Integer.MAX_VALUE;
318
319
320
321
322
323
324 public SplitHandler(QName desiredName) {
325 this.desiredName = desiredName;
326 }
327
328 @Override
329 public void accept(XMLEvent event, SynchronousSink<List<XMLEvent>> sink) {
330 if (event.isStartElement()) {
331 if (this.barrier == Integer.MAX_VALUE) {
332 QName startElementName = event.asStartElement().getName();
333 if (this.desiredName.equals(startElementName)) {
334 this.events = new ArrayList<>();
335 this.barrier = this.elementDepth;
336 }
337 }
338 this.elementDepth++;
339 }
340 if (this.elementDepth > this.barrier) {
341 Assert.state(this.events != null, "No XMLEvent List");
342 this.events.add(event);
343 }
344 if (event.isEndElement()) {
345 this.elementDepth--;
346 if (this.elementDepth == this.barrier) {
347 this.barrier = Integer.MAX_VALUE;
348 Assert.state(this.events != null, "No XMLEvent List");
349 sink.next(this.events);
350 }
351 }
352 }
353 }
354
355 }