/* * Copyright 2014-2017 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.util; import javaslang.collection.Seq; import javaslang.collection.Traversable; import lombok.AccessLevel; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Value; import scala.Function0; import scala.Option; import scala.runtime.AbstractFunction0; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.stream.Collectors; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Slice; import org.springframework.data.util.Streamable; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.concurrent.ListenableFuture; import com.google.common.base.Optional; /** * Converters to potentially wrap the execution of a repository method into a variety of wrapper types potentially being * available on the classpath. Currently supported: * <ul> * <li>{@code java.util.Optional}</li> * <li>{@code com.google.common.base.Optional}</li> * <li>{@code scala.Option} - as of 1.12</li> * <li>{@code java.util.concurrent.Future}</li> * <li>{@code java.util.concurrent.CompletableFuture}</li> * <li>{@code org.springframework.util.concurrent.ListenableFuture<}</li> * <li>{@code javaslang.control.Option} - as of 1.13</li> * <li>{@code javaslang.collection.Seq}, {@code javaslang.collection.Map}, {@code javaslang.collection.Set} - as of * 1.13</li> * <li>{@code io.vavr.collection.Seq}, {@code io.vavr.collection.Map}, {@code io.vavr.collection.Set} - as of 2.0</li> * <li>Reactive wrappers supported by {@link ReactiveWrappers} - as of 2.0</li> * </ul> * * @author Oliver Gierke * @author Mark Paluch * @author Christoph Strobl * @author Maciek Opała * @since 1.8 * @see ReactiveWrappers */ public abstract class QueryExecutionConverters { private static final boolean GUAVA_PRESENT = ClassUtils.isPresent("com.google.common.base.Optional", QueryExecutionConverters.class.getClassLoader()); private static final boolean JDK_8_PRESENT = ClassUtils.isPresent("java.util.Optional", QueryExecutionConverters.class.getClassLoader()); private static final boolean SCALA_PRESENT = ClassUtils.isPresent("scala.Option", QueryExecutionConverters.class.getClassLoader()); private static final boolean JAVASLANG_PRESENT = ClassUtils.isPresent("javaslang.control.Option", QueryExecutionConverters.class.getClassLoader()); private static final boolean VAVR_PRESENT = ClassUtils.isPresent("io.vavr.control.Option", QueryExecutionConverters.class.getClassLoader()); private static final Set<WrapperType> WRAPPER_TYPES = new HashSet<WrapperType>(); private static final Set<WrapperType> UNWRAPPER_TYPES = new HashSet<WrapperType>(); private static final Set<Converter<Object, Object>> UNWRAPPERS = new HashSet<Converter<Object, Object>>(); private static final Set<Class<?>> ALLOWED_PAGEABLE_TYPES = new HashSet<Class<?>>(); static { WRAPPER_TYPES.add(WrapperType.singleValue(Future.class)); UNWRAPPER_TYPES.add(WrapperType.singleValue(Future.class)); WRAPPER_TYPES.add(WrapperType.singleValue(ListenableFuture.class)); UNWRAPPER_TYPES.add(WrapperType.singleValue(ListenableFuture.class)); ALLOWED_PAGEABLE_TYPES.add(Slice.class); ALLOWED_PAGEABLE_TYPES.add(Page.class); ALLOWED_PAGEABLE_TYPES.add(List.class); if (GUAVA_PRESENT) { WRAPPER_TYPES.add(NullableWrapperToGuavaOptionalConverter.getWrapperType()); UNWRAPPER_TYPES.add(NullableWrapperToGuavaOptionalConverter.getWrapperType()); UNWRAPPERS.add(GuavaOptionalUnwrapper.INSTANCE); } if (JDK_8_PRESENT) { WRAPPER_TYPES.add(NullableWrapperToJdk8OptionalConverter.getWrapperType()); UNWRAPPER_TYPES.add(NullableWrapperToJdk8OptionalConverter.getWrapperType()); UNWRAPPERS.add(Jdk8OptionalUnwrapper.INSTANCE); } if (JDK_8_PRESENT) { WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType()); UNWRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType()); } if (SCALA_PRESENT) { WRAPPER_TYPES.add(NullableWrapperToScalaOptionConverter.getWrapperType()); UNWRAPPER_TYPES.add(NullableWrapperToScalaOptionConverter.getWrapperType()); UNWRAPPERS.add(ScalOptionUnwrapper.INSTANCE); } if (JAVASLANG_PRESENT) { WRAPPER_TYPES.add(NullableWrapperToJavaslangOptionConverter.getWrapperType()); WRAPPER_TYPES.add(JavaslangCollections.ToJavaConverter.INSTANCE.getWrapperType()); UNWRAPPERS.add(JavaslangOptionUnwrapper.INSTANCE); ALLOWED_PAGEABLE_TYPES.add(Seq.class); } if (VAVR_PRESENT) { WRAPPER_TYPES.add(NullableWrapperToVavrOptionConverter.getWrapperType()); WRAPPER_TYPES.add(VavrCollections.ToJavaConverter.INSTANCE.getWrapperType()); UNWRAPPERS.add(VavrOptionUnwrapper.INSTANCE); ALLOWED_PAGEABLE_TYPES.add(io.vavr.collection.Seq.class); } if (ReactiveWrappers.isAvailable()) { WRAPPER_TYPES .addAll(ReactiveWrappers.getNoValueTypes().stream().map(WrapperType::noValue).collect(Collectors.toList())); WRAPPER_TYPES.addAll( ReactiveWrappers.getSingleValueTypes().stream().map(WrapperType::singleValue).collect(Collectors.toList())); WRAPPER_TYPES.addAll( ReactiveWrappers.getMultiValueTypes().stream().map(WrapperType::multiValue).collect(Collectors.toList())); } } private QueryExecutionConverters() {} /** * Returns whether the given type is a supported wrapper type. * * @param type must not be {@literal null}. * @return */ public static boolean supports(Class<?> type) { Assert.notNull(type, "Type must not be null!"); for (WrapperType candidate : WRAPPER_TYPES) { if (candidate.getType().isAssignableFrom(type)) { return true; } } return false; } /** * Returns whether the given wrapper type supports unwrapping. * * @param type must not be {@literal null}. * @return */ public static boolean supportsUnwrapping(Class<?> type) { Assert.notNull(type, "Type must not be null!"); for (WrapperType candidate : UNWRAPPER_TYPES) { if (candidate.getType().isAssignableFrom(type)) { return true; } } return false; } public static boolean isSingleValue(Class<?> type) { for (WrapperType candidate : WRAPPER_TYPES) { if (candidate.getType().isAssignableFrom(type)) { return candidate.isSingleValue(); } } return false; } /** * Returns the types that are supported on paginating query methods. Will include custom collection types of e.g. * Javaslang. * * @return */ public static Set<Class<?>> getAllowedPageableTypes() { return Collections.unmodifiableSet(ALLOWED_PAGEABLE_TYPES); } /** * Registers converters for wrapper types found on the classpath. * * @param conversionService must not be {@literal null}. */ public static void registerConvertersIn(ConfigurableConversionService conversionService) { Assert.notNull(conversionService, "ConversionService must not be null!"); conversionService.removeConvertible(Collection.class, Object.class); if (GUAVA_PRESENT) { conversionService.addConverter(new NullableWrapperToGuavaOptionalConverter(conversionService)); } if (JDK_8_PRESENT) { conversionService.addConverter(new NullableWrapperToJdk8OptionalConverter(conversionService)); conversionService.addConverter(new NullableWrapperToCompletableFutureConverter(conversionService)); } if (SCALA_PRESENT) { conversionService.addConverter(new NullableWrapperToScalaOptionConverter(conversionService)); } if (JAVASLANG_PRESENT) { conversionService.addConverter(new NullableWrapperToJavaslangOptionConverter(conversionService)); conversionService.addConverter(JavaslangCollections.FromJavaConverter.INSTANCE); } if (VAVR_PRESENT) { conversionService.addConverter(new NullableWrapperToVavrOptionConverter(conversionService)); conversionService.addConverter(VavrCollections.FromJavaConverter.INSTANCE); } conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService)); } /** * Unwraps the given source value in case it's one of the currently supported wrapper types detected at runtime. * * @param source can be {@literal null}. * @return */ public static Object unwrap(Object source) { if (source == null || !supports(source.getClass())) { return source; } for (Converter<Object, Object> converter : UNWRAPPERS) { Object result = converter.convert(source); if (result != source) { return result; } } return source; } /** * Base class for converters that create instances of wrapper types such as Google Guava's and JDK 8's * {@code Optional} types. * * @author Oliver Gierke */ @RequiredArgsConstructor private static abstract class AbstractWrapperTypeConverter implements GenericConverter { private final @NonNull ConversionService conversionService; private final @NonNull Object nullValue; private final @NonNull Iterable<Class<?>> wrapperTypes; /** * Creates a new {@link AbstractWrapperTypeConverter} using the given {@link ConversionService} and wrapper type. * * @param conversionService must not be {@literal null}. * @param nullValue must not be {@literal null}. */ protected AbstractWrapperTypeConverter(ConversionService conversionService, Object nullValue) { Assert.notNull(conversionService, "ConversionService must not be null!"); Assert.notNull(nullValue, "Null value must not be null!"); this.conversionService = conversionService; this.nullValue = nullValue; this.wrapperTypes = Collections.singleton(nullValue.getClass()); } /* * (non-Javadoc) * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes() */ @Override public Set<ConvertiblePair> getConvertibleTypes() { return Streamable.of(wrapperTypes)// .map(it -> new ConvertiblePair(NullableWrapper.class, it))// .stream().collect(Collectors.toSet()); } /* * (non-Javadoc) * @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor) */ @Override public final Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { NullableWrapper wrapper = (NullableWrapper) source; Object value = wrapper.getValue(); // TODO: Add Recursive conversion once we move to Spring 4 return value == null ? nullValue : wrap(value); } /** * Wrap the given, non-{@literal null} value into the wrapper type. * * @param source will never be {@literal null}. * @return must not be {@literal null}. */ protected abstract Object wrap(Object source); } /** * A Spring {@link Converter} to support Google Guava's {@link Optional}. * * @author Oliver Gierke */ private static class NullableWrapperToGuavaOptionalConverter extends AbstractWrapperTypeConverter { /** * Creates a new {@link NullableWrapperToGuavaOptionalConverter} using the given {@link ConversionService}. * * @param conversionService must not be {@literal null}. */ public NullableWrapperToGuavaOptionalConverter(ConversionService conversionService) { super(conversionService, Optional.absent(), Collections.singleton(Optional.class)); } /* * (non-Javadoc) * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object) */ @Override protected Object wrap(Object source) { return Optional.of(source); } public static WrapperType getWrapperType() { return WrapperType.singleValue(Optional.class); } } /** * A Spring {@link Converter} to support JDK 8's {@link java.util.Optional}. * * @author Oliver Gierke */ private static class NullableWrapperToJdk8OptionalConverter extends AbstractWrapperTypeConverter { /** * Creates a new {@link NullableWrapperToJdk8OptionalConverter} using the given {@link ConversionService}. * * @param conversionService must not be {@literal null}. */ public NullableWrapperToJdk8OptionalConverter(ConversionService conversionService) { super(conversionService, java.util.Optional.empty()); } /* * (non-Javadoc) * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object) */ @Override protected Object wrap(Object source) { return java.util.Optional.of(source); } public static WrapperType getWrapperType() { return WrapperType.singleValue(java.util.Optional.class); } } /** * A Spring {@link Converter} to support returning {@link Future} instances from repository methods. * * @author Oliver Gierke */ private static class NullableWrapperToFutureConverter extends AbstractWrapperTypeConverter { /** * Creates a new {@link NullableWrapperToFutureConverter} using the given {@link ConversionService}. * * @param conversionService must not be {@literal null}. */ public NullableWrapperToFutureConverter(ConversionService conversionService) { super(conversionService, new AsyncResult<>(null), Arrays.asList(Future.class, ListenableFuture.class)); } /* * (non-Javadoc) * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object) */ @Override protected Object wrap(Object source) { return new AsyncResult<>(source); } } /** * A Spring {@link Converter} to support returning {@link CompletableFuture} instances from repository methods. * * @author Oliver Gierke */ private static class NullableWrapperToCompletableFutureConverter extends AbstractWrapperTypeConverter { /** * Creates a new {@link NullableWrapperToCompletableFutureConverter} using the given {@link ConversionService}. * * @param conversionService must not be {@literal null}. */ public NullableWrapperToCompletableFutureConverter(ConversionService conversionService) { super(conversionService, CompletableFuture.completedFuture(null)); } /* * (non-Javadoc) * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object) */ @Override protected Object wrap(Object source) { return source instanceof CompletableFuture ? source : CompletableFuture.completedFuture(source); } public static WrapperType getWrapperType() { return WrapperType.singleValue(CompletableFuture.class); } } /** * A Spring {@link Converter} to support Scala's {@link Option}. * * @author Oliver Gierke * @since 1.13 */ private static class NullableWrapperToScalaOptionConverter extends AbstractWrapperTypeConverter { public NullableWrapperToScalaOptionConverter(ConversionService conversionService) { super(conversionService, Option.empty(), Collections.singleton(Option.class)); } /* * (non-Javadoc) * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object) */ @Override protected Object wrap(Object source) { return Option.apply(source); } public static WrapperType getWrapperType() { return WrapperType.singleValue(Option.class); } } /** * Converter to convert from {@link NullableWrapper} into JavaSlang's {@link javaslang.control.Option}. * * @author Oliver Gierke * @since 1.13 */ private static class NullableWrapperToJavaslangOptionConverter extends AbstractWrapperTypeConverter { /** * Creates a new {@link NullableWrapperToJavaslangOptionConverter} using the given {@link ConversionService}. * * @param conversionService must not be {@literal null}. */ public NullableWrapperToJavaslangOptionConverter(ConversionService conversionService) { super(conversionService, javaslang.control.Option.none(), Collections.singleton(javaslang.control.Option.class)); } public static WrapperType getWrapperType() { return WrapperType.singleValue(javaslang.control.Option.class); } /* * (non-Javadoc) * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object) */ @Override protected Object wrap(Object source) { return javaslang.control.Option.of(source); } } /** * Converter to convert from {@link NullableWrapper} into JavaSlang's {@link io.vavr.control.Option}. * * @author Oliver Gierke * @since 2.0 */ private static class NullableWrapperToVavrOptionConverter extends AbstractWrapperTypeConverter { /** * Creates a new {@link NullableWrapperToJavaslangOptionConverter} using the given {@link ConversionService}. * * @param conversionService must not be {@literal null}. */ public NullableWrapperToVavrOptionConverter(ConversionService conversionService) { super(conversionService, io.vavr.control.Option.none(), Collections.singleton(io.vavr.control.Option.class)); } public static WrapperType getWrapperType() { return WrapperType.singleValue(io.vavr.control.Option.class); } /* * (non-Javadoc) * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object) */ @Override protected Object wrap(Object source) { return io.vavr.control.Option.of(source); } } /** * A {@link Converter} to unwrap Guava {@link Optional} instances. * * @author Oliver Gierke * @since 1.12 */ private static enum GuavaOptionalUnwrapper implements Converter<Object, Object> { INSTANCE; /* * (non-Javadoc) * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) */ @Override public Object convert(Object source) { return source instanceof Optional ? ((Optional<?>) source).orNull() : source; } } /** * A {@link Converter} to unwrap JDK 8 {@link java.util.Optional} instances. * * @author Oliver Gierke * @since 1.12 */ private static enum Jdk8OptionalUnwrapper implements Converter<Object, Object> { INSTANCE; /* * (non-Javadoc) * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) */ @Override public Object convert(Object source) { return source instanceof java.util.Optional ? ((java.util.Optional<?>) source).orElse(null) : source; } } /** * A {@link Converter} to unwrap a Scala {@link Option} instance. * * @author Oliver Gierke * @author Mark Paluch * @since 1.12 */ private static enum ScalOptionUnwrapper implements Converter<Object, Object> { INSTANCE; private final Function0<Object> alternative = new AbstractFunction0<Object>() { /* * (non-Javadoc) * @see scala.Function0#apply() */ @Override public Option<Object> apply() { return null; } }; /* * (non-Javadoc) * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) */ @Override public Object convert(Object source) { return source instanceof Option ? ((Option<?>) source).getOrElse(alternative) : source; } } /** * Converter to unwrap Javaslang {@link javaslang.control.Option} instances. * * @author Oliver Gierke * @since 1.13 */ private static enum JavaslangOptionUnwrapper implements Converter<Object, Object> { INSTANCE; /* * (non-Javadoc) * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) */ @Override @SuppressWarnings("unchecked") public Object convert(Object source) { if (source instanceof javaslang.control.Option) { return ((javaslang.control.Option<Object>) source).getOrElse(() -> null); } if (source instanceof Traversable) { return JavaslangCollections.ToJavaConverter.INSTANCE.convert(source); } return source; } } /** * Converter to unwrap Vavr {@link io.vavr.control.Option} instances. * * @author Oliver Gierke * @since 2.0 */ private static enum VavrOptionUnwrapper implements Converter<Object, Object> { INSTANCE; /* * (non-Javadoc) * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) */ @Override @SuppressWarnings("unchecked") public Object convert(Object source) { if (source instanceof io.vavr.control.Option) { return ((io.vavr.control.Option<Object>) source).getOrElse(() -> null); } if (source instanceof io.vavr.collection.Traversable) { return VavrCollections.ToJavaConverter.INSTANCE.convert(source); } return source; } } @Value @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public static class WrapperType { enum Cardinality { NONE, SINGLE, MULTI; } Class<?> type; @Getter(AccessLevel.NONE) Cardinality cardinality; public static WrapperType singleValue(Class<?> type) { return new WrapperType(type, Cardinality.SINGLE); } public static WrapperType multiValue(Class<?> type) { return new WrapperType(type, Cardinality.MULTI); } public static WrapperType noValue(Class<?> type) { return new WrapperType(type, Cardinality.NONE); } boolean isSingleValue() { return cardinality.equals(Cardinality.SINGLE); } } }