From 3d17ae5d7c0ce2cfc13ee8b72576b4f3b4df9a46 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 | 7 + .../repository/util/JavaslangCollections.java | 2 +- .../util/QueryExecutionConverters.java | 108 ++++++++++++- .../data/repository/util/VavrCollections.java | 149 ++++++++++++++++++ .../QueryExecutionConvertersUnitTests.java | 110 +++++++++++++ template.mf | 1 + 6 files changed, 373 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/springframework/data/repository/util/VavrCollections.java diff --git a/pom.xml b/pom.xml index 3ce145bc6..a2408919c 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,7 @@ DATACMNS 2.0.4 + 0.9.0 2.11.7 1.4.8 @@ -150,6 +151,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 6ee4b4221..074fd1c93 100644 --- a/src/main/java/org/springframework/data/repository/util/JavaslangCollections.java +++ b/src/main/java/org/springframework/data/repository/util/JavaslangCollections.java @@ -66,7 +66,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 0fd04f906..4cb24fdf9 100644 --- a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java +++ b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java @@ -16,7 +16,6 @@ package org.springframework.data.repository.util; import javaslang.collection.Seq; -import javaslang.collection.Traversable; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.Value; @@ -62,6 +61,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
  • * * * @author Oliver Gierke @@ -79,6 +79,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> UNWRAPPERS = new HashSet>(); @@ -121,6 +123,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); + } } private QueryExecutionConverters() {} @@ -194,6 +206,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)); } @@ -468,7 +485,7 @@ public abstract class QueryExecutionConverters { @Override @SuppressWarnings("unchecked") protected Object wrap(Object source) { - return (javaslang.control.Option) ReflectionUtils.invokeMethod(OF_METHOD, null, source); + return ReflectionUtils.invokeMethod(OF_METHOD, null, source); } @SuppressWarnings("unchecked") @@ -477,6 +494,50 @@ 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 { + + private static final Method OF_METHOD; + private static final Method NONE_METHOD; + + static { + OF_METHOD = ReflectionUtils.findMethod(io.vavr.control.Option.class, "of", Object.class); + NONE_METHOD = ReflectionUtils.findMethod(io.vavr.control.Option.class, "none"); + } + + /** + * Creates a new {@link NullableWrapperToJavaslangOptionConverter} using the given {@link ConversionService}. + * + * @param conversionService must not be {@literal null}. + */ + public NullableWrapperToVavrOptionConverter(ConversionService conversionService) { + super(conversionService, createEmptyOption(), 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 ReflectionUtils.invokeMethod(OF_METHOD, source); + } + + public static WrapperType getWrapperType() { + return WrapperType.singleValue(io.vavr.control.Option.class); + } + + @SuppressWarnings("unchecked") + private static io.vavr.control.Option createEmptyOption() { + return (io.vavr.control.Option) ReflectionUtils.invokeMethod(NONE_METHOD, null); + } + } + /** * A {@link Converter} to unwrap Guava {@link Optional} instances. * @@ -583,7 +644,7 @@ public abstract class QueryExecutionConverters { return ((javaslang.control.Option) source).getOrElse(NULL_SUPPLIER); } - if (source instanceof Traversable) { + if (source instanceof javaslang.collection.Traversable) { return JavaslangCollections.ToJavaConverter.INSTANCE.convert(source); } @@ -591,6 +652,47 @@ 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; + + private static final Supplier NULL_SUPPLIER = new Supplier() { + + /* + * (non-Javadoc) + * @see java.util.function.Supplier#get() + */ + public Object get() { + return null; + } + }; + + /* + * (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_SUPPLIER); + } + + 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..e1de51c96 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/util/VavrCollections.java @@ -0,0 +1,149 @@ +/* + * 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.Traversable; + +import java.lang.reflect.Method; +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; +import org.springframework.util.ReflectionUtils; + +/** + * Converter implementations to map from and to Vavr collections. + * + * @author Oliver Gierke + * @author Christoph Strobl + * @since 1.13 + */ +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 ReflectionUtils.invokeMethod(LIST_FACTORY_METHOD, null, source); + } + + if (source instanceof java.util.Set) { + return ReflectionUtils.invokeMethod(SET_FACTORY_METHOD, null, source); + } + + if (source instanceof java.util.Map) { + return ReflectionUtils.invokeMethod(MAP_FACTORY_METHOD, null, source); + } + + return source; + } + }; + + private static final Set CONVERTIBLE_PAIRS; + private static final Method LIST_FACTORY_METHOD; + private static final Method SET_FACTORY_METHOD; + private static final Method MAP_FACTORY_METHOD; + + 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); + + MAP_FACTORY_METHOD = ReflectionUtils.findMethod(io.vavr.collection.LinkedHashMap.class, "ofAll", Map.class); + LIST_FACTORY_METHOD = ReflectionUtils.findMethod(io.vavr.collection.List.class, "ofAll", Iterable.class); + SET_FACTORY_METHOD = ReflectionUtils.findMethod(io.vavr.collection.LinkedHashSet.class, "ofAll", Iterable.class); + } + } +} 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 6c84837fe..51b211e88 100644 --- a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java @@ -231,6 +231,116 @@ public class QueryExecutionConvertersUnitTests { assertThat(allowedPageableTypes, Matchers.> hasItems(Page.class, Slice.class, List.class, Seq.class)); } + @Test // DATACMNS-1065 + public void unwrapsEmptyVavrOption() { + assertThat(QueryExecutionConverters.unwrap(vavrOptionNone()), is(nullValue())); + } + + @Test // DATACMNS-1065 + public void unwrapsVavrOption() { + assertThat(QueryExecutionConverters.unwrap(vavrOption("string")), is((Object) "string")); + } + + @Test // DATACMNS-1065 + public void conversListToVavr() { + + assertThat(conversionService.canConvert(List.class, io.vavr.collection.Traversable.class), is(true)); + assertThat(conversionService.canConvert(List.class, io.vavr.collection.List.class), is(true)); + assertThat(conversionService.canConvert(List.class, io.vavr.collection.Set.class), is(true)); + assertThat(conversionService.canConvert(List.class, io.vavr.collection.Map.class), is(false)); + + List integers = Arrays.asList(1, 2, 3); + + io.vavr.collection.Traversable result = conversionService.convert(integers, + io.vavr.collection.Traversable.class); + + assertThat(result, is(instanceOf(io.vavr.collection.List.class))); + } + + @Test // DATACMNS-1065 + public void convertsSetToVavr() { + + assertThat(conversionService.canConvert(Set.class, io.vavr.collection.Traversable.class), is(true)); + assertThat(conversionService.canConvert(Set.class, io.vavr.collection.Set.class), is(true)); + assertThat(conversionService.canConvert(Set.class, io.vavr.collection.List.class), is(true)); + assertThat(conversionService.canConvert(Set.class, io.vavr.collection.Map.class), is(false)); + + Set integers = Collections.singleton(1); + + io.vavr.collection.Traversable result = conversionService.convert(integers, + io.vavr.collection.Traversable.class); + + assertThat(result, is(instanceOf(io.vavr.collection.Set.class))); + } + + @Test // DATACMNS-1065 + public void convertsMapToVavr() { + + assertThat(conversionService.canConvert(Map.class, io.vavr.collection.Traversable.class), is(true)); + assertThat(conversionService.canConvert(Map.class, io.vavr.collection.Map.class), is(true)); + assertThat(conversionService.canConvert(Map.class, io.vavr.collection.Set.class), is(false)); + assertThat(conversionService.canConvert(Map.class, io.vavr.collection.List.class), is(false)); + + Map map = Collections.singletonMap("key", "value"); + + io.vavr.collection.Traversable result = conversionService.convert(map, io.vavr.collection.Traversable.class); + + assertThat(result, is(instanceOf(io.vavr.collection.Map.class))); + } + + @Test // DATACMNS-1065 + public void unwrapsVavrCollectionsToJavaOnes() { + + assertThat(unwrap(vavrList(1, 2, 3)), is(instanceOf(List.class))); + assertThat(unwrap(vavrSet(1, 2, 3)), is(instanceOf(Set.class))); + assertThat(unwrap(vavrMap("key", "value")), is(instanceOf(Map.class))); + } + + @Test // DATACMNS-1065 + public void vavrSeqIsASupportedPageableType() { + + Set> allowedPageableTypes = QueryExecutionConverters.getAllowedPageableTypes(); + assertThat(allowedPageableTypes, hasItem(io.vavr.collection.Seq.class)); + } + + // Vavr + + @SuppressWarnings("unchecked") + private static io.vavr.control.Option vavrOptionNone() { + + Method method = ReflectionUtils.findMethod(io.vavr.control.Option.class, "none"); + return (io.vavr.control.Option) ReflectionUtils.invokeMethod(method, null); + } + + @SuppressWarnings("unchecked") + private static io.vavr.control.Option vavrOption(T source) { + + Method method = ReflectionUtils.findMethod(io.vavr.control.Option.class, "of", Object.class); + return (io.vavr.control.Option) ReflectionUtils.invokeMethod(method, null, source); + } + + @SuppressWarnings("unchecked") + private static io.vavr.collection.List vavrList(T... values) { + + Method method = ReflectionUtils.findMethod(io.vavr.collection.List.class, "ofAll", Iterable.class); + return (io.vavr.collection.List) ReflectionUtils.invokeMethod(method, null, Arrays.asList(values)); + } + + @SuppressWarnings("unchecked") + private static io.vavr.collection.Set vavrSet(T... values) { + + Method method = ReflectionUtils.findMethod(io.vavr.collection.HashSet.class, "ofAll", Iterable.class); + return (io.vavr.collection.Set) ReflectionUtils.invokeMethod(method, null, Arrays.asList(values)); + } + + @SuppressWarnings("unchecked") + private static io.vavr.collection.Map vavrMap(K key, V value) { + + Method method = ReflectionUtils.findMethod(io.vavr.collection.HashMap.class, "ofAll", Map.class); + return (io.vavr.collection.Map) ReflectionUtils.invokeMethod(method, null, + Collections.singletonMap(key, value)); + } + @SuppressWarnings("unchecked") private static javaslang.control.Option optionNone() { diff --git a/template.mf b/template.mf index 605b08db6..f22cc38a9 100644 --- a/template.mf +++ b/template.mf @@ -13,6 +13,7 @@ Import-Template: com.google.common.*;version="${guava:[=.=.=,+1.0.0)}";resolution:=optional, com.jayway.jsonpath.*;version="${jsonpath:[=.=.=,+1.0.0]}";resolution:=optional, com.querydsl.*;version="${querydsl:[=.=.=,+1.0.0)}";resolution:=optional, + io.vavr.*;version="${vavr:[=.=.=,+1.0.0)}";resolution:=optional, javaslang.*;version="${javaslang:[=.=.=,+1.0.0)}";resolution:=optional, javax.enterprise.*;version="${cdi:[=.=.=,+1.0.0)}";resolution:=optional, javax.inject.*;version="[1.0.0,2.0.0)";resolution:=optional,