DATACMNS-836 - Add reactive repository support.

We now expose reactive interfaces to facilitate reactive repository support in store-specific modules. Spring Data modules are free to implement their reactive support using either RxJava 1 or Project Reactor (Reactive Streams). We expose a set of base interfaces:

* `ReactiveCrudRepository`
* `ReactiveSortingRepository`
* `RxJava1CrudRepository`
* `RxJava1SortingRepository`

Reactive repositories provide a similar feature coverage to blocking repositories. Reactive paging support is limited to a `Mono<Page>`/`Single<Page>`. Data is fetched in a deferred way to provide a paging experience similar to blocking paging.

A store module can choose either Project Reactor or RxJava 1 to implement reactive repository support. Project Reactor and RxJava types are converted in both directions allowing repositories to be composed of Project Reactor and RxJava 1 query methods. Reactive wrapper type conversion handles wrapper type conversion at repository level. Query/implementation method selection uses multi-pass candidate selection to invoke the most appropriate method (exact arguments, convertible wrappers, assignable arguments).

We also provide ReactiveWrappers to expose metadata about reactive types and their value multiplicity.
This commit is contained in:
Mark Paluch
2016-04-06 16:14:19 +02:00
committed by Oliver Gierke
parent 25be6b7254
commit 2e4d63fddd
21 changed files with 2527 additions and 58 deletions

View File

@@ -18,6 +18,12 @@ package org.springframework.data.repository.core.support;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import rx.Completable;
import rx.Observable;
import rx.Single;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
@@ -26,14 +32,17 @@ import java.util.Optional;
import java.util.Set;
import org.junit.Test;
import org.reactivestreams.Publisher;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.repository.Repository;
/**
* Unit tests for {@link QueryExecutionResultHandler}.
*
*
* @author Oliver Gierke
* @author Mark Paluch
*/
public class QueryExecutionResultHandlerUnitTests {
@@ -111,6 +120,295 @@ public class QueryExecutionResultHandlerUnitTests {
assertThat(handler.postProcessInvocationResult(null, getTypeDescriptorFor("map")), is(instanceOf(Map.class)));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsRxJavaSingleIntoPublisher() throws Exception {
Single<Entity> entity = Single.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("publisher"));
assertThat(result, is(instanceOf(Publisher.class)));
Mono<Entity> mono = Mono.from((Publisher<Entity>) result);
assertThat(mono.block(), is(entity.toBlocking().value()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsRxJavaSingleIntoMono() throws Exception {
Single<Entity> entity = Single.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("mono"));
assertThat(result, is(instanceOf(Mono.class)));
Mono<Entity> mono = (Mono<Entity>) result;
assertThat(mono.block(), is(entity.toBlocking().value()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsRxJavaSingleIntoFlux() throws Exception {
Single<Entity> entity = Single.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("flux"));
assertThat(result, is(instanceOf(Flux.class)));
Flux<Entity> flux = (Flux<Entity>) result;
assertThat(flux.next().block(), is(entity.toBlocking().value()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsRxJavaObservableIntoPublisher() throws Exception {
Observable<Entity> entity = Observable.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("publisher"));
assertThat(result, is(instanceOf(Publisher.class)));
Mono<Entity> mono = Mono.from((Publisher<Entity>) result);
assertThat(mono.block(), is(entity.toBlocking().first()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsRxJavaObservableIntoMono() throws Exception {
Observable<Entity> entity = Observable.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("mono"));
assertThat(result, is(instanceOf(Mono.class)));
Mono<Entity> mono = (Mono<Entity>) result;
assertThat(mono.block(), is(entity.toBlocking().first()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsRxJavaObservableIntoFlux() throws Exception {
Observable<Entity> entity = Observable.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("flux"));
assertThat(result, is(instanceOf(Flux.class)));
Flux<Entity> flux = (Flux<Entity>) result;
assertThat(flux.next().block(), is(entity.toBlocking().first()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsRxJavaObservableIntoSingle() throws Exception {
Observable<Entity> entity = Observable.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("single"));
assertThat(result, is(instanceOf(Single.class)));
Single<Entity> single = (Single<Entity>) result;
assertThat(single.toBlocking().value(), is(entity.toBlocking().first()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsRxJavaSingleIntoObservable() throws Exception {
Single<Entity> entity = Single.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("observable"));
assertThat(result, is(instanceOf(Observable.class)));
Observable<Entity> observable = (Observable<Entity>) result;
assertThat(observable.toBlocking().first(), is(entity.toBlocking().value()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsReactorMonoIntoSingle() throws Exception {
Mono<Entity> entity = Mono.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("single"));
assertThat(result, is(instanceOf(Single.class)));
Single<Entity> single = (Single<Entity>) result;
assertThat(single.toBlocking().value(), is(entity.block()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsReactorMonoIntoCompletable() throws Exception {
Mono<Entity> entity = Mono.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("completable"));
assertThat(result, is(instanceOf(Completable.class)));
Completable completable = (Completable) result;
assertThat(completable.get(), is(nullValue()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsReactorMonoIntoCompletableWithException() throws Exception {
Mono<Entity> entity = Mono.error(new InvalidDataAccessApiUsageException("err"));
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("completable"));
assertThat(result, is(instanceOf(Completable.class)));
Completable completable = (Completable) result;
assertThat(completable.get(), is(instanceOf(InvalidDataAccessApiUsageException.class)));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsRxJavaCompletableIntoMono() throws Exception {
Completable entity = Completable.complete();
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("mono"));
assertThat(result, is(instanceOf(Mono.class)));
Mono mono = (Mono) result;
assertThat(mono.block(), is(nullValue()));
}
/**
* @see DATACMNS-836
*/
@Test(expected = InvalidDataAccessApiUsageException.class)
@SuppressWarnings("unchecked")
public void convertsRxJavaCompletableIntoMonoWithException() throws Exception {
Completable entity = Completable.error(new InvalidDataAccessApiUsageException("err"));
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("mono"));
assertThat(result, is(instanceOf(Mono.class)));
Mono mono = (Mono) result;
mono.block();
fail("Missing InvalidDataAccessApiUsageException");
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsReactorMonoIntoObservable() throws Exception {
Mono<Entity> entity = Mono.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("observable"));
assertThat(result, is(instanceOf(Observable.class)));
Observable<Entity> observable = (Observable<Entity>) result;
assertThat(observable.toBlocking().first(), is(entity.block()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsReactorFluxIntoSingle() throws Exception {
Flux<Entity> entity = Flux.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("single"));
assertThat(result, is(instanceOf(Single.class)));
Single<Entity> single = (Single<Entity>) result;
assertThat(single.toBlocking().value(), is(entity.next().block()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsReactorFluxIntoObservable() throws Exception {
Flux<Entity> entity = Flux.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("observable"));
assertThat(result, is(instanceOf(Observable.class)));
Observable<Entity> observable = (Observable<Entity>) result;
assertThat(observable.toBlocking().first(), is(entity.next().block()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsReactorFluxIntoMono() throws Exception {
Flux<Entity> entity = Flux.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("mono"));
assertThat(result, is(instanceOf(Mono.class)));
Mono<Entity> mono = (Mono<Entity>) result;
assertThat(mono.block(), is(entity.next().block()));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void convertsReactorMonoIntoFlux() throws Exception {
Mono<Entity> entity = Mono.just(new Entity());
Object result = handler.postProcessInvocationResult(entity, getTypeDescriptorFor("flux"));
assertThat(result, is(instanceOf(Flux.class)));
Flux<Entity> flux = (Flux<Entity>) result;
assertThat(flux.next().block(), is(entity.block()));
}
private static TypeDescriptor getTypeDescriptorFor(String methodName) throws Exception {
Method method = Sample.class.getMethod(methodName);
@@ -128,6 +426,18 @@ public class QueryExecutionResultHandlerUnitTests {
com.google.common.base.Optional<Entity> guavaOptional();
Map<Integer, Entity> map();
Publisher<Entity> publisher();
Mono<Entity> mono();
Flux<Entity> flux();
Observable<Entity> observable();
Single<Entity> single();
Completable completable();
}
static class Entity {}

View File

@@ -51,16 +51,16 @@ public class QueryExecutorMethodInterceptorUnitTests {
when(information.getQueryMethods()).thenReturn(Arrays.asList(Object.class.getMethod("toString")));
when(factory.getQueryLookupStrategy(any(Key.class))).thenReturn(null);
factory.new QueryExecutorMethodInterceptor(information, null, new Object());
factory.new QueryExecutorMethodInterceptor(information);
}
@Test
public void skipsQueryLookupsIfQueryLookupStrategyIsNull() {
when(information.getQueryMethods()).thenReturn(Collections.<Method> emptySet());
when(information.getQueryMethods()).thenReturn(Collections.<Method>emptySet());
when(factory.getQueryLookupStrategy(any(Key.class))).thenReturn(strategy);
factory.new QueryExecutorMethodInterceptor(information, null, new Object());
factory.new QueryExecutorMethodInterceptor(information);
verify(strategy, times(0)).resolveQuery(any(Method.class), any(RepositoryMetadata.class),
any(ProjectionFactory.class), any(NamedQueries.class));
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright 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.data.repository.core.support;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import rx.Observable;
import java.io.Serializable;
import java.lang.reflect.Method;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import org.reactivestreams.Publisher;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.data.repository.reactive.ReactivePagingAndSortingRepository;
import org.springframework.data.repository.reactive.RxJavaCrudRepository;
import org.springframework.data.repository.util.QueryExecutionConverters;
/**
* Unit tests for {@link ConvertingMethodParameterRepositoryInformation}.
*
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class ReactiveRepositoryInformationUnitTests {
static final Class<ReactiveJavaInterfaceWithGenerics> REPOSITORY = ReactiveJavaInterfaceWithGenerics.class;
@Test
public void discoversMethodWithoutComparingReturnType() throws Exception {
Method method = RxJavaInterfaceWithGenerics.class.getMethod("deleteAll");
RepositoryMetadata metadata = new DefaultRepositoryMetadata(RxJavaInterfaceWithGenerics.class);
DefaultRepositoryInformation information = new DefaultRepositoryInformation(metadata, REPOSITORY, null);
Method reference = information.getTargetClassMethod(method);
assertEquals(ReactiveCrudRepository.class, reference.getDeclaringClass());
assertThat(reference.getName(), is("deleteAll"));
}
@Test
public void discoversMethodWithConvertibleArguments() throws Exception {
DefaultConversionService conversionService = new DefaultConversionService();
QueryExecutionConverters.registerConvertersIn(conversionService);
Method method = RxJavaInterfaceWithGenerics.class.getMethod("save", Observable.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(RxJavaInterfaceWithGenerics.class);
DefaultRepositoryInformation information = new ReactiveRepositoryInformation(metadata, REPOSITORY, null,
conversionService);
Method reference = information.getTargetClassMethod(method);
assertEquals(ReactiveCrudRepository.class, reference.getDeclaringClass());
assertThat(reference.getName(), is("save"));
assertThat(reference.getParameterTypes()[0], is(equalTo(Publisher.class)));
}
@Test
public void discoversMethodAssignableArguments() throws Exception {
DefaultConversionService conversionService = new DefaultConversionService();
QueryExecutionConverters.registerConvertersIn(conversionService);
Method method = ReactivePagingAndSortingRepository.class.getMethod("save", Publisher.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(ReactiveJavaInterfaceWithGenerics.class);
DefaultRepositoryInformation information = new ReactiveRepositoryInformation(metadata, REPOSITORY, null,
conversionService);
Method reference = information.getTargetClassMethod(method);
assertEquals(ReactiveCrudRepository.class, reference.getDeclaringClass());
assertThat(reference.getName(), is("save"));
assertThat(reference.getParameterTypes()[0], is(equalTo(Publisher.class)));
}
@Test
public void discoversMethodExactIterableArguments() throws Exception {
DefaultConversionService conversionService = new DefaultConversionService();
QueryExecutionConverters.registerConvertersIn(conversionService);
Method method = ReactiveJavaInterfaceWithGenerics.class.getMethod("save", Iterable.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(ReactiveJavaInterfaceWithGenerics.class);
DefaultRepositoryInformation information = new ReactiveRepositoryInformation(metadata, REPOSITORY, null,
conversionService);
Method reference = information.getTargetClassMethod(method);
assertEquals(ReactiveCrudRepository.class, reference.getDeclaringClass());
assertThat(reference.getName(), is("save"));
assertThat(reference.getParameterTypes()[0], is(equalTo(Iterable.class)));
}
@Test
public void discoversMethodExactObjectArguments() throws Exception {
DefaultConversionService conversionService = new DefaultConversionService();
QueryExecutionConverters.registerConvertersIn(conversionService);
Method method = ReactiveJavaInterfaceWithGenerics.class.getMethod("save", Object.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(ReactiveJavaInterfaceWithGenerics.class);
DefaultRepositoryInformation information = new ReactiveRepositoryInformation(metadata, REPOSITORY, null,
conversionService);
Method reference = information.getTargetClassMethod(method);
assertEquals(ReactiveCrudRepository.class, reference.getDeclaringClass());
assertThat(reference.getName(), is("save"));
assertThat(reference.getParameterTypes()[0], is(equalTo(Object.class)));
}
interface RxJavaInterfaceWithGenerics extends RxJavaCrudRepository<User, String> {}
interface ReactiveJavaInterfaceWithGenerics extends ReactiveCrudRepository<User, String> {}
static abstract class DummyGenericReactiveRepositorySupport<T, ID extends Serializable>
implements ReactiveCrudRepository<T, ID> {
}
static class User {
String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 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.data.repository.core.support;
import static org.mockito.Mockito.*;
import java.io.Serializable;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.reactive.ReactivePagingAndSortingRepository;
import org.springframework.data.repository.util.QueryExecutionConverters;
import reactor.core.publisher.Mono;
import rx.Single;
/**
* Unit tests for {@link RepositoryFactorySupport} using reactive wrapper types.
*
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class ReactiveWrapperRepositoryFactorySupportUnitTests {
public @Rule ExpectedException exception = ExpectedException.none();
DummyRepositoryFactory factory;
@Mock ReactivePagingAndSortingRepository<Object, Serializable> backingRepo;
@Mock ObjectRepositoryCustom customImplementation;
@Before
public void setUp() {
DefaultConversionService defaultConversionService = new DefaultConversionService();
QueryExecutionConverters.registerConvertersIn(defaultConversionService);
factory = new DummyRepositoryFactory(backingRepo);
factory.setConversionService(defaultConversionService);
}
/**
* @see DATACMNS-836
*/
@Test
public void invokesCustomMethodIfItRedeclaresACRUDOne() {
ObjectRepository repository = factory.getRepository(ObjectRepository.class, customImplementation);
repository.findOne(1);
verify(customImplementation, times(1)).findOne(1);
verify(backingRepo, times(0)).findOne(1);
}
/**
* @see DATACMNS-836
*/
@Test
public void callsMethodOnBaseImplementationWithExactArguments() {
Serializable id = 1L;
ConvertingRepository repository = factory.getRepository(ConvertingRepository.class);
repository.exists(id);
verify(backingRepo, times(1)).exists(id);
}
/**
* @see DATACMNS-836
*/
@Test
public void doesNotCallMethodOnBaseEvenIfDeclaredTypeCouldBeConverted() {
Long id = 1L;
ConvertingRepository repository = factory.getRepository(ConvertingRepository.class);
repository.exists(id);
verifyZeroInteractions(backingRepo);
}
/**
* @see DATACMNS-836
*/
@Test
public void callsMethodOnBaseImplementationWithTypeConversion() {
Single<Long> ids = Single.just(1L);
ConvertingRepository repository = factory.getRepository(ConvertingRepository.class);
repository.exists(ids);
verify(backingRepo, times(1)).exists(any(Mono.class));
}
interface ConvertingRepository extends Repository<Object, Long> {
Single<Boolean> exists(Single<Long> id);
Single<Boolean> exists(Serializable id);
Single<Boolean> exists(Long id);
}
interface ObjectRepository
extends Repository<Object, Serializable>, RepositoryFactorySupportUnitTests.ObjectRepositoryCustom {
}
interface ObjectRepositoryCustom {
Object findOne(Serializable id);
}
}

View File

@@ -17,12 +17,14 @@ package org.springframework.data.repository.query;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import rx.Single;
import java.lang.reflect.Method;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.reactivestreams.Publisher;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.test.util.ReflectionTestUtils;
@@ -31,6 +33,7 @@ import org.springframework.test.util.ReflectionTestUtils;
* Unit test for {@link Parameters}.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
public class ParametersUnitTests {
@@ -176,6 +179,28 @@ public class ParametersUnitTests {
assertThat(parameters.getParameter(0).getType(), is(typeCompatibleWith(String.class)));
}
/**
* @see DATACMNS-836
*/
@Test
public void keepsReactiveStreamsWrapper() throws Exception {
Parameters<?, Parameter> parameters = getParametersFor("methodWithPublisher", Publisher.class);
assertThat(parameters.getParameter(0).getType(), is(typeCompatibleWith(Publisher.class)));
}
/**
* @see DATACMNS-836
*/
@Test
public void keepsRxJavaWrapper() throws Exception {
Parameters<?, Parameter> parameters = getParametersFor("methodWithSingle", Single.class);
assertThat(parameters.getParameter(0).getType(), is(typeCompatibleWith(Single.class)));
}
private Parameters<?, Parameter> getParametersFor(String methodName, Class<?>... parameterTypes)
throws SecurityException, NoSuchMethodException {
@@ -209,5 +234,9 @@ public class ParametersUnitTests {
<T> T dynamicBind(Class<T> type, Class<?> one, Class<Object> two);
void methodWithOptional(Optional<String> optional);
void methodWithPublisher(Publisher<String> publisher);
void methodWithSingle(Single<String> single);
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright 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.data.repository.query;
import static org.assertj.core.api.AssertionsForClassTypes.*;
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;
/**
* Unit tests for {@link ReactiveWrapperConverters}.
*
* @author Mark Paluch
*/
public class ReactiveWrapperConvertersUnitTests {
/**
* @see DATACMNS-836
*/
@Test
public void shouldSupportReactorTypes() {
assertThat(ReactiveWrapperConverters.supports(Mono.class)).isTrue();
assertThat(ReactiveWrapperConverters.supports(Flux.class)).isTrue();
assertThat(ReactiveWrapperConverters.supports(Publisher.class)).isTrue();
assertThat(ReactiveWrapperConverters.supports(Object.class)).isFalse();
}
/**
* @see DATACMNS-836
*/
@Test
public void shouldSupportRxJavaTypes() {
assertThat(ReactiveWrapperConverters.supports(Single.class)).isTrue();
assertThat(ReactiveWrapperConverters.supports(Observable.class)).isTrue();
assertThat(ReactiveWrapperConverters.supports(Completable.class)).isFalse();
}
/**
* @see DATACMNS-836
*/
@Test
public void isSingleLikeShouldReportCorrectSingleTypes() {
assertThat(ReactiveWrapperConverters.isSingleLike(Mono.class)).isTrue();
assertThat(ReactiveWrapperConverters.isSingleLike(Flux.class)).isFalse();
assertThat(ReactiveWrapperConverters.isSingleLike(Single.class)).isTrue();
assertThat(ReactiveWrapperConverters.isSingleLike(Observable.class)).isFalse();
assertThat(ReactiveWrapperConverters.isSingleLike(Publisher.class)).isFalse();
}
/**
* @see DATACMNS-836
*/
@Test
public void isCollectionLikeShouldReportCorrectCollectionTypes() {
assertThat(ReactiveWrapperConverters.isCollectionLike(Mono.class)).isFalse();
assertThat(ReactiveWrapperConverters.isCollectionLike(Flux.class)).isTrue();
assertThat(ReactiveWrapperConverters.isCollectionLike(Single.class)).isFalse();
assertThat(ReactiveWrapperConverters.isCollectionLike(Observable.class)).isTrue();
assertThat(ReactiveWrapperConverters.isCollectionLike(Publisher.class)).isTrue();
}
/**
* @see DATACMNS-836
*/
@Test
public void toWrapperShouldCastMonoToMono() {
Mono<String> foo = Mono.just("foo");
assertThat(ReactiveWrapperConverters.toWrapper(foo, Mono.class)).isSameAs(foo);
}
/**
* @see DATACMNS-836
*/
@Test
public void toWrapperShouldConvertMonoToSingle() {
Mono<String> foo = Mono.just("foo");
assertThat(ReactiveWrapperConverters.toWrapper(foo, Single.class)).isInstanceOf(Single.class);
}
/**
* @see DATACMNS-836
*/
@Test
public void toWrapperShouldConvertMonoToFlux() {
Mono<String> foo = Mono.just("foo");
assertThat(ReactiveWrapperConverters.toWrapper(foo, Flux.class)).isInstanceOf(Flux.class);
}
/**
* @see DATACMNS-836
*/
@Test
public void shouldMapMono() {
Mono<String> foo = Mono.just("foo");
Mono<Long> map = ReactiveWrapperConverters.map(foo, source -> 1L);
assertThat(map.block()).isEqualTo(1L);
}
/**
* @see DATACMNS-836
*/
@Test
public void shouldMapFlux() {
Flux<String> foo = Flux.just("foo");
Flux<Long> map = ReactiveWrapperConverters.map(foo, source -> 1L);
assertThat(map.next().block()).isEqualTo(1L);
}
/**
* @see DATACMNS-836
*/
@Test
public void shouldMapSingle() {
Single<String> foo = Single.just("foo");
Single<Long> map = ReactiveWrapperConverters.map(foo, source -> 1L);
assertThat(map.toBlocking().value()).isEqualTo(1L);
}
/**
* @see DATACMNS-836
*/
@Test
public void shouldMapObservable() {
Observable<String> foo = Observable.just("foo");
Observable<Long> map = ReactiveWrapperConverters.map(foo, source -> 1L);
assertThat(map.toBlocking().first()).isEqualTo(1L);
}
}

View File

@@ -40,10 +40,16 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import rx.Observable;
import rx.Single;
/**
* Unit tests for {@link ResultProcessor}.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
public class ResultProcessorUnitTests {
@@ -260,6 +266,99 @@ public class ResultProcessorUnitTests {
processor.processResult(specialList);
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void supportsMonoWrapper() throws Exception {
Mono<Sample> samples = Mono.just(new Sample("Dave", "Matthews"));
Object result = getProcessor("findMonoSample").processResult(samples);
assertThat(result, is(instanceOf(Mono.class)));
Object content = ((Mono<Object>) result).block();
assertThat(content, is(instanceOf(Sample.class)));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void supportsSingleWrapper() throws Exception {
Single<Sample> samples = Single.just(new Sample("Dave", "Matthews"));
Object result = getProcessor("findSingleSample").processResult(samples);
assertThat(result, is(instanceOf(Single.class)));
Object content = ((Single<Object>) result).toBlocking().value();
assertThat(content, is(instanceOf(Sample.class)));
}
/**
* @see DATACMNS-836
*/
@Test
public void refrainsFromProjectingUsingReactiveWrappersIfThePreparingConverterReturnsACompatibleInstance()
throws Exception {
ResultProcessor processor = getProcessor("findMonoSampleDto");
Object result = processor.processResult(Mono.just(new Sample("Dave", "Matthews")), new Converter<Object, Object>() {
@Override
public Object convert(Object source) {
return new SampleDto();
}
});
assertThat(result, is(instanceOf(Mono.class)));
Object content = ((Mono<Object>) result).block();
assertThat(content, is(instanceOf(SampleDto.class)));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void supportsFluxProjections() throws Exception {
Flux<Sample> samples = Flux.just(new Sample("Dave", "Matthews"));
Object result = getProcessor("findFluxProjection").processResult(samples);
assertThat(result, is(instanceOf(Flux.class)));
List<Object> content = ((Flux<Object>) result).collectList().block();
assertThat(content, is(not(empty())));
assertThat(content.get(0), is(instanceOf(SampleProjection.class)));
}
/**
* @see DATACMNS-836
*/
@Test
@SuppressWarnings("unchecked")
public void supportsObservableProjections() throws Exception {
Observable<Sample> samples = Observable.just(new Sample("Dave", "Matthews"));
Object result = getProcessor("findObservableProjection").processResult(samples);
assertThat(result, is(instanceOf(Observable.class)));
List<Object> content = ((Observable<Object>) result).toList().toBlocking().single();
assertThat(content, is(not(empty())));
assertThat(content.get(0), is(instanceOf(SampleProjection.class)));
}
private static ResultProcessor getProcessor(String methodName, Class<?>... parameters) throws Exception {
return getQueryMethod(methodName, parameters).getResultProcessor();
}
@@ -296,6 +395,16 @@ public class ResultProcessorUnitTests {
<T> T findOneDynamic(Class<T> type);
Stream<SampleProjection> findStreamProjection();
Mono<Sample> findMonoSample();
Mono<SampleDto> findMonoSampleDto();
Single<Sample> findSingleSample();
Flux<SampleProjection> findFluxProjection();
Observable<SampleProjection> findObservableProjection();
}
static class Sample {

View File

@@ -19,6 +19,11 @@ import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import rx.Completable;
import rx.Observable;
import rx.Single;
import scala.Option;
import java.util.concurrent.CompletableFuture;
@@ -26,6 +31,7 @@ import java.util.concurrent.Future;
import org.junit.Before;
import org.junit.Test;
import org.reactivestreams.Publisher;
import org.springframework.core.SpringVersion;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.util.Version;
@@ -66,6 +72,47 @@ public class QueryExecutionConvertersUnitTests {
assertThat(QueryExecutionConverters.supports(Option.class), is(true));
}
/**
* @see DATACMNS-836
*/
@Test
public void registersReactiveWrapperTypes() {
assertThat(QueryExecutionConverters.supports(Publisher.class), is(true));
assertThat(QueryExecutionConverters.supports(Mono.class), is(true));
assertThat(QueryExecutionConverters.supports(Flux.class), is(true));
assertThat(QueryExecutionConverters.supports(Single.class), is(true));
assertThat(QueryExecutionConverters.supports(Completable.class), is(true));
assertThat(QueryExecutionConverters.supports(Observable.class), is(true));
}
/**
* @see DATACMNS-836
*/
@Test
public void registersUnwrapperTypes() {
assertThat(QueryExecutionConverters.supportsUnwrapping(Optional.class), is(true));
assertThat(QueryExecutionConverters.supportsUnwrapping(java.util.Optional.class), is(true));
assertThat(QueryExecutionConverters.supportsUnwrapping(Future.class), is(true));
assertThat(QueryExecutionConverters.supportsUnwrapping(ListenableFuture.class), is(true));
assertThat(QueryExecutionConverters.supportsUnwrapping(Option.class), is(true));
}
/**
* @see DATACMNS-836
*/
@Test
public void doesNotRegisterReactiveUnwrapperTypes() {
assertThat(QueryExecutionConverters.supportsUnwrapping(Publisher.class), is(false));
assertThat(QueryExecutionConverters.supportsUnwrapping(Mono.class), is(false));
assertThat(QueryExecutionConverters.supportsUnwrapping(Flux.class), is(false));
assertThat(QueryExecutionConverters.supportsUnwrapping(Single.class), is(false));
assertThat(QueryExecutionConverters.supportsUnwrapping(Completable.class), is(false));
assertThat(QueryExecutionConverters.supportsUnwrapping(Observable.class), is(false));
}
/**
* @see DATACMNS-714
*/