From 75399814cdc99c26b0efaa044c2b6e596cf57c66 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 21 Mar 2016 14:02:52 +0100 Subject: [PATCH] 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. --- spring-web-reactive/build.gradle | 2 + .../core/codec/support/Jaxb2Decoder.java | 211 ++++++++++----- .../core/codec/support/Jaxb2Encoder.java | 53 ++-- .../codec/support/JaxbContextContainer.java | 56 ++++ .../support/ListBasedXMLEventReader.java | 151 +++++++++++ .../core/codec/support/XmlEventDecoder.java | 145 ++++++++++ .../RequestMappingHandlerAdapter.java | 3 +- .../core/codec/support/Jaxb2DecoderTests.java | 252 +++++++++++++++++- .../core/codec/support/Jaxb2EncoderTests.java | 43 ++- .../codec/support/XmlEventDecoderTests.java | 77 ++++++ .../codec/support/jaxb/XmlRootElement.java | 25 ++ .../support/jaxb/XmlRootElementWithName.java | 27 ++ .../XmlRootElementWithNameAndNamespace.java | 27 ++ .../core/codec/support/jaxb/XmlType.java | 25 ++ .../codec/support/jaxb/XmlTypeWithName.java | 27 ++ .../jaxb/XmlTypeWithNameAndNamespace.java | 27 ++ .../core/codec/support/jaxb/package-info.java | 18 ++ .../RequestMappingIntegrationTests.java | 56 +++- 18 files changed, 1101 insertions(+), 124 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/JaxbContextContainer.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/ListBasedXMLEventReader.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElement.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithName.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithNameAndNamespace.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlType.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithName.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithNameAndNamespace.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 3d7e230b9a..abfdf7fb4f 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -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" diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java index ec1e260fe9..42287d10e7 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Decoder.java @@ -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 { - private final ConcurrentMap, 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 decode(Publisher 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 xmlEventFlux = + this.xmlEventDecoder.decode(inputStream, null, mimeType); + + QName typeName = toQName(outputClass); + Flux> 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}". + *
    + *
  1. The first list, dealing with the first branch of the tree + *
      + *
    1. {@link javax.xml.stream.events.StartElement} {@code child}
    2. + *
    3. {@link javax.xml.stream.events.Characters} {@code foo}
    4. + *
    5. {@link javax.xml.stream.events.EndElement} {@code child}
    6. + *
    + *
  2. The second list, dealing with the second branch of the tree + *
      + *
    1. {@link javax.xml.stream.events.StartElement} {@code child}
    2. + *
    3. {@link javax.xml.stream.events.Characters} {@code bar}
    4. + *
    5. {@link javax.xml.stream.events.EndElement} {@code child}
    6. + *
    + *
  3. + *
+ */ + Flux> split(Flux xmlEventFlux, QName desiredName) { + return xmlEventFlux + .flatMap(new Function>>() { + + private List events = null; + + private int elementDepth = 0; + + private int barrier = Integer.MAX_VALUE; + + @Override + public Publisher> 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(); + 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 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); + } } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java index 5c95b9a00f..d299722342 100644 --- a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/Jaxb2Encoder.java @@ -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 { - private final ConcurrentMap, 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 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 { }); } - 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; - } } diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JaxbContextContainer.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JaxbContextContainer.java new file mode 100644 index 0000000000..46d38ff5ae --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/JaxbContextContainer.java @@ -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, 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; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ListBasedXMLEventReader.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ListBasedXMLEventReader.java new file mode 100644 index 0000000000..95a22c9080 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/ListBasedXMLEventReader.java @@ -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 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() { + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java new file mode 100644 index 0000000000..1a2a77ca60 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/core/codec/support/XmlEventDecoder.java @@ -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: + *
{@code
+ * 
+ *     foo
+ *     bar
+ * }
+ * 
+ * this method with result in a flux with the following events: + *
    + *
  1. {@link javax.xml.stream.events.StartDocument}
  2. + *
  3. {@link javax.xml.stream.events.StartElement} {@code root}
  4. + *
  5. {@link javax.xml.stream.events.StartElement} {@code child}
  6. + *
  7. {@link javax.xml.stream.events.Characters} {@code foo}
  8. + *
  9. {@link javax.xml.stream.events.EndElement} {@code child}
  10. + *
  11. {@link javax.xml.stream.events.StartElement} {@code child}
  12. + *
  13. {@link javax.xml.stream.events.Characters} {@code bar}
  14. + *
  15. {@link javax.xml.stream.events.EndElement} {@code child}
  16. + *
  17. {@link javax.xml.stream.events.EndElement} {@code root}
  18. + *
+ * + * 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 { + + 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 decode(Publisher 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) () -> eventReader); + } + catch (XMLStreamException ex) { + return Flux.error(ex); + } + } + } + + /* + * Separate static class to isolate Aalto dependency. + */ + private static class AaltoDataBufferToXmlEvent + implements Function> { + + private static final AsyncXMLInputFactory inputFactory = + (AsyncXMLInputFactory) XmlEventDecoder.inputFactory; + + private final AsyncXMLStreamReader streamReader = + inputFactory.createAsyncForByteBuffer(); + + private final XMLEventAllocator eventAllocator = + EventAllocatorImpl.getDefaultInstance(); + + @Override + public Publisher apply(DataBuffer dataBuffer) { + try { + streamReader.getInputFeeder().feedInput(dataBuffer.asByteBuffer()); + List 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); + } + + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index 75d98f2a71..4f369a6577 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -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> decoders = Arrays.asList(new ByteBufferDecoder(), - new StringDecoder(), + new StringDecoder(), new Jaxb2Decoder(), new JacksonJsonDecoder(new JsonObjectDecoder())); this.argumentResolvers.add(new RequestParamArgumentResolver()); diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java index 3a5fe365ee..21aa3caf09 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2DecoderTests.java @@ -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 = "" + + "" + + "foofoo" + + "barbar" + + ""; + + private static final String POJO_CHILD = + "" + + "" + + "" + + "foo" + + "bar" + + "" + + "" + + "foofoo" + + "barbar" + + "" + + ""; + 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 xmlEvents = + xmlEventDecoder.decode(Flux.just(stringBuffer(POJO_ROOT)), null, null); + Flux> result = decoder.split(xmlEvents, new QName("pojo")); + + TestSubscriber> 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 xmlEvents = + xmlEventDecoder.decode(Flux.just(stringBuffer(POJO_CHILD)), null, null); + Flux> result = decoder.split(xmlEvents, new QName("pojo")); + + TestSubscriber> 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 source = Flux.just(stringBuffer(POJO_ROOT)); + Flux output = + decoder.decode(source, ResolvableType.forClass(Pojo.class), null); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + + testSubscriber.bindTo(output). + assertNoError(). + assertComplete(). + assertValues(new Pojo("foofoo", "barbar") + + ); + } + + @Test + public void decodeSingleXmlTypeElement() throws Exception { + Flux source = Flux.just(stringBuffer(POJO_ROOT)); + Flux output = + decoder.decode(source, ResolvableType.forClass(TypePojo.class), null); + + TestSubscriber testSubscriber = new TestSubscriber<>(); + + testSubscriber.bindTo(output). + assertNoError(). + assertComplete(). + assertValues(new TypePojo("foofoo", "barbar") + + ); } @Test - public void decode() { - Flux source = Flux.just(stringBuffer( - "barbarfoofoo")); - Flux output = decoder.decode(source, ResolvableType.forClass(Pojo.class), null); + public void decodeMultipleXmlRootElement() throws Exception { + Flux source = Flux.just(stringBuffer(POJO_CHILD)); + Flux output = + decoder.decode(source, ResolvableType.forClass(Pojo.class), null); + TestSubscriber 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 source = Flux.just(stringBuffer(POJO_CHILD)); + Flux output = + decoder.decode(source, ResolvableType.forClass(TypePojo.class), null); + + TestSubscriber 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; + } + + } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java index 0f7a802962..21c37595e6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/Jaxb2EncoderTests.java @@ -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 source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar")); - Flux 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 output = + encoder.encode(source, allocator, ResolvableType.forClass(Pojo.class), + MediaType.APPLICATION_XML).map(chunk -> DataBufferTestUtils + .dumpString(chunk, StandardCharsets.UTF_8)); TestSubscriber testSubscriber = new TestSubscriber<>(); - testSubscriber.bindTo(output) - .assertValues("barbarfoofoo", - "barbarbarfoofoofoo"); + testSubscriber.bindTo(output).assertValuesWith(s -> { + try { + assertXMLEqual("barbarfoofoo", s); + } + catch (SAXException | IOException e) { + fail(e.getMessage()); + } + }); } } diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java new file mode 100644 index 0000000000..6f41f48027 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/XmlEventDecoderTests.java @@ -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 = "" + + "" + + "foofoo" + + "barbar" + + ""; + + private XmlEventDecoder decoder = new XmlEventDecoder(); + + @Test + public void toXMLEvents() { + + Flux events = decoder.decode(Flux.just(stringBuffer(XML)), null, null); + + TestSubscriber 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 diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElement.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElement.java new file mode 100644 index 0000000000..92470a5876 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElement.java @@ -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 { + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithName.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithName.java new file mode 100644 index 0000000000..deb7929916 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithName.java @@ -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 { + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithNameAndNamespace.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithNameAndNamespace.java new file mode 100644 index 0000000000..e4330da2bc --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlRootElementWithNameAndNamespace.java @@ -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 { + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlType.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlType.java new file mode 100644 index 0000000000..49d158674c --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlType.java @@ -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 { + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithName.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithName.java new file mode 100644 index 0000000000..f62be41835 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithName.java @@ -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 { + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithNameAndNamespace.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithNameAndNamespace.java new file mode 100644 index 0000000000..4cf7cb6f6b --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/XmlTypeWithNameAndNamespace.java @@ -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 { + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java new file mode 100644 index 0000000000..0500a2aae5 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/core/codec/support/jaxb/package-info.java @@ -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; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 1249e9aafa..6a4ae62956 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -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 request = RequestEntity.get(url).build(); + RequestEntity 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> 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 request = + RequestEntity.post(url).contentType(MediaType.APPLICATION_XML) + .body(people); + ResponseEntity 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 persons = new ArrayList<>(); + + @XmlElement + public List getPerson() { + return this.persons; + } + } + + }