Add support for rx.Completable as return value

This commit is contained in:
Rossen Stoyanchev 2016-07-27 16:59:53 -04:00
parent 79bc227c9d
commit 143b5c89dd
9 changed files with 106 additions and 21 deletions

View File

@ -97,7 +97,9 @@ public abstract class AbstractMessageWriterResultHandler extends ContentNegotiat
ResolvableType elementType; ResolvableType elementType;
if (adapter != null) { if (adapter != null) {
publisher = adapter.toPublisher(body); publisher = adapter.toPublisher(body);
elementType = ResolvableType.forMethodParameter(bodyType).getGeneric(0); elementType = adapter.getDescriptor().isNoValue() ?
ResolvableType.forClass(Void.class) :
ResolvableType.forMethodParameter(bodyType).getGeneric(0);
} }
else { else {
publisher = Mono.justOrEmpty(body); publisher = Mono.justOrEmpty(body);

View File

@ -17,10 +17,12 @@
package org.springframework.web.reactive.result.method.annotation; package org.springframework.web.reactive.result.method.annotation;
import java.util.List; import java.util.List;
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.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
@ -101,7 +103,9 @@ public class ResponseBodyResultHandler extends AbstractMessageWriterResultHandle
return true; return true;
} }
else { else {
if (getReactiveAdapterRegistry().getAdapterFrom(rawClass, result.getReturnValue()) != null) { Optional<Object> optional = result.getReturnValue();
ReactiveAdapter adapter = getReactiveAdapterRegistry().getAdapterFrom(rawClass, optional);
if (adapter != null && !adapter.getDescriptor().isNoValue()) {
ResolvableType genericType = result.getReturnType().getGeneric(0); ResolvableType genericType = result.getReturnType().getGeneric(0);
if (HttpEntity.class.isAssignableFrom(genericType.getRawClass())) { if (HttpEntity.class.isAssignableFrom(genericType.getRawClass())) {
return true; return true;

View File

@ -85,9 +85,12 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
return true; return true;
} }
else { else {
Optional<Object> returnValue = result.getReturnValue(); Optional<Object> optional = result.getReturnValue();
ReactiveAdapter adapter = getReactiveAdapterRegistry().getAdapterFrom(returnType, returnValue); ReactiveAdapter adapter = getReactiveAdapterRegistry().getAdapterFrom(returnType, optional);
if (adapter != null && !adapter.getDescriptor().isMultiValue()) { if (adapter != null &&
!adapter.getDescriptor().isMultiValue() &&
!adapter.getDescriptor().isNoValue()) {
ResolvableType genericType = result.getReturnType().getGeneric(0); ResolvableType genericType = result.getReturnType().getGeneric(0);
return isSupportedType(genericType.getRawClass()); return isSupportedType(genericType.getRawClass());
} }

View File

@ -142,12 +142,19 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler
if (hasModelAttributeAnnotation(result)) { if (hasModelAttributeAnnotation(result)) {
return true; return true;
} }
if (isSupportedType(clazz)) { Optional<Object> optional = result.getReturnValue();
return true; ReactiveAdapter adapter = getReactiveAdapterRegistry().getAdapterFrom(clazz, optional);
if (adapter != null) {
if (adapter.getDescriptor().isNoValue()) {
return true;
}
else {
clazz = result.getReturnType().getGeneric(0).getRawClass();
return isSupportedType(clazz);
}
} }
if (getReactiveAdapterRegistry().getAdapterFrom(clazz, result.getReturnValue()) != null) { else if (isSupportedType(clazz)) {
clazz = result.getReturnType().getGeneric(0).getRawClass(); return true;
return isSupportedType(clazz);
} }
return false; return false;
} }
@ -181,7 +188,8 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler
else { else {
valueMono = Mono.empty(); valueMono = Mono.empty();
} }
elementType = returnType.getGeneric(0); elementType = adapter.getDescriptor().isNoValue() ?
ResolvableType.forClass(Void.class) : returnType.getGeneric(0);
} }
else { else {
valueMono = Mono.justOrEmpty(result.getReturnValue()); valueMono = Mono.justOrEmpty(result.getReturnValue());

View File

@ -34,6 +34,7 @@ import org.junit.Test;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.TestSubscriber; import reactor.test.TestSubscriber;
import rx.Completable;
import rx.Observable; import rx.Observable;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
@ -110,6 +111,7 @@ public class MessageWriterResultHandlerTests {
public void voidReturnType() throws Exception { public void voidReturnType() throws Exception {
testVoidReturnType(null, ResolvableType.forType(void.class)); testVoidReturnType(null, ResolvableType.forType(void.class));
testVoidReturnType(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, Void.class)); testVoidReturnType(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, Void.class));
testVoidReturnType(Completable.complete(), ResolvableType.forClass(Completable.class));
testVoidReturnType(Flux.empty(), ResolvableType.forClassWithGenerics(Flux.class, Void.class)); testVoidReturnType(Flux.empty(), ResolvableType.forClassWithGenerics(Flux.class, Void.class));
testVoidReturnType(Observable.empty(), ResolvableType.forClassWithGenerics(Observable.class, Void.class)); testVoidReturnType(Observable.empty(), ResolvableType.forClassWithGenerics(Observable.class, Void.class));
} }
@ -269,6 +271,8 @@ public class MessageWriterResultHandlerTests {
Mono<Void> monoVoid() { return null; } Mono<Void> monoVoid() { return null; }
Completable completable() { return null; }
Flux<Void> fluxVoid() { return null; } Flux<Void> fluxVoid() { return null; }
Observable<Void> observableVoid() { return null; } Observable<Void> observableVoid() { return null; }

View File

@ -28,6 +28,7 @@ import org.junit.Test;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import rx.Completable;
import rx.Observable; import rx.Observable;
import rx.Single; import rx.Single;
@ -54,9 +55,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.WebReactiveConfiguration; import org.springframework.web.reactive.config.WebReactiveConfiguration;
import static java.util.Arrays.*; import static java.util.Arrays.asList;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.springframework.http.MediaType.*; import static org.junit.Assert.assertTrue;
import static org.springframework.http.MediaType.APPLICATION_XML;
/** /**
@ -233,6 +235,24 @@ public class RequestMappingMessageConversionIntegrationTests extends AbstractReq
assertEquals(2, getApplicationContext().getBean(PersonCreateController.class).persons.size()); assertEquals(2, getApplicationContext().getBean(PersonCreateController.class).persons.size());
} }
@Test
public void personCreateWithMono() throws Exception {
ResponseEntity<Void> entity = performPost(
"/person-create/mono", JSON, new Person("Robert"), null, Void.class);
assertEquals(HttpStatus.OK, entity.getStatusCode());
assertEquals(1, getApplicationContext().getBean(PersonCreateController.class).persons.size());
}
@Test
public void personCreateWithSingle() throws Exception {
ResponseEntity<Void> entity = performPost(
"/person-create/single", JSON, new Person("Robert"), null, Void.class);
assertEquals(HttpStatus.OK, entity.getStatusCode());
assertEquals(1, getApplicationContext().getBean(PersonCreateController.class).persons.size());
}
@Test @Test
public void personCreateWithFluxJson() throws Exception { public void personCreateWithFluxJson() throws Exception {
ResponseEntity<Void> entity = performPost("/person-create/flux", JSON, ResponseEntity<Void> entity = performPost("/person-create/flux", JSON,
@ -415,18 +435,28 @@ public class RequestMappingMessageConversionIntegrationTests extends AbstractReq
final List<Person> persons = new ArrayList<>(); final List<Person> persons = new ArrayList<>();
@PostMapping("/publisher") @PostMapping("/publisher")
public Publisher<Void> createWithPublisher(@RequestBody Publisher<Person> personStream) { public Publisher<Void> createWithPublisher(@RequestBody Publisher<Person> publisher) {
return Flux.from(personStream).doOnNext(persons::add).then(); return Flux.from(publisher).doOnNext(persons::add).then();
}
@PostMapping("/mono")
public Mono<Void> createWithMono(@RequestBody Mono<Person> mono) {
return mono.doOnNext(persons::add).then();
}
@PostMapping("/single")
public Completable createWithSingle(@RequestBody Single<Person> single) {
return single.map(persons::add).toCompletable();
} }
@PostMapping("/flux") @PostMapping("/flux")
public Mono<Void> createWithFlux(@RequestBody Flux<Person> personStream) { public Mono<Void> createWithFlux(@RequestBody Flux<Person> flux) {
return personStream.doOnNext(persons::add).then(); return flux.doOnNext(persons::add).then();
} }
@PostMapping("/observable") @PostMapping("/observable")
public Observable<Void> createWithObservable(@RequestBody Observable<Person> personStream) { public Observable<Void> createWithObservable(@RequestBody Observable<Person> observable) {
return personStream.toList().doOnNext(persons::addAll).flatMap(document -> Observable.empty()); return observable.toList().doOnNext(persons::addAll).flatMap(document -> Observable.empty());
} }
} }

View File

@ -24,6 +24,8 @@ import java.util.List;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import rx.Completable;
import rx.Single;
import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.codec.CharSequenceEncoder;
@ -50,7 +52,7 @@ 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;
/** /**
@ -106,6 +108,9 @@ public class ResponseBodyResultHandlerTests {
controller = new TestRestController(); controller = new TestRestController();
testSupports(controller, "handleToString", true); testSupports(controller, "handleToString", true);
testSupports(controller, "handleToMonoString", true);
testSupports(controller, "handleToSingleString", true);
testSupports(controller, "handleToCompletable", true);
testSupports(controller, "handleToResponseEntity", false); testSupports(controller, "handleToResponseEntity", false);
testSupports(controller, "handleToMonoResponseEntity", false); testSupports(controller, "handleToMonoResponseEntity", false);
} }
@ -134,6 +139,18 @@ public class ResponseBodyResultHandlerTests {
return null; return null;
} }
public Mono<String> handleToMonoString() {
return null;
}
public Single<String> handleToSingleString() {
return null;
}
public Completable handleToCompletable() {
return null;
}
public ResponseEntity<String> handleToResponseEntity() { public ResponseEntity<String> handleToResponseEntity() {
return null; return null;
} }

View File

@ -28,6 +28,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.TestSubscriber; import reactor.test.TestSubscriber;
import rx.Completable;
import rx.Single; import rx.Single;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
@ -117,8 +118,13 @@ public class ResponseEntityResultHandlerTests {
type = forClassWithGenerics(CompletableFuture.class, responseEntity(String.class)); type = forClassWithGenerics(CompletableFuture.class, responseEntity(String.class));
assertTrue(this.resultHandler.supports(handlerResult(value, type))); assertTrue(this.resultHandler.supports(handlerResult(value, type)));
// False
type = ResolvableType.forClass(String.class); type = ResolvableType.forClass(String.class);
assertFalse(this.resultHandler.supports(handlerResult(value, type))); assertFalse(this.resultHandler.supports(handlerResult(value, type)));
type = ResolvableType.forClass(Completable.class);
assertFalse(this.resultHandler.supports(handlerResult(value, type)));
} }
@Test @Test
@ -212,6 +218,8 @@ public class ResponseEntityResultHandlerTests {
CompletableFuture<ResponseEntity<String>> completableFuture() { return null; } CompletableFuture<ResponseEntity<String>> completableFuture() { return null; }
String string() { return null; } String string() { return null; }
Completable completable() { return null; }
} }
} }

View File

@ -33,6 +33,7 @@ import org.junit.Test;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.TestSubscriber; import reactor.test.TestSubscriber;
import rx.Completable;
import rx.Single; import rx.Single;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
@ -98,6 +99,8 @@ public class ViewResolutionResultHandlerTests {
testSupports(ResolvableType.forClassWithGenerics(Mono.class, View.class), true); testSupports(ResolvableType.forClassWithGenerics(Mono.class, View.class), true);
testSupports(ResolvableType.forClassWithGenerics(Single.class, String.class), true); testSupports(ResolvableType.forClassWithGenerics(Single.class, String.class), true);
testSupports(ResolvableType.forClassWithGenerics(Single.class, View.class), true); testSupports(ResolvableType.forClassWithGenerics(Single.class, View.class), true);
testSupports(ResolvableType.forClassWithGenerics(Mono.class, Void.class), true);
testSupports(ResolvableType.forClass(Completable.class), true);
testSupports(ResolvableType.forClass(Model.class), true); testSupports(ResolvableType.forClass(Model.class), true);
testSupports(ResolvableType.forClass(Map.class), true); testSupports(ResolvableType.forClass(Map.class), true);
testSupports(ResolvableType.forClass(TestBean.class), true); testSupports(ResolvableType.forClass(TestBean.class), true);
@ -169,6 +172,8 @@ public class ViewResolutionResultHandlerTests {
public void defaultViewName() throws Exception { public void defaultViewName() throws Exception {
testDefaultViewName(null, ResolvableType.forClass(String.class)); testDefaultViewName(null, ResolvableType.forClass(String.class));
testDefaultViewName(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, String.class)); testDefaultViewName(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, String.class));
testDefaultViewName(Mono.empty(), ResolvableType.forClassWithGenerics(Mono.class, Void.class));
testDefaultViewName(Completable.complete(), ResolvableType.forClass(Completable.class));
} }
private void testDefaultViewName(Object returnValue, ResolvableType type) private void testDefaultViewName(Object returnValue, ResolvableType type)
@ -387,10 +392,14 @@ public class ViewResolutionResultHandlerTests {
Mono<View> monoView() { return null; } Mono<View> monoView() { return null; }
Mono<Void> monoVoid() { return null; }
Single<String> singleString() { return null; } Single<String> singleString() { return null; }
Single<View> singleView() { return null; } Single<View> singleView() { return null; }
Completable completable() { return null; }
Model model() { return null; } Model model() { return null; }
Map map() { return null; } Map map() { return null; }