DATACMNS-1762 - Extract NullableWrapperConverters from QueryExecutionConverters.

Nullable wrappers such as Java 8 Optional, Guava Optional, Scala Option and Vavr Option are now handled in NullableWrapperConverters and are no longer coupled to QueryExecutionConverters so that this functionality can be reused.

Original Pull Request: #459
This commit is contained in:
Mark Paluch
2020-07-16 15:07:51 +02:00
committed by Christoph Strobl
parent 9ced611ea5
commit 04f59f1307
6 changed files with 810 additions and 306 deletions

View File

@@ -27,9 +27,9 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.repository.util.NullableWrapper;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.util.NullableWrapper;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;

View File

@@ -23,12 +23,13 @@ import org.springframework.lang.Nullable;
* convert {@literal null} into an object of some sort.
*
* @author Oliver Gierke
* @author Mark Paluch
* @since 1.8
* @see QueryExecutionConverters
* @deprecated since 2.4, use {@link org.springframework.data.util.NullableWrapper} instead.
*/
public class NullableWrapper {
private final @Nullable Object value;
@Deprecated
public class NullableWrapper extends org.springframework.data.util.NullableWrapper {
/**
* Creates a new {@link NullableWrapper} for the given value.
@@ -36,28 +37,6 @@ public class NullableWrapper {
* @param value can be {@literal null}.
*/
public NullableWrapper(@Nullable Object value) {
this.value = value;
}
/**
* Returns the type of the contained value. WIll fall back to {@link Object} in case the value is {@literal null}.
*
* @return will never be {@literal null}.
*/
public Class<?> getValueType() {
Object value = this.value;
return value == null ? Object.class : value.getClass();
}
/**
* Returns the backing value.
*
* @return the value can be {@literal null}.
*/
@Nullable
public Object getValue() {
return value;
super(value);
}
}

View File

@@ -15,10 +15,6 @@
*/
package org.springframework.data.repository.util;
import scala.Function0;
import scala.Option;
import scala.runtime.AbstractFunction0;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -42,10 +38,11 @@ import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Slice;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.util.NullableWrapper;
import org.springframework.data.util.NullableWrapperConverters;
import org.springframework.data.util.StreamUtils;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.util.Assert;
@@ -54,19 +51,13 @@ import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ObjectUtils;
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>
@@ -79,23 +70,17 @@ import com.google.common.base.Optional;
* @author Maciek Opała
* @author Jens Schauder
* @since 1.8
* @see ReactiveWrappers
* @see NullableWrapperConverters
*/
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 VAVR_PRESENT = ClassUtils.isPresent("io.vavr.control.Option",
private static final boolean VAVR_PRESENT = ClassUtils.isPresent("io.vavr.control.Try",
QueryExecutionConverters.class.getClassLoader());
private static final Set<WrapperType> WRAPPER_TYPES = new HashSet<WrapperType>();
private static final Set<WrapperType> WRAPPER_TYPES = new HashSet<>();
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<?>>();
private static final Set<Converter<Object, Object>> UNWRAPPERS = new HashSet<>();
private static final Set<Class<?>> ALLOWED_PAGEABLE_TYPES = new HashSet<>();
private static final Map<Class<?>, ExecutionAdapter> EXECUTION_ADAPTER = new HashMap<>();
private static final Map<Class<?>, Boolean> supportsCache = new ConcurrentReferenceHashMap<>();
@@ -110,35 +95,12 @@ public abstract class QueryExecutionConverters {
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);
}
WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType());
if (VAVR_PRESENT) {
WRAPPER_TYPES.add(NullableWrapperToVavrOptionConverter.getWrapperType());
WRAPPER_TYPES.add(VavrCollections.ToJavaConverter.INSTANCE.getWrapperType());
UNWRAPPERS.add(VavrOptionUnwrapper.INSTANCE);
UNWRAPPERS.add(VavrTraversableUnwrapper.INSTANCE);
// Try support
WRAPPER_TYPES.add(WrapperType.singleValue(io.vavr.control.Try.class));
@@ -168,7 +130,7 @@ public abstract class QueryExecutionConverters {
}
}
return false;
return NullableWrapperConverters.supports(type);
});
}
@@ -182,6 +144,10 @@ public abstract class QueryExecutionConverters {
Assert.notNull(type, "Type must not be null!");
if (NullableWrapperConverters.supportsUnwrapping(type)) {
return NullableWrapperConverters.supportsUnwrapping(type);
}
for (WrapperType candidate : UNWRAPPER_TYPES) {
if (candidate.getType().isAssignableFrom(type)) {
return true;
@@ -193,22 +159,22 @@ public abstract class QueryExecutionConverters {
public static boolean isSingleValue(Class<?> type) {
if (NullableWrapperConverters.supports(type)) {
return NullableWrapperConverters.isSingleValue(type);
}
for (WrapperType candidate : WRAPPER_TYPES) {
if (candidate.getType().isAssignableFrom(type)) {
return candidate.isSingleValue();
}
}
if (ReactiveWrappers.supports(type) && ReactiveWrappers.isSingleValueType(type)) {
return true;
}
return false;
}
/**
* Returns the types that are supported on paginating query methods. Will include custom collection types of e.g.
* Javaslang.
* Vavr.
*
* @return
*/
@@ -227,25 +193,14 @@ public abstract class QueryExecutionConverters {
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));
}
NullableWrapperConverters.registerConvertersIn(conversionService);
if (VAVR_PRESENT) {
conversionService.addConverter(new NullableWrapperToVavrOptionConverter(conversionService));
conversionService.addConverter(VavrCollections.FromJavaConverter.INSTANCE);
}
conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService));
conversionService.addConverter(new NullableWrapperToCompletableFutureConverter());
conversionService.addConverter(new NullableWrapperToFutureConverter());
conversionService.addConverter(new IterableToStreamableConverter());
}
@@ -258,6 +213,8 @@ public abstract class QueryExecutionConverters {
@Nullable
public static Object unwrap(@Nullable Object source) {
source = NullableWrapperConverters.unwrap(source);
if (source == null || !supports(source.getClass())) {
return source;
}
@@ -326,7 +283,6 @@ public abstract class QueryExecutionConverters {
*/
private static abstract class AbstractWrapperTypeConverter implements GenericConverter {
private final ConversionService conversionService;
private final Object nullValue;
private final Iterable<Class<?>> wrapperTypes;
@@ -336,19 +292,16 @@ public abstract class QueryExecutionConverters {
* @param conversionService must not be {@literal null}.
* @param nullValue must not be {@literal null}.
*/
protected AbstractWrapperTypeConverter(ConversionService conversionService, Object nullValue) {
protected AbstractWrapperTypeConverter(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());
}
public AbstractWrapperTypeConverter(ConversionService conversionService, Object nullValue,
public AbstractWrapperTypeConverter(Object nullValue,
Iterable<Class<?>> wrapperTypes) {
this.conversionService = conversionService;
this.nullValue = nullValue;
this.wrapperTypes = wrapperTypes;
}
@@ -378,7 +331,7 @@ public abstract class QueryExecutionConverters {
return null;
}
NullableWrapper wrapper = (NullableWrapper) source;
org.springframework.data.util.NullableWrapper wrapper = (NullableWrapper) source;
Object value = wrapper.getValue();
// TODO: Add Recursive conversion once we move to Spring 4
@@ -394,66 +347,6 @@ public abstract class QueryExecutionConverters {
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.
*
@@ -466,8 +359,8 @@ public abstract class QueryExecutionConverters {
*
* @param conversionService must not be {@literal null}.
*/
public NullableWrapperToFutureConverter(ConversionService conversionService) {
super(conversionService, new AsyncResult<>(null), Arrays.asList(Future.class, ListenableFuture.class));
public NullableWrapperToFutureConverter() {
super(new AsyncResult<>(null), Arrays.asList(Future.class, ListenableFuture.class));
}
/*
@@ -492,8 +385,8 @@ public abstract class QueryExecutionConverters {
*
* @param conversionService must not be {@literal null}.
*/
public NullableWrapperToCompletableFutureConverter(ConversionService conversionService) {
super(conversionService, CompletableFuture.completedFuture(null));
public NullableWrapperToCompletableFutureConverter() {
super(CompletableFuture.completedFuture(null));
}
/*
@@ -511,146 +404,12 @@ public abstract class QueryExecutionConverters {
}
/**
* 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 io.vavr.control.Option}.
* Converter to unwrap Vavr {@link io.vavr.collection.Traversable} instances.
*
* @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 enum GuavaOptionalUnwrapper implements Converter<Object, Object> {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Nullable
@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 enum Jdk8OptionalUnwrapper implements Converter<Object, Object> {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Nullable
@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 enum ScalOptionUnwrapper implements Converter<Object, Object> {
INSTANCE;
private final Function0<Object> alternative = new AbstractFunction0<Object>() {
/*
* (non-Javadoc)
* @see scala.Function0#apply()
*/
@Nullable
@Override
public Option<Object> apply() {
return null;
}
};
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Nullable
@Override
public Object convert(Object source) {
return source instanceof Option ? ((Option<?>) source).getOrElse(alternative) : source;
}
}
/**
* Converter to unwrap Vavr {@link io.vavr.control.Option} instances.
*
* @author Oliver Gierke
* @since 2.0
*/
private enum VavrOptionUnwrapper implements Converter<Object, Object> {
private enum VavrTraversableUnwrapper implements Converter<Object, Object> {
INSTANCE;
@@ -663,10 +422,6 @@ public abstract class QueryExecutionConverters {
@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);
}
@@ -675,6 +430,7 @@ public abstract class QueryExecutionConverters {
}
}
private static class IterableToStreamableConverter implements ConditionalGenericConverter {
private static final TypeDescriptor STREAMABLE = TypeDescriptor.valueOf(Streamable.class);
@@ -688,7 +444,6 @@ public abstract class QueryExecutionConverters {
* (non-Javadoc)
* @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes()
*/
@NonNull
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Iterable.class, Object.class));

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2014-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.util;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.Nullable;
/**
* Simple value object to wrap a nullable delegate. Used to be able to write {@link Converter} implementations that
* convert {@literal null} into an object of some sort.
*
* @author Oliver Gierke
* @author Mark Paluch
* @since 2.4
* @see NullableWrapperConverters
*/
public class NullableWrapper {
private final @Nullable Object value;
/**
* Creates a new {@link NullableWrapper} for the given value.
*
* @param value can be {@literal null}.
*/
public NullableWrapper(@Nullable Object value) {
this.value = value;
}
/**
* Returns the type of the contained value. WIll fall back to {@link Object} in case the value is {@literal null}.
*
* @return will never be {@literal null}.
*/
public Class<?> getValueType() {
Object value = this.value;
return value == null ? Object.class : value.getClass();
}
/**
* Returns the backing value.
*
* @return the value can be {@literal null}.
*/
@Nullable
public Object getValue() {
return value;
}
}

View File

@@ -0,0 +1,579 @@
/*
* Copyright 2014-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.util;
import scala.Function0;
import scala.Option;
import scala.runtime.AbstractFunction0;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ObjectUtils;
import com.google.common.base.Optional;
/**
* Converters to wrap and unwrap nullable 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}</li>
* <li>{@code javaslang.control.Option}</li>
* <li>{@code io.vavr.control.Option}</li>
* </ul>
*
* @author Oliver Gierke
* @author Mark Paluch
* @author Christoph Strobl
* @author Maciek Opała
* @author Jens Schauder
* @since 2.4
*/
public abstract class NullableWrapperConverters {
private static final boolean GUAVA_PRESENT = ClassUtils.isPresent("com.google.common.base.Optional",
NullableWrapperConverters.class.getClassLoader());
private static final boolean SCALA_PRESENT = ClassUtils.isPresent("scala.Option",
NullableWrapperConverters.class.getClassLoader());
private static final boolean VAVR_PRESENT = ClassUtils.isPresent("io.vavr.control.Option",
NullableWrapperConverters.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 Map<Class<?>, Boolean> supportsCache = new ConcurrentReferenceHashMap<>();
static {
WRAPPER_TYPES.add(NullableWrapperToJdk8OptionalConverter.getWrapperType());
UNWRAPPER_TYPES.add(NullableWrapperToJdk8OptionalConverter.getWrapperType());
UNWRAPPERS.add(Jdk8OptionalUnwrapper.INSTANCE);
if (GUAVA_PRESENT) {
WRAPPER_TYPES.add(NullableWrapperToGuavaOptionalConverter.getWrapperType());
UNWRAPPER_TYPES.add(NullableWrapperToGuavaOptionalConverter.getWrapperType());
UNWRAPPERS.add(GuavaOptionalUnwrapper.INSTANCE);
}
if (SCALA_PRESENT) {
WRAPPER_TYPES.add(NullableWrapperToScalaOptionConverter.getWrapperType());
UNWRAPPER_TYPES.add(NullableWrapperToScalaOptionConverter.getWrapperType());
UNWRAPPERS.add(ScalOptionUnwrapper.INSTANCE);
}
if (VAVR_PRESENT) {
WRAPPER_TYPES.add(NullableWrapperToVavrOptionConverter.getWrapperType());
UNWRAPPERS.add(VavrOptionUnwrapper.INSTANCE);
}
}
private NullableWrapperConverters() {}
/**
* 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!");
return supportsCache.computeIfAbsent(type, key -> {
for (WrapperType candidate : WRAPPER_TYPES) {
if (candidate.getType().isAssignableFrom(key)) {
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;
}
/**
* Registers converters for wrapper types found on the classpath.
*
* @param registry must not be {@literal null}.
*/
public static void registerConvertersIn(ConverterRegistry registry) {
Assert.notNull(registry, "ConversionService must not be null!");
registry.addConverter(NullableWrapperToJdk8OptionalConverter.INSTANCE);
if (GUAVA_PRESENT) {
registry.addConverter(NullableWrapperToGuavaOptionalConverter.INSTANCE);
}
if (SCALA_PRESENT) {
registry.addConverter(NullableWrapperToScalaOptionConverter.INSTANCE);
}
if (VAVR_PRESENT) {
registry.addConverter(NullableWrapperToVavrOptionConverter.INSTANCE);
}
}
/**
* 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
*/
@Nullable
public static Object unwrap(@Nullable 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;
}
/**
* Recursively unwraps well known wrapper types from the given {@link TypeInformation}.
*
* @param type must not be {@literal null}.
* @return will never be {@literal null}.
*/
public static TypeInformation<?> unwrapActualType(TypeInformation<?> type) {
Assert.notNull(type, "type must not be null");
Class<?> rawType = type.getType();
boolean needToUnwrap = supports(rawType) //
|| Stream.class.isAssignableFrom(rawType);
return needToUnwrap ? unwrapActualType(type.getRequiredComponentType()) : type;
}
/**
* 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
*/
private static abstract class AbstractWrapperTypeConverter implements GenericConverter {
private final Object nullValue;
private final Iterable<Class<?>> wrapperTypes;
/**
* Creates a new {@link AbstractWrapperTypeConverter} using the given wrapper type.
*
* @param conversionService must not be {@literal null}.
* @param nullValue must not be {@literal null}.
*/
protected AbstractWrapperTypeConverter(Object nullValue) {
Assert.notNull(nullValue, "Null value must not be null!");
this.nullValue = nullValue;
this.wrapperTypes = Collections.singleton(nullValue.getClass());
}
public AbstractWrapperTypeConverter(Object nullValue, Iterable<Class<?>> wrapperTypes) {
this.nullValue = nullValue;
this.wrapperTypes = wrapperTypes;
}
/*
* (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(StreamUtils.toUnmodifiableSet());
}
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
*/
@Nullable
@Override
public final Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
NullableWrapper wrapper = (NullableWrapper) source;
Object value = wrapper.getValue();
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 JDK 8's {@link java.util.Optional}.
*
* @author Oliver Gierke
*/
private static class NullableWrapperToJdk8OptionalConverter extends AbstractWrapperTypeConverter {
public static final NullableWrapperToJdk8OptionalConverter INSTANCE = new NullableWrapperToJdk8OptionalConverter();
private NullableWrapperToJdk8OptionalConverter() {
super(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 Google Guava's {@link Optional}.
*
* @author Oliver Gierke
*/
private static class NullableWrapperToGuavaOptionalConverter extends AbstractWrapperTypeConverter {
public static final NullableWrapperToGuavaOptionalConverter INSTANCE = new NullableWrapperToGuavaOptionalConverter();
private NullableWrapperToGuavaOptionalConverter() {
super(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 Scala's {@link Option}.
*
* @author Oliver Gierke
*/
private static class NullableWrapperToScalaOptionConverter extends AbstractWrapperTypeConverter {
public static final NullableWrapperToScalaOptionConverter INSTANCE = new NullableWrapperToScalaOptionConverter();
private NullableWrapperToScalaOptionConverter() {
super(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 io.vavr.control.Option}.
*
* @author Oliver Gierke
*/
private static class NullableWrapperToVavrOptionConverter extends AbstractWrapperTypeConverter {
public static final NullableWrapperToVavrOptionConverter INSTANCE = new NullableWrapperToVavrOptionConverter();
private NullableWrapperToVavrOptionConverter() {
super(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
*/
private enum GuavaOptionalUnwrapper implements Converter<Object, Object> {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Nullable
@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
*/
private enum Jdk8OptionalUnwrapper implements Converter<Object, Object> {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Nullable
@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 enum ScalOptionUnwrapper implements Converter<Object, Object> {
INSTANCE;
private final Function0<Object> alternative = new AbstractFunction0<Object>() {
/*
* (non-Javadoc)
* @see scala.Function0#apply()
*/
@Nullable
@Override
public Option<Object> apply() {
return null;
}
};
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Nullable
@Override
public Object convert(Object source) {
return source instanceof Option ? ((Option<?>) source).getOrElse(alternative) : source;
}
}
/**
* Converter to unwrap Vavr {@link io.vavr.control.Option} instances.
*
* @author Oliver Gierke
* @since 2.0
*/
private enum VavrOptionUnwrapper implements Converter<Object, Object> {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Nullable
@Override
@SuppressWarnings("unchecked")
public Object convert(Object source) {
if (source instanceof io.vavr.control.Option) {
return ((io.vavr.control.Option<Object>) source).getOrElse(() -> null);
}
return source;
}
}
private static final class WrapperType {
private WrapperType(Class<?> type, Cardinality cardinality) {
this.type = type;
this.cardinality = cardinality;
}
public Class<?> getType() {
return this.type;
}
public Cardinality getCardinality() {
return cardinality;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof WrapperType)) {
return false;
}
WrapperType that = (WrapperType) o;
if (!ObjectUtils.nullSafeEquals(type, that.type)) {
return false;
}
return cardinality == that.cardinality;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(type);
result = 31 * result + ObjectUtils.nullSafeHashCode(cardinality);
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "WrapperType(type=" + this.getType() + ", cardinality=" + this.getCardinality() + ")";
}
enum Cardinality {
NONE, SINGLE, MULTI;
}
private final Class<?> type;
private final 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);
}
public boolean isSingleValue() {
return cardinality.equals(Cardinality.SINGLE);
}
}
}