Merge pull request #77 from poutsma/refactor_jaxb2decoder
Improve Jaxb2Decoder
This commit is contained in:
@@ -105,6 +105,7 @@ dependencies {
|
||||
optional "org.eclipse.jetty:jetty-server:${jettyVersion}"
|
||||
optional "org.eclipse.jetty:jetty-servlet:${jettyVersion}"
|
||||
optional("org.freemarker:freemarker:2.3.23")
|
||||
optional("com.fasterxml:aalto-xml:1.0.0")
|
||||
|
||||
provided "javax.servlet:javax.servlet-api:3.1.0"
|
||||
|
||||
@@ -118,6 +119,7 @@ dependencies {
|
||||
}
|
||||
testCompile "org.hamcrest:hamcrest-all:1.3"
|
||||
testCompile "com.squareup.okhttp3:mockwebserver:3.0.1"
|
||||
testCompile("xmlunit:xmlunit:1.6")
|
||||
|
||||
// Needed to run Javadoc without error
|
||||
optional "org.apache.httpcomponents:httpclient:4.5.1"
|
||||
|
||||
@@ -16,30 +16,28 @@
|
||||
|
||||
package org.springframework.core.codec.support;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.bind.JAXBElement;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.UnmarshalException;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import javax.xml.transform.Source;
|
||||
import javax.xml.transform.sax.SAXSource;
|
||||
import javax.xml.transform.stream.StreamSource;
|
||||
import javax.xml.bind.annotation.XmlSchema;
|
||||
import javax.xml.bind.annotation.XmlType;
|
||||
import javax.xml.namespace.QName;
|
||||
import javax.xml.stream.XMLEventReader;
|
||||
import javax.xml.stream.events.XMLEvent;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.XMLReader;
|
||||
import org.xml.sax.helpers.XMLReaderFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.CodecException;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.support.DataBufferUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
@@ -47,87 +45,170 @@ import org.springframework.util.MimeTypeUtils;
|
||||
* Decode from a bytes stream of XML elements to a stream of {@code Object} (POJO).
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Arjen Poutsma
|
||||
* @see Jaxb2Encoder
|
||||
*/
|
||||
public class Jaxb2Decoder extends AbstractDecoder<Object> {
|
||||
|
||||
private final ConcurrentMap<Class<?>, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64);
|
||||
/**
|
||||
* The default value for JAXB annotations.
|
||||
* @see XmlRootElement#name()
|
||||
* @see XmlRootElement#namespace()
|
||||
* @see XmlType#name()
|
||||
* @see XmlType#namespace()
|
||||
*/
|
||||
private final static String JAXB_DEFAULT_ANNOTATION_VALUE = "##default";
|
||||
|
||||
private final XmlEventDecoder xmlEventDecoder = new XmlEventDecoder();
|
||||
|
||||
private final JaxbContextContainer jaxbContexts = new JaxbContextContainer();
|
||||
|
||||
public Jaxb2Decoder() {
|
||||
super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) {
|
||||
if (super.canDecode(type, mimeType, hints)) {
|
||||
Class<?> outputClass = type.getRawClass();
|
||||
return outputClass.isAnnotationPresent(XmlRootElement.class) ||
|
||||
outputClass.isAnnotationPresent(XmlType.class);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType type,
|
||||
MimeType mimeType, Object... hints) {
|
||||
|
||||
Class<?> outputClass = type.getRawClass();
|
||||
try {
|
||||
Source source = processSource(
|
||||
new StreamSource(DataBufferUtils.toInputStream(inputStream)));
|
||||
Unmarshaller unmarshaller = createUnmarshaller(outputClass);
|
||||
if (outputClass.isAnnotationPresent(XmlRootElement.class)) {
|
||||
return Flux.just(unmarshaller.unmarshal(source));
|
||||
}
|
||||
else {
|
||||
JAXBElement<?> jaxbElement = unmarshaller.unmarshal(source, outputClass);
|
||||
return Flux.just(jaxbElement.getValue());
|
||||
}
|
||||
}
|
||||
catch (UnmarshalException ex) {
|
||||
return Flux.error(
|
||||
new CodecException("Could not unmarshal to [" + outputClass + "]: " + ex.getMessage(), ex));
|
||||
}
|
||||
catch (JAXBException ex) {
|
||||
return Flux.error(new CodecException("Could not instantiate JAXBContext: " +
|
||||
ex.getMessage(), ex));
|
||||
}
|
||||
Flux<XMLEvent> xmlEventFlux =
|
||||
this.xmlEventDecoder.decode(inputStream, null, mimeType);
|
||||
|
||||
QName typeName = toQName(outputClass);
|
||||
Flux<List<XMLEvent>> splitEvents = split(xmlEventFlux, typeName);
|
||||
|
||||
return splitEvents.map(events -> unmarshal(events, outputClass));
|
||||
}
|
||||
|
||||
protected Source processSource(Source source) {
|
||||
if (source instanceof StreamSource) {
|
||||
StreamSource streamSource = (StreamSource) source;
|
||||
InputSource inputSource = new InputSource(streamSource.getInputStream());
|
||||
try {
|
||||
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
|
||||
return new SAXSource(xmlReader, inputSource);
|
||||
}
|
||||
catch (SAXException ex) {
|
||||
throw new CodecException("Error while processing the source", ex);
|
||||
}
|
||||
/**
|
||||
* Returns the qualified name for the given class, according to the mapping rules
|
||||
* in the JAXB specification.
|
||||
*/
|
||||
QName toQName(Class<?> outputClass) {
|
||||
String localPart;
|
||||
String namespaceUri;
|
||||
|
||||
if (outputClass.isAnnotationPresent(XmlRootElement.class)) {
|
||||
XmlRootElement annotation = outputClass.getAnnotation(XmlRootElement.class);
|
||||
localPart = annotation.name();
|
||||
namespaceUri = annotation.namespace();
|
||||
}
|
||||
else if (outputClass.isAnnotationPresent(XmlType.class)) {
|
||||
XmlType annotation = outputClass.getAnnotation(XmlType.class);
|
||||
localPart = annotation.name();
|
||||
namespaceUri = annotation.namespace();
|
||||
}
|
||||
else {
|
||||
return source;
|
||||
throw new IllegalArgumentException("Outputclass [" + outputClass + "] is " +
|
||||
"neither annotated with @XmlRootElement nor @XmlType");
|
||||
}
|
||||
|
||||
if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(localPart)) {
|
||||
localPart = ClassUtils.getShortNameAsProperty(outputClass);
|
||||
}
|
||||
if (JAXB_DEFAULT_ANNOTATION_VALUE.equals(namespaceUri)) {
|
||||
Package outputClassPackage = outputClass.getPackage();
|
||||
if (outputClassPackage != null &&
|
||||
outputClassPackage.isAnnotationPresent(XmlSchema.class)) {
|
||||
XmlSchema annotation = outputClassPackage.getAnnotation(XmlSchema.class);
|
||||
namespaceUri = annotation.namespace();
|
||||
}
|
||||
else {
|
||||
namespaceUri = XMLConstants.NULL_NS_URI;
|
||||
}
|
||||
}
|
||||
return new QName(namespaceUri, localPart);
|
||||
}
|
||||
|
||||
protected final Unmarshaller createUnmarshaller(Class<?> clazz) throws JAXBException {
|
||||
/**
|
||||
* Split a flux of {@link XMLEvent}s into a flux of XMLEvent lists, one list for each
|
||||
* branch of the tree that starts with the given qualified name.
|
||||
* That is, given the XMLEvents shown
|
||||
* {@linkplain XmlEventDecoder here},
|
||||
* and the {@code desiredName} "{@code child}", this method
|
||||
* returns a flux of two lists, each of which containing the events of a particular
|
||||
* branch of the tree that starts with "{@code child}".
|
||||
* <ol>
|
||||
* <li>The first list, dealing with the first branch of the tree
|
||||
* <ol>
|
||||
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
|
||||
* <li>{@link javax.xml.stream.events.Characters} {@code foo}</li>
|
||||
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
|
||||
* </ol>
|
||||
* <li>The second list, dealing with the second branch of the tree
|
||||
* <ol>
|
||||
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
|
||||
* <li>{@link javax.xml.stream.events.Characters} {@code bar}</li>
|
||||
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
|
||||
* </ol>
|
||||
* </li>
|
||||
* </ol>
|
||||
*/
|
||||
Flux<List<XMLEvent>> split(Flux<XMLEvent> xmlEventFlux, QName desiredName) {
|
||||
return xmlEventFlux
|
||||
.flatMap(new Function<XMLEvent, Publisher<? extends List<XMLEvent>>>() {
|
||||
|
||||
private List<XMLEvent> events = null;
|
||||
|
||||
private int elementDepth = 0;
|
||||
|
||||
private int barrier = Integer.MAX_VALUE;
|
||||
|
||||
@Override
|
||||
public Publisher<? extends List<XMLEvent>> apply(XMLEvent event) {
|
||||
if (event.isStartElement()) {
|
||||
if (this.barrier == Integer.MAX_VALUE) {
|
||||
QName startElementName = event.asStartElement().getName();
|
||||
if (desiredName.equals(startElementName)) {
|
||||
this.events = new ArrayList<XMLEvent>();
|
||||
this.barrier = this.elementDepth;
|
||||
}
|
||||
}
|
||||
this.elementDepth++;
|
||||
}
|
||||
if (this.elementDepth > this.barrier) {
|
||||
this.events.add(event);
|
||||
}
|
||||
if (event.isEndElement()) {
|
||||
this.elementDepth--;
|
||||
if (this.elementDepth == this.barrier) {
|
||||
this.barrier = Integer.MAX_VALUE;
|
||||
return Mono.just(this.events);
|
||||
}
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Object unmarshal(List<XMLEvent> eventFlux, Class<?> outputClass) {
|
||||
try {
|
||||
JAXBContext jaxbContext = getJaxbContext(clazz);
|
||||
return jaxbContext.createUnmarshaller();
|
||||
Unmarshaller unmarshaller = this.jaxbContexts.createUnmarshaller(outputClass);
|
||||
XMLEventReader eventReader = new ListBasedXMLEventReader(eventFlux);
|
||||
if (outputClass.isAnnotationPresent(XmlRootElement.class)) {
|
||||
return unmarshaller.unmarshal(eventReader);
|
||||
}
|
||||
else {
|
||||
JAXBElement<?> jaxbElement =
|
||||
unmarshaller.unmarshal(eventReader, outputClass);
|
||||
return jaxbElement.getValue();
|
||||
}
|
||||
}
|
||||
catch (JAXBException ex) {
|
||||
throw new CodecException("Could not create Unmarshaller for class " +
|
||||
"[" + clazz + "]: " + ex.getMessage(), ex);
|
||||
throw new CodecException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected final JAXBContext getJaxbContext(Class<?> clazz) {
|
||||
Assert.notNull(clazz, "'clazz' must not be null");
|
||||
JAXBContext jaxbContext = this.jaxbContexts.get(clazz);
|
||||
if (jaxbContext == null) {
|
||||
try {
|
||||
jaxbContext = JAXBContext.newInstance(clazz);
|
||||
this.jaxbContexts.putIfAbsent(clazz, jaxbContext);
|
||||
}
|
||||
catch (JAXBException ex) {
|
||||
throw new CodecException("Could not instantiate JAXBContext for class " +
|
||||
"[" + clazz + "]: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
return jaxbContext;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,12 +18,11 @@ package org.springframework.core.codec.support;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.MarshalException;
|
||||
import javax.xml.bind.Marshaller;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import javax.xml.bind.annotation.XmlType;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -32,7 +31,6 @@ import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.CodecException;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferAllocator;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
@@ -41,27 +39,43 @@ import org.springframework.util.MimeTypeUtils;
|
||||
* Encode from an {@code Object} stream to a byte stream of XML elements.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Arjen Poutsma
|
||||
* @see Jaxb2Decoder
|
||||
*/
|
||||
public class Jaxb2Encoder extends AbstractEncoder<Object> {
|
||||
|
||||
private final ConcurrentMap<Class<?>, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64);
|
||||
private final JaxbContextContainer jaxbContexts = new JaxbContextContainer();
|
||||
|
||||
public Jaxb2Encoder() {
|
||||
super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) {
|
||||
if (super.canEncode(type, mimeType, hints)) {
|
||||
Class<?> outputClass = type.getRawClass();
|
||||
return outputClass.isAnnotationPresent(XmlRootElement.class) ||
|
||||
outputClass.isAnnotationPresent(XmlType.class);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> encode(Publisher<?> inputStream,
|
||||
DataBufferAllocator allocator, ResolvableType type, MimeType mimeType,
|
||||
Object... hints) {
|
||||
|
||||
return Flux.from(inputStream).map(value -> {
|
||||
return Flux.from(inputStream).
|
||||
take(1). // only map 1 value to ensure valid XML output
|
||||
map(value -> {
|
||||
try {
|
||||
DataBuffer buffer = allocator.allocateBuffer(1024);
|
||||
OutputStream outputStream = buffer.asOutputStream();
|
||||
Class<?> clazz = ClassUtils.getUserClass(value);
|
||||
Marshaller marshaller = createMarshaller(clazz);
|
||||
Marshaller marshaller = jaxbContexts.createMarshaller(clazz);
|
||||
marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
|
||||
marshaller.marshal(value, outputStream);
|
||||
return buffer;
|
||||
@@ -75,32 +89,7 @@ public class Jaxb2Encoder extends AbstractEncoder<Object> {
|
||||
});
|
||||
}
|
||||
|
||||
protected final Marshaller createMarshaller(Class<?> clazz) {
|
||||
try {
|
||||
JAXBContext jaxbContext = getJaxbContext(clazz);
|
||||
return jaxbContext.createMarshaller();
|
||||
}
|
||||
catch (JAXBException ex) {
|
||||
throw new CodecException("Could not create Marshaller for class " +
|
||||
"[" + clazz + "]: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected final JAXBContext getJaxbContext(Class<?> clazz) {
|
||||
Assert.notNull(clazz, "'clazz' must not be null");
|
||||
JAXBContext jaxbContext = this.jaxbContexts.get(clazz);
|
||||
if (jaxbContext == null) {
|
||||
try {
|
||||
jaxbContext = JAXBContext.newInstance(clazz);
|
||||
this.jaxbContexts.putIfAbsent(clazz, jaxbContext);
|
||||
}
|
||||
catch (JAXBException ex) {
|
||||
throw new CodecException("Could not instantiate JAXBContext for class " +
|
||||
"[" + clazz + "]: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
return jaxbContext;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.codec.support;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.Marshaller;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
final class JaxbContextContainer {
|
||||
|
||||
private final ConcurrentMap<Class<?>, JAXBContext> jaxbContexts =
|
||||
new ConcurrentHashMap<>(64);
|
||||
|
||||
public Marshaller createMarshaller(Class<?> clazz) throws JAXBException {
|
||||
JAXBContext jaxbContext = getJaxbContext(clazz);
|
||||
return jaxbContext.createMarshaller();
|
||||
}
|
||||
|
||||
public Unmarshaller createUnmarshaller(Class<?> clazz) throws JAXBException {
|
||||
JAXBContext jaxbContext = getJaxbContext(clazz);
|
||||
return jaxbContext.createUnmarshaller();
|
||||
}
|
||||
|
||||
private JAXBContext getJaxbContext(Class<?> clazz) throws JAXBException {
|
||||
Assert.notNull(clazz, "'clazz' must not be null");
|
||||
JAXBContext jaxbContext = this.jaxbContexts.get(clazz);
|
||||
if (jaxbContext == null) {
|
||||
jaxbContext = JAXBContext.newInstance(clazz);
|
||||
this.jaxbContexts.putIfAbsent(clazz, jaxbContext);
|
||||
}
|
||||
return jaxbContext;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.codec.support;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import javax.xml.stream.XMLEventReader;
|
||||
import javax.xml.stream.XMLStreamConstants;
|
||||
import javax.xml.stream.XMLStreamException;
|
||||
import javax.xml.stream.events.Characters;
|
||||
import javax.xml.stream.events.XMLEvent;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* TODO: move to org.springframework.util.xml when merging, hidden behind StaxUtils
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
class ListBasedXMLEventReader implements XMLEventReader {
|
||||
|
||||
private final XMLEvent[] events;
|
||||
|
||||
private int cursor = 0;
|
||||
|
||||
public ListBasedXMLEventReader(List<XMLEvent> events) {
|
||||
Assert.notNull(events, "'events' must not be null");
|
||||
this.events = events.toArray(new XMLEvent[events.size()]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
Assert.notNull(events, "'events' must not be null");
|
||||
return cursor != events.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XMLEvent nextEvent() {
|
||||
if (cursor < events.length) {
|
||||
return events[cursor++];
|
||||
}
|
||||
else {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public XMLEvent peek() {
|
||||
if (cursor < events.length) {
|
||||
return events[cursor];
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object next() {
|
||||
return nextEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an {@code UnsupportedOperationException} when called.
|
||||
* @throws UnsupportedOperationException when called
|
||||
*/
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException(
|
||||
"remove not supported on " + ClassUtils.getShortName(getClass()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getElementText() throws XMLStreamException {
|
||||
if (!peek().isStartElement()) {
|
||||
throw new XMLStreamException("Not at START_ELEMENT");
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
while (true) {
|
||||
XMLEvent event = nextEvent();
|
||||
if (event.isEndElement()) {
|
||||
break;
|
||||
}
|
||||
else if (!event.isCharacters()) {
|
||||
throw new XMLStreamException(
|
||||
"Unexpected event [" + event + "] in getElementText()");
|
||||
}
|
||||
Characters characters = event.asCharacters();
|
||||
if (!characters.isIgnorableWhiteSpace()) {
|
||||
builder.append(event.asCharacters().getData());
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public XMLEvent nextTag() throws XMLStreamException {
|
||||
while (true) {
|
||||
XMLEvent event = nextEvent();
|
||||
switch (event.getEventType()) {
|
||||
case XMLStreamConstants.START_ELEMENT:
|
||||
case XMLStreamConstants.END_ELEMENT:
|
||||
return event;
|
||||
case XMLStreamConstants.END_DOCUMENT:
|
||||
return null;
|
||||
case XMLStreamConstants.SPACE:
|
||||
case XMLStreamConstants.COMMENT:
|
||||
case XMLStreamConstants.PROCESSING_INSTRUCTION:
|
||||
continue;
|
||||
case XMLStreamConstants.CDATA:
|
||||
case XMLStreamConstants.CHARACTERS:
|
||||
if (!event.asCharacters().isWhiteSpace()) {
|
||||
throw new XMLStreamException(
|
||||
"Non-ignorable whitespace CDATA or CHARACTERS event in nextTag()");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new XMLStreamException("Received event [" + event +
|
||||
"], instead of START_ELEMENT or END_ELEMENT.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an {@code IllegalArgumentException} when called.
|
||||
* @throws IllegalArgumentException when called.
|
||||
*/
|
||||
@Override
|
||||
public Object getProperty(String name) throws IllegalArgumentException {
|
||||
throw new IllegalArgumentException("Property not supported: [" + name + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.codec.support;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import javax.xml.stream.XMLEventReader;
|
||||
import javax.xml.stream.XMLInputFactory;
|
||||
import javax.xml.stream.XMLStreamException;
|
||||
import javax.xml.stream.events.XMLEvent;
|
||||
import javax.xml.stream.util.XMLEventAllocator;
|
||||
|
||||
import com.fasterxml.aalto.AsyncByteBufferFeeder;
|
||||
import com.fasterxml.aalto.AsyncXMLInputFactory;
|
||||
import com.fasterxml.aalto.AsyncXMLStreamReader;
|
||||
import com.fasterxml.aalto.evt.EventAllocatorImpl;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.support.DataBufferUtils;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
/**
|
||||
* Decodes a {@link DataBuffer} stream into a stream of {@link XMLEvent}s. That is, given
|
||||
* the following XML:
|
||||
* <pre>{@code
|
||||
* <root>
|
||||
* <child>foo</child>
|
||||
* <child>bar</child>
|
||||
* </root>}
|
||||
* </pre>
|
||||
* this method with result in a flux with the following events:
|
||||
* <ol>
|
||||
* <li>{@link javax.xml.stream.events.StartDocument}</li>
|
||||
* <li>{@link javax.xml.stream.events.StartElement} {@code root}</li>
|
||||
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
|
||||
* <li>{@link javax.xml.stream.events.Characters} {@code foo}</li>
|
||||
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
|
||||
* <li>{@link javax.xml.stream.events.StartElement} {@code child}</li>
|
||||
* <li>{@link javax.xml.stream.events.Characters} {@code bar}</li>
|
||||
* <li>{@link javax.xml.stream.events.EndElement} {@code child}</li>
|
||||
* <li>{@link javax.xml.stream.events.EndElement} {@code root}</li>
|
||||
* </ol>
|
||||
*
|
||||
* Note that this decoder is not registered by default, but used internally by other
|
||||
* decoders who are.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
|
||||
|
||||
private static final boolean aaltoPresent = ClassUtils
|
||||
.isPresent("com.fasterxml.aalto.AsyncXMLStreamReader",
|
||||
XmlEventDecoder.class.getClassLoader());
|
||||
|
||||
private static final XMLInputFactory inputFactory = XMLInputFactory.newFactory();
|
||||
|
||||
public XmlEventDecoder() {
|
||||
super(MimeTypeUtils.APPLICATION_XML, MimeTypeUtils.TEXT_XML);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<XMLEvent> decode(Publisher<DataBuffer> inputStream, ResolvableType type,
|
||||
MimeType mimeType, Object... hints) {
|
||||
if (aaltoPresent) {
|
||||
return Flux.from(inputStream).flatMap(new AaltoDataBufferToXmlEvent());
|
||||
}
|
||||
else {
|
||||
try {
|
||||
InputStream blockingStream = DataBufferUtils.toInputStream(inputStream);
|
||||
|
||||
XMLEventReader eventReader =
|
||||
inputFactory.createXMLEventReader(blockingStream);
|
||||
|
||||
return Flux.fromIterable((Iterable<XMLEvent>) () -> eventReader);
|
||||
}
|
||||
catch (XMLStreamException ex) {
|
||||
return Flux.error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Separate static class to isolate Aalto dependency.
|
||||
*/
|
||||
private static class AaltoDataBufferToXmlEvent
|
||||
implements Function<DataBuffer, Publisher<? extends XMLEvent>> {
|
||||
|
||||
private static final AsyncXMLInputFactory inputFactory =
|
||||
(AsyncXMLInputFactory) XmlEventDecoder.inputFactory;
|
||||
|
||||
private final AsyncXMLStreamReader<AsyncByteBufferFeeder> streamReader =
|
||||
inputFactory.createAsyncForByteBuffer();
|
||||
|
||||
private final XMLEventAllocator eventAllocator =
|
||||
EventAllocatorImpl.getDefaultInstance();
|
||||
|
||||
@Override
|
||||
public Publisher<? extends XMLEvent> apply(DataBuffer dataBuffer) {
|
||||
try {
|
||||
streamReader.getInputFeeder().feedInput(dataBuffer.asByteBuffer());
|
||||
List<XMLEvent> events = new ArrayList<>();
|
||||
while (true) {
|
||||
if (streamReader.next() == AsyncXMLStreamReader.EVENT_INCOMPLETE) {
|
||||
// no more events with what currently has been fed to the reader
|
||||
break;
|
||||
}
|
||||
else {
|
||||
XMLEvent event = eventAllocator.allocate(streamReader);
|
||||
events.add(event);
|
||||
if (event.isEndDocument()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Flux.fromIterable(events);
|
||||
}
|
||||
catch (XMLStreamException ex) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.support.ByteBufferDecoder;
|
||||
import org.springframework.core.codec.support.JacksonJsonDecoder;
|
||||
import org.springframework.core.codec.support.Jaxb2Decoder;
|
||||
import org.springframework.core.codec.support.JsonObjectDecoder;
|
||||
import org.springframework.core.codec.support.StringDecoder;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
@@ -100,7 +101,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin
|
||||
if (ObjectUtils.isEmpty(this.argumentResolvers)) {
|
||||
|
||||
List<Decoder<?>> decoders = Arrays.asList(new ByteBufferDecoder(),
|
||||
new StringDecoder(),
|
||||
new StringDecoder(), new Jaxb2Decoder(),
|
||||
new JacksonJsonDecoder(new JsonObjectDecoder()));
|
||||
|
||||
this.argumentResolvers.add(new RequestParamArgumentResolver());
|
||||
|
||||
@@ -16,38 +16,270 @@
|
||||
|
||||
package org.springframework.core.codec.support;
|
||||
|
||||
import java.util.List;
|
||||
import javax.xml.namespace.QName;
|
||||
import javax.xml.stream.events.XMLEvent;
|
||||
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.test.TestSubscriber;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.support.jaxb.XmlRootElement;
|
||||
import org.springframework.core.codec.support.jaxb.XmlRootElementWithName;
|
||||
import org.springframework.core.codec.support.jaxb.XmlRootElementWithNameAndNamespace;
|
||||
import org.springframework.core.codec.support.jaxb.XmlType;
|
||||
import org.springframework.core.codec.support.jaxb.XmlTypeWithName;
|
||||
import org.springframework.core.codec.support.jaxb.XmlTypeWithNameAndNamespace;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import reactor.core.test.TestSubscriber;
|
||||
|
||||
/**
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public class Jaxb2DecoderTests extends AbstractAllocatingTestCase {
|
||||
|
||||
private static final String POJO_ROOT = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<pojo>" +
|
||||
"<foo>foofoo</foo>" +
|
||||
"<bar>barbar</bar>" +
|
||||
"</pojo>";
|
||||
|
||||
private static final String POJO_CHILD =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<root>" +
|
||||
"<pojo>" +
|
||||
"<foo>foo</foo>" +
|
||||
"<bar>bar</bar>" +
|
||||
"</pojo>" +
|
||||
"<pojo>" +
|
||||
"<foo>foofoo</foo>" +
|
||||
"<bar>barbar</bar>" +
|
||||
"</pojo>" +
|
||||
"<root/>";
|
||||
|
||||
private final Jaxb2Decoder decoder = new Jaxb2Decoder();
|
||||
|
||||
private final XmlEventDecoder xmlEventDecoder = new XmlEventDecoder();
|
||||
|
||||
|
||||
@Test
|
||||
public void canDecode() {
|
||||
assertTrue(decoder.canDecode(null, MediaType.APPLICATION_XML));
|
||||
assertTrue(decoder.canDecode(null, MediaType.TEXT_XML));
|
||||
assertFalse(decoder.canDecode(null, MediaType.APPLICATION_JSON));
|
||||
assertTrue(decoder.canDecode(ResolvableType.forClass(Pojo.class),
|
||||
MediaType.APPLICATION_XML));
|
||||
assertTrue(decoder.canDecode(ResolvableType.forClass(Pojo.class),
|
||||
MediaType.TEXT_XML));
|
||||
assertFalse(decoder.canDecode(ResolvableType.forClass(Pojo.class),
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
assertTrue(decoder.canDecode(ResolvableType.forClass(TypePojo.class),
|
||||
MediaType.APPLICATION_XML));
|
||||
|
||||
assertFalse(decoder.canDecode(ResolvableType.forClass(getClass()),
|
||||
MediaType.APPLICATION_XML));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decode() {
|
||||
Flux<DataBuffer> source = Flux.just(stringBuffer(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><pojo><bar>barbar</bar><foo>foofoo</foo></pojo>"));
|
||||
Flux<Object> output = decoder.decode(source, ResolvableType.forClass(Pojo.class), null);
|
||||
TestSubscriber<Object> testSubscriber = new TestSubscriber<>();
|
||||
testSubscriber.bindTo(output)
|
||||
.assertValues(new Pojo("foofoo", "barbar"));
|
||||
public void splitOneBranches() {
|
||||
Flux<XMLEvent> xmlEvents =
|
||||
xmlEventDecoder.decode(Flux.just(stringBuffer(POJO_ROOT)), null, null);
|
||||
Flux<List<XMLEvent>> result = decoder.split(xmlEvents, new QName("pojo"));
|
||||
|
||||
TestSubscriber<List<XMLEvent>> resultSubscriber = new TestSubscriber<>();
|
||||
resultSubscriber.bindTo(result).
|
||||
assertNoError().
|
||||
assertComplete().
|
||||
assertValuesWith(events -> {
|
||||
assertEquals(8, events.size());
|
||||
assertStartElement(events.get(0), "pojo");
|
||||
assertStartElement(events.get(1), "foo");
|
||||
assertCharacters(events.get(2), "foofoo");
|
||||
assertEndElement(events.get(3), "foo");
|
||||
assertStartElement(events.get(4), "bar");
|
||||
assertCharacters(events.get(5), "barbar");
|
||||
assertEndElement(events.get(6), "bar");
|
||||
assertEndElement(events.get(7), "pojo");
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splitMultipleBranches() {
|
||||
Flux<XMLEvent> xmlEvents =
|
||||
xmlEventDecoder.decode(Flux.just(stringBuffer(POJO_CHILD)), null, null);
|
||||
Flux<List<XMLEvent>> result = decoder.split(xmlEvents, new QName("pojo"));
|
||||
|
||||
TestSubscriber<List<XMLEvent>> resultSubscriber = new TestSubscriber<>();
|
||||
resultSubscriber.bindTo(result).
|
||||
assertNoError().
|
||||
assertComplete().
|
||||
assertValuesWith(events -> {
|
||||
assertEquals(8, events.size());
|
||||
assertStartElement(events.get(0), "pojo");
|
||||
assertStartElement(events.get(1), "foo");
|
||||
assertCharacters(events.get(2), "foo");
|
||||
assertEndElement(events.get(3), "foo");
|
||||
assertStartElement(events.get(4), "bar");
|
||||
assertCharacters(events.get(5), "bar");
|
||||
assertEndElement(events.get(6), "bar");
|
||||
assertEndElement(events.get(7), "pojo");
|
||||
}, events -> {
|
||||
assertEquals(8, events.size());
|
||||
assertStartElement(events.get(0), "pojo");
|
||||
assertStartElement(events.get(1), "foo");
|
||||
assertCharacters(events.get(2), "foofoo");
|
||||
assertEndElement(events.get(3), "foo");
|
||||
assertStartElement(events.get(4), "bar");
|
||||
assertCharacters(events.get(5), "barbar");
|
||||
assertEndElement(events.get(6), "bar");
|
||||
assertEndElement(events.get(7), "pojo");
|
||||
});
|
||||
}
|
||||
|
||||
private static void assertStartElement(XMLEvent event, String expectedLocalName) {
|
||||
assertTrue(event.isStartElement());
|
||||
assertEquals(expectedLocalName, event.asStartElement().getName().getLocalPart());
|
||||
}
|
||||
|
||||
private static void assertEndElement(XMLEvent event, String expectedLocalName) {
|
||||
assertTrue(event.isEndElement());
|
||||
assertEquals(expectedLocalName, event.asEndElement().getName().getLocalPart());
|
||||
}
|
||||
|
||||
private static void assertCharacters(XMLEvent event, String expectedData) {
|
||||
assertTrue(event.isCharacters());
|
||||
assertEquals(expectedData, event.asCharacters().getData());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeSingleXmlRootElement() throws Exception {
|
||||
Flux<DataBuffer> source = Flux.just(stringBuffer(POJO_ROOT));
|
||||
Flux<Object> output =
|
||||
decoder.decode(source, ResolvableType.forClass(Pojo.class), null);
|
||||
|
||||
TestSubscriber<Object> testSubscriber = new TestSubscriber<>();
|
||||
|
||||
testSubscriber.bindTo(output).
|
||||
assertNoError().
|
||||
assertComplete().
|
||||
assertValues(new Pojo("foofoo", "barbar")
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeSingleXmlTypeElement() throws Exception {
|
||||
Flux<DataBuffer> source = Flux.just(stringBuffer(POJO_ROOT));
|
||||
Flux<Object> output =
|
||||
decoder.decode(source, ResolvableType.forClass(TypePojo.class), null);
|
||||
|
||||
TestSubscriber<Object> testSubscriber = new TestSubscriber<>();
|
||||
|
||||
testSubscriber.bindTo(output).
|
||||
assertNoError().
|
||||
assertComplete().
|
||||
assertValues(new TypePojo("foofoo", "barbar")
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeMultipleXmlRootElement() throws Exception {
|
||||
Flux<DataBuffer> source = Flux.just(stringBuffer(POJO_CHILD));
|
||||
Flux<Object> output =
|
||||
decoder.decode(source, ResolvableType.forClass(Pojo.class), null);
|
||||
|
||||
TestSubscriber<Object> testSubscriber = new TestSubscriber<>();
|
||||
|
||||
testSubscriber.bindTo(output).
|
||||
assertNoError().
|
||||
assertComplete().
|
||||
assertValues(new Pojo("foo", "bar"), new Pojo("foofoo", "barbar")
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeMultipleXmlTypeElement() throws Exception {
|
||||
Flux<DataBuffer> source = Flux.just(stringBuffer(POJO_CHILD));
|
||||
Flux<Object> output =
|
||||
decoder.decode(source, ResolvableType.forClass(TypePojo.class), null);
|
||||
|
||||
TestSubscriber<Object> testSubscriber = new TestSubscriber<>();
|
||||
|
||||
testSubscriber.bindTo(output).
|
||||
assertNoError().
|
||||
assertComplete().
|
||||
assertValues(new TypePojo("foo", "bar"), new TypePojo("foofoo", "barbar")
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toExpectedQName() {
|
||||
assertEquals(new QName("pojo"), decoder.toQName(Pojo.class));
|
||||
assertEquals(new QName("pojo"), decoder.toQName(TypePojo.class));
|
||||
|
||||
assertEquals(new QName("namespace", "name"),
|
||||
decoder.toQName(XmlRootElementWithNameAndNamespace.class));
|
||||
assertEquals(new QName("namespace", "name"),
|
||||
decoder.toQName(XmlRootElementWithName.class));
|
||||
assertEquals(new QName("namespace", "xmlRootElement"),
|
||||
decoder.toQName(XmlRootElement.class));
|
||||
|
||||
assertEquals(new QName("namespace", "name"),
|
||||
decoder.toQName(XmlTypeWithNameAndNamespace.class));
|
||||
assertEquals(new QName("namespace", "name"),
|
||||
decoder.toQName(XmlTypeWithName.class));
|
||||
assertEquals(new QName("namespace", "xmlType"), decoder.toQName(XmlType.class));
|
||||
|
||||
}
|
||||
|
||||
@javax.xml.bind.annotation.XmlType(name = "pojo")
|
||||
public static class TypePojo {
|
||||
|
||||
private String foo;
|
||||
|
||||
private String bar;
|
||||
|
||||
public TypePojo() {
|
||||
}
|
||||
|
||||
public TypePojo(String foo, String bar) {
|
||||
this.foo = foo;
|
||||
this.bar = bar;
|
||||
}
|
||||
|
||||
public String getFoo() {
|
||||
return this.foo;
|
||||
}
|
||||
|
||||
public void setFoo(String foo) {
|
||||
this.foo = foo;
|
||||
}
|
||||
|
||||
public String getBar() {
|
||||
return this.bar;
|
||||
}
|
||||
|
||||
public void setBar(String bar) {
|
||||
this.bar = bar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o instanceof TypePojo) {
|
||||
TypePojo other = (TypePojo) o;
|
||||
return this.foo.equals(other.foo) && this.bar.equals(other.bar);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,20 +16,27 @@
|
||||
|
||||
package org.springframework.core.codec.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.xml.sax.SAXException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.test.TestSubscriber;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual;
|
||||
import static org.custommonkey.xmlunit.XMLAssert.fail;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @author Sebastien Deleuze
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
public class Jaxb2EncoderTests extends AbstractAllocatingTestCase {
|
||||
|
||||
@@ -42,23 +49,37 @@ public class Jaxb2EncoderTests extends AbstractAllocatingTestCase {
|
||||
|
||||
@Test
|
||||
public void canEncode() {
|
||||
assertTrue(encoder.canEncode(null, MediaType.APPLICATION_XML));
|
||||
assertTrue(encoder.canEncode(null, MediaType.TEXT_XML));
|
||||
assertFalse(encoder.canEncode(null, MediaType.APPLICATION_JSON));
|
||||
assertTrue(encoder.canEncode(ResolvableType.forClass(Pojo.class),
|
||||
MediaType.APPLICATION_XML));
|
||||
assertTrue(encoder.canEncode(ResolvableType.forClass(Pojo.class),
|
||||
MediaType.TEXT_XML));
|
||||
assertFalse(encoder.canEncode(ResolvableType.forClass(Pojo.class),
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
assertTrue(encoder.canEncode(
|
||||
ResolvableType.forClass(Jaxb2DecoderTests.TypePojo.class),
|
||||
MediaType.APPLICATION_XML));
|
||||
|
||||
assertFalse(encoder.canEncode(ResolvableType.forClass(getClass()),
|
||||
MediaType.APPLICATION_XML));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encode() {
|
||||
Flux<Pojo> source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar"));
|
||||
Flux<String> output = encoder.encode(source, allocator, null, null).map(chunk -> {
|
||||
byte[] b = new byte[chunk.readableByteCount()];
|
||||
chunk.read(b);
|
||||
return new String(b, StandardCharsets.UTF_8);
|
||||
});
|
||||
Flux<String> output =
|
||||
encoder.encode(source, allocator, ResolvableType.forClass(Pojo.class),
|
||||
MediaType.APPLICATION_XML).map(chunk -> DataBufferTestUtils
|
||||
.dumpString(chunk, StandardCharsets.UTF_8));
|
||||
TestSubscriber<String> testSubscriber = new TestSubscriber<>();
|
||||
testSubscriber.bindTo(output)
|
||||
.assertValues("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><pojo><bar>barbar</bar><foo>foofoo</foo></pojo>",
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><pojo><bar>barbarbar</bar><foo>foofoofoo</foo></pojo>");
|
||||
testSubscriber.bindTo(output).assertValuesWith(s -> {
|
||||
try {
|
||||
assertXMLEqual("<pojo><bar>barbar</bar><foo>foofoo</foo></pojo>", s);
|
||||
}
|
||||
catch (SAXException | IOException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.codec.support;
|
||||
|
||||
import javax.xml.stream.events.XMLEvent;
|
||||
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.test.TestSubscriber;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
public class XmlEventDecoderTests extends AbstractAllocatingTestCase {
|
||||
|
||||
private static final String XML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
|
||||
"<pojo>" +
|
||||
"<foo>foofoo</foo>" +
|
||||
"<bar>barbar</bar>" +
|
||||
"</pojo>";
|
||||
|
||||
private XmlEventDecoder decoder = new XmlEventDecoder();
|
||||
|
||||
@Test
|
||||
public void toXMLEvents() {
|
||||
|
||||
Flux<XMLEvent> events = decoder.decode(Flux.just(stringBuffer(XML)), null, null);
|
||||
|
||||
TestSubscriber<XMLEvent> testSubscriber = new TestSubscriber<>();
|
||||
testSubscriber.bindTo(events).
|
||||
assertNoError().
|
||||
assertComplete().
|
||||
assertValuesWith(e -> assertTrue(e.isStartDocument()),
|
||||
e -> assertStartElement(e, "pojo"),
|
||||
e -> assertStartElement(e, "foo"),
|
||||
e -> assertCharacters(e, "foofoo"),
|
||||
e -> assertEndElement(e, "foo"),
|
||||
e -> assertStartElement(e, "bar"),
|
||||
e -> assertCharacters(e, "barbar"),
|
||||
e -> assertEndElement(e, "bar"),
|
||||
e -> assertEndElement(e, "pojo"));
|
||||
}
|
||||
|
||||
private static void assertStartElement(XMLEvent event, String expectedLocalName) {
|
||||
assertTrue(event.isStartElement());
|
||||
assertEquals(expectedLocalName, event.asStartElement().getName().getLocalPart());
|
||||
}
|
||||
|
||||
private static void assertEndElement(XMLEvent event, String expectedLocalName) {
|
||||
assertTrue(event + " is no end element", event.isEndElement());
|
||||
assertEquals(expectedLocalName, event.asEndElement().getName().getLocalPart());
|
||||
}
|
||||
|
||||
private static void assertCharacters(XMLEvent event, String expectedData) {
|
||||
assertTrue(event.isCharacters());
|
||||
assertEquals(expectedData, event.asCharacters().getData());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.codec.support.jaxb;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
@javax.xml.bind.annotation.XmlRootElement
|
||||
public class XmlRootElement {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.codec.support.jaxb;
|
||||
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
@XmlRootElement(name = "name")
|
||||
public class XmlRootElementWithName {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.codec.support.jaxb;
|
||||
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
@XmlRootElement(name = "name", namespace = "namespace")
|
||||
public class XmlRootElementWithNameAndNamespace {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.codec.support.jaxb;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
@javax.xml.bind.annotation.XmlType
|
||||
public class XmlType {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.codec.support.jaxb;
|
||||
|
||||
import javax.xml.bind.annotation.XmlType;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
@XmlType(name = "name")
|
||||
public class XmlTypeWithName {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core.codec.support.jaxb;
|
||||
|
||||
import javax.xml.bind.annotation.XmlType;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
*/
|
||||
@XmlType(name = "name", namespace = "namespace")
|
||||
public class XmlTypeWithNameAndNamespace {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@javax.xml.bind.annotation.XmlSchema(namespace = "namespace")
|
||||
package org.springframework.core.codec.support.jaxb;
|
||||
@@ -24,6 +24,8 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
@@ -114,7 +116,8 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
URI url = new URI("http://localhost:" + port + "/raw");
|
||||
RequestEntity<Void> request = RequestEntity.get(url).build();
|
||||
RequestEntity<Void> request =
|
||||
RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build();
|
||||
Person person = restTemplate.exchange(request, Person.class).getBody();
|
||||
|
||||
assertEquals(new Person("Robert"), person);
|
||||
@@ -262,17 +265,32 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
|
||||
@Test
|
||||
public void publisherCreate() throws Exception {
|
||||
create("http://localhost:" + this.port + "/publisher-create");
|
||||
createJson("http://localhost:" + this.port + "/publisher-create");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publisherCreateXml() throws Exception {
|
||||
createXml("http://localhost:" + this.port + "/publisher-create");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fluxCreate() throws Exception {
|
||||
create("http://localhost:" + this.port + "/flux-create");
|
||||
createJson("http://localhost:" + this.port + "/flux-create");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fluxCreateXml() throws Exception {
|
||||
createXml("http://localhost:" + this.port + "/flux-create");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void observableCreate() throws Exception {
|
||||
create("http://localhost:" + this.port + "/observable-create");
|
||||
createJson("http://localhost:" + this.port + "/observable-create");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void observableCreateXml() throws Exception {
|
||||
createXml("http://localhost:" + this.port + "/observable-create");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -337,7 +355,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
assertEquals("MARIE", results.get(1).getName());
|
||||
}
|
||||
|
||||
private void create(String requestUrl) throws Exception {
|
||||
private void createJson(String requestUrl) throws Exception {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
URI url = new URI(requestUrl);
|
||||
RequestEntity<List<Person>> request = RequestEntity.post(url)
|
||||
@@ -349,6 +367,21 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
assertEquals(2, this.wac.getBean(TestRestController.class).persons.size());
|
||||
}
|
||||
|
||||
private void createXml(String requestUrl) throws Exception {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
URI url = new URI(requestUrl);
|
||||
People people = new People();
|
||||
people.getPerson().add(new Person("Robert"));
|
||||
people.getPerson().add(new Person("Marie"));
|
||||
RequestEntity<People> request =
|
||||
RequestEntity.post(url).contentType(MediaType.APPLICATION_XML)
|
||||
.body(people);
|
||||
ResponseEntity<Void> response = restTemplate.exchange(request, Void.class);
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertEquals(2, this.wac.getBean(TestRestController.class).persons.size());
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@SuppressWarnings("unused")
|
||||
@@ -609,6 +642,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
|
||||
}
|
||||
|
||||
@XmlRootElement
|
||||
private static class Person {
|
||||
|
||||
private String name;
|
||||
@@ -654,4 +688,16 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||
}
|
||||
}
|
||||
|
||||
@XmlRootElement
|
||||
private static class People {
|
||||
|
||||
private List<Person> persons = new ArrayList<>();
|
||||
|
||||
@XmlElement
|
||||
public List<Person> getPerson() {
|
||||
return this.persons;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user