提交 75399814 编写于 作者: A Arjen Poutsma

Improve Jaxb2Decoder

- Introcuces XmlEventDecoder which decodes from DataBuffer to
  javax.xml.stream.events.XMLEvent. It uses the Aalto async XML API if
  available, but falls back to a blocking default if not.

- Refacors Jaxb2Decoder to use said XmlEventDecoder, and split the
  stream of events into separate substreams by using the JAXB annotation
  value, one stream for each part of the tree that can be unmarshaled to
  the given type.

- Various improvements in the JAXB code.
上级 a8f27af5
......@@ -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());
}
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));
}
/**
* 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();
}
catch (UnmarshalException ex) {
return Flux.error(
new CodecException("Could not unmarshal to [" + outputClass + "]: " + ex.getMessage(), ex));
else if (outputClass.isAnnotationPresent(XmlType.class)) {
XmlType annotation = outputClass.getAnnotation(XmlType.class);
localPart = annotation.name();
namespaceUri = annotation.namespace();
}
catch (JAXBException ex) {
return Flux.error(new CodecException("Could not instantiate JAXBContext: " +
ex.getMessage(), ex));
else {
throw new IllegalArgumentException("Outputclass [" + outputClass + "] is " +
"neither annotated with @XmlRootElement nor @XmlType");
}
}
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);
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();
}
catch (SAXException ex) {
throw new CodecException("Error while processing the source", ex);
else {
namespaceUri = XMLConstants.NULL_NS_URI;
}
}
else {
return source;
}
return new QName(namespaceUri, localPart);
}
protected final Unmarshaller createUnmarshaller(Class<?> clazz) throws JAXBException {
try {
JAXBContext jaxbContext = getJaxbContext(clazz);
return jaxbContext.createUnmarshaller();
}
catch (JAXBException ex) {
throw new CodecException("Could not create Unmarshaller for class " +
"[" + clazz + "]: " + ex.getMessage(), ex);
}
/**
* 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();
}
});
}
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);
private Object unmarshal(List<XMLEvent> eventFlux, Class<?> outputClass) {
try {
Unmarshaller unmarshaller = this.jaxbContexts.createUnmarshaller(outputClass);
XMLEventReader eventReader = new ListBasedXMLEventReader(eventFlux);
if (outputClass.isAnnotationPresent(XmlRootElement.class)) {
return unmarshaller.unmarshal(eventReader);
}
catch (JAXBException ex) {
throw new CodecException("Could not instantiate JAXBContext for class " +
"[" + clazz + "]: " + ex.getMessage(), ex);
else {
JAXBElement<?> jaxbElement =
unmarshaller.unmarshal(eventReader, outputClass);
return jaxbElement.getValue();
}
}
return jaxbContext;
catch (JAXBException ex) {
throw new CodecException(ex.getMessage(), ex);
}
}
}
......@@ -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;
}
}
/*
* 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;
}
}
/*
* 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() {
}
}
/*
* 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 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 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);
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)
.assertValues(new Pojo("foofoo", "barbar"));
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());
}
});
}
}
/*
* 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());
}
}
\ No newline at end of file
/*
* 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 {
}
/*
* 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 {
}
/*
* 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 {
}
/*
* 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 {
}
/*
* 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 {
}
/*
* 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 {
}
/*
* 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;
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册