From 34ce83c657c35cad68cbfea395161e7ec912764f Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 11 May 2017 12:36:23 +0200 Subject: [PATCH] DATACMNS-1065 - Added support for Vavr (successor of Javaslang). Basically duplicated the Javaslang support to now work on Vavr types as well. --- pom.xml | 8 +- .../repository/util/JavaslangCollections.java | 2 +- .../util/QueryExecutionConverters.java | 79 ++++++++++ .../data/repository/util/VavrCollections.java | 142 ++++++++++++++++++ .../QueryExecutionConvertersUnitTests.java | 72 +++++++++ 5 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/util/VavrCollections.java diff --git a/pom.xml b/pom.xml index 1db9b70eb..5a7ea733b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,6 @@ org.springframework.data spring-data-commons - 2.0.0.BUILD-SNAPSHOT Spring Data Core @@ -17,6 +16,7 @@ 2.0.4 + 0.9.0 2.11.7 1.4.8 @@ -175,6 +175,12 @@ ${javaslang} true + + io.vavr + vavr + ${vavr} + true + javax.el diff --git a/src/main/java/org/springframework/data/repository/util/JavaslangCollections.java b/src/main/java/org/springframework/data/repository/util/JavaslangCollections.java index 85631d63d..a012be42b 100644 --- a/src/main/java/org/springframework/data/repository/util/JavaslangCollections.java +++ b/src/main/java/org/springframework/data/repository/util/JavaslangCollections.java @@ -67,7 +67,7 @@ class JavaslangCollections { return ((javaslang.collection.Set) source).toJavaSet(); } - throw new IllegalArgumentException("Unsupported Javaslang collection " + source); + throw new IllegalArgumentException("Unsupported Javaslang collection " + source.getClass()); } } diff --git a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java index a4594358d..50fb5d486 100644 --- a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java +++ b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java @@ -64,6 +64,7 @@ import com.google.common.base.Optional; *
  • {@code javaslang.control.Option} - as of 1.13
  • *
  • {@code javaslang.collection.Seq}, {@code javaslang.collection.Map}, {@code javaslang.collection.Set} - as of * 1.13
  • + *
  • {@code io.vavr.collection.Seq}, {@code io.vavr.collection.Map}, {@code io.vavr.collection.Set} - as of 2.0
  • *
  • Reactive wrappers supported by {@link ReactiveWrappers} - as of 2.0
  • * * @@ -84,6 +85,8 @@ public abstract class QueryExecutionConverters { 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 WRAPPER_TYPES = new HashSet(); private static final Set UNWRAPPER_TYPES = new HashSet(); @@ -134,6 +137,16 @@ public abstract class QueryExecutionConverters { 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())); @@ -234,6 +247,11 @@ public abstract class QueryExecutionConverters { conversionService.addConverter(JavaslangCollections.FromJavaConverter.INSTANCE); } + if (VAVR_PRESENT) { + conversionService.addConverter(new NullableWrapperToVavrOptionConverter(conversionService)); + conversionService.addConverter(VavrCollections.FromJavaConverter.INSTANCE); + } + conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService)); } @@ -498,6 +516,37 @@ public abstract class QueryExecutionConverters { } } + /** + * 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. * @@ -601,6 +650,36 @@ public abstract class QueryExecutionConverters { } } + /** + * Converter to unwrap Vavr {@link io.vavr.control.Option} instances. + * + * @author Oliver Gierke + * @since 2.0 + */ + private static enum VavrOptionUnwrapper implements Converter { + + 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) 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 { diff --git a/src/main/java/org/springframework/data/repository/util/VavrCollections.java b/src/main/java/org/springframework/data/repository/util/VavrCollections.java new file mode 100644 index 000000000..e794bef4c --- /dev/null +++ b/src/main/java/org/springframework/data/repository/util/VavrCollections.java @@ -0,0 +1,142 @@ +/* + * Copyright 2016-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 io.vavr.collection.LinkedHashMap; +import io.vavr.collection.LinkedHashSet; +import io.vavr.collection.Traversable; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.repository.util.QueryExecutionConverters.WrapperType; + +/** + * Converter implementations to map from and to Vavr collections. + * + * @author Oliver Gierke + * @author Christoph Strobl + * @since 2.0 + */ +class VavrCollections { + + public enum ToJavaConverter implements Converter { + + INSTANCE; + + public WrapperType getWrapperType() { + return WrapperType.multiValue(io.vavr.collection.Traversable.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + if (source instanceof io.vavr.collection.Seq) { + return ((io.vavr.collection.Seq) source).toJavaList(); + } + + if (source instanceof io.vavr.collection.Map) { + return ((io.vavr.collection.Map) source).toJavaMap(); + } + + if (source instanceof io.vavr.collection.Set) { + return ((io.vavr.collection.Set) source).toJavaSet(); + } + + throw new IllegalArgumentException("Unsupported Javaslang collection " + source.getClass()); + } + } + + public enum FromJavaConverter implements ConditionalGenericConverter { + + INSTANCE { + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes() + */ + @Override + public java.util.Set getConvertibleTypes() { + return CONVERTIBLE_PAIRS; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.ConditionalConverter#matches(org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor) + */ + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + + // Prevent collections to be mapped to maps + if (sourceType.isCollection() && io.vavr.collection.Map.class.isAssignableFrom(targetType.getType())) { + return false; + } + + // Prevent maps to be mapped to collections + if (sourceType.isMap() && !(io.vavr.collection.Map.class.isAssignableFrom(targetType.getType()) + || targetType.getType().equals(Traversable.class))) { + return false; + } + + return true; + } + + /* + * (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 Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + + if (source instanceof List) { + return io.vavr.collection.List.ofAll((Iterable) source); + } + + if (source instanceof java.util.Set) { + return LinkedHashSet.ofAll((Iterable) source); + } + + if (source instanceof java.util.Map) { + return LinkedHashMap.ofAll((java.util.Map) source); + } + + return source; + } + }; + + private static final Set CONVERTIBLE_PAIRS; + + static { + + Set pairs = new HashSet<>(); + pairs.add(new ConvertiblePair(Collection.class, io.vavr.collection.Traversable.class)); + pairs.add(new ConvertiblePair(Map.class, io.vavr.collection.Traversable.class)); + + CONVERTIBLE_PAIRS = Collections.unmodifiableSet(pairs); + } + } +} diff --git a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java index a2278be26..2460e7271 100755 --- a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java @@ -266,4 +266,76 @@ public class QueryExecutionConvertersUnitTests { Set> allowedPageableTypes = QueryExecutionConverters.getAllowedPageableTypes(); assertThat(allowedPageableTypes).contains(Page.class, Slice.class, List.class, Seq.class); } + + @Test // DATACMNS-1065 + public void unwrapsEmptyVavrOption() { + assertThat(QueryExecutionConverters.unwrap(io.vavr.control.Option.none())).isNull(); + } + + @Test // DATACMNS-1065 + public void unwrapsVavrOption() { + assertThat(QueryExecutionConverters.unwrap(io.vavr.control.Option.of("string"))).isEqualTo("string"); + } + + @Test // DATACMNS-1065 + public void conversListToVavr() { + + assertThat(conversionService.canConvert(List.class, io.vavr.collection.Traversable.class)).isTrue(); + assertThat(conversionService.canConvert(List.class, io.vavr.collection.List.class)).isTrue(); + assertThat(conversionService.canConvert(List.class, io.vavr.collection.Set.class)).isTrue(); + assertThat(conversionService.canConvert(List.class, io.vavr.collection.Map.class)).isFalse(); + + List integers = Arrays.asList(1, 2, 3); + + io.vavr.collection.Traversable result = conversionService.convert(integers, + io.vavr.collection.Traversable.class); + + assertThat(result).isInstanceOf(io.vavr.collection.List.class); + } + + @Test // DATACMNS-1065 + public void convertsSetToVavr() { + + assertThat(conversionService.canConvert(Set.class, io.vavr.collection.Traversable.class)).isTrue(); + assertThat(conversionService.canConvert(Set.class, io.vavr.collection.Set.class)).isTrue(); + assertThat(conversionService.canConvert(Set.class, io.vavr.collection.List.class)).isTrue(); + assertThat(conversionService.canConvert(Set.class, io.vavr.collection.Map.class)).isFalse(); + + Set integers = Collections.singleton(1); + + io.vavr.collection.Traversable result = conversionService.convert(integers, + io.vavr.collection.Traversable.class); + + assertThat(result).isInstanceOf(io.vavr.collection.Set.class); + } + + @Test // DATACMNS-1065 + public void convertsMapToVavr() { + + assertThat(conversionService.canConvert(Map.class, io.vavr.collection.Traversable.class)).isTrue(); + assertThat(conversionService.canConvert(Map.class, io.vavr.collection.Map.class)).isTrue(); + assertThat(conversionService.canConvert(Map.class, io.vavr.collection.Set.class)).isFalse(); + assertThat(conversionService.canConvert(Map.class, io.vavr.collection.List.class)).isFalse(); + + Map map = Collections.singletonMap("key", "value"); + + io.vavr.collection.Traversable result = conversionService.convert(map, io.vavr.collection.Traversable.class); + + assertThat(result).isInstanceOf(io.vavr.collection.Map.class); + } + + @Test // DATACMNS-1065 + public void unwrapsVavrCollectionsToJavaOnes() { + + assertThat(unwrap(io.vavr.collection.List.of(1, 2, 3))).isInstanceOf(List.class); + assertThat(unwrap(io.vavr.collection.LinkedHashSet.of(1, 2, 3))).isInstanceOf(Set.class); + assertThat(unwrap(io.vavr.collection.LinkedHashMap.of("key", "value"))).isInstanceOf(Map.class); + } + + @Test // DATACMNS-1065 + public void vavrSeqIsASupportedPageableType() { + + Set> allowedPageableTypes = QueryExecutionConverters.getAllowedPageableTypes(); + assertThat(allowedPageableTypes).contains(io.vavr.collection.Seq.class); + } }