From d3260d3caac5059d1bb1d2fd2c999749332c74ca Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 May 2020 11:54:40 +0200 Subject: [PATCH] DATACMNS-1653 - Add support for RxJava 3. We now include adapters and registry information for RxJava 3 types. Original pull request: #444. --- pom.xml | 7 + .../reactive/RxJava3CrudRepository.java | 189 ++++++++++++++++++ .../reactive/RxJava3SortingRepository.java | 43 ++++ .../util/ReactiveWrapperConverters.java | 94 +++++++++ .../repository/util/ReactiveWrappers.java | 24 ++- .../ReactiveWrapperConvertersUnitTests.java | 42 ++++ .../util/ReactiveWrappersUnitTests.java | 19 +- 7 files changed, 413 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/reactive/RxJava3CrudRepository.java create mode 100644 src/main/java/org/springframework/data/repository/reactive/RxJava3SortingRepository.java diff --git a/pom.xml b/pom.xml index c1b076135..3fdbfcb98 100644 --- a/pom.xml +++ b/pom.xml @@ -134,6 +134,13 @@ true + + io.reactivex.rxjava3 + rxjava + ${rxjava3} + true + + diff --git a/src/main/java/org/springframework/data/repository/reactive/RxJava3CrudRepository.java b/src/main/java/org/springframework/data/repository/reactive/RxJava3CrudRepository.java new file mode 100644 index 000000000..2897b5d01 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/reactive/RxJava3CrudRepository.java @@ -0,0 +1,189 @@ +/* + * Copyright 2020 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 + * + * https://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.reactive; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Single; + +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.Repository; + +/** + * Interface for generic CRUD operations on a repository for a specific type. This repository follows reactive paradigms + * and uses RxJava 3 types. + * + * @author Mark Paluch + * @since 2.4 + * @see Maybe + * @see Single + * @see Flowable + * @see Completable + */ +@NoRepositoryBean +public interface RxJava3CrudRepository extends Repository { + + /** + * Saves a given entity. Use the returned instance for further operations as the save operation might have changed the + * entity instance completely. + * + * @param entity must not be {@literal null}. + * @return {@link Single} emitting the saved entity. + * @throws IllegalArgumentException in case the given {@literal entity} is {@literal null}. + */ + Single save(S entity); + + /** + * Saves all given entities. + * + * @param entities must not be {@literal null}. + * @return {@link Flowable} emitting the saved entities. + * @throws IllegalArgumentException in case the given {@link Iterable entities} or one of its entities is + * {@literal null}. + */ + Flowable saveAll(Iterable entities); + + /** + * Saves all given entities. + * + * @param entityStream must not be {@literal null}. + * @return {@link Flowable} emitting the saved entities. + * @throws IllegalArgumentException in case the given {@link Flowable entityStream} is {@literal null}. + */ + Flowable saveAll(Flowable entityStream); + + /** + * Retrieves an entity by its id. + * + * @param id must not be {@literal null}. + * @return {@link Maybe} emitting the entity with the given id or {@link Maybe#empty()} if none found. + * @throws IllegalArgumentException in case the given {@literal id} is {@literal null}. + */ + Maybe findById(ID id); + + /** + * Retrieves an entity by its id supplied by a {@link Single}. + * + * @param id must not be {@literal null}. Uses the first emitted element to perform the find-query. + * @return {@link Maybe} emitting the entity with the given id or {@link Maybe#empty()} if none found. + * @throws IllegalArgumentException in case the given {@link Single id} is {@literal null}. + */ + Maybe findById(Single id); + + /** + * Returns whether an entity with the given {@literal id} exists. + * + * @param id must not be {@literal null}. + * @return {@link Single} emitting {@literal true} if an entity with the given id exists, {@literal false} otherwise. + * @throws IllegalArgumentException in case the given {@literal id} is {@literal null}. + */ + Single existsById(ID id); + + /** + * Returns whether an entity with the given id, supplied by a {@link Single}, exists. + * + * @param id must not be {@literal null}. + * @return {@link Single} emitting {@literal true} if an entity with the given id exists, {@literal false} otherwise. + * @throws IllegalArgumentException in case the given {@link Single id} is {@literal null}. + */ + Single existsById(Single id); + + /** + * Returns all instances of the type. + * + * @return {@link Flowable} emitting all entities. + */ + Flowable findAll(); + + /** + * Returns all instances of the type {@code T} with the given IDs. + *

+ * If some or all ids are not found, no entities are returned for these IDs. + *

+ * Note that the order of elements in the result is not guaranteed. + * + * @param ids must not be {@literal null} nor contain any {@literal null} values. + * @return {@link Flowable} emitting the found entities. The size can be equal or less than the number of given + * {@literal ids}. + * @throws IllegalArgumentException in case the given {@link Iterable ids} or one of its items is {@literal null}. + */ + Flowable findAllById(Iterable ids); + + /** + * Returns all instances of the type {@code T} with the given IDs supplied by a {@link Flowable}. + *

+ * If some or all ids are not found, no entities are returned for these IDs. + *

+ * Note that the order of elements in the result is not guaranteed. + * + * @param idStream must not be {@literal null}. + * @return {@link Flowable} emitting the found entities. + * @throws IllegalArgumentException in case the given {@link Flowable idStream} is {@literal null}. + */ + Flowable findAllById(Flowable idStream); + + /** + * Returns the number of entities available. + * + * @return {@link Single} emitting the number of entities. + */ + Single count(); + + /** + * Deletes the entity with the given id. + * + * @param id must not be {@literal null}. + * @return {@link Completable} signaling when operation has completed. + * @throws IllegalArgumentException in case the given {@literal id} is {@literal null}. + */ + Completable deleteById(ID id); + + /** + * Deletes a given entity. + * + * @param entity must not be {@literal null}. + * @return {@link Completable} signaling when operation has completed. + * @throws IllegalArgumentException in case the given entity is {@literal null}. + */ + Completable delete(T entity); + + /** + * Deletes the given entities. + * + * @param entities must not be {@literal null}. + * @return {@link Completable} signaling when operation has completed. + * @throws IllegalArgumentException in case the given {@link Iterable entities} or one of its entities is + * {@literal null}. + */ + Completable deleteAll(Iterable entities); + + /** + * Deletes the given entities supplied by a {@link Flowable}. + * + * @param entityStream must not be {@literal null}. + * @return {@link Completable} signaling when operation has completed. + * @throws IllegalArgumentException in case the given {@link Flowable entityStream} is {@literal null}. + */ + Completable deleteAll(Flowable entityStream); + + /** + * Deletes all entities managed by the repository. + * + * @return {@link Completable} signaling when operation has completed. + */ + Completable deleteAll(); +} diff --git a/src/main/java/org/springframework/data/repository/reactive/RxJava3SortingRepository.java b/src/main/java/org/springframework/data/repository/reactive/RxJava3SortingRepository.java new file mode 100644 index 000000000..1b8829343 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/reactive/RxJava3SortingRepository.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 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 + * + * https://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.reactive; + +import io.reactivex.rxjava3.core.Flowable; + +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.NoRepositoryBean; + +/** + * Extension of {@link RxJava3CrudRepository} to provide additional methods to retrieve entities using the sorting + * abstraction. + * + * @author Mark Paluch + * @since 2.4 + * @see Sort + * @see Flowable + * @see RxJava2CrudRepository + */ +@NoRepositoryBean +public interface RxJava3SortingRepository extends RxJava3CrudRepository { + + /** + * Returns all entities sorted by the given options. + * + * @param sort must not be {@literal null}. + * @return all entities sorted by the given options. + */ + Flowable findAll(Sort sort); +} diff --git a/src/main/java/org/springframework/data/repository/util/ReactiveWrapperConverters.java b/src/main/java/org/springframework/data/repository/util/ReactiveWrapperConverters.java index 4e10ab163..288bb9214 100644 --- a/src/main/java/org/springframework/data/repository/util/ReactiveWrapperConverters.java +++ b/src/main/java/org/springframework/data/repository/util/ReactiveWrapperConverters.java @@ -52,6 +52,8 @@ import org.springframework.util.ClassUtils; *

* This class discovers reactive wrapper availability and their conversion support based on the class path. Reactive * wrapper types might be supported/on the class path but conversion may require additional dependencies. + *

+ * Note: As of Spring Data 2.4, support for RxJava 1.x is deprecated in favor of RxJava 2 and 3. * * @author Mark Paluch * @author Oliver Gierke @@ -80,6 +82,14 @@ public abstract class ReactiveWrapperConverters { REACTIVE_WRAPPERS.add(RxJava2FlowableWrapper.INSTANCE); } + if (ReactiveWrappers.isAvailable(ReactiveLibrary.RXJAVA3)) { + + REACTIVE_WRAPPERS.add(RxJava3SingleWrapper.INSTANCE); + REACTIVE_WRAPPERS.add(RxJava3MaybeWrapper.INSTANCE); + REACTIVE_WRAPPERS.add(RxJava3ObservableWrapper.INSTANCE); + REACTIVE_WRAPPERS.add(RxJava3FlowableWrapper.INSTANCE); + } + if (ReactiveWrappers.isAvailable(ReactiveLibrary.PROJECT_REACTOR)) { REACTIVE_WRAPPERS.add(FluxWrapper.INSTANCE); @@ -278,6 +288,10 @@ public abstract class ReactiveWrapperConverters { } } + // ------------------------------------------------------------------------- + // RxJava 1 converters + // ------------------------------------------------------------------------- + /** * Wrapper for RxJava 1's {@link Single}. */ @@ -314,6 +328,10 @@ public abstract class ReactiveWrapperConverters { } } + // ------------------------------------------------------------------------- + // RxJava 2 converters + // ------------------------------------------------------------------------- + /** * Wrapper for RxJava 2's {@link io.reactivex.Single}. */ @@ -386,6 +404,82 @@ public abstract class ReactiveWrapperConverters { } } + // ------------------------------------------------------------------------- + // RxJava 3 converters + // ------------------------------------------------------------------------- + + /** + * Wrapper for RxJava 3's {@link io.reactivex.rxjava3.core.Single}. + */ + private static enum RxJava3SingleWrapper implements ReactiveTypeWrapper> { + + INSTANCE; + + @Override + public Class> getWrapperClass() { + return io.reactivex.rxjava3.core.Single.class; + } + + @Override + public io.reactivex.rxjava3.core.Single map(Object wrapper, Function function) { + return ((io.reactivex.rxjava3.core.Single) wrapper).map(function::apply); + } + } + + /** + * Wrapper for RxJava 3's {@link io.reactivex.rxjava3.core.Maybe}. + */ + private static enum RxJava3MaybeWrapper implements ReactiveTypeWrapper> { + + INSTANCE; + + @Override + public Class> getWrapperClass() { + return io.reactivex.rxjava3.core.Maybe.class; + } + + @Override + public io.reactivex.rxjava3.core.Maybe map(Object wrapper, Function function) { + return ((io.reactivex.rxjava3.core.Maybe) wrapper).map(function::apply); + } + } + + /** + * Wrapper for RxJava 3's {@link io.reactivex.rxjava3.core.Observable}. + */ + private static enum RxJava3ObservableWrapper implements ReactiveTypeWrapper> { + + INSTANCE; + + @Override + public Class> getWrapperClass() { + return io.reactivex.rxjava3.core.Observable.class; + } + + @Override + public io.reactivex.rxjava3.core.Observable map(Object wrapper, Function function) { + return ((io.reactivex.rxjava3.core.Observable) wrapper).map(function::apply); + } + } + + /** + * Wrapper for RxJava 3's {@link io.reactivex.rxjava3.core.Flowable}. + */ + private static enum RxJava3FlowableWrapper implements ReactiveTypeWrapper> { + + INSTANCE; + + @Override + public Class> getWrapperClass() { + return io.reactivex.rxjava3.core.Flowable.class; + } + + @Override + public io.reactivex.rxjava3.core.Flowable map(Object wrapper, Function function) { + return ((io.reactivex.rxjava3.core.Flowable) wrapper).map(function::apply); + } + } + // ------------------------------------------------------------------------- // ReactiveStreams converters // ------------------------------------------------------------------------- diff --git a/src/main/java/org/springframework/data/repository/util/ReactiveWrappers.java b/src/main/java/org/springframework/data/repository/util/ReactiveWrappers.java index 1b8802d3d..9003da6d0 100644 --- a/src/main/java/org/springframework/data/repository/util/ReactiveWrappers.java +++ b/src/main/java/org/springframework/data/repository/util/ReactiveWrappers.java @@ -37,6 +37,8 @@ import org.springframework.util.ClassUtils; *

* Supported types are discovered by their availability on the class path. This class is typically used to determine * multiplicity and whether a reactive wrapper type is acceptable for a specific operation. + *

+ * Note: As of Spring Data 2.4, support for RxJava 1.x is deprecated in favor of RxJava 2 and 3. * * @author Mark Paluch * @author Christoph Strobl @@ -51,6 +53,11 @@ import org.springframework.util.ClassUtils; * @see io.reactivex.Observable * @see io.reactivex.Completable * @see io.reactivex.Flowable + * @see io.reactivex.rxjava3.core.Single + * @see io.reactivex.rxjava3.core.Maybe + * @see io.reactivex.rxjava3.core.Observable + * @see io.reactivex.rxjava3.core.Completable + * @see io.reactivex.rxjava3.core.Flowable * @see Mono * @see Flux */ @@ -59,12 +66,16 @@ public abstract class ReactiveWrappers { private static final boolean PROJECT_REACTOR_PRESENT = ClassUtils.isPresent("reactor.core.publisher.Mono", ReactiveWrappers.class.getClassLoader()); + @Deprecated private static final boolean RXJAVA1_PRESENT = ClassUtils.isPresent("rx.Completable", ReactiveWrappers.class.getClassLoader()); private static final boolean RXJAVA2_PRESENT = ClassUtils.isPresent("io.reactivex.Flowable", ReactiveWrappers.class.getClassLoader()); + private static final boolean RXJAVA3_PRESENT = ClassUtils.isPresent("io.reactivex.rxjava3.core.Flowable", + ReactiveWrappers.class.getClassLoader()); + private static final boolean KOTLIN_COROUTINES_PRESENT = ClassUtils.isPresent("kotlinx.coroutines.flow.Flow", ReactiveWrappers.class.getClassLoader()) && ClassUtils.isPresent("kotlinx.coroutines.reactive.ReactiveFlowKt", ReactiveWrappers.class.getClassLoader()) @@ -77,8 +88,15 @@ public abstract class ReactiveWrappers { * * @author Mark Paluch */ - public static enum ReactiveLibrary { - PROJECT_REACTOR, RXJAVA1, RXJAVA2, KOTLIN_COROUTINES; + public enum ReactiveLibrary { + + PROJECT_REACTOR, + + /** + * @deprecated since 2.4, use RxJava 2 or 3 instead. + */ + @Deprecated + RXJAVA1, RXJAVA2, RXJAVA3, KOTLIN_COROUTINES; } /** @@ -108,6 +126,8 @@ public abstract class ReactiveWrappers { return RXJAVA1_PRESENT; case RXJAVA2: return RXJAVA2_PRESENT; + case RXJAVA3: + return RXJAVA3_PRESENT; case KOTLIN_COROUTINES: return PROJECT_REACTOR_PRESENT && KOTLIN_COROUTINES_PRESENT; default: diff --git a/src/test/java/org/springframework/data/repository/util/ReactiveWrapperConvertersUnitTests.java b/src/test/java/org/springframework/data/repository/util/ReactiveWrapperConvertersUnitTests.java index cc3f4191d..bb64dfd30 100644 --- a/src/test/java/org/springframework/data/repository/util/ReactiveWrapperConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/ReactiveWrapperConvertersUnitTests.java @@ -61,6 +61,16 @@ class ReactiveWrapperConvertersUnitTests { assertThat(ReactiveWrapperConverters.supports(io.reactivex.Completable.class)).isTrue(); } + @Test // DATACMNS-1653 + void shouldSupportRxJava3Types() { + + assertThat(ReactiveWrapperConverters.supports(io.reactivex.rxjava3.core.Single.class)).isTrue(); + assertThat(ReactiveWrapperConverters.supports(io.reactivex.rxjava3.core.Maybe.class)).isTrue(); + assertThat(ReactiveWrapperConverters.supports(io.reactivex.rxjava3.core.Observable.class)).isTrue(); + assertThat(ReactiveWrapperConverters.supports(io.reactivex.rxjava3.core.Flowable.class)).isTrue(); + assertThat(ReactiveWrapperConverters.supports(io.reactivex.rxjava3.core.Completable.class)).isTrue(); + } + @Test // DATACMNS-836 void toWrapperShouldCastMonoToMono() { @@ -261,4 +271,36 @@ class ReactiveWrapperConvertersUnitTests { io.reactivex.Flowable map = ReactiveWrapperConverters.map(foo, source -> 1L); assertThat(map.blockingFirst()).isEqualTo(1L); } + + @Test // DATACMNS-1653 + void shouldMapRxJava3Single() { + + io.reactivex.rxjava3.core.Single foo = io.reactivex.rxjava3.core.Single.just("foo"); + io.reactivex.rxjava3.core.Single map = ReactiveWrapperConverters.map(foo, source -> 1L); + assertThat(map.blockingGet()).isEqualTo(1L); + } + + @Test // DATACMNS-1653 + void shouldMapRxJava3Maybe() { + + io.reactivex.rxjava3.core.Maybe foo = io.reactivex.rxjava3.core.Maybe.just("foo"); + io.reactivex.rxjava3.core.Maybe map = ReactiveWrapperConverters.map(foo, source -> 1L); + assertThat(map.toSingle().blockingGet()).isEqualTo(1L); + } + + @Test // DATACMNS-1653 + void shouldMapRxJava3Observable() { + + io.reactivex.rxjava3.core.Observable foo = io.reactivex.rxjava3.core.Observable.just("foo"); + io.reactivex.rxjava3.core.Observable map = ReactiveWrapperConverters.map(foo, source -> 1L); + assertThat(map.blockingFirst()).isEqualTo(1L); + } + + @Test // DATACMNS-1653 + void shouldMapRxJava3Flowable() { + + io.reactivex.rxjava3.core.Flowable foo = io.reactivex.rxjava3.core.Flowable.just("foo"); + io.reactivex.rxjava3.core.Flowable map = ReactiveWrapperConverters.map(foo, source -> 1L); + assertThat(map.blockingFirst()).isEqualTo(1L); + } } diff --git a/src/test/java/org/springframework/data/repository/util/ReactiveWrappersUnitTests.java b/src/test/java/org/springframework/data/repository/util/ReactiveWrappersUnitTests.java index 25bdccebf..ef89aa92c 100644 --- a/src/test/java/org/springframework/data/repository/util/ReactiveWrappersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/ReactiveWrappersUnitTests.java @@ -34,7 +34,7 @@ import org.reactivestreams.Publisher; */ class ReactiveWrappersUnitTests { - @Test // DATACMNS-836 + @Test // DATACMNS-836, DATACMNS-1653 void isSingleLikeShouldReportCorrectNoTypes() { assertThat(ReactiveWrappers.isNoValueType(Mono.class)).isFalse(); @@ -47,9 +47,13 @@ class ReactiveWrappersUnitTests { assertThat(ReactiveWrappers.isNoValueType(io.reactivex.Maybe.class)).isFalse(); assertThat(ReactiveWrappers.isNoValueType(Flowable.class)).isFalse(); assertThat(ReactiveWrappers.isNoValueType(io.reactivex.Observable.class)).isFalse(); + assertThat(ReactiveWrappers.isNoValueType(io.reactivex.rxjava3.core.Single.class)).isFalse(); + assertThat(ReactiveWrappers.isNoValueType(io.reactivex.rxjava3.core.Maybe.class)).isFalse(); + assertThat(ReactiveWrappers.isNoValueType(io.reactivex.rxjava3.core.Flowable.class)).isFalse(); + assertThat(ReactiveWrappers.isNoValueType(io.reactivex.rxjava3.core.Observable.class)).isFalse(); } - @Test // DATACMNS-836 + @Test // DATACMNS-836, DATACMNS-1653 void isSingleLikeShouldReportCorrectSingleTypes() { assertThat(ReactiveWrappers.isSingleValueType(Mono.class)).isTrue(); @@ -63,9 +67,14 @@ class ReactiveWrappersUnitTests { assertThat(ReactiveWrappers.isSingleValueType(io.reactivex.Maybe.class)).isTrue(); assertThat(ReactiveWrappers.isSingleValueType(Flowable.class)).isFalse(); assertThat(ReactiveWrappers.isSingleValueType(io.reactivex.Observable.class)).isFalse(); + assertThat(ReactiveWrappers.isSingleValueType(io.reactivex.rxjava3.core.Single.class)).isTrue(); + assertThat(ReactiveWrappers.isSingleValueType(io.reactivex.rxjava3.core.Completable.class)).isFalse(); + assertThat(ReactiveWrappers.isSingleValueType(io.reactivex.rxjava3.core.Maybe.class)).isTrue(); + assertThat(ReactiveWrappers.isSingleValueType(io.reactivex.rxjava3.core.Flowable.class)).isFalse(); + assertThat(ReactiveWrappers.isSingleValueType(io.reactivex.rxjava3.core.Observable.class)).isFalse(); } - @Test // DATACMNS-836 + @Test // DATACMNS-836, DATACMNS-1653 void isCollectionLikeShouldReportCorrectCollectionTypes() { assertThat(ReactiveWrappers.isMultiValueType(Mono.class)).isFalse(); @@ -78,5 +87,9 @@ class ReactiveWrappersUnitTests { assertThat(ReactiveWrappers.isSingleValueType(io.reactivex.Completable.class)).isFalse(); assertThat(ReactiveWrappers.isMultiValueType(Flowable.class)).isTrue(); assertThat(ReactiveWrappers.isMultiValueType(io.reactivex.Observable.class)).isTrue(); + assertThat(ReactiveWrappers.isMultiValueType(io.reactivex.rxjava3.core.Single.class)).isFalse(); + assertThat(ReactiveWrappers.isSingleValueType(io.reactivex.rxjava3.core.Completable.class)).isFalse(); + assertThat(ReactiveWrappers.isMultiValueType(io.reactivex.rxjava3.core.Flowable.class)).isTrue(); + assertThat(ReactiveWrappers.isMultiValueType(io.reactivex.rxjava3.core.Observable.class)).isTrue(); } }