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:
committed by
Oliver Gierke
parent
25be6b7254
commit
2e4d63fddd
@@ -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 {}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user