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; + } + } + + }