Add Codec support

This commit adds support for Publisher based codecs that allows to convert
byte stream to object stream and vice & versa.

Jackson, JAXB2 and String codec implementations are provided.
This commit is contained in:
Sebastien Deleuze
2015-09-08 14:10:18 +02:00
parent 5ddbbf4673
commit 881db0688b
28 changed files with 2008 additions and 21 deletions

View File

@@ -24,6 +24,7 @@ dependencies {
compile "org.reactivestreams:reactive-streams:1.0.0"
compile "io.projectreactor:reactor-stream:2.0.5.RELEASE"
compile "commons-logging:commons-logging:1.2"
compile "com.fasterxml.jackson.core:jackson-databind:2.6.1"
optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT"
optional "io.reactivex:rxjava-reactive-streams:1.0.1"

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2002-2015 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.reactive.codec;
import org.springframework.core.NestedRuntimeException;
/**
* Codec related exception, usually used as a wrapper for a cause exception.
*
* @author Sebastien Deleuze
*/
public class CodecException extends NestedRuntimeException {
public CodecException(String msg, Throwable cause) {
super(msg, cause);
}
public CodecException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2002-2015 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.reactive.codec.decoder;
import java.nio.ByteBuffer;
import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.encoder.MessageToByteEncoder;
/**
* Decode from a bytes stream to a message stream.
*
* @author Sebastien Deleuze
* @see MessageToByteEncoder
*/
public interface ByteToMessageDecoder<T> {
/**
* Indicate whether the given type and media type can be processed by this decoder.
* @param type the (potentially generic) type to ultimately decode to.
* Could be different from {@code T} type.
* @param mediaType the media type to decode from.
* Typically the value of a {@code Content-Type} header for HTTP request.
* @param hints Additional information about how to do decode, optional.
* @return {@code true} if decodable; {@code false} otherwise
*/
boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints);
/**
* Decode a bytes stream to a message stream.
* @param inputStream the input stream that represent the whole object to decode.
* @param type the (potentially generic) type to ultimately decode to.
* Could be different from {@code T} type.
* @param hints Additional information about how to do decode, optional.
* @return the decoded message stream
*/
Publisher<T> decode(Publisher<ByteBuffer> inputStream, ResolvableType type, MediaType mediaType, Object... hints);
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2002-2015 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.reactive.codec.decoder;
import java.io.IOException;
import java.nio.ByteBuffer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import org.reactivestreams.Publisher;
import reactor.rx.Streams;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.CodecException;
import org.springframework.reactive.codec.encoder.JacksonJsonEncoder;
import org.springframework.reactive.io.ByteBufferInputStream;
/**
* Decode from a bytes stream of JSON objects to a stream of {@code Object} (POJO).
*
* @author Sebastien Deleuze
* @see JacksonJsonEncoder
*/
public class JacksonJsonDecoder implements ByteToMessageDecoder<Object> {
private final ObjectMapper mapper;
public JacksonJsonDecoder() {
this(new ObjectMapper());
}
public JacksonJsonDecoder(ObjectMapper mapper) {
this.mapper = mapper;
}
@Override
public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON);
}
@Override
public Publisher<Object> decode(Publisher<ByteBuffer> inputStream, ResolvableType type, MediaType mediaType, Object... hints) {
ObjectReader reader = mapper.readerFor(type.getRawClass());
return Streams.wrap(inputStream)
.map(chunk -> {
try {
return reader.readValue(new ByteBufferInputStream(chunk));
}
catch (IOException e) {
throw new CodecException("Error while reading the data", e);
}
});
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2002-2015 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.reactive.codec.decoder;
import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.xml.bind.JAXBContext;
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 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.io.buffer.Buffer;
import reactor.rx.Stream;
import reactor.rx.Streams;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.CodecException;
import org.springframework.reactive.codec.encoder.Jaxb2Encoder;
import org.springframework.reactive.io.ByteArrayPublisherInputStream;
import org.springframework.util.Assert;
/**
* Decode from a bytes stream of XML elements to a stream of {@code Object} (POJO).
*
* @author Sebastien Deleuze
* @see Jaxb2Encoder
*/
public class Jaxb2Decoder implements ByteToMessageDecoder<Object> {
private final ConcurrentMap<Class<?>, JAXBContext> jaxbContexts = new ConcurrentHashMap<Class<?>, JAXBContext>(64);
@Override
public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.APPLICATION_XML) || mediaType.isCompatibleWith(MediaType.TEXT_XML);
}
@Override
public Publisher<Object> decode(Publisher<ByteBuffer> inputStream, ResolvableType type, MediaType mediaType, Object... hints) {
Stream<byte[]> stream = Streams.wrap(inputStream).map(chunk -> new Buffer(chunk).asBytes());
Class<?> outputClass = type.getRawClass();
try {
Source source = processSource(new StreamSource(new ByteArrayPublisherInputStream(stream)));
Unmarshaller unmarshaller = createUnmarshaller(outputClass);
if (outputClass.isAnnotationPresent(XmlRootElement.class)) {
return Streams.just(unmarshaller.unmarshal(source));
}
else {
JAXBElement<?> jaxbElement = unmarshaller.unmarshal(source, outputClass);
return Streams.just(jaxbElement.getValue());
}
}
catch (UnmarshalException ex) {
return Streams.fail(new CodecException("Could not unmarshal to [" + outputClass + "]: " + ex.getMessage(), ex));
}
catch (JAXBException ex) {
return Streams.fail(new CodecException("Could not instantiate JAXBContext: " + ex.getMessage(), ex));
}
}
protected Source processSource(Source source) {
if (source instanceof StreamSource) {
StreamSource streamSource = (StreamSource) source;
InputSource inputSource = new InputSource(streamSource.getInputStream());
try {
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
return new SAXSource(xmlReader, inputSource);
}
catch (SAXException ex) {
throw new CodecException("Error while processing the source", ex);
}
}
else {
return source;
}
}
protected final Unmarshaller createUnmarshaller(Class<?> clazz) throws JAXBException {
try {
JAXBContext jaxbContext = getJaxbContext(clazz);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
return unmarshaller;
}
catch (JAXBException ex) {
throw new CodecException(
"Could not create Unmarshaller 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;
}
}

View File

@@ -0,0 +1,263 @@
/*
* Copyright 2002-2015 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.reactive.codec.decoder;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import org.reactivestreams.Publisher;
import reactor.fn.Function;
import reactor.rx.Streams;
import rx.Observable;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.encoder.JsonObjectEncoder;
/**
* Decode an arbitrary split byte stream representing JSON objects to a bye stream
* where each chunk is a well-formed JSON object.
*
* If {@code Hints.STREAM_ARRAY_ELEMENTS} is enabled, each element of top level JSON array
* will be streamed as an individual JSON object.
*
* This class does not do any real parsing or validation. A sequence of bytes is considered a JSON object/array
* if it contains a matching number of opening and closing braces/brackets.
*
* Based on <a href=https://github.com/netty/netty/blob/master/codec/src/main/java/io/netty/handler/codec/json/JsonObjectDecoder.java">Netty {@code JsonObjectDecoder}</a>
*
* @author Sebastien Deleuze
* @see JsonObjectEncoder
*/
public class JsonObjectDecoder implements ByteToMessageDecoder<ByteBuffer> {
private static final int ST_CORRUPTED = -1;
private static final int ST_INIT = 0;
private static final int ST_DECODING_NORMAL = 1;
private static final int ST_DECODING_ARRAY_STREAM = 2;
private final int maxObjectLength;
private final boolean streamArrayElements;
public JsonObjectDecoder() {
// 1 MB
this(1024 * 1024);
}
public JsonObjectDecoder(int maxObjectLength) {
this(maxObjectLength, true);
}
public JsonObjectDecoder(boolean streamArrayElements) {
this(1024 * 1024, streamArrayElements);
}
/**
* @param maxObjectLength maximum number of bytes a JSON object/array may use (including braces and all).
* Objects exceeding this length are dropped and an {@link IllegalStateException}
* is thrown.
* @param streamArrayElements if set to true and the "top level" JSON object is an array, each of its entries
* is passed through the pipeline individually and immediately after it was fully
* received, allowing for arrays with "infinitely" many elements.
*
*/
public JsonObjectDecoder(int maxObjectLength, boolean streamArrayElements) {
if (maxObjectLength < 1) {
throw new IllegalArgumentException("maxObjectLength must be a positive int");
}
this.maxObjectLength = maxObjectLength;
this.streamArrayElements = streamArrayElements;
}
@Override
public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) &&
(Observable.class.isAssignableFrom(type.getRawClass()) || Publisher.class.isAssignableFrom(type.getRawClass()));
}
@Override
public Publisher<ByteBuffer> decode(Publisher<ByteBuffer> inputStream, ResolvableType type, MediaType mediaType, Object... hints) {
return Streams.wrap(inputStream).flatMap(new Function<ByteBuffer, Publisher<? extends ByteBuffer>>() {
int openBraces;
int idx;
int state;
boolean insideString;
ByteBuf in;
Integer wrtIdx;
@Override
public Publisher<? extends ByteBuffer> apply(ByteBuffer b) {
List<ByteBuffer> chunks = new ArrayList<>();
if (in == null) {
in = Unpooled.copiedBuffer(b);
wrtIdx = in.writerIndex();
}
else {
in = Unpooled.copiedBuffer(in, Unpooled.copiedBuffer(b));
wrtIdx = in.writerIndex();
}
if (state == ST_CORRUPTED) {
in.skipBytes(in.readableBytes());
return Streams.fail(new IllegalStateException("Corrupted stream"));
}
if (wrtIdx > maxObjectLength) {
// buffer size exceeded maxObjectLength; discarding the complete buffer.
in.skipBytes(in.readableBytes());
reset();
return Streams.fail(new IllegalStateException(
"object length exceeds " + maxObjectLength + ": " +
wrtIdx +
" bytes discarded"));
}
for (/* use current idx */; idx < wrtIdx; idx++) {
byte c = in.getByte(idx);
if (state == ST_DECODING_NORMAL) {
decodeByte(c, in, idx);
// All opening braces/brackets have been closed. That's enough to conclude
// that the JSON object/array is complete.
if (openBraces == 0) {
ByteBuf json = extractObject(in, in.readerIndex(),
idx + 1 - in.readerIndex());
if (json != null) {
chunks.add(json.nioBuffer());
}
// The JSON object/array was extracted => discard the bytes from
// the input buffer.
in.readerIndex(idx + 1);
// Reset the object state to get ready for the next JSON object/text
// coming along the byte stream.
reset();
}
}
else if (state == ST_DECODING_ARRAY_STREAM) {
decodeByte(c, in, idx);
if (!insideString && (openBraces == 1 && c == ',' ||
openBraces == 0 && c == ']')) {
// skip leading spaces. No range check is needed and the loop will terminate
// because the byte at position idx is not a whitespace.
for (int i = in.readerIndex(); Character.isWhitespace(in.getByte(i)); i++) {
in.skipBytes(1);
}
// skip trailing spaces.
int idxNoSpaces = idx - 1;
while (idxNoSpaces >= in.readerIndex() &&
Character.isWhitespace(in.getByte(idxNoSpaces))) {
idxNoSpaces--;
}
ByteBuf json = extractObject(in, in.readerIndex(),
idxNoSpaces + 1 - in.readerIndex());
if (json != null) {
chunks.add(json.nioBuffer());
}
in.readerIndex(idx + 1);
if (c == ']') {
reset();
}
}
// JSON object/array detected. Accumulate bytes until all braces/brackets are closed.
}
else if (c == '{' || c == '[') {
initDecoding(c, streamArrayElements);
if (state == ST_DECODING_ARRAY_STREAM) {
// Discard the array bracket
in.skipBytes(1);
}
// Discard leading spaces in front of a JSON object/array.
}
else if (Character.isWhitespace(c)) {
in.skipBytes(1);
}
else {
state = ST_CORRUPTED;
return Streams.fail(new IllegalStateException(
"invalid JSON received at byte position " + idx +
": " + ByteBufUtil.hexDump(in)));
}
}
if (in.readableBytes() == 0) {
idx = 0;
}
return Streams.from(chunks);
}
/**
* Override this method if you want to filter the json objects/arrays that get passed through the pipeline.
*/
@SuppressWarnings("UnusedParameters")
protected ByteBuf extractObject(ByteBuf buffer, int index, int length) {
return buffer.slice(index, length).retain();
}
private void decodeByte(byte c, ByteBuf in, int idx) {
if ((c == '{' || c == '[') && !insideString) {
openBraces++;
}
else if ((c == '}' || c == ']') && !insideString) {
openBraces--;
}
else if (c == '"') {
// start of a new JSON string. It's necessary to detect strings as they may
// also contain braces/brackets and that could lead to incorrect results.
if (!insideString) {
insideString = true;
// If the double quote wasn't escaped then this is the end of a string.
}
else if (in.getByte(idx - 1) != '\\') {
insideString = false;
}
}
}
private void initDecoding(byte openingBrace, boolean streamArrayElements) {
openBraces = 1;
if (openingBrace == '[' && streamArrayElements) {
state = ST_DECODING_ARRAY_STREAM;
}
else {
state = ST_DECODING_NORMAL;
}
}
private void reset() {
insideString = false;
state = ST_INIT;
openBraces = 0;
}
});
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2002-2015 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.reactive.codec.decoder;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.reactivestreams.Publisher;
import reactor.io.buffer.Buffer;
import reactor.rx.Streams;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.encoder.StringEncoder;
import org.springframework.reactive.codec.support.HintUtils;
/**
* Decode from a bytes stream to a String stream.
*
* @author Sebastien Deleuze
* @see StringEncoder
*/
public class StringDecoder implements ByteToMessageDecoder<String> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
@Override
public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN);
}
@Override
public Publisher<String> decode(Publisher<ByteBuffer> inputStream, ResolvableType type, MediaType mediaType, Object... hints) {
Charset charset = HintUtils.getHintByClass(Charset.class, hints, DEFAULT_CHARSET);
return Streams.wrap(inputStream).map(chunk -> new String(new Buffer(chunk).asBytes(), charset));
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2002-2015 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.reactive.codec.encoder;
import java.io.IOException;
import java.nio.ByteBuffer;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.reactivestreams.Publisher;
import reactor.io.buffer.Buffer;
import reactor.rx.Stream;
import reactor.rx.Streams;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.CodecException;
import org.springframework.reactive.codec.decoder.JacksonJsonDecoder;
import org.springframework.reactive.io.BufferOutputStream;
/**
* Encode from an {@code Object} stream to a byte stream of JSON objects.
*
* @author Sebastien Deleuze
* @see JacksonJsonDecoder
*/
public class JacksonJsonEncoder implements MessageToByteEncoder<Object> {
private final ObjectMapper mapper;
public JacksonJsonEncoder() {
this(new ObjectMapper());
}
public JacksonJsonEncoder(ObjectMapper mapper) {
this.mapper = mapper;
}
@Override
public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON);
}
@Override
public Publisher<ByteBuffer> encode(Publisher<? extends Object> messageStream, ResolvableType type, MediaType mediaType, Object... hints) {
Stream<ByteBuffer> stream = Streams.wrap(messageStream).map(value -> {
Buffer buffer = new Buffer();
BufferOutputStream outputStream = new BufferOutputStream(buffer);
try {
this.mapper.writeValue(outputStream, value);
}
catch (IOException e) {
throw new CodecException("Error while writing the data", e);
}
buffer.flip();
return buffer.byteBuffer();
});
return stream;
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2002-2015 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.reactive.codec.encoder;
import java.nio.ByteBuffer;
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 org.reactivestreams.Publisher;
import reactor.io.buffer.Buffer;
import reactor.rx.Streams;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.CodecException;
import org.springframework.reactive.codec.decoder.Jaxb2Decoder;
import org.springframework.reactive.io.BufferOutputStream;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Encode from an {@code Object} stream to a byte stream of XML elements.
*
* @author Sebastien Deleuze
* @see Jaxb2Decoder
*/
public class Jaxb2Encoder implements MessageToByteEncoder<Object> {
private final ConcurrentMap<Class<?>, JAXBContext> jaxbContexts = new ConcurrentHashMap<Class<?>, JAXBContext>(64);
@Override
public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.APPLICATION_XML) || mediaType.isCompatibleWith(MediaType.TEXT_XML);
}
@Override
public Publisher<ByteBuffer> encode(Publisher<? extends Object> messageStream, ResolvableType type, MediaType mediaType, Object... hints) {
return Streams.wrap(messageStream).map(value -> {
try {
Buffer buffer = new Buffer();
BufferOutputStream outputStream = new BufferOutputStream(buffer);
Class<?> clazz = ClassUtils.getUserClass(value);
Marshaller marshaller = createMarshaller(clazz);
marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
marshaller.marshal(value, outputStream);
buffer.flip();
return buffer.byteBuffer();
}
catch (MarshalException ex) {
throw new CodecException(
"Could not marshal [" + value + "]: " + ex.getMessage(), ex);
}
catch (JAXBException ex) {
throw new CodecException(
"Could not instantiate JAXBContext: " + ex.getMessage(), ex);
}
});
}
protected final Marshaller createMarshaller(Class<?> clazz) {
try {
JAXBContext jaxbContext = getJaxbContext(clazz);
Marshaller marshaller = jaxbContext.createMarshaller();
return marshaller;
}
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;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2002-2015 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.reactive.codec.encoder;
import java.nio.ByteBuffer;
import org.reactivestreams.Publisher;
import rx.Observable;
import rx.RxReactiveStreams;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.decoder.JsonObjectDecoder;
/**
* Encode a bye stream of individual JSON element to a byte stream representing a single
* JSON array when {@code Hints.ENCODE_AS_ARRAY} is enabled.
*
* @author Sebastien Deleuze
* @see JsonObjectDecoder
*/
public class JsonObjectEncoder implements MessageToByteEncoder<ByteBuffer> {
private final ByteBuffer START_ARRAY = ByteBuffer.wrap("[".getBytes());
private final ByteBuffer END_ARRAY = ByteBuffer.wrap("]".getBytes());
private final ByteBuffer COMMA = ByteBuffer.wrap(",".getBytes());
@Override
public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) &&
(Observable.class.isAssignableFrom(type.getRawClass()) || Publisher.class.isAssignableFrom(type.getRawClass()));
}
@Override
public Publisher<ByteBuffer> encode(Publisher<? extends ByteBuffer> messageStream, ResolvableType type, MediaType mediaType, Object... hints) {
// TODO We use RxJava Observable because there is no skipLast() operator in Reactor
// TODO Merge some chunks, there is no need to have chunks with only '[', ']' or ',' characters
return RxReactiveStreams.toPublisher(
Observable.concat(
Observable.just(START_ARRAY),
RxReactiveStreams.toObservable(messageStream).flatMap(b -> Observable.just(b, COMMA)).skipLast(1),
Observable.just(END_ARRAY)));
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2002-2015 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.reactive.codec.encoder;
import java.nio.ByteBuffer;
import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.decoder.ByteToMessageDecoder;
/**
* Encode from a message stream to a bytes stream.
*
* @author Sebastien Deleuze
* @see ByteToMessageDecoder
*/
public interface MessageToByteEncoder<T> {
/**
* Indicate whether the given type and media type can be processed by this encoder.
* @param type the (potentially generic) type to ultimately encode from.
* Could be different from {@code T} type.
* @param mediaType the media type to encode.
* Typically the value of an {@code Accept} header for HTTP request.
* @param hints Additional information about how to encode, optional.
* @return {@code true} if encodable; {@code false} otherwise
*/
boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints);
/**
* Encode a given message stream to the given output byte stream.
* @param messageStream the message stream to encode.
* @param type the (potentially generic) type to ultimately encode from.
* Could be different from {@code T} type.
* @param mediaType the media type to encode.
* Typically the value of an {@code Accept} header for HTTP request.
* @param hints Additional information about how to encode, optional.
* @return the encoded bytes stream
*/
Publisher<ByteBuffer> encode(Publisher<? extends T> messageStream, ResolvableType type, MediaType mediaType, Object... hints);
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2002-2015 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.reactive.codec.encoder;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.reactivestreams.Publisher;
import reactor.rx.Streams;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.decoder.StringDecoder;
import org.springframework.reactive.codec.support.HintUtils;
/**
* Encode from a String stream to a bytes stream.
*
* @author Sebastien Deleuze
* @see StringDecoder
*/
public class StringEncoder implements MessageToByteEncoder<String> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
@Override
public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN);
}
@Override
public Publisher<ByteBuffer> encode(Publisher<? extends String> elementStream, ResolvableType type, MediaType mediaType, Object... hints) {
final Charset charset = HintUtils.getHintByClass(Charset.class, hints, DEFAULT_CHARSET);
return Streams.wrap(elementStream).map(s -> ByteBuffer.wrap(s.getBytes(charset)));
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2002-2015 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.reactive.codec.support;
import org.springframework.reactive.codec.decoder.ByteToMessageDecoder;
import org.springframework.reactive.codec.encoder.MessageToByteEncoder;
/**
* Utility methods for dealing with codec hints.
*
* @author Sebastien Deleuze
* @see Hints
* @see MessageToByteEncoder
* @see ByteToMessageDecoder
*/
public abstract class HintUtils {
public static <T> T getHintByClass(Class<T> clazz, Object[] hints) {
return getHintByClass(clazz, hints, null);
}
public static <T> T getHintByClass(Class<T> clazz, Object[] hints, T defaultValue) {
for (Object hint : hints) {
if (hint.getClass().isAssignableFrom(clazz)) {
return (T)hint;
}
}
return defaultValue;
}
public static boolean containsHint(Object hint, Object[] hints) {
for (Object h : hints) {
if (h.equals(hint)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2002-2015 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.reactive.io;
import java.io.IOException;
import java.io.OutputStream;
import reactor.io.buffer.Buffer;
/**
* @author Sebastien Deleuze
*/
public class BufferOutputStream extends OutputStream {
private Buffer buffer;
public BufferOutputStream(Buffer buffer) {
this.buffer = buffer;
}
public void write(int b) throws IOException {
buffer.append(b);
}
public void write(byte[] bytes, int off, int len)
throws IOException {
buffer.append(bytes, off, len);
}
public Buffer getBuffer() {
return buffer;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2002-2015 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.reactive.io;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* Simple {@link InputStream} implementation that exposes currently
* available content of a {@link ByteBuffer}.
*
* From Jackson <a href="https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ByteBufferBackedInputStream.java">ByteBufferBackedInputStream</a>
*/
public class ByteBufferInputStream extends InputStream {
protected final ByteBuffer b;
public ByteBufferInputStream(ByteBuffer buf) { b = buf; }
@Override public int available() { return b.remaining(); }
@Override
public int read() throws IOException { return b.hasRemaining() ? (b.get() & 0xFF) : -1; }
@Override
public int read(byte[] bytes, int off, int len) throws IOException {
if (!b.hasRemaining()) return -1;
len = Math.min(len, b.remaining());
b.get(bytes, off, len);
return len;
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2002-2015 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.reactive.web.dispatch.method.annotation;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.reactivestreams.Publisher;
import reactor.rx.Stream;
import reactor.rx.Streams;
import rx.Observable;
import rx.RxReactiveStreams;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.decoder.ByteToMessageDecoder;
import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver;
import org.springframework.reactive.web.http.ServerHttpRequest;
import org.springframework.web.bind.annotation.RequestBody;
/**
* @author Sebastien Deleuze
*/
public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final List<ByteToMessageDecoder<?>> deserializers;
private final List<ByteToMessageDecoder<ByteBuffer>> preProcessors;
public RequestBodyArgumentResolver(List<ByteToMessageDecoder<?>> deserializers) {
this(deserializers, Collections.EMPTY_LIST);
}
public RequestBodyArgumentResolver(List<ByteToMessageDecoder<?>> deserializers, List<ByteToMessageDecoder<ByteBuffer>> preProcessors) {
this.deserializers = deserializers;
this.preProcessors = preProcessors;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ServerHttpRequest request) {
MediaType mediaType = resolveMediaType(request);
ResolvableType type = ResolvableType.forMethodParameter(parameter);
List<Object> hints = new ArrayList<>();
hints.add(UTF_8);
// TODO: Refactor type conversion
ResolvableType readType = type;
if (Observable.class.isAssignableFrom(type.getRawClass()) || Publisher.class.isAssignableFrom(type.getRawClass())) {
readType = type.getGeneric(0);
}
ByteToMessageDecoder<?> deserializer = resolveDeserializers(request, type, mediaType, hints.toArray());
if (deserializer != null) {
Publisher<ByteBuffer> inputStream = Streams.wrap(request.getBody()).map(bytes -> ByteBuffer.wrap(bytes));
List<ByteToMessageDecoder<ByteBuffer>> preProcessors = resolvePreProcessors(request, type, mediaType, hints.toArray());
for (ByteToMessageDecoder<ByteBuffer> preProcessor : preProcessors) {
inputStream = preProcessor.decode(inputStream, type, mediaType, hints.toArray());
}
Publisher<?> elementStream = deserializer.decode(inputStream, readType, mediaType, UTF_8);
// TODO: Refactor type conversion
if (Stream.class.isAssignableFrom(type.getRawClass())) {
return Streams.wrap(elementStream);
}
else if (Observable.class.isAssignableFrom(type.getRawClass())) {
return RxReactiveStreams.toObservable(elementStream);
}
else if (Publisher.class.isAssignableFrom(type.getRawClass())) {
return elementStream;
}
else {
try {
return Streams.wrap(elementStream).next().await();
} catch(InterruptedException ex) {
throw new IllegalStateException("Timeout before getter the value");
}
}
}
throw new IllegalStateException("Argument type not supported: " + type);
}
private MediaType resolveMediaType(ServerHttpRequest request) {
String acceptHeader = request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
List<MediaType> mediaTypes = MediaType.parseMediaTypes(acceptHeader);
MediaType.sortBySpecificityAndQuality(mediaTypes);
return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN);
}
private ByteToMessageDecoder<?> resolveDeserializers(ServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) {
for (ByteToMessageDecoder<?> deserializer : this.deserializers) {
if (deserializer.canDecode(type, mediaType, hints)) {
return deserializer;
}
}
return null;
}
private List<ByteToMessageDecoder<ByteBuffer>> resolvePreProcessors(ServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) {
List<ByteToMessageDecoder<ByteBuffer>> preProcessors = new ArrayList<>();
for (ByteToMessageDecoder<ByteBuffer> preProcessor : this.preProcessors) {
if (preProcessor.canDecode(type, mediaType, hints)) {
preProcessors.add(preProcessor);
}
}
return preProcessors;
}
}

View File

@@ -16,12 +16,16 @@
package org.springframework.reactive.web.dispatch.method.annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.reactivestreams.Publisher;
import reactor.rx.Streams;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.reactive.codec.decoder.JacksonJsonDecoder;
import org.springframework.reactive.codec.decoder.JsonObjectDecoder;
import org.springframework.reactive.codec.decoder.StringDecoder;
import org.springframework.reactive.web.dispatch.HandlerAdapter;
import org.springframework.reactive.web.dispatch.HandlerResult;
import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver;
@@ -50,6 +54,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin
if (this.argumentResolvers == null) {
this.argumentResolvers = new ArrayList<>();
this.argumentResolvers.add(new RequestParamArgumentResolver());
this.argumentResolvers.add(new RequestBodyArgumentResolver(Arrays.asList(new StringDecoder(), new JacksonJsonDecoder()), Arrays.asList(new JsonObjectDecoder(true))));
}
}

View File

@@ -16,16 +16,25 @@
package org.springframework.reactive.web.dispatch.method.annotation;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.reactivestreams.Publisher;
import reactor.io.buffer.Buffer;
import reactor.rx.Streams;
import rx.Observable;
import rx.RxReactiveStreams;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.encoder.MessageToByteEncoder;
import org.springframework.reactive.web.dispatch.HandlerResult;
import org.springframework.reactive.web.dispatch.HandlerResultHandler;
import org.springframework.reactive.web.http.ServerHttpRequest;
@@ -35,8 +44,7 @@ import org.springframework.web.method.HandlerMethod;
/**
* For now a simple {@code String} or {@code Publisher<String>} to
* "text/plain;charset=UTF-8" conversion.
* First version using {@link MessageToByteEncoder}s
*
* @author Rossen Stoyanchev
*/
@@ -45,9 +53,21 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final List<MessageToByteEncoder<?>> serializers;
private final List<MessageToByteEncoder<ByteBuffer>> postProcessors;
private int order = Ordered.LOWEST_PRECEDENCE;
public ResponseBodyResultHandler(List<MessageToByteEncoder<?>> serializers) {
this(serializers, Collections.EMPTY_LIST);
}
public ResponseBodyResultHandler(List<MessageToByteEncoder<?>> serializers, List<MessageToByteEncoder<ByteBuffer>> postProcessors) {
this.serializers = serializers;
this.postProcessors = postProcessors;
}
public void setOrder(int order) {
this.order = order;
}
@@ -80,20 +100,61 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
return Streams.empty();
}
if (value instanceof String) {
response.getHeaders().setContentType(new MediaType("text", "plain", UTF_8));
return response.writeWith(Streams.just(((String) value).getBytes(UTF_8)));
MediaType mediaType = resolveMediaType(request);
ResolvableType type = ResolvableType.forMethodParameter(returnType);
List<Object> hints = new ArrayList<>();
hints.add(UTF_8);
MessageToByteEncoder<Object> serializer = (MessageToByteEncoder<Object>)resolveSerializer(request, type, mediaType, hints.toArray());
if (serializer != null) {
Publisher<Object> elementStream;
// TODO: Refactor type conversion
if (Observable.class.isAssignableFrom(type.getRawClass())) {
elementStream = RxReactiveStreams.toPublisher((Observable) value);
}
else if (Publisher.class.isAssignableFrom(type.getRawClass())) {
elementStream = (Publisher)value;
}
else {
elementStream = Streams.just(value);
}
Publisher<ByteBuffer> outputStream = serializer.encode(elementStream, type, mediaType, hints.toArray());
List<MessageToByteEncoder<ByteBuffer>> postProcessors = resolvePostProcessors(request, type, mediaType, hints.toArray());
for (MessageToByteEncoder<ByteBuffer> postProcessor : postProcessors) {
outputStream = postProcessor.encode(outputStream, type, mediaType, hints.toArray());
}
response.getHeaders().setContentType(mediaType);
return response.writeWith(Streams.wrap(outputStream).map(buffer -> new Buffer(buffer).asBytes()));
}
else if (value instanceof Publisher) {
Class<?> type = ResolvableType.forMethodParameter(returnType).resolveGeneric(0);
if (String.class.equals(type)) {
@SuppressWarnings("unchecked")
Publisher<String> content = (Publisher<String>) value;
return response.writeWith(Streams.wrap(content).map(value1 -> value1.getBytes(UTF_8)));
return Streams.fail(new IllegalStateException(
"Return value type not supported: " + returnType));
}
private MediaType resolveMediaType(ServerHttpRequest request) {
String acceptHeader = request.getHeaders().getFirst(HttpHeaders.ACCEPT);
List<MediaType> mediaTypes = MediaType.parseMediaTypes(acceptHeader);
MediaType.sortBySpecificityAndQuality(mediaTypes);
return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN);
}
private MessageToByteEncoder<?> resolveSerializer(ServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) {
for (MessageToByteEncoder<?> codec : this.serializers) {
if (codec.canEncode(type, mediaType, hints)) {
return codec;
}
}
return null;
}
return Streams.fail(new IllegalStateException("Return value type not supported: " + returnType));
private List<MessageToByteEncoder<ByteBuffer>> resolvePostProcessors(ServerHttpRequest request, ResolvableType type, MediaType mediaType, Object[] hints) {
List<MessageToByteEncoder<ByteBuffer>> postProcessors = new ArrayList<>();
for (MessageToByteEncoder<ByteBuffer> postProcessor : this.postProcessors) {
if (postProcessor.canEncode(type, mediaType, hints)) {
postProcessors.add(postProcessor);
}
}
return postProcessors;
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2002-2015 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.reactive.codec.decoder;
import java.nio.ByteBuffer;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import reactor.io.buffer.Buffer;
import reactor.rx.Stream;
import reactor.rx.Streams;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.converter.Pojo;
/**
* @author Sebastien Deleuze
*/
public class JacksonJsonDecoderTests {
private final JacksonJsonDecoder decoder = new JacksonJsonDecoder();
@Test
public void canDecode() {
assertTrue(decoder.canDecode(null, MediaType.APPLICATION_JSON));
assertFalse(decoder.canDecode(null, MediaType.APPLICATION_XML));
}
@Test
public void decode() throws InterruptedException {
Stream<ByteBuffer> source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer());
List<Object> results = Streams.wrap(decoder.decode(source, ResolvableType.forClass(Pojo.class), null))
.toList().await();
assertEquals(1, results.size());
assertEquals("foofoo", ((Pojo) results.get(0)).getFoo());
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2002-2015 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.reactive.codec.decoder;
import java.nio.ByteBuffer;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import reactor.io.buffer.Buffer;
import reactor.rx.Stream;
import reactor.rx.Streams;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.converter.Pojo;
/**
* @author Sebastien Deleuze
*/
public class Jaxb2DecoderTests {
private final Jaxb2Decoder decoder = new Jaxb2Decoder();
@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));
}
@Test
public void decode() throws InterruptedException {
Stream<ByteBuffer> source = Streams.just(Buffer.wrap("<?xml version=\"1.0\" encoding=\"UTF-8\"?><pojo><bar>barbar</bar><foo>foofoo</foo></pojo>").byteBuffer());
List<Object> results = Streams.wrap(decoder.decode(source, ResolvableType.forClass(Pojo.class), null))
.toList().await();
assertEquals(1, results.size());
assertEquals("foofoo", ((Pojo) results.get(0)).getFoo());
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2002-2015 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.reactive.codec.decoder;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import reactor.io.buffer.Buffer;
import reactor.rx.Stream;
import reactor.rx.Streams;
/**
* @author Sebastien Deleuze
*/
public class JsonObjectDecoderTests {
@Test
public void decodeSingleChunkToArray() throws InterruptedException {
JsonObjectDecoder decoder = new JsonObjectDecoder(true);
Stream<ByteBuffer> source = Streams.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer());
List<String> results = Streams.wrap(decoder.decode(source, null, null)).map(chunk -> {
byte[] b = new byte[chunk.remaining()];
chunk.get(b);
return new String(b, StandardCharsets.UTF_8);
}).toList().await();
assertEquals(2, results.size());
assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0));
assertEquals("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}", results.get(1));
}
@Test
public void decodeMultipleChunksToArray() throws InterruptedException {
JsonObjectDecoder decoder = new JsonObjectDecoder(true);
Stream<ByteBuffer> source = Streams.just(Buffer.wrap("[{\"foo\": \"foofoo\", \"bar\"").byteBuffer(), Buffer.wrap(": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]").byteBuffer());
List<String> results = Streams.wrap(decoder.decode(source, null, null)).map(chunk -> {
byte[] b = new byte[chunk.remaining()];
chunk.get(b);
return new String(b, StandardCharsets.UTF_8);
}).toList().await();
assertEquals(2, results.size());
assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0));
assertEquals("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}", results.get(1));
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2002-2015 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.reactive.codec.decoder;
import java.nio.ByteBuffer;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import reactor.io.buffer.Buffer;
import reactor.rx.Stream;
import reactor.rx.Streams;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.converter.Pojo;
/**
* @author Sebastien Deleuze
*/
public class StringDecoderTests {
private final StringDecoder decoder = new StringDecoder();
@Test
public void canDecode() {
assertTrue(decoder.canDecode(null, MediaType.TEXT_PLAIN));
assertFalse(decoder.canDecode(null, MediaType.APPLICATION_JSON));
}
@Test
public void decode() throws InterruptedException {
Stream<ByteBuffer> source = Streams.just(Buffer.wrap("foo").byteBuffer(), Buffer.wrap("bar").byteBuffer());
List<String> results = Streams.wrap(decoder.decode(source, ResolvableType.forClass(Pojo.class), null))
.toList().await();
assertEquals(2, results.size());
assertEquals("foo", results.get(0));
assertEquals("bar", results.get(1));
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2002-2015 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.reactive.codec.encoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import reactor.rx.Stream;
import reactor.rx.Streams;
import org.springframework.http.MediaType;
import org.springframework.reactive.converter.Pojo;
/**
* @author Sebastien Deleuze
*/
public class JacksonJsonEncoderTests {
private final JacksonJsonEncoder encoder = new JacksonJsonEncoder();
@Test
public void canWrite() {
assertTrue(encoder.canEncode(null, MediaType.APPLICATION_JSON));
assertFalse(encoder.canEncode(null, MediaType.APPLICATION_XML));
}
@Test
public void write() throws InterruptedException {
Stream<Pojo> source = Streams.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar"));
List<String> results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> {
byte[] b = new byte[chunk.remaining()];
chunk.get(b);
return new String(b, StandardCharsets.UTF_8);
}).toList().await();
assertEquals(2, results.size());
assertEquals("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}", results.get(0));
assertEquals("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}", results.get(1));
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2002-2015 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.reactive.codec.encoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import reactor.rx.Stream;
import reactor.rx.Streams;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.decoder.Jaxb2Decoder;
import org.springframework.reactive.converter.Pojo;
/**
* @author Sebastien Deleuze
*/
public class Jaxb2EncoderTests {
private final Jaxb2Encoder encoder = new Jaxb2Encoder();
@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));
}
@Test
public void encode() throws InterruptedException {
Stream<Pojo> source = Streams.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar"));
List<String> results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> {
byte[] b = new byte[chunk.remaining()];
chunk.get(b);
return new String(b, StandardCharsets.UTF_8);
}).toList().await();
assertEquals(2, results.size());
assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><pojo><bar>barbar</bar><foo>foofoo</foo></pojo>", results.get(0));
assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><pojo><bar>barbarbar</bar><foo>foofoofoo</foo></pojo>", results.get(1));
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2002-2015 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.reactive.codec.encoder;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import reactor.io.buffer.Buffer;
import reactor.rx.Stream;
import reactor.rx.Streams;
/**
* @author Sebastien Deleuze
*/
public class JsonObjectEncoderTests {
@Test
public void encodeToArray() throws InterruptedException {
JsonObjectEncoder encoder = new JsonObjectEncoder();
Stream<ByteBuffer> source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").byteBuffer(), Buffer.wrap("{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}").byteBuffer());
List<String> results = Streams.wrap(encoder.encode(source, null, null)).map(chunk -> {
byte[] b = new byte[chunk.remaining()];
chunk.get(b);
return new String(b, StandardCharsets.UTF_8);
}).toList().await();
String result = String.join("", results);
assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]", result);
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2002-2015 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.reactive.codec.encoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import reactor.rx.Streams;
import org.springframework.http.MediaType;
/**
* @author Sebastien Deleuze
*/
public class StringEncoderTests {
private final StringEncoder encoder = new StringEncoder();
@Test
public void canWrite() {
assertTrue(encoder.canEncode(null, MediaType.TEXT_PLAIN));
assertFalse(encoder.canEncode(null, MediaType.APPLICATION_JSON));
}
@Test
public void write() throws InterruptedException {
List<String> results = Streams.wrap(encoder.encode(Streams.just("foo"), null, null))
.map(chunk -> {
byte[] b = new byte[chunk.remaining()];
chunk.get(b);
return new String(b, StandardCharsets.UTF_8);
}).toList().await();
assertEquals(1, results.size());
assertEquals("foo", results.get(0));
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2002-2015 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.reactive.converter;
import javax.xml.bind.annotation.XmlRootElement;
/**
* @author Sebastien Deleuze
*/
@XmlRootElement
public class Pojo {
private String foo;
private String bar;
public Pojo() {
}
public Pojo(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;
}
}

View File

@@ -17,41 +17,48 @@ package org.springframework.reactive.web.dispatch.method.annotation;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.rx.Stream;
import reactor.rx.Streams;
import rx.Observable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.reactive.codec.encoder.JacksonJsonEncoder;
import org.springframework.reactive.codec.encoder.JsonObjectEncoder;
import org.springframework.reactive.codec.encoder.StringEncoder;
import org.springframework.reactive.web.dispatch.DispatcherHandler;
import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests;
import org.springframework.reactive.web.http.HttpHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.support.StaticWebApplicationContext;
import static org.junit.Assert.assertArrayEquals;
/**
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
*/
public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests {
private static final Charset UTF_8 = Charset.forName("UTF-8");
@Override
protected HttpHandler createHttpHandler() {
StaticWebApplicationContext wac = new StaticWebApplicationContext();
wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class);
wac.registerSingleton("handlerAdapter", RequestMappingHandlerAdapter.class);
wac.registerSingleton("responseBodyResultHandler", ResponseBodyResultHandler.class);
wac.getDefaultListableBeanFactory().registerSingleton("responseBodyResultHandler",
new ResponseBodyResultHandler(Arrays.asList(new StringEncoder(), new JacksonJsonEncoder()), Arrays.asList(new JsonObjectEncoder())));
wac.registerSingleton("controller", TestController.class);
wac.refresh();
@@ -67,9 +74,97 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
URI url = new URI("http://localhost:" + port + "/param?name=George");
RequestEntity<Void> request = RequestEntity.get(url).build();
ResponseEntity<byte[]> response = restTemplate.exchange(request, byte[].class);
ResponseEntity<String> response = restTemplate.exchange(request, String.class);
assertArrayEquals("Hello George!".getBytes(UTF_8), response.getBody());
assertEquals("Hello George!", response.getBody());
}
@Test
public void serializeAsPojo() throws Exception {
RestTemplate restTemplate = new RestTemplate();
URI url = new URI("http://localhost:" + port + "/person");
RequestEntity<Void> request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build();
ResponseEntity<Person> response = restTemplate.exchange(request, Person.class);
assertEquals(new Person("Robert"), response.getBody());
}
@Test
public void serializeAsList() throws Exception {
RestTemplate restTemplate = new RestTemplate();
URI url = new URI("http://localhost:" + port + "/list");
RequestEntity<Void> request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build();
List<Person> results = restTemplate.exchange(request, new ParameterizedTypeReference<List<Person>>(){}).getBody();
assertEquals(2, results.size());
assertEquals(new Person("Robert"), results.get(0));
assertEquals(new Person("Marie"), results.get(1));
}
@Test
public void serializeAsPublisher() throws Exception {
RestTemplate restTemplate = new RestTemplate();
URI url = new URI("http://localhost:" + port + "/publisher");
RequestEntity<Void> request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build();
List<Person> results = restTemplate.exchange(request, new ParameterizedTypeReference<List<Person>>(){}).getBody();
assertEquals(2, results.size());
assertEquals(new Person("Robert"), results.get(0));
assertEquals(new Person("Marie"), results.get(1));
}
@Test
public void serializeAsObservable() throws Exception {
RestTemplate restTemplate = new RestTemplate();
URI url = new URI("http://localhost:" + port + "/observable");
RequestEntity<Void> request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build();
List<Person> results = restTemplate.exchange(request, new ParameterizedTypeReference<List<Person>>() {
}).getBody();
assertEquals(2, results.size());
assertEquals(new Person("Robert"), results.get(0));
assertEquals(new Person("Marie"), results.get(1));
}
@Test
public void serializeAsReactorStream() throws Exception {
RestTemplate restTemplate = new RestTemplate();
URI url = new URI("http://localhost:" + port + "/stream");
RequestEntity<Void> request = RequestEntity.get(url).accept(MediaType.APPLICATION_JSON).build();
List<Person> results = restTemplate.exchange(request, new ParameterizedTypeReference<List<Person>>() {
}).getBody();
assertEquals(2, results.size());
assertEquals(new Person("Robert"), results.get(0));
assertEquals(new Person("Marie"), results.get(1));
}
@Test
public void echo() throws Exception {
RestTemplate restTemplate = new RestTemplate();
URI url = new URI("http://localhost:" + port + "/echo");
List<Person> persons = Arrays.asList(new Person("Robert"), new Person("Marie"));
RequestEntity<List<Person>> request = RequestEntity
.post(url)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(persons);
List<Person> results = restTemplate.exchange(request, new ParameterizedTypeReference<List<Person>>(){}).getBody();
assertEquals(2, results.size());
assertEquals("Robert", results.get(0).getName());
assertEquals("Marie", results.get(1).getName());
}
@@ -82,6 +177,73 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
public Publisher<String> handleWithParam(@RequestParam String name) {
return Streams.just("Hello ", name, "!");
}
@RequestMapping("/person")
@ResponseBody
public Person personResponseBody() {
return new Person("Robert");
}
@RequestMapping("/list")
@ResponseBody
public List<Person> listResponseBody() {
return Arrays.asList(new Person("Robert"), new Person("Marie"));
}
@RequestMapping("/publisher")
@ResponseBody
public Publisher<Person> publisherResponseBody() {
return Streams.just(new Person("Robert"), new Person("Marie"));
}
@RequestMapping("/observable")
@ResponseBody
public Observable<Person> observableResponseBody() {
return Observable.just(new Person("Robert"), new Person("Marie"));
}
@RequestMapping("/stream")
@ResponseBody
public Stream<Person> reactorStreamResponseBody() {
return Streams.just(new Person("Robert"), new Person("Marie"));
}
@RequestMapping("/echo")
@ResponseBody
public Publisher<Person> echo(@RequestBody Publisher<Person> persons) {
return persons;
}
}
private static class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
return name.equals(((Person)o).name);
}
@Override
public int hashCode() {
return name.hashCode();
}
}
}