Add handler method parameter and result converters

This commit introduces the following changes:
 - Publisher -> Observable/Stream/etc. conversion is now managed
    in a dedicated ConversionService instead of directly in
    RequestBodyArgumentResolver and ResponseBodyResultHandler
 - More isolated logic that decides if the stream should be
    serialized as a JSON array or not
 - Publisher<ByteBuffer> are now handled by regular
   ByteBufferEncoder and ByteBufferDecoder
 - Handle Publisher<Void> return value properly
 - Ensure that the headers are properly written even for response
   without body
 - Improve JsonObjectEncoder to autodetect JSON arrays
This commit is contained in:
Sebastien Deleuze 2015-10-19 17:00:52 +02:00
parent cf2c1514af
commit adc50bbfb9
32 changed files with 758 additions and 202 deletions

View File

@ -31,28 +31,28 @@ configurations.all {
}
dependencies {
compile "org.springframework:spring-core:4.2.0.RELEASE"
compile "org.springframework:spring-web:4.2.0.RELEASE"
compile "org.springframework:spring-core:4.2.2.RELEASE"
compile "org.springframework:spring-web:4.2.2.RELEASE"
compile "org.reactivestreams:reactive-streams:1.0.0"
compile "io.projectreactor:reactor-core:2.1.0.BUILD-SNAPSHOT"
compile "commons-logging:commons-logging:1.2"
optional "com.fasterxml.jackson.core:jackson-databind:2.6.1"
optional "com.fasterxml.jackson.core:jackson-databind:2.6.2"
optional "io.reactivex:rxnetty:0.5.0-SNAPSHOT"
optional "io.projectreactor:reactor-net:2.1.0.BUILD-SNAPSHOT"
optional 'org.apache.tomcat:tomcat-util:8.0.24'
optional 'org.apache.tomcat.embed:tomcat-embed-core:8.0.24'
optional 'org.apache.tomcat:tomcat-util:8.0.28'
optional 'org.apache.tomcat.embed:tomcat-embed-core:8.0.28'
optional 'org.eclipse.jetty:jetty-server:9.3.2.v20150730'
optional 'org.eclipse.jetty:jetty-servlet:9.3.2.v20150730'
optional 'org.eclipse.jetty:jetty-server:9.3.5.v20151012'
optional 'org.eclipse.jetty:jetty-servlet:9.3.5.v20151012'
provided "javax.servlet:javax.servlet-api:3.1.0"
testCompile "junit:junit:4.12"
testCompile "org.springframework:spring-test:4.2.0.RELEASE"
testCompile "org.springframework:spring-test:4.2.2.RELEASE"
testCompile "org.slf4j:slf4j-jcl:1.7.12"
testCompile "org.slf4j:jul-to-slf4j:1.7.12"

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.core.convert.support;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import org.reactivestreams.Publisher;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
/**
* @author Sebastien Deleuze
*/
public class ReactiveStreamsToCompletableFutureConverter implements GenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
Set<GenericConverter.ConvertiblePair> convertibleTypes = new LinkedHashSet<>();
convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, CompletableFuture.class));
convertibleTypes.add(new GenericConverter.ConvertiblePair(CompletableFuture.class, Publisher.class));
return convertibleTypes;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source != null) {
if (CompletableFuture.class.isAssignableFrom(source.getClass())) {
return reactor.core.publisher.convert.CompletableFutureConverter.from((CompletableFuture)source);
} else if (CompletableFuture.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) {
return reactor.core.publisher.convert.CompletableFutureConverter.fromSingle((Publisher)source);
}
}
return null;
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.convert.support;
import java.util.LinkedHashSet;
import java.util.Set;
import org.reactivestreams.Publisher;
import reactor.rx.Promise;
import reactor.rx.Stream;
import reactor.rx.Streams;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
/**
* @author Stephane Maldini
* @author Sebastien Deleuze
*/
public final class ReactiveStreamsToReactorConverter implements GenericConverter {
@Override
public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
Set<GenericConverter.ConvertiblePair> convertibleTypes = new LinkedHashSet<>();
convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, Stream.class));
convertibleTypes.add(new GenericConverter.ConvertiblePair(Stream.class, Publisher.class));
convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, Promise.class));
convertibleTypes.add(new GenericConverter.ConvertiblePair(Promise.class, Publisher.class));
return convertibleTypes;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source != null) {
if (Stream.class.isAssignableFrom(source.getClass())) {
return source;
} else if (Stream.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) {
return Streams.wrap((Publisher)source);
} else if (Promise.class.isAssignableFrom(source.getClass())) {
return ((Promise<?>)source);
} else if (Promise.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) {
return Streams.wrap((Publisher)source).next();
}
}
return null;
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.convert.support;
import java.util.LinkedHashSet;
import java.util.Set;
import org.reactivestreams.Publisher;
import reactor.core.publisher.convert.RxJava1Converter;
import rx.Observable;
import rx.Single;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
/**
* TODO Avoid classpath exception for older RxJava1 version without Single type
* @author Stephane Maldini
* @author Sebastien Deleuze
*/
public final class ReactiveStreamsToRxJava1Converter implements GenericConverter {
@Override
public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
Set<GenericConverter.ConvertiblePair> convertibleTypes = new LinkedHashSet<>();
convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, Observable.class));
convertibleTypes.add(new GenericConverter.ConvertiblePair(Observable.class, Publisher.class));
convertibleTypes.add(new GenericConverter.ConvertiblePair(Publisher.class, Single.class));
convertibleTypes.add(new GenericConverter.ConvertiblePair(Single.class, Publisher.class));
return convertibleTypes;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source != null) {
if (Observable.class.isAssignableFrom(source.getClass())) {
return RxJava1Converter.from((Observable) source);
}
else if (Observable.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) {
return RxJava1Converter.from((Publisher)source);
}
else if (Single.class.isAssignableFrom(source.getClass())) {
return reactor.core.publisher.convert.RxJava1SingleConverter.from((Single) source);
} else if (Single.class.isAssignableFrom(targetType.getResolvableType().getRawClass())) {
return reactor.core.publisher.convert.RxJava1SingleConverter.from((Publisher)source);
}
}
return null;
}
}

View File

@ -0,0 +1,40 @@
/*
* 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;
/**
* @author Sebastien Deleuze
*/
public class ByteBufferDecoder implements ByteToMessageDecoder<ByteBuffer> {
@Override
public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) {
return ByteBuffer.class.isAssignableFrom(type.getRawClass());
}
@Override
public Publisher<ByteBuffer> decode(Publisher<ByteBuffer> inputStream, ResolvableType type, MediaType mediaType, Object... hints) {
return inputStream;
}
}

View File

@ -34,8 +34,7 @@ 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 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.
* @param hints Additional information about how to do decode, optional.
@ -46,8 +45,7 @@ public interface ByteToMessageDecoder<T> {
/**
* 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 type the stream element type to ultimately decode to.
* @param hints Additional information about how to do decode, optional.
* @return the decoded message stream
*/

View File

@ -23,22 +23,18 @@ import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.encoder.JsonObjectEncoder;
import reactor.Publishers;
import reactor.fn.Function;
import reactor.rx.Promise;
import rx.Observable;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Decode an arbitrary split byte stream representing JSON objects to a bye stream
* Decode an arbitrary split byte stream representing JSON objects to a byte 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.
*
@ -90,8 +86,7 @@ public class JsonObjectDecoder implements ByteToMessageDecoder<ByteBuffer> {
@Override
public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) && !Promise.class.isAssignableFrom(type.getRawClass()) &&
(Observable.class.isAssignableFrom(type.getRawClass()) || Publisher.class.isAssignableFrom(type.getRawClass()));
return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON);
}
@Override

View File

@ -41,7 +41,8 @@ public class StringDecoder implements ByteToMessageDecoder<String> {
@Override
public boolean canDecode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN);
return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN)
&& String.class.isAssignableFrom(type.getRawClass());
}
@Override

View File

@ -0,0 +1,41 @@
/*
* 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;
/**
* @author Sebastien Deleuze
*/
public class ByteBufferEncoder implements MessageToByteEncoder<ByteBuffer> {
@Override
public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) {
return ByteBuffer.class.isAssignableFrom(type.getRawClass());
}
@Override
public Publisher<ByteBuffer> encode(Publisher<? extends ByteBuffer> messageStream, ResolvableType type, MediaType mediaType, Object... hints) {
return (Publisher<ByteBuffer>)messageStream;
}
}

View File

@ -21,21 +21,17 @@ import org.reactivestreams.Subscriber;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.decoder.JsonObjectDecoder;
import org.springframework.util.ClassUtils;
import reactor.core.subscriber.SubscriberBarrier;
import reactor.io.buffer.Buffer;
import reactor.rx.Promise;
import rx.Observable;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static reactor.Publishers.*;
import reactor.io.buffer.Buffer;
/**
* 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.
* Encode a byte stream of individual JSON element to a byte stream representing a single
* JSON array when if it contains more than one element.
*
* @author Sebastien Deleuze
* @author Stephane Maldini
@ -44,57 +40,31 @@ import static reactor.Publishers.*;
*/
public class JsonObjectEncoder implements MessageToByteEncoder<ByteBuffer> {
private static final boolean rxJava1Present =
ClassUtils.isPresent("rx.Observable", JsonObjectEncoder.class.getClassLoader());
private static final boolean reactorPresent =
ClassUtils.isPresent("reactor.rx.Promise", JsonObjectEncoder.class.getClassLoader());
final ByteBuffer START_ARRAY = ByteBuffer.wrap("[".getBytes());
final ByteBuffer END_ARRAY = ByteBuffer.wrap("]".getBytes());
final ByteBuffer COMMA = ByteBuffer.wrap(",".getBytes());
@Override
public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) &&
!(reactorPresent && Promise.class.isAssignableFrom(type.getRawClass())) &&
(rxJava1Present && Observable.class.isAssignableFrom(type.getRawClass())
|| Publisher.class.isAssignableFrom(type.getRawClass()));
return mediaType.isCompatibleWith(MediaType.APPLICATION_JSON);
}
@Override
public Publisher<ByteBuffer> encode(Publisher<? extends ByteBuffer> messageStream, ResolvableType type, MediaType
mediaType, Object... hints) {
//TODO Merge some chunks, there is no need to have chunks with only '[', ']' or ',' characters
return
concat(
from(
Arrays.<Publisher<ByteBuffer>>asList(
just(START_ARRAY),
lift(
flatMap(messageStream, (ByteBuffer b) -> from(Arrays.asList(b, COMMA))),
sub -> new SkipLastBarrier(sub)
),
just(END_ARRAY)
)
)
);
public Publisher<ByteBuffer> encode(Publisher<? extends ByteBuffer> messageStream,
ResolvableType type, MediaType mediaType, Object... hints) {
return lift(messageStream, sub -> new JsonEncoderBarrier(sub));
}
private static class SkipLastBarrier extends SubscriberBarrier<ByteBuffer, ByteBuffer> {
private static class JsonEncoderBarrier extends SubscriberBarrier<ByteBuffer, ByteBuffer> {
public SkipLastBarrier(Subscriber<? super ByteBuffer> subscriber) {
public JsonEncoderBarrier(Subscriber<? super ByteBuffer> subscriber) {
super(subscriber);
}
ByteBuffer prev = null;
long count = 0;
@Override
protected void doNext(ByteBuffer next) {
if (prev == null) {
count++;
if (count == 1) {
prev = next;
doRequest(1);
return;
@ -102,8 +72,27 @@ public class JsonObjectEncoder implements MessageToByteEncoder<ByteBuffer> {
ByteBuffer tmp = prev;
prev = next;
subscriber.onNext(tmp);
Buffer buffer = new Buffer();
if (count == 2) {
buffer.append("[");
}
buffer.append(tmp);
buffer.append(",");
buffer.flip();
subscriber.onNext(buffer.byteBuffer());
}
@Override
protected void doComplete() {
Buffer buffer = new Buffer();
buffer.append(prev);
if (count > 1) {
buffer.append("]");
}
buffer.flip();
subscriber.onNext(buffer.byteBuffer());
subscriber.onComplete();
}
}
}

View File

@ -34,8 +34,7 @@ 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 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.
@ -46,8 +45,7 @@ public interface MessageToByteEncoder<T> {
/**
* 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 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.

View File

@ -40,7 +40,8 @@ public class StringEncoder implements MessageToByteEncoder<String> {
@Override
public boolean canEncode(ResolvableType type, MediaType mediaType, Object... hints) {
return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN);
return mediaType.isCompatibleWith(MediaType.TEXT_PLAIN)
&& String.class.isAssignableFrom(type.getRawClass());
}
@Override

View File

@ -101,6 +101,7 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware {
if (handler == null) {
// No exception handling mechanism yet
response.setStatusCode(HttpStatus.NOT_FOUND);
response.writeHeaders();
return Publishers.empty();
}

View File

@ -16,6 +16,8 @@
package org.springframework.reactive.web.dispatch;
import java.util.Arrays;
import org.reactivestreams.Publisher;
import reactor.Publishers;
@ -45,6 +47,8 @@ public class SimpleHandlerResultHandler implements Ordered, HandlerResultHandler
@Override
public Publisher<Void> handleResult(ServerHttpRequest request, ServerHttpResponse response, HandlerResult result) {
return Publishers.completable((Publisher<?>)result.getValue());
Publisher<Void> handleComplete = Publishers.completable((Publisher<?>)result.getValue());
return Publishers.concat(Publishers.from(Arrays.asList(handleComplete, response.writeHeaders())));
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2011-2015 Pivotal Software Inc, All Rights Reserved.
*
* 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 reactor.core.publisher.convert.DependencyUtils;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.convert.support.ReactiveStreamsToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactiveStreamsToReactorConverter;
import org.springframework.core.convert.support.ReactiveStreamsToRxJava1Converter;
/**
* TODO temporary class designed to be replaced by org.springframework.core.convert.support.DefaultConversionService when it will contain Reactive Streams converter
* @author Sebastien Deleuze
*/
class DefaultConversionService extends GenericConversionService {
public DefaultConversionService() {
addDefaultConverters(this);
}
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
converterRegistry.addConverter(new ReactiveStreamsToCompletableFutureConverter());
if (DependencyUtils.hasReactorStream()) {
converterRegistry.addConverter(new ReactiveStreamsToReactorConverter());
}
if (DependencyUtils.hasRxJava1()) {
converterRegistry.addConverter(new ReactiveStreamsToRxJava1Converter());
}
}
}

View File

@ -16,27 +16,10 @@
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 java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.reactivestreams.Publisher;
import reactor.Publishers;
import reactor.core.publisher.convert.CompletableFutureConverter;
import reactor.core.publisher.convert.RxJava1Converter;
import reactor.core.publisher.convert.RxJava1SingleConverter;
import reactor.rx.Promise;
import reactor.rx.Stream;
import reactor.rx.Streams;
import rx.Observable;
import rx.Single;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.decoder.ByteToMessageDecoder;
@ -44,8 +27,15 @@ import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentRes
import org.springframework.reactive.web.http.ServerHttpRequest;
import org.springframework.web.bind.annotation.RequestBody;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author Sebastien Deleuze
* @author Stephane Maldini
*/
public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
@ -53,14 +43,19 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
private final List<ByteToMessageDecoder<?>> deserializers;
private final List<ByteToMessageDecoder<ByteBuffer>> preProcessors;
private final ConversionService conversionService;
public RequestBodyArgumentResolver(List<ByteToMessageDecoder<?>> deserializers) {
this(deserializers, Collections.EMPTY_LIST);
public RequestBodyArgumentResolver(List<ByteToMessageDecoder<?>> deserializers,
ConversionService conversionService) {
this(deserializers, conversionService, Collections.EMPTY_LIST);
}
public RequestBodyArgumentResolver(List<ByteToMessageDecoder<?>> deserializers, List<ByteToMessageDecoder<ByteBuffer>> preProcessors) {
public RequestBodyArgumentResolver(List<ByteToMessageDecoder<?>> deserializers,
ConversionService conversionService,
List<ByteToMessageDecoder<ByteBuffer>> preProcessors) {
this.deserializers = deserializers;
this.conversionService = conversionService;
this.preProcessors = preProcessors;
}
@ -70,61 +65,31 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
}
@Override
@SuppressWarnings("unchecked")
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()) ||
Single.class.isAssignableFrom(type.getRawClass()) ||
Promise.class.isAssignableFrom(type.getRawClass()) ||
Publisher.class.isAssignableFrom(type.getRawClass()) ||
CompletableFuture.class.isAssignableFrom(type.getRawClass())) {
readType = type.getGeneric(0);
}
ByteToMessageDecoder<?> deserializer = resolveDeserializers(request, type, mediaType, hints.toArray());
Publisher<ByteBuffer> inputStream = request.getBody();
Publisher<?> elementStream = inputStream;
ResolvableType elementType = type.hasGenerics() ? type.getGeneric(0) : type;
ByteToMessageDecoder<?> deserializer = resolveDeserializers(request, elementType, mediaType, hints.toArray());
if (deserializer != null) {
Publisher<ByteBuffer> inputStream = request.getBody();
List<ByteToMessageDecoder<ByteBuffer>> preProcessors = resolvePreProcessors(request, type, mediaType, hints.toArray());
List<ByteToMessageDecoder<ByteBuffer>> preProcessors =
resolvePreProcessors(request, elementType, 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 (Promise.class.isAssignableFrom(type.getRawClass())) {
return Streams.wrap(elementStream).take(1).next();
}
else if (Observable.class.isAssignableFrom(type.getRawClass())) {
return RxJava1Converter.from(elementStream);
}
else if (Single.class.isAssignableFrom(type.getRawClass())) {
return RxJava1SingleConverter.from(elementStream);
}
else if (CompletableFuture.class.isAssignableFrom(type.getRawClass())) {
return CompletableFutureConverter.fromSingle(elementStream);
}
else if (Publisher.class.isAssignableFrom(type.getRawClass())) {
return elementStream;
}
else {
try {
return Publishers.toReadQueue(elementStream, 1, true).poll(30, TimeUnit.SECONDS);
} catch(InterruptedException ex) {
return Publishers.error(new IllegalStateException("Timeout before getter the value"));
}
inputStream = preProcessor.decode(inputStream, elementType, mediaType, hints.toArray());
}
elementStream = deserializer.decode(inputStream, elementType, mediaType, hints.toArray());
}
if (conversionService.canConvert(Publisher.class, type.getRawClass())) {
return conversionService.convert(elementStream, type.getRawClass());
}
else {
return elementStream;
}
return Publishers.error(new IllegalStateException("Argument type not supported: " + type));
}
private MediaType resolveMediaType(ServerHttpRequest request) {

View File

@ -15,11 +15,14 @@
*/
package org.springframework.reactive.web.dispatch.method.annotation;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.reactive.codec.decoder.ByteBufferDecoder;
import org.springframework.reactive.codec.decoder.ByteToMessageDecoder;
import org.springframework.reactive.codec.decoder.JacksonJsonDecoder;
import org.springframework.reactive.codec.decoder.JsonObjectDecoder;
import org.springframework.reactive.codec.decoder.StringDecoder;
@ -51,7 +54,11 @@ 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))));
List<ByteToMessageDecoder<?>> deserializers = Arrays.asList(new ByteBufferDecoder(),
new StringDecoder(), new JacksonJsonDecoder());
List<ByteToMessageDecoder<ByteBuffer>> preProcessors = Arrays.asList(new JsonObjectDecoder());
this.argumentResolvers.add(new RequestBodyArgumentResolver(deserializers,
new DefaultConversionService(), preProcessors));
}
}

View File

@ -18,9 +18,9 @@ package org.springframework.reactive.web.dispatch.method.annotation;
import org.reactivestreams.Publisher;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.reactive.codec.encoder.MessageToByteEncoder;
@ -31,26 +31,20 @@ import org.springframework.reactive.web.http.ServerHttpResponse;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.HandlerMethod;
import reactor.Publishers;
import reactor.core.publisher.convert.CompletableFutureConverter;
import reactor.core.publisher.convert.RxJava1Converter;
import reactor.core.publisher.convert.RxJava1SingleConverter;
import reactor.rx.Promise;
import rx.Observable;
import rx.Single;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* First version using {@link MessageToByteEncoder}s
*
* @author Rossen Stoyanchev
* @author Stephane Maldini
* @author Sebastien Deleuze
*/
public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered {
@ -59,6 +53,7 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
private final List<MessageToByteEncoder<?>> serializers;
private final List<MessageToByteEncoder<ByteBuffer>> postProcessors;
private final ConversionService conversionService;
private int order = 0;
@ -68,8 +63,14 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
}
public ResponseBodyResultHandler(List<MessageToByteEncoder<?>> serializers, List<MessageToByteEncoder<ByteBuffer>> postProcessors) {
this(serializers, postProcessors, new DefaultConversionService());
}
public ResponseBodyResultHandler(List<MessageToByteEncoder<?>> serializers, List<MessageToByteEncoder<ByteBuffer>>
postProcessors, ConversionService conversionService) {
this.serializers = serializers;
this.postProcessors = postProcessors;
this.conversionService = conversionService;
}
public void setOrder(int order) {
@ -87,14 +88,13 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
Object handler = result.getHandler();
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Type publisherVoidType = new ParameterizedTypeReference<Publisher<Void>>(){}.getType();
return AnnotatedElementUtils.isAnnotated(handlerMethod.getMethod(), ResponseBody.class.getName()) &&
!handlerMethod.getReturnType().getGenericParameterType().equals(publisherVoidType);
return AnnotatedElementUtils.isAnnotated(handlerMethod.getMethod(), ResponseBody.class.getName());
}
return false;
}
@Override
@SuppressWarnings("unchecked")
public Publisher<Void> handleResult(ServerHttpRequest request, ServerHttpResponse response,
HandlerResult result) {
@ -106,38 +106,27 @@ public class ResponseBodyResultHandler implements HandlerResultHandler, Ordered
return Publishers.empty();
}
MediaType mediaType = resolveMediaType(request);
ResolvableType type = ResolvableType.forMethodParameter(returnType);
MediaType mediaType = resolveMediaType(request);
List<Object> hints = new ArrayList<>();
hints.add(UTF_8);
MessageToByteEncoder<Object> serializer = (MessageToByteEncoder<Object>)resolveSerializer(request, type, mediaType, hints.toArray());
Publisher<Object> elementStream;
ResolvableType elementType;
if (conversionService.canConvert(type.getRawClass(), Publisher.class)) {
elementStream = conversionService.convert(value, Publisher.class);
elementType = type.getGeneric(0);
}
else {
elementStream = Publishers.just(value);
elementType = type;
}
MessageToByteEncoder<Object> serializer = (MessageToByteEncoder<Object>) resolveSerializer(request, elementType, mediaType, hints.toArray());
if (serializer != null) {
Publisher<Object> elementStream;
// TODO: Refactor type conversion
if (Promise.class.isAssignableFrom(type.getRawClass())) {
elementStream = ((Promise)value).stream();
}
else if (Observable.class.isAssignableFrom(type.getRawClass())) {
elementStream = RxJava1Converter.from((Observable) value);
}
else if (Single.class.isAssignableFrom(type.getRawClass())) {
elementStream = RxJava1SingleConverter.from((Single)value);
}
else if (CompletableFuture.class.isAssignableFrom(type.getRawClass())) {
elementStream = CompletableFutureConverter.from((CompletableFuture) value);
}
else if (Publisher.class.isAssignableFrom(type.getRawClass())) {
elementStream = (Publisher)value;
}
else {
elementStream = Publishers.just(value);
}
Publisher<ByteBuffer> outputStream = serializer.encode(elementStream, type, mediaType, hints.toArray());
List<MessageToByteEncoder<ByteBuffer>> postProcessors = resolvePostProcessors(request, type, mediaType, hints.toArray());
List<MessageToByteEncoder<ByteBuffer>> postProcessors = resolvePostProcessors(request, elementType, mediaType, hints.toArray());
for (MessageToByteEncoder<ByteBuffer> postProcessor : postProcessors) {
outputStream = postProcessor.encode(outputStream, type, mediaType, hints.toArray());
outputStream = postProcessor.encode(outputStream, elementType, mediaType, hints.toArray());
}
response.getHeaders().setContentType(mediaType);
return response.writeWith(outputStream);

View File

@ -30,9 +30,18 @@ public interface ServerHttpResponse extends HttpMessage {
void setStatusCode(HttpStatus status);
/**
* Write the response headers. This method must be invoked to send responses without body.
* @return A {@code Publisher<Void>} used to signal the demand, and receive a notification
* when the handling is complete (success or error) including the flush of the data on the
* network.
*/
Publisher<Void> writeHeaders();
/**
* Write the provided reactive stream of bytes to the response body. Most servers
* support multiple {@code writeWith} calls.
* support multiple {@code writeWith} calls. Headers are written automatically
* before the body, so not need to call {@link #writeHeaders()} explicitly.
* @param contentPublisher the stream to write in the response body.
* @return A {@code Publisher<Void>} used to signal the demand, and receive a notification
* when the handling is complete (success or error) including the flush of the data on the

View File

@ -15,13 +15,13 @@
*/
package org.springframework.reactive.web.http.reactor;
import org.reactivestreams.Publisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.reactive.web.http.ServerHttpRequest;
import org.springframework.util.Assert;
import reactor.io.buffer.Buffer;
import reactor.io.net.http.HttpChannel;
import reactor.rx.Stream;
import java.net.URI;
import java.net.URISyntaxException;
@ -72,7 +72,7 @@ public class ReactorServerHttpRequest implements ServerHttpRequest {
}
@Override
public Publisher<ByteBuffer> getBody() {
public Stream<ByteBuffer> getBody() {
return this.channel.map(Buffer::byteBuffer);
}

View File

@ -24,6 +24,7 @@ import reactor.Publishers;
import reactor.io.buffer.Buffer;
import reactor.io.net.http.HttpChannel;
import reactor.io.net.http.model.Status;
import reactor.rx.Stream;
import java.nio.ByteBuffer;
@ -57,18 +58,28 @@ public class ReactorServerHttpResponse implements ServerHttpResponse {
}
@Override
public Publisher<Void> writeWith(Publisher<ByteBuffer> contentPublisher) {
writeHeaders();
public Publisher<Void> writeHeaders() {
if (this.headersWritten) {
return Publishers.empty();
}
applyHeaders();
return this.channel.writeHeaders();
}
@Override
public Stream<Void> writeWith(Publisher<ByteBuffer> contentPublisher) {
applyHeaders();
return this.channel.writeWith(Publishers.map(contentPublisher, Buffer::new));
}
private void writeHeaders() {
private void applyHeaders() {
if (!this.headersWritten) {
for (String name : this.headers.keySet()) {
for (String value : this.headers.get(name)) {
this.channel.responseHeaders().add(name, value);
}
}
this.headersWritten = true;
}
}
}

View File

@ -23,6 +23,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.reactive.web.http.ServerHttpResponse;
import org.springframework.util.Assert;
import reactor.Publishers;
import reactor.core.publisher.convert.RxJava1Converter;
import reactor.io.buffer.Buffer;
import rx.Observable;
@ -59,24 +60,30 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse {
return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
}
public Observable<Void> writeWith(Observable<ByteBuffer> contentPublisher) {
return this.response.writeBytes(contentPublisher.map(content -> new Buffer(content).asBytes()));
@Override
public Publisher<Void> writeHeaders() {
if (this.headersWritten) {
return Publishers.empty();
}
applyHeaders();
return RxJava1Converter.from(this.response.sendHeaders());
}
@Override
public Publisher<Void> writeWith(Publisher<ByteBuffer> contentPublisher) {
writeHeaders();
applyHeaders();
Observable<byte[]> contentObservable = RxJava1Converter.from(contentPublisher).map(content -> new Buffer(content).asBytes());
return RxJava1Converter.from(this.response.writeBytes(contentObservable));
}
private void writeHeaders() {
private void applyHeaders() {
if (!this.headersWritten) {
for (String name : this.headers.keySet()) {
for (String value : this.headers.get(name)) {
this.response.addHeader(name, value);
}
}
this.headersWritten = true;
}
}
}

View File

@ -22,6 +22,7 @@ import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.reactivestreams.Publisher;
import reactor.Publishers;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@ -61,13 +62,19 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
}
@Override
public Publisher<Void> writeHeaders() {
applyHeaders();
return Publishers.empty();
}
@Override
public Publisher<Void> writeWith(final Publisher<ByteBuffer> contentPublisher) {
writeHeaders();
applyHeaders();
return (s -> contentPublisher.subscribe(responseSubscriber));
}
private void writeHeaders() {
private void applyHeaders() {
if (!this.headersWritten) {
for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
String headerName = entry.getKey();

View File

@ -2,6 +2,7 @@ log4j.rootCategory=WARN, stdout
log4j.logger.org.springframework.reactive=DEBUG
log4j.logger.org.springframework.web=DEBUG
log4j.logger.reactor=INFO
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout

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.decoder;
import java.nio.ByteBuffer;
import java.util.List;
import static org.junit.Assert.*;
import org.junit.Test;
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;
/**
* @author Sebastien Deleuze
*/
public class ByteBufferDecoderTests {
private final ByteBufferDecoder decoder = new ByteBufferDecoder();
@Test
public void canDecode() {
assertTrue(decoder.canDecode(ResolvableType.forClass(ByteBuffer.class), MediaType.TEXT_PLAIN));
assertFalse(decoder.canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN));
assertTrue(decoder.canDecode(ResolvableType.forClass(ByteBuffer.class), MediaType.APPLICATION_JSON));
}
@Test
public void decode() throws InterruptedException {
ByteBuffer fooBuffer = Buffer.wrap("foo").byteBuffer();
ByteBuffer barBuffer = Buffer.wrap("bar").byteBuffer();
Stream<ByteBuffer> source = Streams.just(fooBuffer, barBuffer);
List<ByteBuffer> results = Streams.wrap(decoder.decode(source,
ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null)).toList().await();
assertEquals(2, results.size());
assertEquals(fooBuffer, results.get(0));
assertEquals(barBuffer, results.get(1));
}
}

View File

@ -31,9 +31,35 @@ import reactor.rx.Streams;
*/
public class JsonObjectDecoderTests {
@Test
public void decodeSingleChunkToJsonObject() throws InterruptedException {
JsonObjectDecoder decoder = new JsonObjectDecoder();
Stream<ByteBuffer> source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").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(1, results.size());
assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0));
}
@Test
public void decodeMultipleChunksToJsonObject() throws InterruptedException {
JsonObjectDecoder decoder = new JsonObjectDecoder();
Stream<ByteBuffer> source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\"").byteBuffer(), Buffer.wrap(", \"bar\": \"barbar\"}").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(1, results.size());
assertEquals("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}", results.get(0));
}
@Test
public void decodeSingleChunkToArray() throws InterruptedException {
JsonObjectDecoder decoder = new JsonObjectDecoder(true);
JsonObjectDecoder decoder = new JsonObjectDecoder();
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()];
@ -47,7 +73,7 @@ public class JsonObjectDecoderTests {
@Test
public void decodeMultipleChunksToArray() throws InterruptedException {
JsonObjectDecoder decoder = new JsonObjectDecoder(true);
JsonObjectDecoder decoder = new JsonObjectDecoder();
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()];

View File

@ -23,13 +23,13 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
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.Pojo;
/**
* @author Sebastien Deleuze
@ -40,15 +40,16 @@ public class StringDecoderTests {
@Test
public void canDecode() {
assertTrue(decoder.canDecode(null, MediaType.TEXT_PLAIN));
assertFalse(decoder.canDecode(null, MediaType.APPLICATION_JSON));
assertTrue(decoder.canDecode(ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN));
assertFalse(decoder.canDecode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN));
assertFalse(decoder.canDecode(ResolvableType.forClass(String.class), 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();
List<String> results = Streams.wrap(decoder.decode(source,
ResolvableType.forClassWithGenerics(Publisher.class, String.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.ByteBuffer;
import java.util.List;
import static org.junit.Assert.*;
import org.junit.Test;
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;
/**
* @author Sebastien Deleuze
*/
public class ByteBufferDecoderEncoder {
private final ByteBufferEncoder encoder = new ByteBufferEncoder();
@Test
public void canDecode() {
assertTrue(encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), MediaType.TEXT_PLAIN));
assertFalse(encoder.canEncode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN));
assertTrue(encoder.canEncode(ResolvableType.forClass(ByteBuffer.class), MediaType.APPLICATION_JSON));
}
@Test
public void decode() throws InterruptedException {
ByteBuffer fooBuffer = Buffer.wrap("foo").byteBuffer();
ByteBuffer barBuffer = Buffer.wrap("bar").byteBuffer();
Stream<ByteBuffer> source = Streams.just(fooBuffer, barBuffer);
List<ByteBuffer> results = Streams.wrap(encoder.encode(source,
ResolvableType.forClassWithGenerics(Publisher.class, ByteBuffer.class), null)).toList().await();
assertEquals(2, results.size());
assertEquals(fooBuffer, results.get(0));
assertEquals(barBuffer, results.get(1));
}
}

View File

@ -32,9 +32,24 @@ import reactor.rx.Streams;
public class JsonObjectEncoderTests {
@Test
public void encodeToArray() throws InterruptedException {
public void encodeSingleElement() 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());
Stream<ByteBuffer> source = Streams.just(Buffer.wrap("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}").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\"}", result);
}
@Test
public void encodeTwoElements() 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);
@ -44,4 +59,21 @@ public class JsonObjectEncoderTests {
assertEquals("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]", result);
}
@Test
public void encodeThreeElements() 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(),
Buffer.wrap("{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}").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\"},{\"foo\": \"foofoofoofoo\", \"bar\": \"barbarbarbar\"}]", result);
}
}

View File

@ -23,8 +23,10 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.rx.Streams;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
/**
@ -36,8 +38,9 @@ public class StringEncoderTests {
@Test
public void canWrite() {
assertTrue(encoder.canEncode(null, MediaType.TEXT_PLAIN));
assertFalse(encoder.canEncode(null, MediaType.APPLICATION_JSON));
assertTrue(encoder.canEncode(ResolvableType.forClass(String.class), MediaType.TEXT_PLAIN));
assertFalse(encoder.canEncode(ResolvableType.forClass(Integer.class), MediaType.TEXT_PLAIN));
assertFalse(encoder.canEncode(ResolvableType.forClass(String.class), MediaType.APPLICATION_JSON));
}
@Test

View File

@ -17,13 +17,20 @@ package org.springframework.reactive.web.dispatch.method.annotation;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.core.ResolvableType;
import reactor.io.buffer.Buffer;
import reactor.rx.Promise;
import reactor.rx.Promises;
import reactor.rx.Stream;
@ -32,13 +39,16 @@ import rx.Observable;
import rx.Single;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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;
import org.springframework.reactive.web.http.AbstractHttpHandlerIntegrationTests;
import org.springframework.reactive.web.http.HttpHandler;
import org.springframework.stereotype.Controller;
@ -52,18 +62,25 @@ import org.springframework.web.context.support.StaticWebApplicationContext;
/**
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @author Stephane Maldini
*/
public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrationTests {
private TestController controller;
@Override
protected HttpHandler createHttpHandler() {
StaticWebApplicationContext wac = new StaticWebApplicationContext();
DefaultListableBeanFactory factory = wac.getDefaultListableBeanFactory();
wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class);
wac.registerSingleton("handlerAdapter", RequestMappingHandlerAdapter.class);
wac.getDefaultListableBeanFactory().registerSingleton("responseBodyResultHandler",
new ResponseBodyResultHandler(Arrays.asList(new StringEncoder(), new JacksonJsonEncoder()), Arrays.asList(new JsonObjectEncoder())));
wac.registerSingleton("controller", TestController.class);
factory.registerSingleton("responseBodyResultHandler",
new ResponseBodyResultHandler(Arrays.asList(new ByteBufferEncoder(), new StringEncoder(), new JacksonJsonEncoder()), Arrays.asList
(new JsonObjectEncoder())));
wac.registerSingleton("simpleResultHandler", SimpleHandlerResultHandler.class);
this.controller = new TestController();
factory.registerSingleton("controller", this.controller);
wac.refresh();
DispatcherHandler dispatcherHandler = new DispatcherHandler();
@ -83,6 +100,30 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
assertEquals("Hello George!", response.getBody());
}
@Test
public void rawPojoResponse() throws Exception {
RestTemplate restTemplate = new RestTemplate();
URI url = new URI("http://localhost:" + port + "/raw");
RequestEntity<Void> request = RequestEntity.get(url).build();
Person person = restTemplate.exchange(request, Person.class).getBody();
assertEquals(new Person("Robert"), person);
}
@Test
public void rawHelloResponse() throws Exception {
RestTemplate restTemplate = new RestTemplate();
URI url = new URI("http://localhost:" + port + "/raw-observable");
RequestEntity<Void> request = RequestEntity.get(url).build();
ResponseEntity<String> response = restTemplate.exchange(request, String.class);
assertEquals("Hello!", response.getBody());
}
@Test
public void serializeAsPojo() throws Exception {
serializeAsPojo("http://localhost:" + port + "/person");
@ -153,6 +194,19 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
capitalizePojo("http://localhost:" + port + "/promise-capitalize");
}
@Test
public void create() throws Exception {
RestTemplate restTemplate = new RestTemplate();
URI url = new URI("http://localhost:" + port + "/create");
List<Person> persons = Arrays.asList(new Person("Robert"), new Person("Marie"));
RequestEntity<List<Person>> request = RequestEntity.post(url).contentType(MediaType.APPLICATION_JSON).body(persons);
ResponseEntity<Void> response = restTemplate.exchange(request, Void.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(2, this.controller.persons.size());
}
public void serializeAsPojo(String requestUrl) throws Exception {
RestTemplate restTemplate = new RestTemplate();
@ -164,6 +218,17 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
assertEquals(new Person("Robert"), response.getBody());
}
public void postAsPojo(String requestUrl) throws Exception {
RestTemplate restTemplate = new RestTemplate();
URI url = new URI(requestUrl);
RequestEntity<Person> request = RequestEntity.post(url).accept(MediaType.APPLICATION_JSON).body(new Person
("Robert"));
ResponseEntity<Person> response = restTemplate.exchange(request, Person.class);
assertEquals(new Person("Robert"), response.getBody());
}
public void serializeAsCollection(String requestUrl) throws Exception {
RestTemplate restTemplate = new RestTemplate();
@ -214,6 +279,8 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
@SuppressWarnings("unused")
private static class TestController {
final List<Person> persons = new ArrayList<>();
@RequestMapping("/param")
@ResponseBody
public Publisher<String> handleWithParam(@RequestParam String name) {
@ -232,6 +299,19 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
return CompletableFuture.completedFuture(new Person("Robert"));
}
@RequestMapping("/raw")
@ResponseBody
public Publisher<ByteBuffer> rawResponseBody() {
JacksonJsonEncoder encoder = new JacksonJsonEncoder();
return encoder.encode(Streams.just(new Person("Robert")), ResolvableType.forClass(Person.class), MediaType.APPLICATION_JSON);
}
@RequestMapping("/raw-observable")
@ResponseBody
public Observable<ByteBuffer> rawObservableResponseBody() {
return Observable.just(Buffer.wrap("Hello!").byteBuffer());
}
@RequestMapping("/single")
@ResponseBody
public Single<Person> singleResponseBody() {
@ -322,6 +402,13 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
});
}
@RequestMapping("/create")
public Publisher<Void> create(@RequestBody Stream<Person> personStream) {
return personStream.toList().onSuccess(personList -> persons.addAll(personList)).after();
}
//TODO add mixed and T request mappings tests
}
private static class Person {

View File

@ -44,7 +44,7 @@ public class ResponseBodyResultHandlerTests {
assertTrue(resultHandler.supports(new HandlerResult(publisherStringMethod, null)));
HandlerMethod publisherVoidMethod = new HandlerMethod(controller, TestController.class.getMethod("publisherVoid"));
assertFalse(resultHandler.supports(new HandlerResult(publisherVoidMethod, null)));
assertTrue(resultHandler.supports(new HandlerResult(publisherVoidMethod, null)));
}