Refactor codecs

This commit introduces the following changes:
 - MessageToByteEncoder/Decoder renamed to Encoder/Decoder
 - JsonObjectEncoder/Decoder are now used directly in
   JacksonJsonEncoder/Decoder
 - Codec uses MimeType instead of MediaType since they
   are not specific to HTTP
 - Default MimeType are now managed thanks to
   Encoder/Decoder#getSupportedMimeTypes()
 - AbstractEncoder/Decoder takes care of generic MimeType related behavior
This commit is contained in:
Sebastien Deleuze
2015-11-04 17:48:53 +01:00
committed by Rossen Stoyanchev
parent 141d75791d
commit bdc5b38cb1
21 changed files with 351 additions and 255 deletions

View File

@@ -0,0 +1,52 @@
/*
* 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.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.core.ResolvableType;
import org.springframework.util.MimeType;
/**
* @author Sebastien Deleuze
*/
public abstract class AbstractDecoder<T> implements Decoder<T> {
private List<MimeType> supportedMimeTypes = Collections.emptyList();
public AbstractDecoder(MimeType... supportedMimeTypes) {
this.supportedMimeTypes = Arrays.asList(supportedMimeTypes);
}
@Override
public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) {
for (MimeType supportedMimeType : this.supportedMimeTypes) {
if (supportedMimeType.isCompatibleWith(mimeType)) {
return true;
}
}
return false;
}
@Override
public List<MimeType> getSupportedMimeTypes() {
return this.supportedMimeTypes;
}
}

View File

@@ -21,22 +21,28 @@ import java.nio.ByteBuffer;
import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* @author Sebastien Deleuze
*/
public class ByteBufferDecoder implements ByteToMessageDecoder<ByteBuffer> {
public class ByteBufferDecoder extends AbstractDecoder<ByteBuffer> {
public ByteBufferDecoder() {
super(MimeTypeUtils.ALL);
}
@Override
public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) {
return ByteBuffer.class.isAssignableFrom(type.getRawClass());
public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) {
return super.canDecode(type, mimeType, hints)
&& ByteBuffer.class.isAssignableFrom(type.getRawClass());
}
@Override
public Publisher<ByteBuffer> decode(Publisher<ByteBuffer> inputStream, ResolvableType type,
MediaType mediaType, Object... hints) {
MimeType mimeType, Object... hints) {
return inputStream;
}
}
}

View File

@@ -17,39 +17,45 @@
package org.springframework.reactive.codec.decoder;
import java.nio.ByteBuffer;
import java.util.List;
import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.encoder.MessageToByteEncoder;
import org.springframework.reactive.codec.encoder.Encoder;
import org.springframework.util.MimeType;
/**
* Decode from a bytes stream to a message stream.
* Decode from a bytes stream to a stream of {@code T}.
*
* @author Sebastien Deleuze
* @see MessageToByteEncoder
* @see Encoder
*/
public interface ByteToMessageDecoder<T> {
public interface Decoder<T> {
/**
* Indicate whether the given type and media type can be processed by this decoder.
* @param type the stream element type to ultimately decode to.
* @param mediaType the media type to decode from.
* Typically the value of a {@code Content-Type} header for HTTP request.
* Indicate whether the given type and mime type can be processed by this decoder.
* @param type the stream element type to process.
* @param mimeType the mime type to process.
* @param hints Additional information about how to do decode, optional.
* @return {@code true} if decodable; {@code false} otherwise
* @return {@code true} if can process; {@code false} otherwise
*/
boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints);
boolean canDecode(ResolvableType type, MimeType mimeType, 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 stream element type to ultimately decode to.
* Decode an input {@link ByteBuffer} stream to an output stream of {@code T}.
* @param inputStream the input stream to process.
* @param type the stream element type to process.
* @param mimeType the mime type to process.
* @param hints Additional information about how to do decode, optional.
* @return the decoded message stream
* @return the output stream
*/
Publisher<T> decode(Publisher<ByteBuffer> inputStream, ResolvableType type,
MediaType mediaType, Object... hints);
Publisher<T> decode(Publisher<ByteBuffer> inputStream, ResolvableType type, MimeType mimeType, Object... hints);
/**
* Return the list of {@link MimeType} objects supported by this codec.
* @return the list of supported mime types
*/
List<MimeType> getSupportedMimeTypes();
}

View File

@@ -18,6 +18,7 @@ package org.springframework.reactive.codec.decoder;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
@@ -25,10 +26,10 @@ import org.reactivestreams.Publisher;
import reactor.Publishers;
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;
import org.springframework.util.MimeType;
/**
* Decode from a bytes stream of JSON objects to a stream of {@code Object} (POJO).
@@ -36,31 +37,36 @@ import org.springframework.reactive.io.ByteBufferInputStream;
* @author Sebastien Deleuze
* @see JacksonJsonEncoder
*/
public class JacksonJsonDecoder implements ByteToMessageDecoder<Object> {
public class JacksonJsonDecoder extends AbstractDecoder<Object> {
private final ObjectMapper mapper;
private Decoder<ByteBuffer> preProcessor;
public JacksonJsonDecoder() {
this(new ObjectMapper());
this(new ObjectMapper(), null);
}
public JacksonJsonDecoder(ObjectMapper mapper) {
public JacksonJsonDecoder(Decoder<ByteBuffer> preProcessor) {
this(new ObjectMapper(), preProcessor);
}
public JacksonJsonDecoder(ObjectMapper mapper, Decoder<ByteBuffer> preProcessor) {
super(new MimeType("application", "json", StandardCharsets.UTF_8),
new MimeType("application", "*+json", StandardCharsets.UTF_8));
this.mapper = mapper;
}
@Override
public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON);
this.preProcessor = preProcessor;
}
@Override
public Publisher<Object> decode(Publisher<ByteBuffer> inputStream, ResolvableType type,
MediaType mediaType, Object... hints) {
MimeType mimeType, Object... hints) {
ObjectReader reader = this.mapper.readerFor(type.getRawClass());
return Publishers.map(inputStream, chunk -> {
Publisher<ByteBuffer> decodedStream = this.preProcessor == null ? inputStream :
this.preProcessor.decode(inputStream, type, mimeType, hints);
return Publishers.map(decodedStream, chunk -> {
try {
return reader.readValue(new ByteBufferInputStream(chunk));
}

View File

@@ -42,6 +42,7 @@ import org.springframework.reactive.codec.CodecException;
import org.springframework.reactive.codec.encoder.Jaxb2Encoder;
import org.springframework.reactive.io.ByteBufferPublisherInputStream;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* Decode from a bytes stream of XML elements to a stream of {@code Object} (POJO).
@@ -49,20 +50,17 @@ import org.springframework.util.Assert;
* @author Sebastien Deleuze
* @see Jaxb2Encoder
*/
public class Jaxb2Decoder implements ByteToMessageDecoder<Object> {
public class Jaxb2Decoder extends AbstractDecoder<Object> {
private final ConcurrentMap<Class<?>, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64);
@Override
public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) {
return (mediaType.isCompatibleWith(MediaType.APPLICATION_XML) ||
mediaType.isCompatibleWith(MediaType.TEXT_XML));
public Jaxb2Decoder() {
super(MediaType.APPLICATION_XML, MediaType.TEXT_XML);
}
@Override
public Publisher<Object> decode(Publisher<ByteBuffer> inputStream, ResolvableType type,
MediaType mediaType, Object... hints) {
MimeType mimeType, Object... hints) {
Class<?> outputClass = type.getRawClass();
try {
@@ -129,4 +127,5 @@ public class Jaxb2Decoder implements ByteToMessageDecoder<Object> {
}
return jaxbContext;
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.reactive.codec.decoder;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -28,8 +29,8 @@ import reactor.Publishers;
import reactor.fn.Function;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.encoder.JsonObjectEncoder;
import org.springframework.util.MimeType;
/**
* Decode an arbitrary split byte stream representing JSON objects to a byte
@@ -45,7 +46,7 @@ import org.springframework.reactive.codec.encoder.JsonObjectEncoder;
* @author Sebastien Deleuze
* @see JsonObjectEncoder
*/
public class JsonObjectDecoder implements ByteToMessageDecoder<ByteBuffer> {
public class JsonObjectDecoder extends AbstractDecoder<ByteBuffer> {
private static final int ST_CORRUPTED = -1;
@@ -85,6 +86,8 @@ public class JsonObjectDecoder implements ByteToMessageDecoder<ByteBuffer> {
* "infinitely" many elements.
*/
public JsonObjectDecoder(int maxObjectLength, boolean streamArrayElements) {
super(new MimeType("application", "json", StandardCharsets.UTF_8),
new MimeType("application", "*+json", StandardCharsets.UTF_8));
if (maxObjectLength < 1) {
throw new IllegalArgumentException("maxObjectLength must be a positive int");
}
@@ -92,15 +95,9 @@ public class JsonObjectDecoder implements ByteToMessageDecoder<ByteBuffer> {
this.streamArrayElements = streamArrayElements;
}
@Override
public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON);
}
@Override
public Publisher<ByteBuffer> decode(Publisher<ByteBuffer> inputStream, ResolvableType type,
MediaType mediaType, Object... hints) {
MimeType mimeType, Object... hints) {
return Publishers.flatMap(inputStream, new Function<ByteBuffer, Publisher<? extends ByteBuffer>>() {
@@ -261,4 +258,5 @@ public class JsonObjectDecoder implements ByteToMessageDecoder<ByteBuffer> {
}
});
}
}

View File

@@ -25,9 +25,8 @@ import reactor.Publishers;
import reactor.io.buffer.Buffer;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.encoder.StringEncoder;
import org.springframework.reactive.codec.support.HintUtils;
import org.springframework.util.MimeType;
/**
* Decode from a bytes stream to a String stream.
@@ -35,22 +34,31 @@ import org.springframework.reactive.codec.support.HintUtils;
* @author Sebastien Deleuze
* @see StringEncoder
*/
public class StringDecoder implements ByteToMessageDecoder<String> {
public class StringDecoder extends AbstractDecoder<String> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
public StringDecoder() {
super(new MimeType("text", "plain", DEFAULT_CHARSET));
}
@Override
public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN)
public boolean canDecode(ResolvableType type, MimeType mimeType, Object... hints) {
return super.canDecode(type, mimeType, hints)
&& String.class.isAssignableFrom(type.getRawClass());
}
@Override
public Publisher<String> decode(Publisher<ByteBuffer> inputStream, ResolvableType type,
MediaType mediaType, Object... hints) {
MimeType mimeType, Object... hints) {
Charset charset = HintUtils.getHintByClass(Charset.class, hints, DEFAULT_CHARSET);
Charset charset;
if (mimeType != null && mimeType.getCharSet() != null) {
charset = mimeType.getCharSet();
}
else {
charset = DEFAULT_CHARSET;
}
return Publishers.map(inputStream, chunk -> new String(new Buffer(chunk).asBytes(), charset));
}

View File

@@ -0,0 +1,52 @@
/*
* 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.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.core.ResolvableType;
import org.springframework.util.MimeType;
/**
* @author Sebastien Deleuze
*/
public abstract class AbstractEncoder<T> implements Encoder<T> {
private List<MimeType> supportedMimeTypes = Collections.emptyList();
public AbstractEncoder(MimeType... supportedMimeTypes) {
this.supportedMimeTypes = Arrays.asList(supportedMimeTypes);
}
@Override
public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) {
for (MimeType supportedMimeType : this.supportedMimeTypes) {
if (supportedMimeType.isCompatibleWith(mimeType)) {
return true;
}
}
return false;
}
@Override
public List<MimeType> getSupportedMimeTypes() {
return this.supportedMimeTypes;
}
}

View File

@@ -21,24 +21,28 @@ import java.nio.ByteBuffer;
import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* @author Sebastien Deleuze
*/
public class ByteBufferEncoder implements MessageToByteEncoder<ByteBuffer> {
public class ByteBufferEncoder extends AbstractEncoder<ByteBuffer> {
@Override
public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) {
return ByteBuffer.class.isAssignableFrom(type.getRawClass());
public ByteBufferEncoder() {
super(MimeTypeUtils.ALL);
}
@Override
@SuppressWarnings("unchecked")
public Publisher<ByteBuffer> encode(Publisher<? extends ByteBuffer> messageStream,
ResolvableType type, MediaType mediaType, Object... hints) {
return (Publisher<ByteBuffer>) messageStream;
public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) {
return super.canEncode(type, mimeType, hints)
&& ByteBuffer.class.isAssignableFrom(type.getRawClass());
}
}
@Override
public Publisher<ByteBuffer> encode(Publisher<? extends ByteBuffer> inputStream, ResolvableType type,
MimeType mimeType, Object... hints) {
return (Publisher<ByteBuffer>)inputStream;
}
}

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 java.util.List;
import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.reactive.codec.decoder.Decoder;
import org.springframework.util.MimeType;
/**
* Encode from a stream of {@code T} to a bytes stream.
*
* @author Sebastien Deleuze
* @see Decoder
*/
public interface Encoder<T> {
/**
* Indicate whether the given type and mime type can be processed by this encoder.
* @param type the stream element type to process.
* @param mimeType the mime type to process.
* @param hints Additional information about how to do decode, optional.
* @return {@code true} if can process; {@code false} otherwise
*/
boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints);
/**
* Encode an input stream of {@code T} to an output {@link ByteBuffer} stream.
* @param inputStream the input stream to process.
* @param type the stream element type to process.
* @param mimeType the mime type to process.
* @param hints Additional information about how to do decode, optional.
* @return the output stream
*/
Publisher<ByteBuffer> encode(Publisher<? extends T> inputStream, ResolvableType type, MimeType mimeType, Object... hints);
/**
* Return the list of {@link MimeType} objects supported by this codec.
* @return the list of supported mime types
*/
List<MimeType> getSupportedMimeTypes();
}

View File

@@ -18,6 +18,7 @@ package org.springframework.reactive.codec.encoder;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.reactivestreams.Publisher;
@@ -25,10 +26,10 @@ import reactor.Publishers;
import reactor.io.buffer.Buffer;
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;
import org.springframework.util.MimeType;
/**
* Encode from an {@code Object} stream to a byte stream of JSON objects.
@@ -36,28 +37,32 @@ import org.springframework.reactive.io.BufferOutputStream;
* @author Sebastien Deleuze
* @see JacksonJsonDecoder
*/
public class JacksonJsonEncoder implements MessageToByteEncoder<Object> {
public class JacksonJsonEncoder extends AbstractEncoder<Object> {
private final ObjectMapper mapper;
private Encoder<ByteBuffer> postProcessor;
public JacksonJsonEncoder() {
this(new ObjectMapper());
this(new ObjectMapper(), null);
}
public JacksonJsonEncoder(ObjectMapper mapper) {
public JacksonJsonEncoder(Encoder<ByteBuffer> postProcessor) {
this(new ObjectMapper(), postProcessor);
}
public JacksonJsonEncoder(ObjectMapper mapper, Encoder<ByteBuffer> postProcessor) {
super(new MimeType("application", "json", StandardCharsets.UTF_8),
new MimeType("application", "*+json", StandardCharsets.UTF_8));
this.mapper = mapper;
this.postProcessor = postProcessor;
}
@Override
public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON);
}
public Publisher<ByteBuffer> encode(Publisher<? extends Object> inputStream,
ResolvableType type, MimeType mimeType, Object... hints) {
@Override
public Publisher<ByteBuffer> encode(Publisher<? extends Object> messageStream,
ResolvableType type, MediaType mediaType, Object... hints) {
return Publishers.map(messageStream, value -> {
Publisher<ByteBuffer> stream = Publishers.map(inputStream, value -> {
Buffer buffer = new Buffer();
BufferOutputStream outputStream = new BufferOutputStream(buffer);
try {
@@ -68,6 +73,7 @@ public class JacksonJsonEncoder implements MessageToByteEncoder<Object> {
buffer.flip();
return buffer.byteBuffer();
});
return this.postProcessor == null ? stream : this.postProcessor.encode(stream, type, mimeType, hints);
}
}

View File

@@ -36,6 +36,7 @@ import org.springframework.reactive.codec.decoder.Jaxb2Decoder;
import org.springframework.reactive.io.BufferOutputStream;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
/**
* Encode from an {@code Object} stream to a byte stream of XML elements.
@@ -43,20 +44,18 @@ import org.springframework.util.ClassUtils;
* @author Sebastien Deleuze
* @see Jaxb2Decoder
*/
public class Jaxb2Encoder implements MessageToByteEncoder<Object> {
public class Jaxb2Encoder extends AbstractEncoder<Object> {
private final ConcurrentMap<Class<?>, JAXBContext> jaxbContexts = new ConcurrentHashMap<>(64);
@Override
public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) {
return (mediaType.isCompatibleWith(MediaType.APPLICATION_XML) ||
mediaType.isCompatibleWith(MediaType.TEXT_XML));
public Jaxb2Encoder() {
super(MediaType.APPLICATION_XML, MediaType.TEXT_XML);
}
@Override
public Publisher<ByteBuffer> encode(Publisher<? extends Object> messageStream, ResolvableType type,
MediaType mediaType, Object... hints) {
MimeType mimeType, Object... hints) {
return Publishers.map(messageStream, value -> {
try {

View File

@@ -17,6 +17,7 @@
package org.springframework.reactive.codec.encoder;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
@@ -27,8 +28,8 @@ import reactor.core.support.BackpressureUtils;
import reactor.io.buffer.Buffer;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.decoder.JsonObjectDecoder;
import org.springframework.util.MimeType;
import static reactor.Publishers.lift;
@@ -41,17 +42,16 @@ import static reactor.Publishers.lift;
*
* @see JsonObjectDecoder
*/
public class JsonObjectEncoder implements MessageToByteEncoder<ByteBuffer> {
public class JsonObjectEncoder extends AbstractEncoder<ByteBuffer> {
@Override
public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON);
public JsonObjectEncoder() {
super(new MimeType("application", "json", StandardCharsets.UTF_8),
new MimeType("application", "*+json", StandardCharsets.UTF_8));
}
@Override
public Publisher<ByteBuffer> encode(Publisher<? extends ByteBuffer> messageStream,
ResolvableType type, MediaType mediaType, Object... hints) {
ResolvableType type, MimeType mimeType, Object... hints) {
//noinspection Convert2MethodRef
return lift(messageStream, bbs -> new JsonEncoderBarrier(bbs));

View File

@@ -1,57 +0,0 @@
/*
* 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 stream element type to encode.
* @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 stream element type to encode.
* @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

@@ -24,9 +24,8 @@ import org.reactivestreams.Publisher;
import reactor.Publishers;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.decoder.StringDecoder;
import org.springframework.reactive.codec.support.HintUtils;
import org.springframework.util.MimeType;
/**
* Encode from a String stream to a bytes stream.
@@ -34,22 +33,32 @@ import org.springframework.reactive.codec.support.HintUtils;
* @author Sebastien Deleuze
* @see StringDecoder
*/
public class StringEncoder implements MessageToByteEncoder<String> {
public class StringEncoder extends AbstractEncoder<String> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
public StringEncoder() {
super(new MimeType("text", "plain", DEFAULT_CHARSET));
}
@Override
public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN)
public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) {
return super.canEncode(type, mimeType, hints)
&& String.class.isAssignableFrom(type.getRawClass());
}
@Override
public Publisher<ByteBuffer> encode(Publisher<? extends String> elementStream,
ResolvableType type, MediaType mediaType, Object... hints) {
ResolvableType type, MimeType mimeType, Object... hints) {
final Charset charset = HintUtils.getHintByClass(Charset.class, hints, DEFAULT_CHARSET);
Charset charset;
if (mimeType != null && mimeType.getCharSet() != null) {
charset = mimeType.getCharSet();
}
else {
charset = DEFAULT_CHARSET;
}
return Publishers.map(elementStream, s -> ByteBuffer.wrap(s.getBytes(charset)));
}

View File

@@ -16,15 +16,15 @@
package org.springframework.reactive.codec.support;
import org.springframework.reactive.codec.decoder.ByteToMessageDecoder;
import org.springframework.reactive.codec.encoder.MessageToByteEncoder;
import org.springframework.reactive.codec.decoder.Decoder;
import org.springframework.reactive.codec.encoder.Encoder;
/**
* Utility methods for dealing with codec hints.
*
* @author Sebastien Deleuze
* @see MessageToByteEncoder
* @see ByteToMessageDecoder
* @see Encoder
* @see Decoder
*/
public abstract class HintUtils {

View File

@@ -17,10 +17,6 @@
package org.springframework.reactive.web.dispatch.method.annotation;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.reactivestreams.Publisher;
@@ -32,8 +28,7 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.ReactiveServerHttpRequest;
import org.springframework.reactive.codec.decoder.ByteToMessageDecoder;
import org.springframework.reactive.codec.decoder.JsonObjectDecoder;
import org.springframework.reactive.codec.decoder.Decoder;
import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestBody;
@@ -44,21 +39,15 @@ import org.springframework.web.bind.annotation.RequestBody;
*/
public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final List<ByteToMessageDecoder<?>> decoders;
private final List<Decoder<?>> deserializers;
private final ConversionService conversionService;
// TODO: remove field
private final List<ByteToMessageDecoder<ByteBuffer>> preProcessors = Arrays.asList(new JsonObjectDecoder());
public RequestBodyArgumentResolver(List<ByteToMessageDecoder<?>> decoders, ConversionService service) {
Assert.notEmpty(decoders, "At least one decoder is required.");
public RequestBodyArgumentResolver(List<Decoder<?>> deserializers, ConversionService service) {
Assert.notEmpty(deserializers, "At least one deserializer is required.");
Assert.notNull(service, "'conversionService' is required.");
this.decoders = decoders;
this.deserializers = deserializers;
this.conversionService = service;
}
@@ -72,20 +61,12 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
public Publisher<Object> resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request) {
MediaType mediaType = resolveMediaType(request);
ResolvableType type = ResolvableType.forMethodParameter(parameter);
List<Object> hints = new ArrayList<>();
hints.add(UTF_8);
Publisher<ByteBuffer> inputStream = request.getBody();
Publisher<?> elementStream = inputStream;
ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type;
ByteToMessageDecoder<?> decoder = resolveDecoder(elementType, mediaType, hints.toArray());
if (decoder != null) {
List<ByteToMessageDecoder<ByteBuffer>> preProcessors = resolvePreProcessors(
elementType, mediaType,hints.toArray());
for (ByteToMessageDecoder<ByteBuffer> preProcessor : preProcessors) {
inputStream = preProcessor.decode(inputStream, elementType, mediaType, hints.toArray());
}
elementStream = decoder.decode(inputStream, elementType, mediaType, hints.toArray());
Decoder<?> deserializer = resolveDeserializer(elementType, mediaType);
if (deserializer != null) {
elementStream = deserializer.decode(inputStream, elementType, mediaType);
}
if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) {
return Publishers.just(this.conversionService.convert(elementStream, type.getRawClass()));
@@ -100,8 +81,8 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN);
}
private ByteToMessageDecoder<?> resolveDecoder(ResolvableType type, MediaType mediaType, Object[] hints) {
for (ByteToMessageDecoder<?> deserializer : this.decoders) {
private Decoder<?> resolveDeserializer(ResolvableType type, MediaType mediaType, Object... hints) {
for (Decoder<?> deserializer : this.deserializers) {
if (deserializer.canDecode(type, mediaType, hints)) {
return deserializer;
}
@@ -109,16 +90,4 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
return null;
}
private List<ByteToMessageDecoder<ByteBuffer>> resolvePreProcessors(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

@@ -28,8 +28,9 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.http.server.ReactiveServerHttpRequest;
import org.springframework.http.server.ReactiveServerHttpResponse;
import org.springframework.reactive.codec.decoder.ByteBufferDecoder;
import org.springframework.reactive.codec.decoder.ByteToMessageDecoder;
import org.springframework.reactive.codec.decoder.Decoder;
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;
@@ -70,8 +71,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin
public void afterPropertiesSet() throws Exception {
if (this.argumentResolvers == null) {
List<ByteToMessageDecoder<?>> decoders = Arrays.asList(new ByteBufferDecoder(),
new StringDecoder(), new JacksonJsonDecoder());
List<Decoder<?>> decoders = Arrays.asList(new ByteBufferDecoder(),
new StringDecoder(), new JacksonJsonDecoder(new JsonObjectDecoder()));
this.argumentResolvers = new ArrayList<>();
this.argumentResolvers.add(new RequestParamArgumentResolver());

View File

@@ -18,9 +18,6 @@ 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.Arrays;
import java.util.List;
import org.reactivestreams.Publisher;
@@ -35,42 +32,33 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.ReactiveServerHttpRequest;
import org.springframework.http.server.ReactiveServerHttpResponse;
import org.springframework.reactive.codec.encoder.JsonObjectEncoder;
import org.springframework.reactive.codec.encoder.MessageToByteEncoder;
import org.springframework.reactive.codec.encoder.Encoder;
import org.springframework.reactive.web.dispatch.HandlerResult;
import org.springframework.reactive.web.dispatch.HandlerResultHandler;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.HandlerMethod;
/**
* First version using {@link MessageToByteEncoder}s
*
* @author Rossen Stoyanchev
* @author Stephane Maldini
* @author Sebastien Deleuze
*/
public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered {
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final List<MessageToByteEncoder<?>> serializers;
private final List<Encoder<?>> serializers;
private final ConversionService conversionService;
private int order = 0;
// TODO: remove field
private final List<MessageToByteEncoder<ByteBuffer>> postProcessors = Arrays.asList(new JsonObjectEncoder());
public ResponseBodyResultHandler(List<MessageToByteEncoder<?>> encoders, ConversionService service) {
Assert.notEmpty(encoders, "At least one encoder is required.");
public ResponseBodyResultHandler(List<Encoder<?>> serializers, ConversionService service) {
Assert.notEmpty(serializers, "At least one serializers is required.");
Assert.notNull(service, "'conversionService' is required.");
this.serializers = encoders;
this.serializers = serializers;
this.conversionService = service;
}
@@ -110,9 +98,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
ResolvableType type = ResolvableType.forMethodParameter(returnType);
MediaType mediaType = resolveMediaType(request);
List<Object> hints = new ArrayList<>();
hints.add(UTF_8);
Publisher<Object> elementStream;
Publisher<?> elementStream;
ResolvableType elementType;
if (conversionService.canConvert(type.getRawClass(), Publisher.class)) {
elementStream = conversionService.convert(value, Publisher.class);
@@ -123,17 +109,20 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
elementType = type;
}
MessageToByteEncoder<Object> encoder = (MessageToByteEncoder<Object>) resolveEncoder(
elementType, mediaType, hints.toArray());
Encoder<?> serializer = resolveSerializer(elementType, mediaType);
if (encoder != null) {
Publisher<ByteBuffer> outputStream = encoder.encode(elementStream, type, mediaType, hints.toArray());
List<MessageToByteEncoder<ByteBuffer>> postProcessors = resolvePostProcessors(
elementType, mediaType, hints.toArray());
for (MessageToByteEncoder<ByteBuffer> postProcessor : postProcessors) {
outputStream = postProcessor.encode(outputStream, elementType, mediaType, hints.toArray());
if (serializer != null) {
Publisher<ByteBuffer> outputStream = serializer.encode((Publisher)elementStream, type, mediaType);
if (mediaType == null || mediaType.isWildcardType() || mediaType.isWildcardSubtype()) {
List<MimeType> mimeTypes = serializer.getSupportedMimeTypes();
if (!mimeTypes.isEmpty()) {
MimeType mimeType = mimeTypes.get(0);
mediaType = new MediaType(mimeType.getType(), mimeType.getSubtype(), mimeType.getParameters());
}
}
if (mediaType != null && !mediaType.equals(MediaType.ALL)) {
response.getHeaders().setContentType(mediaType);
}
response.getHeaders().setContentType(mediaType);
return response.setBody(outputStream);
}
String returnTypeName = returnType.getParameterType().getName();
@@ -148,25 +137,13 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
return ( mediaTypes.size() > 0 ? mediaTypes.get(0) : MediaType.TEXT_PLAIN);
}
private MessageToByteEncoder<?> resolveEncoder(ResolvableType type, MediaType mediaType, Object[] hints) {
for (MessageToByteEncoder<?> codec : this.serializers) {
if (codec.canEncode(type, mediaType, hints)) {
return codec;
private Encoder<?> resolveSerializer(ResolvableType type, MediaType mediaType, Object... hints) {
for (Encoder<?> serializer : this.serializers) {
if (serializer.canEncode(type, mediaType, hints)) {
return serializer;
}
}
return null;
}
private List<MessageToByteEncoder<ByteBuffer>> resolvePostProcessors(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

@@ -32,7 +32,7 @@ import org.springframework.http.MediaType;
/**
* @author Sebastien Deleuze
*/
public class ByteBufferDecoderEncoder {
public class ByteBufferEncoderTests {
private final ByteBufferEncoder encoder = new ByteBufferEncoder();

View File

@@ -39,7 +39,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactiveStreamsToReactorConverter;
@@ -50,6 +49,7 @@ import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.reactive.codec.encoder.ByteBufferEncoder;
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.dispatch.SimpleHandlerResultHandler;
@@ -290,7 +290,7 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
@Bean
public ResponseBodyResultHandler responseBodyResultHandler() {
return new ResponseBodyResultHandler(Arrays.asList(
new ByteBufferEncoder(), new StringEncoder(),new JacksonJsonEncoder()),
new ByteBufferEncoder(), new StringEncoder(), new JacksonJsonEncoder(new JsonObjectEncoder())),
conversionService());
}