From 6190baa3db51dda52fc6071d9ac963fc8bd99f7e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 2 Nov 2022 10:50:03 +0100 Subject: [PATCH] Move `ReactiveWrappers` into `o.s.d.util` package. ReactiveWrappers is not tied to repositories so it does not need to reside in the repository.util package. This utility is now located in the data.util package for a better module design and to prevent package cycles. See #2708 --- .../AuditingBeanRegistrationAotProcessor.java | 5 +- .../data/querydsl/aot/QuerydslHints.java | 4 +- .../aot/hint/RepositoryRuntimeHints.java | 7 +- .../support/AbstractRepositoryMetadata.java | 2 +- .../core/support/MethodLookups.java | 2 +- .../ReactiveRepositoryFactorySupport.java | 3 +- .../core/support/RepositoryComposition.java | 2 +- .../core/support/RepositoryMethodInvoker.java | 3 +- .../util/ReactiveWrapperConverters.java | 20 +- .../repository/util/ReactiveWrappers.java | 95 ++----- .../data/util/ReactiveWrappers.java | 245 ++++++++++++++++++ .../util/ReactiveWrappersUnitTests.java | 4 +- 12 files changed, 285 insertions(+), 107 deletions(-) create mode 100644 src/main/java/org/springframework/data/util/ReactiveWrappers.java rename src/test/java/org/springframework/data/{repository => }/util/ReactiveWrappersUnitTests.java (97%) diff --git a/src/main/java/org/springframework/data/aot/AuditingBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/AuditingBeanRegistrationAotProcessor.java index 126b4a6f0..a46bcb8bd 100644 --- a/src/main/java/org/springframework/data/aot/AuditingBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/aot/AuditingBeanRegistrationAotProcessor.java @@ -25,6 +25,7 @@ import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.DecoratingProxy; import org.springframework.data.domain.AuditorAware; import org.springframework.data.domain.ReactiveAuditorAware; +import org.springframework.data.util.ReactiveWrappers; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -38,8 +39,6 @@ import org.springframework.util.ClassUtils; */ class AuditingBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { - private static final boolean PROJECT_REACTOR_PRESENT = ClassUtils.isPresent("reactor.core.publisher.Flux", - AuditingBeanRegistrationAotProcessor.class.getClassLoader()); @Nullable @Override @@ -50,7 +49,7 @@ class AuditingBeanRegistrationAotProcessor implements BeanRegistrationAotProcess generationContext.getRuntimeHints()); } - if (PROJECT_REACTOR_PRESENT && isReactiveAuditorAware(registeredBean)) { + if (ReactiveWrappers.PROJECT_REACTOR_PRESENT && isReactiveAuditorAware(registeredBean)) { return (generationContext, beanRegistrationCode) -> registerSpringProxy(ReactiveAuditorAware.class, generationContext.getRuntimeHints()); } diff --git a/src/main/java/org/springframework/data/querydsl/aot/QuerydslHints.java b/src/main/java/org/springframework/data/querydsl/aot/QuerydslHints.java index ca210b4c9..6b2e708cf 100644 --- a/src/main/java/org/springframework/data/querydsl/aot/QuerydslHints.java +++ b/src/main/java/org/springframework/data/querydsl/aot/QuerydslHints.java @@ -24,8 +24,8 @@ import org.springframework.aot.hint.TypeReference; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.QuerydslUtils; import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.util.ReactiveWrappers; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; import com.querydsl.core.types.Predicate; @@ -50,7 +50,7 @@ class QuerydslHints implements RuntimeHintsRegistrar { builder.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); }); - if (ClassUtils.isPresent("reactor.core.publisher.Flux", classLoader)) { + if (ReactiveWrappers.PROJECT_REACTOR_PRESENT) { // repository infrastructure hints.reflection().registerTypes(Arrays.asList( // diff --git a/src/main/java/org/springframework/data/repository/aot/hint/RepositoryRuntimeHints.java b/src/main/java/org/springframework/data/repository/aot/hint/RepositoryRuntimeHints.java index 20209798a..7661324d5 100644 --- a/src/main/java/org/springframework/data/repository/aot/hint/RepositoryRuntimeHints.java +++ b/src/main/java/org/springframework/data/repository/aot/hint/RepositoryRuntimeHints.java @@ -36,8 +36,8 @@ import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuer import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; +import org.springframework.data.util.ReactiveWrappers; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; /** * {@link RuntimeHintsRegistrar} holding required hints to bootstrap data repositories.
@@ -49,9 +49,6 @@ import org.springframework.util.ClassUtils; */ class RepositoryRuntimeHints implements RuntimeHintsRegistrar { - private static final boolean PROJECT_REACTOR_PRESENT = ClassUtils.isPresent("reactor.core.publisher.Flux", - RepositoryRuntimeHints.class.getClassLoader()); - @Override public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { @@ -71,7 +68,7 @@ class RepositoryRuntimeHints implements RuntimeHintsRegistrar { builder.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); }); - if (PROJECT_REACTOR_PRESENT) { + if (ReactiveWrappers.PROJECT_REACTOR_PRESENT) { // repository infrastructure hints.reflection().registerTypes(Arrays.asList( // diff --git a/src/main/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadata.java b/src/main/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadata.java index 44230bbc2..4d5fc411c 100644 --- a/src/main/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadata.java +++ b/src/main/java/org/springframework/data/repository/core/support/AbstractRepositoryMetadata.java @@ -28,9 +28,9 @@ import org.springframework.data.repository.core.CrudMethods; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.util.QueryExecutionConverters; import org.springframework.data.repository.util.ReactiveWrapperConverters; -import org.springframework.data.repository.util.ReactiveWrappers; import org.springframework.data.util.KotlinReflectionUtils; import org.springframework.data.util.Lazy; +import org.springframework.data.util.ReactiveWrappers; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; diff --git a/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java b/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java index 3e4c28864..d75a25a52 100644 --- a/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java +++ b/src/main/java/org/springframework/data/repository/core/support/MethodLookups.java @@ -36,7 +36,7 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.MethodLookup.MethodPredicate; import org.springframework.data.repository.util.QueryExecutionConverters; import org.springframework.data.repository.util.ReactiveWrapperConverters; -import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.data.util.ReactiveWrappers; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryFactorySupport.java index f749f944e..2a060d364 100644 --- a/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/ReactiveRepositoryFactorySupport.java @@ -19,13 +19,12 @@ import java.lang.reflect.Method; import java.util.Arrays; import org.reactivestreams.Publisher; - import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.util.ReactiveWrapperConverters; -import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.data.util.ReactiveWrappers; import org.springframework.util.ClassUtils; /** diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java index 69d81a5bd..36ff4f992 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java @@ -33,7 +33,7 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.MethodLookup.InvokedMethod; import org.springframework.data.repository.core.support.RepositoryInvocationMulticaster.NoOpRepositoryInvocationMulticaster; import org.springframework.data.repository.util.ReactiveWrapperConverters; -import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.data.util.ReactiveWrappers; import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java index f3b3f886b..94d4185f4 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java @@ -27,15 +27,14 @@ import java.util.Collection; import java.util.stream.Stream; import org.reactivestreams.Publisher; - import org.springframework.core.KotlinDetector; import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult; import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult.State; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.util.ReactiveWrapperConverters; -import org.springframework.data.repository.util.ReactiveWrappers; import org.springframework.data.util.KotlinReflectionUtils; +import org.springframework.data.util.ReactiveWrappers; import org.springframework.lang.Nullable; /** 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 b4e5bdd72..7437da382 100644 --- a/src/main/java/org/springframework/data/repository/util/ReactiveWrapperConverters.java +++ b/src/main/java/org/springframework/data/repository/util/ReactiveWrapperConverters.java @@ -27,7 +27,6 @@ import java.util.Optional; import java.util.function.Function; import org.reactivestreams.Publisher; - import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.convert.ConversionService; @@ -37,7 +36,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.data.repository.util.ReactiveWrappers.ReactiveLibrary; +import org.springframework.data.util.ReactiveWrappers.ReactiveLibrary; import org.springframework.data.util.TypeInformation; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; @@ -56,7 +55,7 @@ import org.springframework.util.ClassUtils; * @author Christoph Strobl * @author Hantsy Bai * @since 2.0 - * @see ReactiveWrappers + * @see org.springframework.data.util.ReactiveWrappers * @see ReactiveAdapterRegistry */ public abstract class ReactiveWrapperConverters { @@ -64,11 +63,14 @@ public abstract class ReactiveWrapperConverters { private static final List> REACTIVE_WRAPPERS = new ArrayList<>(); private static final GenericConversionService GENERIC_CONVERSION_SERVICE = new GenericConversionService(); - private static final boolean RXJAVA3_PRESENT = ReactiveWrappers.isAvailable(ReactiveLibrary.RXJAVA3); - private static final boolean REACTOR_PRESENT = ReactiveWrappers.isAvailable(ReactiveLibrary.PROJECT_REACTOR); - private static final boolean KOTLIN_COROUTNES_PRESENT = ReactiveWrappers + private static final boolean RXJAVA3_PRESENT = org.springframework.data.util.ReactiveWrappers + .isAvailable(ReactiveLibrary.RXJAVA3); + private static final boolean REACTOR_PRESENT = org.springframework.data.util.ReactiveWrappers + .isAvailable(ReactiveLibrary.PROJECT_REACTOR); + private static final boolean KOTLIN_COROUTNES_PRESENT = org.springframework.data.util.ReactiveWrappers .isAvailable(ReactiveLibrary.KOTLIN_COROUTINES); - private static final boolean MUTINY_PRESENT = ReactiveWrappers.isAvailable(ReactiveLibrary.MUTINY); + private static final boolean MUTINY_PRESENT = org.springframework.data.util.ReactiveWrappers + .isAvailable(ReactiveLibrary.MUTINY); static { @@ -548,8 +550,8 @@ public abstract class ReactiveWrapperConverters { static { - if (ReactiveWrappers.isAvailable(ReactiveLibrary.PROJECT_REACTOR)) { - REACTIVE_ADAPTER_REGISTRY = new ReactiveAdapterRegistry(); + if (org.springframework.data.util.ReactiveWrappers.PROJECT_REACTOR_PRESENT) { + REACTIVE_ADAPTER_REGISTRY = ReactiveAdapterRegistry.getSharedInstance(); } else { REACTIVE_ADAPTER_REGISTRY = null; } 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 eaa52d194..a75863cf5 100644 --- a/src/main/java/org/springframework/data/repository/util/ReactiveWrappers.java +++ b/src/main/java/org/springframework/data/repository/util/ReactiveWrappers.java @@ -18,18 +18,7 @@ package org.springframework.data.repository.util; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.Arrays; -import java.util.Map; -import java.util.Optional; - -import org.springframework.core.ReactiveAdapter; -import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.core.ReactiveTypeDescriptor; -import org.springframework.data.util.ProxyUtils; -import org.springframework.data.util.ReflectionUtils; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.ConcurrentReferenceHashMap; /** * Utility class to expose details about reactive wrapper types. This class exposes whether a reactive wrapper is @@ -54,33 +43,21 @@ import org.springframework.util.ConcurrentReferenceHashMap; * @see io.smallrye.mutiny.Uni * @see Mono * @see Flux + * @deprecated since 3.0, use {@link org.springframework.data.util.ReactiveWrappers} instead as the utility was moved + * into the {@code org.springframework.data.util} package. */ +@Deprecated(since = "3.0", forRemoval = true) public abstract class ReactiveWrappers { - private static final boolean PROJECT_REACTOR_PRESENT = ClassUtils.isPresent("reactor.core.publisher.Flux", - 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.reactor.MonoKt", - ReactiveWrappers.class.getClassLoader()); - - private static final boolean MUTINY_PRESENT = ClassUtils.isPresent("io.smallrye.mutiny.Multi", - ReactiveWrappers.class.getClassLoader()); - - private static final Map, Boolean> IS_REACTIVE_TYPE = new ConcurrentReferenceHashMap<>(); - - private static final boolean IS_REACTIVE_AVAILABLE = Arrays.stream(ReactiveLibrary.values()) - .anyMatch(ReactiveWrappers::isAvailable); - private ReactiveWrappers() {} /** * Enumeration of supported reactive libraries. * * @author Mark Paluch + * @deprecated use {@link org.springframework.data.util.ReactiveWrappers.ReactiveLibrary} instead. */ + @Deprecated(since = "3.0", forRemoval = true) public enum ReactiveLibrary { PROJECT_REACTOR, RXJAVA3, KOTLIN_COROUTINES, MUTINY; @@ -93,7 +70,7 @@ public abstract class ReactiveWrappers { * @return {@literal true} if reactive support is available. */ public static boolean isAvailable() { - return IS_REACTIVE_AVAILABLE; + return org.springframework.data.util.ReactiveWrappers.isAvailable(); } /** @@ -108,13 +85,14 @@ public abstract class ReactiveWrappers { switch (reactiveLibrary) { case PROJECT_REACTOR: - return PROJECT_REACTOR_PRESENT; + return org.springframework.data.util.ReactiveWrappers.PROJECT_REACTOR_PRESENT; case RXJAVA3: - return RXJAVA3_PRESENT; + return org.springframework.data.util.ReactiveWrappers.RXJAVA3_PRESENT; case KOTLIN_COROUTINES: - return PROJECT_REACTOR_PRESENT && KOTLIN_COROUTINES_PRESENT; + return org.springframework.data.util.ReactiveWrappers.PROJECT_REACTOR_PRESENT + && org.springframework.data.util.ReactiveWrappers.KOTLIN_COROUTINES_PRESENT; case MUTINY: - return MUTINY_PRESENT; + return org.springframework.data.util.ReactiveWrappers.MUTINY_PRESENT; default: throw new IllegalArgumentException(String.format("Reactive library %s not supported", reactiveLibrary)); } @@ -127,7 +105,7 @@ public abstract class ReactiveWrappers { * @return {@literal true} if the {@code type} is a supported reactive wrapper type. */ public static boolean supports(Class type) { - return isAvailable() && IS_REACTIVE_TYPE.computeIfAbsent(type, key -> isWrapper(ProxyUtils.getUserClass(key))); + return org.springframework.data.util.ReactiveWrappers.supports(type); } /** @@ -140,9 +118,7 @@ public abstract class ReactiveWrappers { Assert.notNull(type, "Type must not be null"); - return Arrays.stream(type.getMethods())// - .flatMap(ReflectionUtils::returnTypeAndParameters)// - .anyMatch(ReactiveWrappers::supports); + return org.springframework.data.util.ReactiveWrappers.usesReactiveType(type); } /** @@ -155,7 +131,7 @@ public abstract class ReactiveWrappers { Assert.notNull(type, "Candidate type must not be null"); - return findDescriptor(type).map(ReactiveTypeDescriptor::isNoValue).orElse(false); + return org.springframework.data.util.ReactiveWrappers.isNoValueType(type); } /** @@ -168,7 +144,7 @@ public abstract class ReactiveWrappers { Assert.notNull(type, "Candidate type must not be null"); - return findDescriptor(type).map(it -> !it.isMultiValue() && !it.isNoValue()).orElse(false); + return org.springframework.data.util.ReactiveWrappers.isSingleValueType(type); } /** @@ -183,46 +159,7 @@ public abstract class ReactiveWrappers { Assert.notNull(type, "Candidate type must not be null"); - // Prevent single-types with a multi-hierarchy supertype to be reported as multi type - // See Mono implements Publisher - return isSingleValueType(type) ? false - : findDescriptor(type).map(ReactiveTypeDescriptor::isMultiValue).orElse(false); + return org.springframework.data.util.ReactiveWrappers.isMultiValueType(type); } - /** - * Returns whether the given type is a reactive wrapper type. - * - * @param type must not be {@literal null}. - * @return - */ - private static boolean isWrapper(Class type) { - - Assert.notNull(type, "Candidate type must not be null"); - - return isNoValueType(type) || isSingleValueType(type) || isMultiValueType(type); - } - - /** - * Looks up a {@link ReactiveTypeDescriptor} for the given wrapper type. - * - * @param type must not be {@literal null}. - * @return - */ - private static Optional findDescriptor(Class type) { - - Assert.notNull(type, "Wrapper type must not be null"); - - ReactiveAdapterRegistry adapterRegistry = ReactiveWrapperConverters.RegistryHolder.REACTIVE_ADAPTER_REGISTRY; - - if (adapterRegistry == null) { - return Optional.empty(); - } - - ReactiveAdapter adapter = adapterRegistry.getAdapter(type); - if (adapter != null && adapter.getDescriptor().isDeferred()) { - return Optional.of(adapter.getDescriptor()); - } - - return Optional.empty(); - } } diff --git a/src/main/java/org/springframework/data/util/ReactiveWrappers.java b/src/main/java/org/springframework/data/util/ReactiveWrappers.java new file mode 100644 index 000000000..871afd6b3 --- /dev/null +++ b/src/main/java/org/springframework/data/util/ReactiveWrappers.java @@ -0,0 +1,245 @@ +/* + * Copyright 2016-2022 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.util; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; + +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.ReactiveTypeDescriptor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Utility class to expose details about reactive wrapper types. This class exposes whether a reactive wrapper is + * supported in general and whether a particular type is suitable for no-value/single-value/multi-value usage. + *

+ * 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. + * + * @author Mark Paluch + * @author Christoph Strobl + * @author Oliver Gierke + * @author Gerrit Meier + * @author Hantsy Bai + * @since 3.0 + * @see org.reactivestreams.Publisher + * @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 io.smallrye.mutiny.Multi + * @see io.smallrye.mutiny.Uni + * @see Mono + * @see Flux + */ +public abstract class ReactiveWrappers { + + public static final boolean PROJECT_REACTOR_PRESENT = ClassUtils.isPresent("reactor.core.publisher.Flux", + ReactiveWrappers.class.getClassLoader()); + + public static final boolean RXJAVA3_PRESENT = ClassUtils.isPresent("io.reactivex.rxjava3.core.Flowable", + ReactiveWrappers.class.getClassLoader()); + + public static final boolean KOTLIN_COROUTINES_PRESENT = ClassUtils.isPresent("kotlinx.coroutines.reactor.MonoKt", + ReactiveWrappers.class.getClassLoader()); + + public static final boolean MUTINY_PRESENT = ClassUtils.isPresent("io.smallrye.mutiny.Multi", + ReactiveWrappers.class.getClassLoader()); + + public static final boolean IS_REACTIVE_AVAILABLE = Arrays.stream(ReactiveLibrary.values()) + .anyMatch(ReactiveWrappers::isAvailable); + private static final Map, Boolean> IS_REACTIVE_TYPE = new ConcurrentReferenceHashMap<>(); + + private ReactiveWrappers() {} + + /** + * Enumeration of supported reactive libraries. + * + * @author Mark Paluch + */ + public enum ReactiveLibrary { + + PROJECT_REACTOR, RXJAVA3, KOTLIN_COROUTINES, MUTINY; + } + + /** + * Returns {@literal true} if reactive support is available. More specifically, whether any of the libraries defined + * in {@link ReactiveLibrary} are on the class path. + * + * @return {@literal true} if reactive support is available. + */ + public static boolean isAvailable() { + return IS_REACTIVE_AVAILABLE; + } + + /** + * Returns {@literal true} if the {@link ReactiveLibrary} is available. + * + * @param reactiveLibrary must not be {@literal null}. + * @return {@literal true} if the {@link ReactiveLibrary} is available. + */ + public static boolean isAvailable(ReactiveLibrary reactiveLibrary) { + + Assert.notNull(reactiveLibrary, "Reactive library must not be null"); + + switch (reactiveLibrary) { + case PROJECT_REACTOR: + return PROJECT_REACTOR_PRESENT; + case RXJAVA3: + return RXJAVA3_PRESENT; + case KOTLIN_COROUTINES: + return PROJECT_REACTOR_PRESENT && KOTLIN_COROUTINES_PRESENT; + case MUTINY: + return MUTINY_PRESENT; + default: + throw new IllegalArgumentException(String.format("Reactive library %s not supported", reactiveLibrary)); + } + } + + /** + * Returns {@literal true} if the {@code type} is a supported reactive wrapper type. + * + * @param type must not be {@literal null}. + * @return {@literal true} if the {@code type} is a supported reactive wrapper type. + */ + public static boolean supports(Class type) { + return isAvailable() && IS_REACTIVE_TYPE.computeIfAbsent(type, key -> isWrapper(ProxyUtils.getUserClass(key))); + } + + /** + * Returns whether the given type uses any reactive wrapper type in its method signatures. + * + * @param type must not be {@literal null}. + * @return + */ + public static boolean usesReactiveType(Class type) { + + Assert.notNull(type, "Type must not be null"); + + return Arrays.stream(type.getMethods())// + .flatMap(ReflectionUtils::returnTypeAndParameters)// + .anyMatch(ReactiveWrappers::supports); + } + + /** + * Returns {@literal true} if {@code type} is a reactive wrapper type that contains no value. + * + * @param type must not be {@literal null}. + * @return {@literal true} if {@code type} is a reactive wrapper type that contains no value. + */ + public static boolean isNoValueType(Class type) { + + Assert.notNull(type, "Candidate type must not be null"); + + return findDescriptor(type).map(ReactiveTypeDescriptor::isNoValue).orElse(false); + } + + /** + * Returns {@literal true} if {@code type} is a reactive wrapper type for a single value. + * + * @param type must not be {@literal null}. + * @return {@literal true} if {@code type} is a reactive wrapper type for a single value. + */ + public static boolean isSingleValueType(Class type) { + + Assert.notNull(type, "Candidate type must not be null"); + + return findDescriptor(type).map(it -> !it.isMultiValue() && !it.isNoValue()).orElse(false); + } + + /** + * Returns {@literal true} if {@code type} is a reactive wrapper type supporting multiple values ({@code 0..N} + * elements). + * + * @param type must not be {@literal null}. + * @return {@literal true} if {@code type} is a reactive wrapper type supporting multiple values ({@code 0..N} + * elements). + */ + public static boolean isMultiValueType(Class type) { + + Assert.notNull(type, "Candidate type must not be null"); + + // Prevent single-types with a multi-hierarchy supertype to be reported as multi type + // See Mono implements Publisher + return isSingleValueType(type) ? false + : findDescriptor(type).map(ReactiveTypeDescriptor::isMultiValue).orElse(false); + } + + /** + * Returns whether the given type is a reactive wrapper type. + * + * @param type must not be {@literal null}. + * @return + */ + private static boolean isWrapper(Class type) { + + Assert.notNull(type, "Candidate type must not be null"); + + return isNoValueType(type) || isSingleValueType(type) || isMultiValueType(type); + } + + /** + * Looks up a {@link ReactiveTypeDescriptor} for the given wrapper type. + * + * @param type must not be {@literal null}. + * @return + */ + private static Optional findDescriptor(Class type) { + + Assert.notNull(type, "Wrapper type must not be null"); + + ReactiveAdapterRegistry adapterRegistry = RegistryHolder.REACTIVE_ADAPTER_REGISTRY; + + if (adapterRegistry == null) { + return Optional.empty(); + } + + ReactiveAdapter adapter = adapterRegistry.getAdapter(type); + if (adapter != null && adapter.getDescriptor().isDeferred()) { + return Optional.of(adapter.getDescriptor()); + } + + return Optional.empty(); + } + + /** + * Holder for delayed initialization of {@link ReactiveAdapterRegistry}. + * + * @author Mark Paluch + */ + static class RegistryHolder { + + static final @Nullable ReactiveAdapterRegistry REACTIVE_ADAPTER_REGISTRY; + + static { + + if (ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)) { + REACTIVE_ADAPTER_REGISTRY = ReactiveAdapterRegistry.getSharedInstance(); + } else { + REACTIVE_ADAPTER_REGISTRY = null; + } + } + } +} diff --git a/src/test/java/org/springframework/data/repository/util/ReactiveWrappersUnitTests.java b/src/test/java/org/springframework/data/util/ReactiveWrappersUnitTests.java similarity index 97% rename from src/test/java/org/springframework/data/repository/util/ReactiveWrappersUnitTests.java rename to src/test/java/org/springframework/data/util/ReactiveWrappersUnitTests.java index 11db7f428..c87b298b9 100644 --- a/src/test/java/org/springframework/data/repository/util/ReactiveWrappersUnitTests.java +++ b/src/test/java/org/springframework/data/util/ReactiveWrappersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2022 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.repository.util; +package org.springframework.data.util; import static org.assertj.core.api.Assertions.*;