Browse Source

Add ReactiveAdapterRegistry

Issue: SPR-14159
pull/1112/merge
Rossen Stoyanchev 10 years ago
parent
commit
101220bad1
  1. 116
      spring-core/src/main/java/org/springframework/core/ReactiveAdapter.java
  2. 282
      spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java
  3. 62
      spring-core/src/main/java/org/springframework/core/convert/support/MonoToCompletableFutureConverter.java
  4. 81
      spring-core/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java
  5. 56
      spring-core/src/test/java/org/springframework/core/convert/support/MonoToCompletableFutureConverterTests.java
  6. 80
      spring-core/src/test/java/org/springframework/core/convert/support/ReactiveAdapterRegistryTests.java
  7. 69
      spring-core/src/test/java/org/springframework/core/convert/support/ReactorToRxJava1ConverterTests.java
  8. 21
      spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java
  9. 24
      spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java
  10. 58
      spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java
  11. 67
      spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java
  12. 65
      spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java
  13. 43
      spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java
  14. 26
      spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java
  15. 18
      spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java
  16. 49
      spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java
  17. 74
      spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java
  18. 36
      spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java
  19. 4
      spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java
  20. 3
      spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java
  21. 9
      spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java
  22. 3
      spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleUrlHandlerMappingIntegrationTests.java
  23. 21
      spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java
  24. 20
      spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageReaderArgumentResolverTests.java
  25. 33
      spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerTests.java
  26. 14
      spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java
  27. 11
      spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java
  28. 19
      spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java
  29. 20
      spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java

116
spring-core/src/main/java/org/springframework/core/ReactiveAdapter.java

@ -0,0 +1,116 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Contract for adapting to and from {@link Flux} and {@link Mono}.
*
* <p>An adapter supports a specific adaptee type whose stream semantics can be
* checked via {@link #getDescriptor()}.
*
* <p>Use the {@link ReactiveAdapterRegistry} to obtain an adapter for a
* supported adaptee type or to register additional adapters.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public interface ReactiveAdapter {
/**
* Return a descriptor with further information about the adaptee.
*/
Descriptor getDescriptor();
/**
* Adapt the given Object to a {@link Mono}
* @param source the source object to adapt
* @return the resulting {@link Mono} possibly empty
*/
<T> Mono<T> toMono(Object source);
/**
* Adapt the given Object to a {@link Flux}.
* @param source the source object to adapt
* @return the resulting {@link Flux} possibly empty
*/
<T> Flux<T> toFlux(Object source);
/**
* Adapt the given Object to a Publisher.
* @param source the source object to adapt
* @return the resulting {@link Mono} or {@link Flux} possibly empty
*/
<T> Publisher<T> toPublisher(Object source);
/**
* Adapt the given Publisher to the target adaptee.
* @param publisher the publisher to adapt
* @return the resulting adaptee
*/
Object fromPublisher(Publisher<?> publisher);
/**
* A descriptor with information about the adaptee stream semantics.
*/
class Descriptor {
private final boolean isMultiValue;
private final boolean supportsEmpty;
private final boolean isNoValue;
public Descriptor(boolean isMultiValue, boolean canBeEmpty, boolean isNoValue) {
this.isMultiValue = isMultiValue;
this.supportsEmpty = canBeEmpty;
this.isNoValue = isNoValue;
}
/**
* Return {@code true} if the adaptee implies 0..N values can be produced
* and is therefore a good fit to adapt to {@link Flux}. A {@code false}
* return value implies the adaptee will produce 1 value at most and is
* therefore a good fit for {@link Mono}.
*/
public boolean isMultiValue() {
return this.isMultiValue;
}
/**
* Return {@code true} if the adaptee can complete without values.
*/
public boolean supportsEmpty() {
return this.supportsEmpty;
}
/**
* Return {@code true} if the adaptee implies no values will be produced,
* i.e. providing only completion or error signal.
*/
public boolean isNoValue() {
return this.isNoValue;
}
}
}

282
spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java

@ -0,0 +1,282 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.function.Predicate;
import org.reactivestreams.Publisher;
import reactor.adapter.RxJava1Adapter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import rx.Completable;
import rx.Observable;
import rx.Single;
import org.springframework.core.ReactiveAdapter.Descriptor;
import org.springframework.util.ClassUtils;
/**
* A registry of adapters to adapt to {@link Flux} and {@link Mono}.
*
* <p>By default there are adapters for {@link CompletableFuture}, RxJava 1, and
* also for a any Reactive Streams {@link Publisher}. Additional adapters can be
* registered via {@link #registerFluxAdapter) and {@link #registerMonoAdapter}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ReactiveAdapterRegistry {
private static final boolean rxJava1Present =
ClassUtils.isPresent("rx.Observable", ReactiveAdapterRegistry.class.getClassLoader());
private final Map<Class<?>, ReactiveAdapter> adapterMap = new LinkedHashMap<>();
/**
* Create a registry and auto-register default adapters.
*/
public ReactiveAdapterRegistry() {
// Flux and Mono ahead of Publisher...
registerMonoAdapter(Mono.class,
source -> (Mono<?>) source, source -> source, new Descriptor(false, true, false));
registerFluxAdapter(
Flux.class, source -> (Flux<?>) source, source -> source);
registerFluxAdapter(
Publisher.class, source -> Flux.from((Publisher<?>) source), source -> source);
registerMonoAdapter(CompletableFuture.class,
source -> Mono.fromFuture((CompletableFuture<?>) source),
source -> Mono.from((Publisher<?>) source).toFuture(),
new Descriptor(false, true, false)
);
if (rxJava1Present) {
new RxJava1AdapterRegistrar().register(this);
}
}
/**
* Register an adapter for adapting to and from a {@link Mono}. The provided
* functions can assume that input will never be {@code null} and also that
* any {@link Optional} wrapper is unwrapped.
*/
public void registerMonoAdapter(Class<?> adapteeType,
Function<Object, Mono<?>> toAdapter, Function<Mono<?>, Object> fromAdapter,
Descriptor descriptor) {
this.adapterMap.put(adapteeType, new MonoReactiveAdapter(toAdapter, fromAdapter, descriptor));
}
/**
* Register an adapter for adapting to and from a {@link Flux}. The provided
* functions can assume that input will never be {@code null} and also that
* any {@link Optional} wrapper is unwrapped.
*/
public void registerFluxAdapter(Class<?> adapteeType,
Function<Object, Flux<?>> toAdapter, Function<Flux<?>, Object> fromAdapter) {
this.adapterMap.put(adapteeType, new FluxReactiveAdapter(toAdapter, fromAdapter));
}
/**
* Get the adapter for the given adaptee type to adapt from.
*/
public ReactiveAdapter getAdapterFrom(Class<?> adapteeType) {
return getAdapterFrom(adapteeType, null);
}
/**
* Get the adapter for the given adaptee type to adapt from.
* If the instance is not {@code null} its actual type is used to check.
*/
public ReactiveAdapter getAdapterFrom(Class<?> adapteeType, Object adaptee) {
Class<?> actualType = getActualType(adapteeType, adaptee);
return getAdapterInternal(supportedType -> supportedType.isAssignableFrom(actualType));
}
/**
* Get the adapter for the given adaptee type to adapt to.
*/
public ReactiveAdapter getAdapterTo(Class<?> adapteeType) {
return getAdapterTo(adapteeType, null);
}
/**
* Get the adapter for the given adaptee type to adapt to.
* If the instance is not {@code null} its actual type is used to check.
*/
public ReactiveAdapter getAdapterTo(Class<?> adapteeType, Object adaptee) {
Class<?> actualType = getActualType(adapteeType, adaptee);
return getAdapterInternal(supportedType -> supportedType.equals(actualType));
}
private static Class<?> getActualType(Class<?> adapteeType, Object adaptee) {
adaptee = unwrapOptional(adaptee);
return (adaptee != null ? adaptee.getClass() : adapteeType);
}
private static Object unwrapOptional(Object value) {
if (value != null && value instanceof Optional) {
value = ((Optional<?>) value).orElse(null);
}
return value;
}
private ReactiveAdapter getAdapterInternal(Predicate<Class<?>> adapteeTypePredicate) {
return this.adapterMap.keySet().stream()
.filter(adapteeTypePredicate)
.map(this.adapterMap::get)
.findFirst()
.orElse(null);
}
@SuppressWarnings("unchecked")
private static class MonoReactiveAdapter implements ReactiveAdapter {
private final Function<Object, Mono<?>> toAdapter;
private final Function<Mono<?>, Object> fromAdapter;
private final Descriptor descriptor;
MonoReactiveAdapter(Function<Object, Mono<?>> to, Function<Mono<?>, Object> from, Descriptor descriptor) {
this.toAdapter = to;
this.fromAdapter = from;
this.descriptor = descriptor;
}
@Override
public Descriptor getDescriptor() {
return this.descriptor;
}
@Override
public <T> Mono<T> toMono(Object source) {
source = unwrapOptional(source);
if (source == null) {
return Mono.empty();
}
return (Mono<T>) this.toAdapter.apply(source);
}
@Override
public <T> Flux<T> toFlux(Object source) {
source = unwrapOptional(source);
if (source == null) {
return Flux.empty();
}
return (Flux<T>) this.toMono(source).flux();
}
@Override
public <T> Publisher<T> toPublisher(Object source) {
return toMono(source);
}
@Override
public Object fromPublisher(Publisher<?> source) {
return (source != null ? this.fromAdapter.apply((Mono<?>) source) : null);
}
}
@SuppressWarnings("unchecked")
private static class FluxReactiveAdapter implements ReactiveAdapter {
private final Function<Object, Flux<?>> toAdapter;
private final Function<Flux<?>, Object> fromAdapter;
private final Descriptor descriptor = new Descriptor(true, true, false);
FluxReactiveAdapter(Function<Object, Flux<?>> to, Function<Flux<?>, Object> from) {
this.toAdapter = to;
this.fromAdapter = from;
}
@Override
public Descriptor getDescriptor() {
return this.descriptor;
}
@Override
public <T> Mono<T> toMono(Object source) {
source = unwrapOptional(source);
if (source == null) {
return Mono.empty();
}
return (Mono<T>) this.toAdapter.apply(source).next();
}
@Override
public <T> Flux<T> toFlux(Object source) {
source = unwrapOptional(source);
if (source == null) {
return Flux.empty();
}
return (Flux<T>) this.toAdapter.apply(source);
}
@Override
public <T> Publisher<T> toPublisher(Object source) {
return toFlux(source);
}
@Override
public Object fromPublisher(Publisher<?> source) {
return (source != null ? this.fromAdapter.apply((Flux<?>) source) : null);
}
}
private static class RxJava1AdapterRegistrar {
public void register(ReactiveAdapterRegistry registry) {
registry.registerFluxAdapter(Observable.class,
source -> RxJava1Adapter.observableToFlux((Observable<?>) source),
RxJava1Adapter::publisherToObservable
);
registry.registerMonoAdapter(Single.class,
source -> RxJava1Adapter.singleToMono((Single<?>) source),
RxJava1Adapter::publisherToSingle,
new Descriptor(false, false, false)
);
registry.registerMonoAdapter(Completable.class,
source -> RxJava1Adapter.completableToMono((Completable) source),
RxJava1Adapter::publisherToCompletable,
new Descriptor(false, true, true)
);
}
}
}

62
spring-core/src/main/java/org/springframework/core/convert/support/MonoToCompletableFutureConverter.java

@ -1,62 +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.core.convert.support;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
/**
* Converter to adapt {@link CompletableFuture} to Reactive Streams and
* Reactor {@link Mono}.
*
* @author Sebastien Deleuze
* @since 5.0
*/
public class MonoToCompletableFutureConverter implements GenericConverter {
@Override
public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
Set<GenericConverter.ConvertiblePair> pairs = new LinkedHashSet<>(2);
pairs.add(new GenericConverter.ConvertiblePair(Mono.class, CompletableFuture.class));
pairs.add(new GenericConverter.ConvertiblePair(CompletableFuture.class, Mono.class));
return pairs;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
else if (CompletableFuture.class.isAssignableFrom(sourceType.getType())) {
return Mono.fromFuture((CompletableFuture<?>) source);
}
else if (CompletableFuture.class.isAssignableFrom(targetType.getType())) {
return Mono.from((Publisher<?>) source).toFuture();
}
return null;
}
}

81
spring-core/src/main/java/org/springframework/core/convert/support/ReactorToRxJava1Converter.java

@ -1,81 +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.core.convert.support;
import java.util.LinkedHashSet;
import java.util.Set;
import org.reactivestreams.Publisher;
import reactor.adapter.RxJava1Adapter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import rx.Completable;
import rx.Observable;
import rx.Single;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
/**
* Converter to adapt RxJava1 {@link Observable}, {@link Single}, and
* {@link Completable} to Reactive Streams and Reactor types.
*
* @author Stephane Maldini
* @author Sebastien Deleuze
* @since 5.0
*/
public final class ReactorToRxJava1Converter implements GenericConverter {
@Override
public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
Set<GenericConverter.ConvertiblePair> pairs = new LinkedHashSet<>(6);
pairs.add(new GenericConverter.ConvertiblePair(Flux.class, Observable.class));
pairs.add(new GenericConverter.ConvertiblePair(Observable.class, Flux.class));
pairs.add(new GenericConverter.ConvertiblePair(Mono.class, Single.class));
pairs.add(new GenericConverter.ConvertiblePair(Single.class, Mono.class));
pairs.add(new GenericConverter.ConvertiblePair(Mono.class, Completable.class));
pairs.add(new GenericConverter.ConvertiblePair(Completable.class, Mono.class));
return pairs;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
if (Observable.class.isAssignableFrom(sourceType.getType())) {
return RxJava1Adapter.observableToFlux((Observable<?>) source);
}
else if (Observable.class.isAssignableFrom(targetType.getType())) {
return RxJava1Adapter.publisherToObservable((Publisher<?>) source);
}
else if (Single.class.isAssignableFrom(sourceType.getType())) {
return RxJava1Adapter.singleToMono((Single<?>) source);
}
else if (Single.class.isAssignableFrom(targetType.getType())) {
return RxJava1Adapter.publisherToSingle((Publisher<?>) source);
}
else if (Completable.class.isAssignableFrom(sourceType.getType())) {
return RxJava1Adapter.completableToMono((Completable) source);
}
else if (Completable.class.isAssignableFrom(targetType.getType())) {
return RxJava1Adapter.publisherToCompletable((Publisher<?>) source);
}
return null;
}
}

56
spring-core/src/test/java/org/springframework/core/convert/support/MonoToCompletableFutureConverterTests.java

@ -1,56 +0,0 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.convert.support;
import java.util.concurrent.CompletableFuture;
import org.junit.Before;
import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Unit tests for {@link ReactorToRxJava1Converter}.
* @author Rossen Stoyanchev
*/
public class MonoToCompletableFutureConverterTests {
private GenericConversionService conversionService;
@Before
public void setUp() throws Exception {
this.conversionService = new GenericConversionService();
this.conversionService.addConverter(new MonoToCompletableFutureConverter());
}
@Test
public void canConvert() throws Exception {
assertTrue(this.conversionService.canConvert(Mono.class, CompletableFuture.class));
assertTrue(this.conversionService.canConvert(CompletableFuture.class, Mono.class));
assertFalse(this.conversionService.canConvert(Flux.class, CompletableFuture.class));
assertFalse(this.conversionService.canConvert(CompletableFuture.class, Flux.class));
assertFalse(this.conversionService.canConvert(Publisher.class, CompletableFuture.class));
assertFalse(this.conversionService.canConvert(CompletableFuture.class, Publisher.class));
}
}

80
spring-core/src/test/java/org/springframework/core/convert/support/ReactiveAdapterRegistryTests.java

@ -0,0 +1,80 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.convert.support;
import java.util.concurrent.CompletableFuture;
import org.junit.Before;
import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import rx.Completable;
import rx.Observable;
import rx.Single;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* Unit tests for {@link ReactiveAdapterRegistry}.
* @author Rossen Stoyanchev
*/
public class ReactiveAdapterRegistryTests {
private ReactiveAdapterRegistry adapterRegistry;
@Before
public void setUp() throws Exception {
this.adapterRegistry = new ReactiveAdapterRegistry();
}
@Test
public void getDefaultAdapters() throws Exception {
testMonoAdapter(Mono.class);
testFluxAdapter(Flux.class);
testFluxAdapter(Publisher.class);
testMonoAdapter(CompletableFuture.class);
testFluxAdapter(Observable.class);
testMonoAdapter(Single.class);
testMonoAdapter(Completable.class);
}
private void testFluxAdapter(Class<?> adapteeType) {
ReactiveAdapter adapter = this.adapterRegistry.getAdapterFrom(adapteeType);
assertNotNull(adapter);
assertTrue(adapter.getDescriptor().isMultiValue());
adapter = this.adapterRegistry.getAdapterTo(adapteeType);
assertNotNull(adapter);
assertTrue(adapter.getDescriptor().isMultiValue());
}
private void testMonoAdapter(Class<?> adapteeType) {
ReactiveAdapter adapter = this.adapterRegistry.getAdapterFrom(adapteeType);
assertNotNull(adapter);
assertFalse(adapter.getDescriptor().isMultiValue());
adapter = this.adapterRegistry.getAdapterTo(adapteeType);
assertNotNull(adapter);
assertFalse(adapter.getDescriptor().isMultiValue());
}
}

69
spring-core/src/test/java/org/springframework/core/convert/support/ReactorToRxJava1ConverterTests.java

@ -1,69 +0,0 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.convert.support;
import org.junit.Before;
import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import rx.Completable;
import rx.Observable;
import rx.Single;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Unit tests for {@link ReactorToRxJava1Converter}.
* @author Rossen Stoyanchev
*/
public class ReactorToRxJava1ConverterTests {
private GenericConversionService conversionService;
@Before
public void setUp() throws Exception {
this.conversionService = new GenericConversionService();
this.conversionService.addConverter(new ReactorToRxJava1Converter());
}
@Test
public void canConvert() throws Exception {
assertTrue(this.conversionService.canConvert(Flux.class, Observable.class));
assertTrue(this.conversionService.canConvert(Observable.class, Flux.class));
assertTrue(this.conversionService.canConvert(Mono.class, Single.class));
assertTrue(this.conversionService.canConvert(Single.class, Mono.class));
assertTrue(this.conversionService.canConvert(Mono.class, Completable.class));
assertTrue(this.conversionService.canConvert(Completable.class, Mono.class));
assertFalse(this.conversionService.canConvert(Flux.class, Single.class));
assertFalse(this.conversionService.canConvert(Single.class, Flux.class));
assertFalse(this.conversionService.canConvert(Flux.class, Completable.class));
assertFalse(this.conversionService.canConvert(Completable.class, Flux.class));
assertFalse(this.conversionService.canConvert(Mono.class, Observable.class));
assertFalse(this.conversionService.canConvert(Observable.class, Mono.class));
assertFalse(this.conversionService.canConvert(Publisher.class, Observable.class));
assertFalse(this.conversionService.canConvert(Observable.class, Publisher.class));
}
}

21
spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java

@ -34,8 +34,6 @@ import org.springframework.core.codec.ResourceDecoder;
import org.springframework.core.codec.StringDecoder; import org.springframework.core.codec.StringDecoder;
import org.springframework.core.codec.StringEncoder; import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.format.Formatter; import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry; import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.DefaultFormattingConversionService;
@ -274,17 +272,8 @@ public class WebReactiveConfiguration implements ApplicationContextAware {
/** /**
* Override to add custom {@link Converter}s and {@link Formatter}s. * Override to add custom {@link Converter}s and {@link Formatter}s.
* <p>By default this method method registers:
* <ul>
* <li>{@link MonoToCompletableFutureConverter}
* <li>{@link ReactorToRxJava1Converter}
* </ul>
*/ */
protected void addFormatters(FormatterRegistry registry) { protected void addFormatters(FormatterRegistry registry) {
registry.addConverter(new MonoToCompletableFutureConverter());
if (rxJava1Present) {
registry.addConverter(new ReactorToRxJava1Converter());
}
} }
/** /**
@ -334,19 +323,17 @@ public class WebReactiveConfiguration implements ApplicationContextAware {
@Bean @Bean
public SimpleResultHandler simpleResultHandler() { public SimpleResultHandler simpleResultHandler() {
return new SimpleResultHandler(mvcConversionService()); return new SimpleResultHandler();
} }
@Bean @Bean
public ResponseEntityResultHandler responseEntityResultHandler() { public ResponseEntityResultHandler responseEntityResultHandler() {
return new ResponseEntityResultHandler(getMessageWriters(), mvcConversionService(), return new ResponseEntityResultHandler(getMessageWriters(), mvcContentTypeResolver());
mvcContentTypeResolver());
} }
@Bean @Bean
public ResponseBodyResultHandler responseBodyResultHandler() { public ResponseBodyResultHandler responseBodyResultHandler() {
return new ResponseBodyResultHandler(getMessageWriters(), mvcConversionService(), return new ResponseBodyResultHandler(getMessageWriters(), mvcContentTypeResolver());
mvcContentTypeResolver());
} }
/** /**
@ -405,7 +392,7 @@ public class WebReactiveConfiguration implements ApplicationContextAware {
ViewResolverRegistry registry = new ViewResolverRegistry(getApplicationContext()); ViewResolverRegistry registry = new ViewResolverRegistry(getApplicationContext());
configureViewResolvers(registry); configureViewResolvers(registry);
List<ViewResolver> resolvers = registry.getViewResolvers(); List<ViewResolver> resolvers = registry.getViewResolvers();
ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, mvcConversionService()); ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolvers, mvcContentTypeResolver());
handler.setDefaultViews(registry.getDefaultViews()); handler.setDefaultViews(registry.getDefaultViews());
handler.setOrder(registry.getOrder()); handler.setOrder(registry.getOrder());
return handler; return handler;

24
spring-web-reactive/src/main/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupport.java

@ -24,7 +24,7 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.convert.ConversionService; import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerMapping;
@ -43,28 +43,32 @@ public abstract class ContentNegotiatingResultHandlerSupport implements Ordered
private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application"); private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application");
private final ConversionService conversionService;
private final RequestedContentTypeResolver contentTypeResolver; private final RequestedContentTypeResolver contentTypeResolver;
private final ReactiveAdapterRegistry adapterRegistry;
private int order = LOWEST_PRECEDENCE; private int order = LOWEST_PRECEDENCE;
protected ContentNegotiatingResultHandlerSupport(ConversionService conversionService, protected ContentNegotiatingResultHandlerSupport(RequestedContentTypeResolver contentTypeResolver) {
RequestedContentTypeResolver contentTypeResolver) { this(contentTypeResolver, new ReactiveAdapterRegistry());
}
protected ContentNegotiatingResultHandlerSupport(RequestedContentTypeResolver contentTypeResolver,
ReactiveAdapterRegistry adapterRegistry) {
Assert.notNull(conversionService, "'conversionService' is required.");
Assert.notNull(contentTypeResolver, "'contentTypeResolver' is required."); Assert.notNull(contentTypeResolver, "'contentTypeResolver' is required.");
this.conversionService = conversionService; Assert.notNull(adapterRegistry, "'adapterRegistry' is required.");
this.contentTypeResolver = contentTypeResolver; this.contentTypeResolver = contentTypeResolver;
this.adapterRegistry = adapterRegistry;
} }
/** /**
* Return the configured {@link ConversionService}. * Return the configured {@link ReactiveAdapterRegistry}.
*/ */
public ConversionService getConversionService() { public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
return this.conversionService; return this.adapterRegistry;
} }
/** /**

58
spring-web-reactive/src/main/java/org/springframework/web/reactive/result/SimpleResultHandler.java

@ -18,14 +18,12 @@ package org.springframework.web.reactive.result;
import java.util.Optional; import java.util.Optional;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.HandlerResultHandler;
@ -45,27 +43,26 @@ import org.springframework.web.server.ServerWebExchange;
*/ */
public class SimpleResultHandler implements Ordered, HandlerResultHandler { public class SimpleResultHandler implements Ordered, HandlerResultHandler {
protected static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class); private ReactiveAdapterRegistry adapterRegistry;
protected static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class);
private ConversionService conversionService;
private int order = Ordered.LOWEST_PRECEDENCE; private int order = Ordered.LOWEST_PRECEDENCE;
public SimpleResultHandler(ConversionService conversionService) { public SimpleResultHandler() {
Assert.notNull(conversionService, "'conversionService' is required."); this.adapterRegistry = new ReactiveAdapterRegistry();
this.conversionService = conversionService; }
public SimpleResultHandler(ReactiveAdapterRegistry adapterRegistry) {
Assert.notNull(adapterRegistry, "'adapterRegistry' is required.");
this.adapterRegistry = adapterRegistry;
} }
/** /**
* Return the configured {@link ConversionService}. * Return the configured {@link ReactiveAdapterRegistry}.
*/ */
public ConversionService getConversionService() { public ReactiveAdapterRegistry getAdapterRegistry() {
return this.conversionService; return this.adapterRegistry;
} }
/** /**
@ -88,37 +85,28 @@ public class SimpleResultHandler implements Ordered, HandlerResultHandler {
@Override @Override
public boolean supports(HandlerResult result) { public boolean supports(HandlerResult result) {
ResolvableType type = result.getReturnType(); ResolvableType type = result.getReturnType();
if (Void.TYPE.equals(type.getRawClass())) { Class<?> rawClass = type.getRawClass();
if (Void.TYPE.equals(rawClass)) {
return true; return true;
} }
TypeDescriptor source = new TypeDescriptor(result.getReturnTypeSource()); ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(rawClass, result.getReturnValue());
if (Publisher.class.isAssignableFrom(type.getRawClass()) || if (adapter != null) {
canConvert(source, MONO_TYPE) || canConvert(source, FLUX_TYPE)) { Class<?> clazz = type.getGeneric(0).getRawClass();
Class<?> clazz = result.getReturnType().getGeneric(0).getRawClass();
return Void.class.equals(clazz); return Void.class.equals(clazz);
} }
return false; return false;
} }
private boolean canConvert(TypeDescriptor source, TypeDescriptor target) {
return getConversionService().canConvert(source, target);
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) { public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
Optional<Object> optional = result.getReturnValue(); Optional<Object> optionalValue = result.getReturnValue();
if (!optional.isPresent()) { if (!optionalValue.isPresent()) {
return Mono.empty(); return Mono.empty();
} }
Object value = optional.get(); Class<?> returnType = result.getReturnType().getRawClass();
if (Publisher.class.isAssignableFrom(result.getReturnType().getRawClass())) { ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(returnType, optionalValue);
return Mono.from((Publisher<?>) value).then(); return adapter.toMono(optionalValue);
}
TypeDescriptor source = new TypeDescriptor(result.getReturnTypeSource());
return canConvert(source, MONO_TYPE) ?
((Mono<Void>) getConversionService().convert(value, source, MONO_TYPE)) :
((Flux<Void>) getConversionService().convert(value, source, FLUX_TYPE)).single();
} }
} }

67
spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java

@ -25,10 +25,10 @@ import reactor.core.publisher.Mono;
import org.springframework.core.Conventions; import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.reactive.HttpMessageReader; import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
@ -57,34 +57,39 @@ import org.springframework.web.server.UnsupportedMediaTypeStatusException;
*/ */
public abstract class AbstractMessageReaderArgumentResolver { public abstract class AbstractMessageReaderArgumentResolver {
private static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class);
private static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class);
private final List<HttpMessageReader<?>> messageReaders; private final List<HttpMessageReader<?>> messageReaders;
private final ConversionService conversionService;
private final Validator validator; private final Validator validator;
private final ReactiveAdapterRegistry adapterRegistry;
private final List<MediaType> supportedMediaTypes; private final List<MediaType> supportedMediaTypes;
/** /**
* Constructor with message converters and a ConversionService. * Constructor with {@link HttpMessageReader}'s and a {@link Validator}.
* @param readers readers to convert from the request body
* @param validator validator to validate decoded objects with
*/
protected AbstractMessageReaderArgumentResolver(List<HttpMessageReader<?>> readers, Validator validator) {
this(readers, validator, new ReactiveAdapterRegistry());
}
/**
* Constructor that also accepts a {@link ReactiveAdapterRegistry}.
* @param messageReaders readers to convert from the request body * @param messageReaders readers to convert from the request body
* @param service for converting to other reactive types from Flux and Mono
* @param validator validator to validate decoded objects with * @param validator validator to validate decoded objects with
* @param adapterRegistry for adapting to other reactive types from Flux and Mono
*/ */
protected AbstractMessageReaderArgumentResolver(List<HttpMessageReader<?>> messageReaders, protected AbstractMessageReaderArgumentResolver(List<HttpMessageReader<?>> messageReaders,
ConversionService service, Validator validator) { Validator validator, ReactiveAdapterRegistry adapterRegistry) {
Assert.notEmpty(messageReaders, "At least one message reader is required."); Assert.notEmpty(messageReaders, "At least one HttpMessageReader is required.");
Assert.notNull(service, "'conversionService' is required."); Assert.notNull(adapterRegistry, "'adapterRegistry' is required");
this.messageReaders = messageReaders; this.messageReaders = messageReaders;
this.conversionService = service;
this.validator = validator; this.validator = validator;
this.adapterRegistry = adapterRegistry;
this.supportedMediaTypes = messageReaders.stream() this.supportedMediaTypes = messageReaders.stream()
.flatMap(converter -> converter.getReadableMediaTypes().stream()) .flatMap(converter -> converter.getReadableMediaTypes().stream())
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -99,22 +104,21 @@ public abstract class AbstractMessageReaderArgumentResolver {
} }
/** /**
* Return the configured {@link ConversionService}. * Return the configured {@link ReactiveAdapterRegistry}.
*/ */
public ConversionService getConversionService() { public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
return this.conversionService; return this.adapterRegistry;
} }
protected Mono<Object> readBody(MethodParameter bodyParameter, boolean isBodyRequired, protected Mono<Object> readBody(MethodParameter bodyParameter, boolean isBodyRequired,
ServerWebExchange exchange) { ServerWebExchange exchange) {
TypeDescriptor typeDescriptor = new TypeDescriptor(bodyParameter); Class<?> bodyType = ResolvableType.forMethodParameter(bodyParameter).resolve();
boolean convertFromMono = getConversionService().canConvert(MONO_TYPE, typeDescriptor); ReactiveAdapter adapter = getReactiveAdapterRegistry().getAdapterTo(bodyType);
boolean convertFromFlux = getConversionService().canConvert(FLUX_TYPE, typeDescriptor);
ResolvableType elementType = ResolvableType.forMethodParameter(bodyParameter); ResolvableType elementType = ResolvableType.forMethodParameter(bodyParameter);
if (convertFromMono || convertFromFlux) { if (adapter != null) {
elementType = elementType.getGeneric(0); elementType = elementType.getGeneric(0);
} }
@ -126,28 +130,28 @@ public abstract class AbstractMessageReaderArgumentResolver {
for (HttpMessageReader<?> reader : getMessageReaders()) { for (HttpMessageReader<?> reader : getMessageReaders()) {
if (reader.canRead(elementType, mediaType)) { if (reader.canRead(elementType, mediaType)) {
if (convertFromFlux) { if (adapter != null && adapter.getDescriptor().isMultiValue()) {
Flux<?> flux = reader.read(elementType, request) Flux<?> flux = reader.read(elementType, request)
.onErrorResumeWith(ex -> Flux.error(getReadError(ex, bodyParameter))); .onErrorResumeWith(ex -> Flux.error(getReadError(ex, bodyParameter)));
if (checkRequired(bodyParameter, isBodyRequired)) { if (checkRequired(adapter, isBodyRequired)) {
flux = flux.switchIfEmpty(Flux.error(getRequiredBodyError(bodyParameter))); flux = flux.switchIfEmpty(Flux.error(getRequiredBodyError(bodyParameter)));
} }
if (this.validator != null) { if (this.validator != null) {
flux = flux.map(applyValidationIfApplicable(bodyParameter)); flux = flux.map(applyValidationIfApplicable(bodyParameter));
} }
return Mono.just(getConversionService().convert(flux, FLUX_TYPE, typeDescriptor)); return Mono.just(adapter.fromPublisher(flux));
} }
else { else {
Mono<?> mono = reader.readMono(elementType, request) Mono<?> mono = reader.readMono(elementType, request)
.otherwise(ex -> Mono.error(getReadError(ex, bodyParameter))); .otherwise(ex -> Mono.error(getReadError(ex, bodyParameter)));
if (checkRequired(bodyParameter, isBodyRequired)) { if (checkRequired(adapter, isBodyRequired)) {
mono = mono.otherwiseIfEmpty(Mono.error(getRequiredBodyError(bodyParameter))); mono = mono.otherwiseIfEmpty(Mono.error(getRequiredBodyError(bodyParameter)));
} }
if (this.validator != null) { if (this.validator != null) {
mono = mono.map(applyValidationIfApplicable(bodyParameter)); mono = mono.map(applyValidationIfApplicable(bodyParameter));
} }
if (convertFromMono) { if (adapter != null) {
return Mono.just(getConversionService().convert(mono, MONO_TYPE, typeDescriptor)); return Mono.just(adapter.fromPublisher(mono));
} }
else { else {
return Mono.from(mono); return Mono.from(mono);
@ -159,11 +163,8 @@ public abstract class AbstractMessageReaderArgumentResolver {
return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes)); return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes));
} }
protected boolean checkRequired(MethodParameter bodyParameter, boolean isBodyRequired) { protected boolean checkRequired(ReactiveAdapter adapter, boolean isBodyRequired) {
if ("rx.Single".equals(bodyParameter.getNestedParameterType().getName())) { return adapter != null && !adapter.getDescriptor().supportsEmpty() || isBodyRequired;
return true;
}
return isBodyRequired;
} }
protected ServerWebInputException getReadError(Throwable ex, MethodParameter parameter) { protected ServerWebInputException getReadError(Throwable ex, MethodParameter parameter) {

65
spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageWriterResultHandler.java

@ -19,13 +19,12 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.reactive.HttpMessageWriter; import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
@ -44,31 +43,42 @@ import org.springframework.web.server.ServerWebExchange;
*/ */
public abstract class AbstractMessageWriterResultHandler extends ContentNegotiatingResultHandlerSupport { public abstract class AbstractMessageWriterResultHandler extends ContentNegotiatingResultHandlerSupport {
protected static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class);
protected static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class);
private final List<HttpMessageWriter<?>> messageWriters; private final List<HttpMessageWriter<?>> messageWriters;
/** /**
* Constructor with message converters, a {@code ConversionService}, and a * Constructor with {@link HttpMessageWriter}s and a
* {@code RequestedContentTypeResolver}. * {@code RequestedContentTypeResolver}.
* *
* @param messageWriters for serializing Objects to the response body stream * @param messageWriters for serializing Objects to the response body stream
* @param conversionService for converting other reactive types (e.g.
* rx.Observable, rx.Single, etc.) to Flux or Mono
* @param contentTypeResolver for resolving the requested content type * @param contentTypeResolver for resolving the requested content type
*/ */
protected AbstractMessageWriterResultHandler(List<HttpMessageWriter<?>> messageWriters, protected AbstractMessageWriterResultHandler(List<HttpMessageWriter<?>> messageWriters,
ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { RequestedContentTypeResolver contentTypeResolver) {
super(conversionService, contentTypeResolver); super(contentTypeResolver);
Assert.notEmpty(messageWriters, "At least one message writer is required."); Assert.notEmpty(messageWriters, "At least one message writer is required.");
this.messageWriters = messageWriters; this.messageWriters = messageWriters;
} }
/**
* Constructor with an additional {@link ReactiveAdapterRegistry}.
*
* @param messageWriters for serializing Objects to the response body stream
* @param contentTypeResolver for resolving the requested content type
* @param adapterRegistry for adapting other reactive types (e.g. rx.Observable,
* rx.Single, etc.) to Flux or Mono
*/
protected AbstractMessageWriterResultHandler(List<HttpMessageWriter<?>> messageWriters,
RequestedContentTypeResolver contentTypeResolver,
ReactiveAdapterRegistry adapterRegistry) {
super(contentTypeResolver, adapterRegistry);
Assert.notEmpty(messageWriters, "At least one message writer is required.");
this.messageWriters = messageWriters;
}
/** /**
* Return the configured message converters. * Return the configured message converters.
*/ */
@ -78,31 +88,20 @@ public abstract class AbstractMessageWriterResultHandler extends ContentNegotiat
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected Mono<Void> writeBody(ServerWebExchange exchange, Object body, protected Mono<Void> writeBody(Object body, MethodParameter bodyType, ServerWebExchange exchange) {
ResolvableType bodyType, MethodParameter bodyTypeParameter) {
Publisher<?> publisher = null; Class<?> bodyClass = bodyType.getParameterType();
ResolvableType elementType; ReactiveAdapter adapter = getReactiveAdapterRegistry().getAdapterFrom(bodyClass, body);
if (Publisher.class.isAssignableFrom(bodyType.getRawClass())) { Publisher<?> publisher;
publisher = (Publisher<?>) body; ResolvableType elementType;
} if (adapter != null) {
else { publisher = adapter.toPublisher(body);
TypeDescriptor descriptor = new TypeDescriptor(bodyTypeParameter); elementType = ResolvableType.forMethodParameter(bodyType).getGeneric(0);
if (getConversionService().canConvert(descriptor, MONO_TYPE)) {
publisher = (Publisher<?>) getConversionService().convert(body, descriptor, MONO_TYPE);
}
else if (getConversionService().canConvert(descriptor, FLUX_TYPE)) {
publisher = (Publisher<?>) getConversionService().convert(body, descriptor, FLUX_TYPE);
}
}
if (publisher != null) {
elementType = bodyType.getGeneric(0);
} }
else { else {
elementType = bodyType;
publisher = Mono.justOrEmpty(body); publisher = Mono.justOrEmpty(body);
elementType = ResolvableType.forMethodParameter(bodyType);
} }
if (void.class == elementType.getRawClass() || Void.class == elementType.getRawClass()) { if (void.class == elementType.getRawClass() || Void.class == elementType.getRawClass()) {

43
spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolver.java

@ -20,8 +20,8 @@ import java.util.List;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity; import org.springframework.http.RequestEntity;
@ -45,26 +45,24 @@ public class HttpEntityArgumentResolver extends AbstractMessageReaderArgumentRes
/** /**
* Constructor with message converters and a ConversionService. * Constructor with {@link HttpMessageReader}'s and a {@link Validator}.
* @param messageReaders readers for de-serializing the request body with * @param readers readers for de-serializing the request body with
* @param service for converting to other reactive types from Flux and Mono * @param validator validator to validate decoded objects with
*/ */
public HttpEntityArgumentResolver(List<HttpMessageReader<?>> messageReaders, public HttpEntityArgumentResolver(List<HttpMessageReader<?>> readers, Validator validator) {
ConversionService service) { super(readers, validator);
this(messageReaders, service, null);
} }
/** /**
* Constructor with message converters and a ConversionService. * Constructor that also accepts a {@link ReactiveAdapterRegistry}.
* @param messageReaders readers for de-serializing the request body with * @param readers readers for de-serializing the request body with
* @param service for converting to other reactive types from Flux and Mono
* @param validator validator to validate decoded objects with * @param validator validator to validate decoded objects with
* @param adapterRegistry for adapting to other reactive types from Flux and Mono
*/ */
public HttpEntityArgumentResolver(List<HttpMessageReader<?>> messageReaders, public HttpEntityArgumentResolver(List<HttpMessageReader<?>> readers, Validator validator,
ConversionService service, Validator validator) { ReactiveAdapterRegistry adapterRegistry) {
super(messageReaders, service, validator); super(readers, validator, adapterRegistry);
} }
@ -77,20 +75,9 @@ public class HttpEntityArgumentResolver extends AbstractMessageReaderArgumentRes
@Override @Override
public Mono<Object> resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) { public Mono<Object> resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) {
ResolvableType entityType; ResolvableType entityType = ResolvableType.forMethodParameter(param);
MethodParameter bodyParameter; MethodParameter bodyParameter = new MethodParameter(param);
bodyParameter.increaseNestingLevel();
if (getConversionService().canConvert(Mono.class, param.getParameterType())) {
entityType = ResolvableType.forMethodParameter(param).getGeneric(0);
bodyParameter = new MethodParameter(param);
bodyParameter.increaseNestingLevel();
bodyParameter.increaseNestingLevel();
}
else {
entityType = ResolvableType.forMethodParameter(param);
bodyParameter = new MethodParameter(param);
bodyParameter.increaseNestingLevel();
}
return readBody(bodyParameter, false, exchange) return readBody(bodyParameter, false, exchange)
.map(body -> createHttpEntity(body, entityType, exchange)) .map(body -> createHttpEntity(body, entityType, exchange))

26
spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolver.java

@ -21,7 +21,7 @@ import java.util.List;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService; import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.converter.reactive.HttpMessageReader; import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
@ -49,26 +49,24 @@ public class RequestBodyArgumentResolver extends AbstractMessageReaderArgumentRe
/** /**
* Constructor with message converters and a ConversionService. * Constructor with {@link HttpMessageReader}'s and a {@link Validator}.
* @param messageReaders readers for de-serializing the request body with * @param readers readers for de-serializing the request body with
* @param service for converting to other reactive types from Flux and Mono * @param validator validator to validate decoded objects with
*/ */
public RequestBodyArgumentResolver(List<HttpMessageReader<?>> messageReaders, public RequestBodyArgumentResolver(List<HttpMessageReader<?>> readers, Validator validator) {
ConversionService service) { super(readers, validator);
this(messageReaders, service, null);
} }
/** /**
* Constructor with message converters and a ConversionService. * Constructor that also accepts a {@link ReactiveAdapterRegistry}.
* @param messageReaders readers for de-serializing the request body with * @param readers readers for de-serializing the request body with
* @param service for converting to other reactive types from Flux and Mono
* @param validator validator to validate decoded objects with * @param validator validator to validate decoded objects with
* @param adapterRegistry for adapting to other reactive types from Flux and Mono
*/ */
public RequestBodyArgumentResolver(List<HttpMessageReader<?>> messageReaders, public RequestBodyArgumentResolver(List<HttpMessageReader<?>> readers, Validator validator,
ConversionService service, Validator validator) { ReactiveAdapterRegistry adapterRegistry) {
super(messageReaders, service, validator); super(readers, validator, adapterRegistry);
} }

18
spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java

@ -31,6 +31,7 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.core.codec.StringDecoder; import org.springframework.core.codec.StringDecoder;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
@ -66,6 +67,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
private final List<HttpMessageReader<?>> messageReaders = new ArrayList<>(10); private final List<HttpMessageReader<?>> messageReaders = new ArrayList<>(10);
private ReactiveAdapterRegistry reactiveAdapters = new ReactiveAdapterRegistry();
private ConversionService conversionService = new DefaultFormattingConversionService(); private ConversionService conversionService = new DefaultFormattingConversionService();
private Validator validator; private Validator validator;
@ -126,6 +129,14 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
return this.messageReaders; return this.messageReaders;
} }
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
this.reactiveAdapters = registry;
}
public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
return this.reactiveAdapters;
}
/** /**
* Configure a ConversionService for type conversion of controller method * Configure a ConversionService for type conversion of controller method
* arguments as well as for converting from different async types to * arguments as well as for converting from different async types to
@ -187,13 +198,15 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
protected List<HandlerMethodArgumentResolver> initArgumentResolvers() { protected List<HandlerMethodArgumentResolver> initArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
// Annotation-based argument resolution
ConversionService cs = getConversionService(); ConversionService cs = getConversionService();
ReactiveAdapterRegistry adapterRegistry = getReactiveAdapterRegistry();
// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(cs, getBeanFactory(), false)); resolvers.add(new RequestParamMethodArgumentResolver(cs, getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver(cs, getBeanFactory())); resolvers.add(new PathVariableMethodArgumentResolver(cs, getBeanFactory()));
resolvers.add(new PathVariableMapMethodArgumentResolver()); resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new RequestBodyArgumentResolver(getMessageReaders(), cs, getValidator())); resolvers.add(new RequestBodyArgumentResolver(getMessageReaders(), getValidator(), adapterRegistry));
resolvers.add(new RequestHeaderMethodArgumentResolver(cs, getBeanFactory())); resolvers.add(new RequestHeaderMethodArgumentResolver(cs, getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new CookieValueMethodArgumentResolver(cs, getBeanFactory())); resolvers.add(new CookieValueMethodArgumentResolver(cs, getBeanFactory()));
@ -202,6 +215,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
resolvers.add(new RequestAttributeMethodArgumentResolver(cs , getBeanFactory())); resolvers.add(new RequestAttributeMethodArgumentResolver(cs , getBeanFactory()));
// Type-based argument resolution // Type-based argument resolution
resolvers.add(new HttpEntityArgumentResolver(getMessageReaders(), getValidator(), adapterRegistry));
resolvers.add(new ModelArgumentResolver()); resolvers.add(new ModelArgumentResolver());
// Custom resolvers // Custom resolvers

49
spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandler.java

@ -21,15 +21,14 @@ import java.util.List;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.converter.reactive.HttpMessageWriter; import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.HandlerResultHandler;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
@ -53,42 +52,41 @@ import org.springframework.web.server.ServerWebExchange;
public class ResponseBodyResultHandler extends AbstractMessageWriterResultHandler public class ResponseBodyResultHandler extends AbstractMessageWriterResultHandler
implements HandlerResultHandler { implements HandlerResultHandler {
/** /**
* Constructor with message converters and a {@code ConversionService} only * Constructor with {@link HttpMessageWriter}s and a
* and creating a {@link HeaderContentTypeResolver}, i.e. using Accept header * {@code RequestedContentTypeResolver}.
* to determine the requested content type.
* *
* @param messageWriters writers for serializing to the response body stream * @param messageWriters writers for serializing to the response body stream
* @param conversionService for converting to Flux and Mono from other reactive types * @param contentTypeResolver for resolving the requested content type
*/ */
public ResponseBodyResultHandler(List<HttpMessageWriter<?>> messageWriters, public ResponseBodyResultHandler(List<HttpMessageWriter<?>> messageWriters,
ConversionService conversionService) { RequestedContentTypeResolver contentTypeResolver) {
this(messageWriters, conversionService, new HeaderContentTypeResolver()); this(messageWriters, contentTypeResolver, new ReactiveAdapterRegistry());
} }
/** /**
* Constructor with message converters, a {@code ConversionService}, and a * Constructor with an additional {@link ReactiveAdapterRegistry}.
* {@code RequestedContentTypeResolver}.
* *
* @param messageWriters writers for serializing to the response body stream * @param messageWriters writers for serializing to the response body stream
* @param conversionService for converting other reactive types (e.g.
* rx.Observable, rx.Single, etc.) to Flux or Mono
* @param contentTypeResolver for resolving the requested content type * @param contentTypeResolver for resolving the requested content type
* @param adapterRegistry for adapting other reactive types (e.g. rx.Observable,
* rx.Single, etc.) to Flux or Mono
*/ */
public ResponseBodyResultHandler(List<HttpMessageWriter<?>> messageWriters, public ResponseBodyResultHandler(List<HttpMessageWriter<?>> messageWriters,
ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { RequestedContentTypeResolver contentTypeResolver,
ReactiveAdapterRegistry adapterRegistry) {
super(messageWriters, conversionService, contentTypeResolver); super(messageWriters, contentTypeResolver, adapterRegistry);
setOrder(100); setOrder(100);
} }
@Override @Override
public boolean supports(HandlerResult result) { public boolean supports(HandlerResult result) {
ResolvableType returnType = result.getReturnType();
MethodParameter parameter = result.getReturnTypeSource(); MethodParameter parameter = result.getReturnTypeSource();
return hasResponseBodyAnnotation(parameter) && !isHttpEntityType(returnType); return hasResponseBodyAnnotation(parameter) && !isHttpEntityType(result);
} }
private boolean hasResponseBodyAnnotation(MethodParameter parameter) { private boolean hasResponseBodyAnnotation(MethodParameter parameter) {
@ -97,26 +95,27 @@ public class ResponseBodyResultHandler extends AbstractMessageWriterResultHandle
parameter.getMethodAnnotation(ResponseBody.class) != null); parameter.getMethodAnnotation(ResponseBody.class) != null);
} }
private boolean isHttpEntityType(ResolvableType returnType) { private boolean isHttpEntityType(HandlerResult result) {
if (HttpEntity.class.isAssignableFrom(returnType.getRawClass())) { Class<?> rawClass = result.getReturnType().getRawClass();
if (HttpEntity.class.isAssignableFrom(rawClass)) {
return true; return true;
} }
else if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { else {
ResolvableType genericType = returnType.getGeneric(0); if (getReactiveAdapterRegistry().getAdapterFrom(rawClass, result.getReturnValue()) != null) {
if (HttpEntity.class.isAssignableFrom(genericType.getRawClass())) { ResolvableType genericType = result.getReturnType().getGeneric(0);
return true; if (HttpEntity.class.isAssignableFrom(genericType.getRawClass())) {
return true;
}
} }
} }
return false; return false;
} }
@Override @Override
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) { public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
Object body = result.getReturnValue().orElse(null); Object body = result.getReturnValue().orElse(null);
ResolvableType bodyType = result.getReturnType();
MethodParameter bodyTypeParameter = result.getReturnTypeSource(); MethodParameter bodyTypeParameter = result.getReturnTypeSource();
return writeBody(exchange, body, bodyType, bodyTypeParameter); return writeBody(body, bodyTypeParameter, exchange);
} }
} }

74
spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java

@ -21,8 +21,9 @@ import java.util.Optional;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.RequestEntity; import org.springframework.http.RequestEntity;
@ -31,7 +32,6 @@ import org.springframework.http.converter.reactive.HttpMessageWriter;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.HandlerResultHandler;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
@ -47,52 +47,55 @@ import org.springframework.web.server.ServerWebExchange;
public class ResponseEntityResultHandler extends AbstractMessageWriterResultHandler public class ResponseEntityResultHandler extends AbstractMessageWriterResultHandler
implements HandlerResultHandler { implements HandlerResultHandler {
/** /**
* Constructor with message converters and a {@code ConversionService} only * Constructor with {@link HttpMessageWriter}s and a
* and creating a {@link HeaderContentTypeResolver}, i.e. using Accept header * {@code RequestedContentTypeResolver}.
* to determine the requested content type.
* *
* @param messageWriters writers for serializing to the response body stream * @param messageWriters writers for serializing to the response body stream
* @param conversionService for converting to Flux and Mono from other reactive types * @param contentTypeResolver for resolving the requested content type
*/ */
public ResponseEntityResultHandler(List<HttpMessageWriter<?>> messageWriters, public ResponseEntityResultHandler(List<HttpMessageWriter<?>> messageWriters,
ConversionService conversionService) { RequestedContentTypeResolver contentTypeResolver) {
this(messageWriters, conversionService, new HeaderContentTypeResolver()); this(messageWriters, contentTypeResolver, new ReactiveAdapterRegistry());
} }
/** /**
* Constructor with message converters, a {@code ConversionService}, and a * Constructor with an additional {@link ReactiveAdapterRegistry}.
* {@code RequestedContentTypeResolver}.
* *
* @param messageWriters writers for serializing to the response body stream * @param messageWriters writers for serializing to the response body stream
* @param conversionService for converting other reactive types (e.g.
* rx.Observable, rx.Single, etc.) to Flux or Mono
* @param contentTypeResolver for resolving the requested content type * @param contentTypeResolver for resolving the requested content type
* @param adapterRegistry for adapting other reactive types (e.g. rx.Observable,
* rx.Single, etc.) to Flux or Mono
*/ */
public ResponseEntityResultHandler(List<HttpMessageWriter<?>> messageWriters, public ResponseEntityResultHandler(List<HttpMessageWriter<?>> messageWriters,
ConversionService conversionService, RequestedContentTypeResolver contentTypeResolver) { RequestedContentTypeResolver contentTypeResolver,
ReactiveAdapterRegistry adapterRegistry) {
super(messageWriters, conversionService, contentTypeResolver); super(messageWriters, contentTypeResolver, adapterRegistry);
setOrder(0); setOrder(0);
} }
@Override @Override
public boolean supports(HandlerResult result) { public boolean supports(HandlerResult result) {
ResolvableType returnType = result.getReturnType(); Class<?> returnType = result.getReturnType().getRawClass();
if (isSupportedType(returnType)) { if (isSupportedType(returnType)) {
return true; return true;
} }
else if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { else {
ResolvableType genericType = result.getReturnType().getGeneric(0); Optional<Object> returnValue = result.getReturnValue();
return isSupportedType(genericType); ReactiveAdapter adapter = getReactiveAdapterRegistry().getAdapterFrom(returnType, returnValue);
if (adapter != null && !adapter.getDescriptor().isMultiValue()) {
ResolvableType genericType = result.getReturnType().getGeneric(0);
return isSupportedType(genericType.getRawClass());
}
} }
return false; return false;
} }
private boolean isSupportedType(ResolvableType returnType) { private boolean isSupportedType(Class<?> clazz) {
Class<?> clazz = returnType.getRawClass();
return (HttpEntity.class.isAssignableFrom(clazz) && !RequestEntity.class.isAssignableFrom(clazz)); return (HttpEntity.class.isAssignableFrom(clazz) && !RequestEntity.class.isAssignableFrom(clazz));
} }
@ -101,25 +104,24 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) { public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
ResolvableType returnType = result.getReturnType(); ResolvableType returnType = result.getReturnType();
MethodParameter bodyType;
ResolvableType bodyType;
MethodParameter bodyTypeParameter;
Mono<?> returnValueMono; Mono<?> returnValueMono;
Optional<Object> optional = result.getReturnValue(); Optional<Object> optionalValue = result.getReturnValue();
if (optional.isPresent() && getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { Class<?> rawClass = returnType.getRawClass();
returnValueMono = getConversionService().convert(optional.get(), Mono.class); ReactiveAdapter adapter = getReactiveAdapterRegistry().getAdapterFrom(rawClass, optionalValue);
bodyType = returnType.getGeneric(0, 0);
bodyTypeParameter = new MethodParameter(result.getReturnTypeSource()); if (adapter != null) {
bodyTypeParameter.increaseNestingLevel(); returnValueMono = adapter.toMono(optionalValue);
bodyTypeParameter.increaseNestingLevel(); bodyType = new MethodParameter(result.getReturnTypeSource());
bodyType.increaseNestingLevel();
bodyType.increaseNestingLevel();
} }
else { else {
returnValueMono = Mono.justOrEmpty(optional); returnValueMono = Mono.justOrEmpty(optionalValue);
bodyType = returnType.getGeneric(0); bodyType = new MethodParameter(result.getReturnTypeSource());
bodyTypeParameter = new MethodParameter(result.getReturnTypeSource()); bodyType.increaseNestingLevel();
bodyTypeParameter.increaseNestingLevel();
} }
return returnValueMono.then(returnValue -> { return returnValueMono.then(returnValue -> {
@ -141,7 +143,7 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
.forEach(entry -> responseHeaders.put(entry.getKey(), entry.getValue())); .forEach(entry -> responseHeaders.put(entry.getKey(), entry.getValue()));
} }
return writeBody(exchange, httpEntity.getBody(), bodyType, bodyTypeParameter); return writeBody(httpEntity.getBody(), bodyType, exchange);
}); });
} }

36
spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java

@ -31,16 +31,16 @@ import org.springframework.core.Conventions;
import org.springframework.core.GenericTypeResolver; import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.convert.ConversionService;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler; import org.springframework.web.reactive.HandlerResultHandler;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport; import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport;
import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.NotAcceptableStatusException;
@ -84,26 +84,28 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler
/** /**
* Constructor with {@code ViewResolver}s and a {@code ConversionService} only * Constructor with {@link ViewResolver}s and a {@link RequestedContentTypeResolver}.
* and creating a {@link HeaderContentTypeResolver}, i.e. using Accept header
* to determine the requested content type.
* @param resolvers the resolver to use * @param resolvers the resolver to use
* @param conversionService for converting other reactive types (e.g. rx.Single) to Mono * @param contentTypeResolver for resolving the requested content type
*/ */
public ViewResolutionResultHandler(List<ViewResolver> resolvers, ConversionService conversionService) { public ViewResolutionResultHandler(List<ViewResolver> resolvers,
this(resolvers, conversionService, new HeaderContentTypeResolver()); RequestedContentTypeResolver contentTypeResolver) {
this(resolvers, contentTypeResolver, new ReactiveAdapterRegistry());
} }
/** /**
* Constructor with {@code ViewResolver}s tand a {@code ConversionService}. * Constructor with {@code ViewResolver}s tand a {@code ConversionService}.
* @param resolvers the resolver to use * @param resolvers the resolver to use
* @param conversionService for converting other reactive types (e.g. rx.Single) to Mono
* @param contentTypeResolver for resolving the requested content type * @param contentTypeResolver for resolving the requested content type
* @param adapterRegistry for adapting from other reactive types (e.g.
* rx.Single) to Mono
*/ */
public ViewResolutionResultHandler(List<ViewResolver> resolvers, ConversionService conversionService, public ViewResolutionResultHandler(List<ViewResolver> resolvers,
RequestedContentTypeResolver contentTypeResolver) { RequestedContentTypeResolver contentTypeResolver,
ReactiveAdapterRegistry adapterRegistry) {
super(conversionService, contentTypeResolver); super(contentTypeResolver, adapterRegistry);
this.viewResolvers.addAll(resolvers); this.viewResolvers.addAll(resolvers);
AnnotationAwareOrderComparator.sort(this.viewResolvers); AnnotationAwareOrderComparator.sort(this.viewResolvers);
} }
@ -143,7 +145,7 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler
if (isSupportedType(clazz)) { if (isSupportedType(clazz)) {
return true; return true;
} }
if (getConversionService().canConvert(clazz, Mono.class)) { if (getReactiveAdapterRegistry().getAdapterFrom(clazz, result.getReturnValue()) != null) {
clazz = result.getReturnType().getGeneric(0).getRawClass(); clazz = result.getReturnType().getGeneric(0).getRawClass();
return isSupportedType(clazz); return isSupportedType(clazz);
} }
@ -168,10 +170,12 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler
ResolvableType elementType; ResolvableType elementType;
ResolvableType returnType = result.getReturnType(); ResolvableType returnType = result.getReturnType();
if (getConversionService().canConvert(returnType.getRawClass(), Mono.class)) { Class<?> rawClass = returnType.getRawClass();
Optional<Object> optionalValue = result.getReturnValue(); Optional<Object> optionalValue = result.getReturnValue();
ReactiveAdapter adapter = getReactiveAdapterRegistry().getAdapterFrom(rawClass, optionalValue);
if (adapter != null) {
if (optionalValue.isPresent()) { if (optionalValue.isPresent()) {
Mono<?> converted = getConversionService().convert(optionalValue.get(), Mono.class); Mono<?> converted = adapter.toMono(optionalValue);
valueMono = converted.map(o -> o); valueMono = converted.map(o -> o);
} }
else { else {

4
spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java

@ -30,7 +30,6 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.StringEncoder; import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@ -43,6 +42,7 @@ import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler;
@ -198,7 +198,7 @@ public class DispatcherHandlerErrorTests {
public ResponseBodyResultHandler resultHandler() { public ResponseBodyResultHandler resultHandler() {
return new ResponseBodyResultHandler( return new ResponseBodyResultHandler(
Collections.singletonList(new EncoderHttpMessageWriter<>(new StringEncoder())), Collections.singletonList(new EncoderHttpMessageWriter<>(new StringEncoder())),
new DefaultConversionService()); new HeaderContentTypeResolver());
} }
@Bean @Bean

3
spring-web-reactive/src/test/java/org/springframework/web/reactive/result/ContentNegotiatingResultHandlerSupportTests.java

@ -24,7 +24,6 @@ import java.util.Set;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpRequest;
@ -125,7 +124,7 @@ public class ContentNegotiatingResultHandlerSupportTests {
} }
public TestResultHandler(RequestedContentTypeResolver contentTypeResolver) { public TestResultHandler(RequestedContentTypeResolver contentTypeResolver) {
super(new GenericConversionService(), contentTypeResolver); super(contentTypeResolver);
} }
} }

9
spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleResultHandlerTests.java

@ -27,10 +27,6 @@ import rx.Observable;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResult;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -47,10 +43,7 @@ public class SimpleResultHandlerTests {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
FormattingConversionService service = new DefaultFormattingConversionService(); this.resultHandler = new SimpleResultHandler();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
this.resultHandler = new SimpleResultHandler(service);
} }

3
spring-web-reactive/src/test/java/org/springframework/web/reactive/result/SimpleUrlHandlerMappingIntegrationTests.java

@ -28,7 +28,6 @@ import reactor.core.publisher.Mono;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBuffer; import org.springframework.core.io.buffer.DefaultDataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory;
@ -147,7 +146,7 @@ public class SimpleUrlHandlerMappingIntegrationTests extends AbstractHttpHandler
@Bean @Bean
public SimpleResultHandler resultHandler() { public SimpleResultHandler resultHandler() {
return new SimpleResultHandler(new DefaultConversionService()); return new SimpleResultHandler();
} }
} }

21
spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/HttpEntityArgumentResolverTests.java

@ -35,12 +35,8 @@ import rx.Single;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.codec.StringDecoder; import org.springframework.core.codec.StringDecoder;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -50,14 +46,20 @@ import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ExtendedModelMap;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.reactive.result.ResolvableMethod;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.MockWebSessionManager;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.springframework.core.ResolvableType.*; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
/** /**
* Unit tests for {@link HttpEntityArgumentResolver}.When adding a test also * Unit tests for {@link HttpEntityArgumentResolver}.When adding a test also
@ -87,12 +89,7 @@ public class HttpEntityArgumentResolverTests {
private HttpEntityArgumentResolver createResolver() { private HttpEntityArgumentResolver createResolver() {
List<HttpMessageReader<?>> readers = new ArrayList<>(); List<HttpMessageReader<?>> readers = new ArrayList<>();
readers.add(new DecoderHttpMessageReader<>(new StringDecoder())); readers.add(new DecoderHttpMessageReader<>(new StringDecoder()));
return new HttpEntityArgumentResolver(readers, mock(Validator.class));
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
return new HttpEntityArgumentResolver(readers, service);
} }

20
spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageReaderArgumentResolverTests.java

@ -42,12 +42,8 @@ import rx.Single;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Decoder;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.http.codec.json.JacksonJsonDecoder;
@ -66,8 +62,12 @@ import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.MockWebSessionManager;
import static org.junit.Assert.*; import static org.junit.Assert.assertArrayEquals;
import static org.springframework.core.ResolvableType.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClass;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
/** /**
* Unit tests for {@link AbstractMessageReaderArgumentResolver}. * Unit tests for {@link AbstractMessageReaderArgumentResolver}.
@ -275,15 +275,9 @@ public class MessageReaderArgumentResolverTests {
@SuppressWarnings("Convert2MethodRef") @SuppressWarnings("Convert2MethodRef")
private AbstractMessageReaderArgumentResolver resolver(Decoder<?>... decoders) { private AbstractMessageReaderArgumentResolver resolver(Decoder<?>... decoders) {
List<HttpMessageReader<?>> readers = new ArrayList<>(); List<HttpMessageReader<?>> readers = new ArrayList<>();
Arrays.asList(decoders).forEach(decoder -> readers.add(new DecoderHttpMessageReader<>(decoder))); Arrays.asList(decoders).forEach(decoder -> readers.add(new DecoderHttpMessageReader<>(decoder)));
return new AbstractMessageReaderArgumentResolver(readers, new TestBeanValidator()) {};
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
return new AbstractMessageReaderArgumentResolver(readers, service, new TestBeanValidator()) {};
} }
private DataBuffer dataBuffer(String body) { private DataBuffer dataBuffer(String body) {

33
spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/MessageWriterResultHandlerTests.java

@ -41,9 +41,6 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.StringEncoder; import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.core.io.buffer.support.DataBufferTestUtils;
@ -64,9 +61,11 @@ import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.MockWebSessionManager;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.springframework.http.MediaType.*; import static org.junit.Assert.assertNull;
import static org.springframework.web.reactive.HandlerMapping.*; import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8;
import static org.springframework.web.reactive.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
/** /**
* Unit tests for {@link AbstractMessageWriterResultHandler}. * Unit tests for {@link AbstractMessageWriterResultHandler}.
@ -93,7 +92,7 @@ public class MessageWriterResultHandlerTests {
public void useDefaultContentType() throws Exception { public void useDefaultContentType() throws Exception {
Resource body = new ClassPathResource("logo.png", getClass()); Resource body = new ClassPathResource("logo.png", getClass());
ResolvableType type = ResolvableType.forType(Resource.class); ResolvableType type = ResolvableType.forType(Resource.class);
this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5)); this.resultHandler.writeBody(body, returnType(type), this.exchange).block(Duration.ofSeconds(5));
assertEquals("image/x-png", this.response.getHeaders().getFirst("Content-Type")); assertEquals("image/x-png", this.response.getHeaders().getFirst("Content-Type"));
} }
@ -105,7 +104,7 @@ public class MessageWriterResultHandlerTests {
String body = "foo"; String body = "foo";
ResolvableType type = ResolvableType.forType(String.class); ResolvableType type = ResolvableType.forType(String.class);
this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5)); this.resultHandler.writeBody(body, returnType(type), this.exchange).block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType());
} }
@ -119,7 +118,7 @@ public class MessageWriterResultHandlerTests {
} }
private void testVoidReturnType(Object body, ResolvableType type) { private void testVoidReturnType(Object body, ResolvableType type) {
this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5)); this.resultHandler.writeBody(body, returnType(type), this.exchange).block(Duration.ofSeconds(5));
assertNull(this.response.getHeaders().get("Content-Type")); assertNull(this.response.getHeaders().get("Content-Type"));
assertNull(this.response.getBody()); assertNull(this.response.getBody());
@ -131,7 +130,7 @@ public class MessageWriterResultHandlerTests {
ResolvableType type = ResolvableType.forType(OutputStream.class); ResolvableType type = ResolvableType.forType(OutputStream.class);
HttpMessageWriter<?> writer = new EncoderHttpMessageWriter<>(new ByteBufferEncoder()); HttpMessageWriter<?> writer = new EncoderHttpMessageWriter<>(new ByteBufferEncoder());
Mono<Void> mono = createResultHandler(writer).writeBody(this.exchange, body, type, returnType(type)); Mono<Void> mono = createResultHandler(writer).writeBody(body, returnType(type), this.exchange);
TestSubscriber.subscribe(mono).assertError(IllegalStateException.class); TestSubscriber.subscribe(mono).assertError(IllegalStateException.class);
} }
@ -140,7 +139,7 @@ public class MessageWriterResultHandlerTests {
public void jacksonTypeOfListElement() throws Exception { public void jacksonTypeOfListElement() throws Exception {
List<ParentClass> body = Arrays.asList(new Foo("foo"), new Bar("bar")); List<ParentClass> body = Arrays.asList(new Foo("foo"), new Bar("bar"));
ResolvableType type = ResolvableType.forClassWithGenerics(List.class, ParentClass.class); ResolvableType type = ResolvableType.forClassWithGenerics(List.class, ParentClass.class);
this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5)); this.resultHandler.writeBody(body, returnType(type), this.exchange).block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType());
assertResponseBody("[{\"type\":\"foo\",\"parentProperty\":\"foo\"}," + assertResponseBody("[{\"type\":\"foo\",\"parentProperty\":\"foo\"}," +
@ -151,7 +150,7 @@ public class MessageWriterResultHandlerTests {
public void jacksonTypeWithSubType() throws Exception { public void jacksonTypeWithSubType() throws Exception {
SimpleBean body = new SimpleBean(123L, "foo"); SimpleBean body = new SimpleBean(123L, "foo");
ResolvableType type = ResolvableType.forClass(Identifiable.class); ResolvableType type = ResolvableType.forClass(Identifiable.class);
this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5)); this.resultHandler.writeBody(body, returnType(type), this.exchange).block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType());
assertResponseBody("{\"id\":123,\"name\":\"foo\"}"); assertResponseBody("{\"id\":123,\"name\":\"foo\"}");
@ -161,7 +160,7 @@ public class MessageWriterResultHandlerTests {
public void jacksonTypeWithSubTypeOfListElement() throws Exception { public void jacksonTypeWithSubTypeOfListElement() throws Exception {
List<SimpleBean> body = Arrays.asList(new SimpleBean(123L, "foo"), new SimpleBean(456L, "bar")); List<SimpleBean> body = Arrays.asList(new SimpleBean(123L, "foo"), new SimpleBean(456L, "bar"));
ResolvableType type = ResolvableType.forClassWithGenerics(List.class, Identifiable.class); ResolvableType type = ResolvableType.forClassWithGenerics(List.class, Identifiable.class);
this.resultHandler.writeBody(this.exchange, body, type, returnType(type)).block(Duration.ofSeconds(5)); this.resultHandler.writeBody(body, returnType(type), this.exchange).block(Duration.ofSeconds(5));
assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType()); assertEquals(APPLICATION_JSON_UTF8, this.response.getHeaders().getContentType());
assertResponseBody("[{\"id\":123,\"name\":\"foo\"},{\"id\":456,\"name\":\"bar\"}]"); assertResponseBody("[{\"id\":123,\"name\":\"foo\"},{\"id\":456,\"name\":\"bar\"}]");
@ -185,14 +184,8 @@ public class MessageWriterResultHandlerTests {
else { else {
writerList = Arrays.asList(writers); writerList = Arrays.asList(writers);
} }
GenericConversionService service = new GenericConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build();
return new AbstractMessageWriterResultHandler(writerList, resolver) {};
return new AbstractMessageWriterResultHandler(writerList, service, resolver) {};
} }
private void assertResponseBody(String responseBody) { private void assertResponseBody(String responseBody) {

14
spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestBodyArgumentResolverTests.java

@ -18,7 +18,6 @@ package org.springframework.web.reactive.result.method.annotation;
import java.net.URI; import java.net.URI;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
@ -38,18 +37,15 @@ import rx.Single;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.codec.StringDecoder; import org.springframework.core.codec.StringDecoder;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.converter.reactive.DecoderHttpMessageReader; import org.springframework.http.converter.reactive.DecoderHttpMessageReader;
import org.springframework.http.converter.reactive.HttpMessageReader; import org.springframework.http.converter.reactive.HttpMessageReader;
import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse; import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ExtendedModelMap;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.reactive.result.ResolvableMethod;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
@ -62,6 +58,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.springframework.core.ResolvableType.forClass; import static org.springframework.core.ResolvableType.forClass;
import static org.springframework.core.ResolvableType.forClassWithGenerics; import static org.springframework.core.ResolvableType.forClassWithGenerics;
@ -93,12 +90,7 @@ public class RequestBodyArgumentResolverTests {
private RequestBodyArgumentResolver resolver() { private RequestBodyArgumentResolver resolver() {
List<HttpMessageReader<?>> readers = new ArrayList<>(); List<HttpMessageReader<?>> readers = new ArrayList<>();
readers.add(new DecoderHttpMessageReader<>(new StringDecoder())); readers.add(new DecoderHttpMessageReader<>(new StringDecoder()));
return new RequestBodyArgumentResolver(readers, mock(Validator.class));
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
return new RequestBodyArgumentResolver(readers, service);
} }

11
spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseBodyResultHandlerTests.java

@ -27,11 +27,6 @@ import reactor.core.publisher.Mono;
import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.StringEncoder; import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.http.codec.json.JacksonJsonEncoder;
@ -99,12 +94,8 @@ public class ResponseBodyResultHandlerTests {
else { else {
writerList = Arrays.asList(writers); writerList = Arrays.asList(writers);
} }
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build();
return new ResponseBodyResultHandler(writerList, resolver);
return new ResponseBodyResultHandler(writerList, new DefaultConversionService(), resolver);
} }
@Test @Test

19
spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java

@ -34,11 +34,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.StringEncoder; import org.springframework.core.codec.StringEncoder;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -59,8 +55,11 @@ import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.MockWebSessionManager; import org.springframework.web.server.session.MockWebSessionManager;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.springframework.core.ResolvableType.*; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
/** /**
* Unit tests for {@link ResponseEntityResultHandler}. When adding a test also * Unit tests for {@link ResponseEntityResultHandler}. When adding a test also
@ -100,14 +99,8 @@ public class ResponseEntityResultHandlerTests {
else { else {
writerList = Arrays.asList(writers); writerList = Arrays.asList(writers);
} }
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build(); RequestedContentTypeResolver resolver = new RequestedContentTypeResolverBuilder().build();
return new ResponseEntityResultHandler(writerList, resolver);
return new ResponseEntityResultHandler(writerList, service, resolver);
} }

20
spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java

@ -38,13 +38,9 @@ import rx.Single;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.support.MonoToCompletableFutureConverter;
import org.springframework.core.convert.support.ReactorToRxJava1Converter;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.buffer.support.DataBufferTestUtils; import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.MockServerHttpRequest; import org.springframework.http.server.reactive.MockServerHttpRequest;
@ -55,6 +51,8 @@ import org.springframework.ui.Model;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.reactive.result.ResolvableMethod;
import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
@ -62,9 +60,10 @@ import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.server.session.WebSessionManager; import org.springframework.web.server.session.WebSessionManager;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*; import static org.junit.Assert.assertNotNull;
import static org.springframework.http.MediaType.*; import static org.mockito.Mockito.mock;
import static org.springframework.http.MediaType.APPLICATION_JSON;
/** /**
* Unit tests for {@link ViewResolutionResultHandler}. * Unit tests for {@link ViewResolutionResultHandler}.
@ -247,12 +246,9 @@ public class ViewResolutionResultHandlerTests {
} }
private ViewResolutionResultHandler createResultHandler(List<View> defaultViews, ViewResolver... resolvers) { private ViewResolutionResultHandler createResultHandler(List<View> defaultViews, ViewResolver... resolvers) {
FormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new MonoToCompletableFutureConverter());
service.addConverter(new ReactorToRxJava1Converter());
List<ViewResolver> resolverList = Arrays.asList(resolvers); List<ViewResolver> resolverList = Arrays.asList(resolvers);
RequestedContentTypeResolver contentTypeResolver = new HeaderContentTypeResolver();
ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolverList, service); ViewResolutionResultHandler handler = new ViewResolutionResultHandler(resolverList, contentTypeResolver);
handler.setDefaultViews(defaultViews); handler.setDefaultViews(defaultViews);
return handler; return handler;
} }

Loading…
Cancel
Save