diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java new file mode 100644 index 0000000000..af899e079d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/AbstractDecoder.java @@ -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 implements Decoder { + + private List 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 getSupportedMimeTypes() { + return this.supportedMimeTypes; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java index 33bef171d0..a06063ae48 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteBufferDecoder.java @@ -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 { +public class ByteBufferDecoder extends AbstractDecoder { + + 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 decode(Publisher inputStream, ResolvableType type, - MediaType mediaType, Object... hints) { + MimeType mimeType, Object... hints) { return inputStream; } -} +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java similarity index 51% rename from spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java rename to spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java index 6c1fed1995..2a7416501e 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/ByteToMessageDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Decoder.java @@ -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 { +public interface Decoder { /** - * 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 decode(Publisher inputStream, ResolvableType type, - MediaType mediaType, Object... hints); + Publisher decode(Publisher 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 getSupportedMimeTypes(); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java index 00a3241461..3775b6520f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JacksonJsonDecoder.java @@ -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 { +public class JacksonJsonDecoder extends AbstractDecoder { private final ObjectMapper mapper; + private Decoder preProcessor; + public JacksonJsonDecoder() { - this(new ObjectMapper()); + this(new ObjectMapper(), null); } - public JacksonJsonDecoder(ObjectMapper mapper) { + public JacksonJsonDecoder(Decoder preProcessor) { + this(new ObjectMapper(), preProcessor); + } + + public JacksonJsonDecoder(ObjectMapper mapper, Decoder 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 decode(Publisher inputStream, ResolvableType type, - MediaType mediaType, Object... hints) { + MimeType mimeType, Object... hints) { ObjectReader reader = this.mapper.readerFor(type.getRawClass()); - return Publishers.map(inputStream, chunk -> { + Publisher decodedStream = this.preProcessor == null ? inputStream : + this.preProcessor.decode(inputStream, type, mimeType, hints); + return Publishers.map(decodedStream, chunk -> { try { return reader.readValue(new ByteBufferInputStream(chunk)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java index 204483481d..917400ddb5 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/Jaxb2Decoder.java @@ -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 { +public class Jaxb2Decoder extends AbstractDecoder { private final ConcurrentMap, 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 decode(Publisher 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 { } return jaxbContext; } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java index 7a2b0bc4c2..5267b82dac 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/JsonObjectDecoder.java @@ -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 { +public class JsonObjectDecoder extends AbstractDecoder { private static final int ST_CORRUPTED = -1; @@ -85,6 +86,8 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { * "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 { this.streamArrayElements = streamArrayElements; } - - @Override - public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) { - return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON); - } - @Override public Publisher decode(Publisher inputStream, ResolvableType type, - MediaType mediaType, Object... hints) { + MimeType mimeType, Object... hints) { return Publishers.flatMap(inputStream, new Function>() { @@ -261,4 +258,5 @@ public class JsonObjectDecoder implements ByteToMessageDecoder { } }); } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java index b5f44ef73d..fa86f751c6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/decoder/StringDecoder.java @@ -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 { +public class StringDecoder extends AbstractDecoder { 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 decode(Publisher 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)); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java new file mode 100644 index 0000000000..c88926e822 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/AbstractEncoder.java @@ -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 implements Encoder { + + private List 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 getSupportedMimeTypes() { + return this.supportedMimeTypes; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java index 321f43357d..1157779714 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/ByteBufferEncoder.java @@ -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 { +public class ByteBufferEncoder extends AbstractEncoder { - @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 encode(Publisher messageStream, - ResolvableType type, MediaType mediaType, Object... hints) { - - return (Publisher) messageStream; + public boolean canEncode(ResolvableType type, MimeType mimeType, Object... hints) { + return super.canEncode(type, mimeType, hints) + && ByteBuffer.class.isAssignableFrom(type.getRawClass()); } -} + @Override + public Publisher encode(Publisher inputStream, ResolvableType type, + MimeType mimeType, Object... hints) { + + return (Publisher)inputStream; + } +} \ No newline at end of file diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java new file mode 100644 index 0000000000..55e5455422 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Encoder.java @@ -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 { + + /** + * 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 encode(Publisher 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 getSupportedMimeTypes(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java index f5e639b752..a2d8e02cc8 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JacksonJsonEncoder.java @@ -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 { +public class JacksonJsonEncoder extends AbstractEncoder { private final ObjectMapper mapper; + private Encoder postProcessor; + public JacksonJsonEncoder() { - this(new ObjectMapper()); + this(new ObjectMapper(), null); } - public JacksonJsonEncoder(ObjectMapper mapper) { + public JacksonJsonEncoder(Encoder postProcessor) { + this(new ObjectMapper(), postProcessor); + } + + public JacksonJsonEncoder(ObjectMapper mapper, Encoder 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 encode(Publisher inputStream, + ResolvableType type, MimeType mimeType, Object... hints) { - @Override - public Publisher encode(Publisher messageStream, - ResolvableType type, MediaType mediaType, Object... hints) { - - return Publishers.map(messageStream, value -> { + Publisher stream = Publishers.map(inputStream, value -> { Buffer buffer = new Buffer(); BufferOutputStream outputStream = new BufferOutputStream(buffer); try { @@ -68,6 +73,7 @@ public class JacksonJsonEncoder implements MessageToByteEncoder { buffer.flip(); return buffer.byteBuffer(); }); + return this.postProcessor == null ? stream : this.postProcessor.encode(stream, type, mimeType, hints); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java index ea3063d5f7..cc9a7509ee 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/Jaxb2Encoder.java @@ -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 { +public class Jaxb2Encoder extends AbstractEncoder { private final ConcurrentMap, 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 encode(Publisher messageStream, ResolvableType type, - MediaType mediaType, Object... hints) { + MimeType mimeType, Object... hints) { return Publishers.map(messageStream, value -> { try { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java index 103a8e4b74..9b5b8775ef 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/JsonObjectEncoder.java @@ -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 { +public class JsonObjectEncoder extends AbstractEncoder { - - @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 encode(Publisher messageStream, - ResolvableType type, MediaType mediaType, Object... hints) { + ResolvableType type, MimeType mimeType, Object... hints) { //noinspection Convert2MethodRef return lift(messageStream, bbs -> new JsonEncoderBarrier(bbs)); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java deleted file mode 100644 index 3075702cda..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/MessageToByteEncoder.java +++ /dev/null @@ -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 { - - /** - * 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 encode(Publisher messageStream, ResolvableType type, - MediaType mediaType, Object... hints); - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java index 708745e75f..2f5e99ca5b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/encoder/StringEncoder.java @@ -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 { +public class StringEncoder extends AbstractEncoder { 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 encode(Publisher 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))); } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java index a15c57b77f..892d64b451 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/codec/support/HintUtils.java @@ -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 { diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java index 60f8aa8055..72cfbe9735 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestBodyArgumentResolver.java @@ -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> decoders; + private final List> deserializers; private final ConversionService conversionService; - // TODO: remove field - private final List> preProcessors = Arrays.asList(new JsonObjectDecoder()); - - public RequestBodyArgumentResolver(List> decoders, ConversionService service) { - Assert.notEmpty(decoders, "At least one decoder is required."); + public RequestBodyArgumentResolver(List> 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 resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request) { MediaType mediaType = resolveMediaType(request); ResolvableType type = ResolvableType.forMethodParameter(parameter); - List hints = new ArrayList<>(); - hints.add(UTF_8); Publisher 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> preProcessors = resolvePreProcessors( - elementType, mediaType,hints.toArray()); - - for (ByteToMessageDecoder 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> resolvePreProcessors(ResolvableType type, - MediaType mediaType, Object[] hints) { - - List> preProcessors = new ArrayList<>(); - for (ByteToMessageDecoder preProcessor : this.preProcessors) { - if (preProcessor.canDecode(type, mediaType, hints)) { - preProcessors.add(preProcessor); - } - } - return preProcessors; - } - } diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java index f604fa0b3c..6d3e94e3e6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingHandlerAdapter.java @@ -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> decoders = Arrays.asList(new ByteBufferDecoder(), - new StringDecoder(), new JacksonJsonDecoder()); + List> decoders = Arrays.asList(new ByteBufferDecoder(), + new StringDecoder(), new JacksonJsonDecoder(new JsonObjectDecoder())); this.argumentResolvers = new ArrayList<>(); this.argumentResolvers.add(new RequestParamArgumentResolver()); diff --git a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java index ec66870464..d1818f3ebe 100644 --- a/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/reactive/web/dispatch/method/annotation/ResponseBodyResultHandler.java @@ -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> serializers; + private final List> serializers; private final ConversionService conversionService; private int order = 0; - // TODO: remove field - private final List> postProcessors = Arrays.asList(new JsonObjectEncoder()); - - - public ResponseBodyResultHandler(List> encoders, ConversionService service) { - Assert.notEmpty(encoders, "At least one encoder is required."); + public ResponseBodyResultHandler(List> 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 hints = new ArrayList<>(); - hints.add(UTF_8); - Publisher 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 encoder = (MessageToByteEncoder) resolveEncoder( - elementType, mediaType, hints.toArray()); + Encoder serializer = resolveSerializer(elementType, mediaType); - if (encoder != null) { - Publisher outputStream = encoder.encode(elementStream, type, mediaType, hints.toArray()); - List> postProcessors = resolvePostProcessors( - elementType, mediaType, hints.toArray()); - for (MessageToByteEncoder postProcessor : postProcessors) { - outputStream = postProcessor.encode(outputStream, elementType, mediaType, hints.toArray()); + if (serializer != null) { + Publisher outputStream = serializer.encode((Publisher)elementStream, type, mediaType); + if (mediaType == null || mediaType.isWildcardType() || mediaType.isWildcardSubtype()) { + List 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> resolvePostProcessors(ResolvableType type, - MediaType mediaType, Object[] hints) { - - List> postProcessors = new ArrayList<>(); - for (MessageToByteEncoder postProcessor : this.postProcessors) { - if (postProcessor.canEncode(type, mediaType, hints)) { - postProcessors.add(postProcessor); - } - } - return postProcessors; - } - } diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferDecoderEncoder.java b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java similarity index 98% rename from spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferDecoderEncoder.java rename to spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java index 3fac6e58a9..4c5a8657e0 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferDecoderEncoder.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/codec/encoder/ByteBufferEncoderTests.java @@ -32,7 +32,7 @@ import org.springframework.http.MediaType; /** * @author Sebastien Deleuze */ -public class ByteBufferDecoderEncoder { +public class ByteBufferEncoderTests { private final ByteBufferEncoder encoder = new ByteBufferEncoder(); diff --git a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java index f8f14cd64e..180aad81cd 100644 --- a/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/reactive/web/dispatch/method/annotation/RequestMappingIntegrationTests.java @@ -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()); }