diff --git a/src/main/java/org/springframework/data/repository/util/JavaslangCollections.java b/src/main/java/org/springframework/data/repository/util/JavaslangCollections.java new file mode 100644 index 000000000..3d1fbf847 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/util/JavaslangCollections.java @@ -0,0 +1,148 @@ +/* + * Copyright 2016 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.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.util.ReflectionUtils; + +/** + * Converter implementations to map from and to Javaslang collections. + * + * @author Oliver Gierke + * @since 1.13 + */ +class JavaslangCollections { + + public enum ToJavaConverter implements Converter { + + INSTANCE; + + public Class getWrapperType() { + return javaslang.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 javaslang.collection.Seq) { + return ((javaslang.collection.Seq) source).toJavaList(); + } + + if (source instanceof javaslang.collection.Map) { + return ((javaslang.collection.Map) source).toJavaMap(); + } + + if (source instanceof javaslang.collection.Set) { + return ((javaslang.collection.Set) source).toJavaSet(); + } + + throw new IllegalArgumentException("Unsupported Javaslang collection " + source); + } + } + + 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() && javaslang.collection.Map.class.isAssignableFrom(targetType.getType())) { + return false; + } + + // Prevent maps to be mapped to collections + if (sourceType.isMap() && !(javaslang.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, javaslang.collection.Traversable.class)); + pairs.add(new ConvertiblePair(Map.class, javaslang.collection.Traversable.class)); + + CONVERTIBLE_PAIRS = Collections.unmodifiableSet(pairs); + + MAP_FACTORY_METHOD = ReflectionUtils.findMethod(javaslang.collection.LinkedHashMap.class, "ofAll", Map.class); + LIST_FACTORY_METHOD = ReflectionUtils.findMethod(javaslang.collection.List.class, "ofAll", Iterable.class); + SET_FACTORY_METHOD = ReflectionUtils.findMethod(javaslang.collection.LinkedHashSet.class, "ofAll", + Iterable.class); + } + } +} 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 354ccb4ec..88a07f24f 100644 --- a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java +++ b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java @@ -15,11 +15,13 @@ */ package org.springframework.data.repository.util; +import javaslang.collection.Traversable; import scala.Function0; import scala.Option; import scala.runtime.AbstractFunction0; import java.lang.reflect.Method; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -105,7 +107,10 @@ public abstract class QueryExecutionConverters { } if (JAVASLANG_PRESENT) { + WRAPPER_TYPES.add(NullableWrapperToJavaslangOptionConverter.getWrapperType()); + WRAPPER_TYPES.add(JavaslangCollections.ToJavaConverter.INSTANCE.getWrapperType()); + UNWRAPPERS.add(JavaslangOptionUnwrapper.INSTANCE); } @@ -165,6 +170,8 @@ public abstract class QueryExecutionConverters { Assert.notNull(conversionService, "ConversionService must not be null!"); + conversionService.removeConvertible(Collection.class, Object.class); + if (GUAVA_PRESENT) { conversionService.addConverter(new NullableWrapperToGuavaOptionalConverter(conversionService)); } @@ -180,6 +187,7 @@ public abstract class QueryExecutionConverters { if (JAVASLANG_PRESENT) { conversionService.addConverter(new NullableWrapperToJavaslangOptionConverter(conversionService)); + conversionService.addConverter(JavaslangCollections.FromJavaConverter.INSTANCE); } conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService)); @@ -566,8 +574,16 @@ public abstract class QueryExecutionConverters { @Override @SuppressWarnings("unchecked") public Object convert(Object source) { - return source instanceof javaslang.control.Option - ? ((javaslang.control.Option) source).getOrElse(NULL_SUPPLIER) : source; + + if (source instanceof javaslang.control.Option) { + return ((javaslang.control.Option) source).getOrElse(NULL_SUPPLIER); + } + + if (source instanceof Traversable) { + return JavaslangCollections.ToJavaConverter.INSTANCE.convert(source); + } + + return source; } } } diff --git a/src/main/java/org/springframework/data/util/TypeDiscoverer.java b/src/main/java/org/springframework/data/util/TypeDiscoverer.java index f26863075..52cdc0596 100644 --- a/src/main/java/org/springframework/data/util/TypeDiscoverer.java +++ b/src/main/java/org/springframework/data/util/TypeDiscoverer.java @@ -35,13 +35,16 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.springframework.beans.BeanUtils; import org.springframework.core.GenericTypeResolver; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; /** @@ -51,6 +54,22 @@ import org.springframework.util.ReflectionUtils; */ class TypeDiscoverer implements TypeInformation { + private static final Iterable> MAP_TYPES; + + static { + + ClassLoader classLoader = TypeDiscoverer.class.getClassLoader(); + + Set> mapTypes = new HashSet>(); + mapTypes.add(Map.class); + + try { + mapTypes.add(ClassUtils.forName("javaslang.collection.Map", classLoader)); + } catch (ClassNotFoundException o_O) {} + + MAP_TYPES = Collections.unmodifiableSet(mapTypes); + } + private final Type type; private final Map, Type> typeVariableMap; private final Map fieldTypes = new ConcurrentHashMap(); @@ -329,7 +348,14 @@ class TypeDiscoverer implements TypeInformation { * @see org.springframework.data.util.TypeInformation#isMap() */ public boolean isMap() { - return Map.class.isAssignableFrom(getType()); + + for (Class mapType : MAP_TYPES) { + if (mapType.isAssignableFrom(getType())) { + return true; + } + } + + return false; } /* @@ -349,7 +375,7 @@ class TypeDiscoverer implements TypeInformation { protected TypeInformation doGetMapValueType() { if (isMap()) { - return getTypeArgument(Map.class, 1); + return getTypeArgument(getBaseType(MAP_TYPES), 1); } List> arguments = getTypeArguments(); @@ -399,7 +425,7 @@ class TypeDiscoverer implements TypeInformation { } if (isMap()) { - return getTypeArgument(Map.class, 0); + return getTypeArgument(getBaseType(MAP_TYPES), 0); } if (Iterable.class.isAssignableFrom(rawType)) { @@ -525,6 +551,17 @@ class TypeDiscoverer implements TypeInformation { return createInfo(arguments[index]); } + private Class getBaseType(Iterable> candidates) { + + for (Class candidate : candidates) { + if (candidate.isAssignableFrom(getType())) { + return candidate; + } + } + + throw new IllegalArgumentException(String.format("Type %s not contained in candidates %s!", getType(), candidates)); + } + /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) 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 98580224e..a6ae57677 100644 --- a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java @@ -17,7 +17,11 @@ package org.springframework.data.repository.util; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.springframework.data.repository.util.QueryExecutionConverters.*; +import javaslang.collection.HashMap; +import javaslang.collection.HashSet; +import javaslang.collection.Traversable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import rx.Completable; @@ -26,6 +30,11 @@ import rx.Single; import scala.Option; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -136,7 +145,7 @@ public class QueryExecutionConvertersUnitTests { public void turnsNullIntoGuavaOptional() { Optional optional = conversionService.convert(new NullableWrapper(null), Optional.class); - assertThat(optional, is(Optional.absent())); + assertThat(optional, is(Optional. absent())); } /** @@ -148,7 +157,7 @@ public class QueryExecutionConvertersUnitTests { java.util.Optional optional = conversionService.convert(new NullableWrapper(null), java.util.Optional.class); - assertThat(optional, is(java.util.Optional.empty())); + assertThat(optional, is(java.util.Optional. empty())); } /** @@ -205,7 +214,7 @@ public class QueryExecutionConvertersUnitTests { public void turnsNullIntoScalaOptionEmpty() { assertThat((Option) conversionService.convert(new NullableWrapper(null), Option.class), - is(Option.empty())); + is(Option. empty())); } /** @@ -262,6 +271,71 @@ public class QueryExecutionConvertersUnitTests { assertThat(QueryExecutionConverters.unwrap(option("string")), is((Object) "string")); } + /** + * @see DATACMNS-940 + */ + @Test + public void conversListToJavaslang() { + + assertThat(conversionService.canConvert(List.class, javaslang.collection.Traversable.class), is(true)); + assertThat(conversionService.canConvert(List.class, javaslang.collection.List.class), is(true)); + assertThat(conversionService.canConvert(List.class, javaslang.collection.Set.class), is(true)); + assertThat(conversionService.canConvert(List.class, javaslang.collection.Map.class), is(false)); + + List integers = Arrays.asList(1, 2, 3); + + Traversable result = conversionService.convert(integers, Traversable.class); + + assertThat(result, is(instanceOf(javaslang.collection.List.class))); + } + + /** + * @see DATACMNS-940 + */ + @Test + public void convertsSetToJavaslang() { + + assertThat(conversionService.canConvert(Set.class, javaslang.collection.Traversable.class), is(true)); + assertThat(conversionService.canConvert(Set.class, javaslang.collection.Set.class), is(true)); + assertThat(conversionService.canConvert(Set.class, javaslang.collection.List.class), is(true)); + assertThat(conversionService.canConvert(Set.class, javaslang.collection.Map.class), is(false)); + + Set integers = Collections.singleton(1); + + Traversable result = conversionService.convert(integers, Traversable.class); + + assertThat(result, is(instanceOf(javaslang.collection.Set.class))); + } + + /** + * @see DATACMNS-940 + */ + @Test + public void convertsMapToJavaslang() { + + assertThat(conversionService.canConvert(Map.class, javaslang.collection.Traversable.class), is(true)); + assertThat(conversionService.canConvert(Map.class, javaslang.collection.Map.class), is(true)); + assertThat(conversionService.canConvert(Map.class, javaslang.collection.Set.class), is(false)); + assertThat(conversionService.canConvert(Map.class, javaslang.collection.List.class), is(false)); + + Map map = Collections.singletonMap("key", "value"); + + Traversable result = conversionService.convert(map, Traversable.class); + + assertThat(result, is(instanceOf(javaslang.collection.Map.class))); + } + + /** + * @see DATACMNS-940 + */ + @Test + public void unwrapsJavaslangCollectionsToJavaOnes() { + + assertThat(unwrap(javaslangList(1, 2, 3)), is(instanceOf(List.class))); + assertThat(unwrap(javaslangSet(1, 2, 3)), is(instanceOf(Set.class))); + assertThat(unwrap(javaslangMap("key", "value")), is(instanceOf(Map.class))); + } + @SuppressWarnings("unchecked") private static javaslang.control.Option optionNone() { @@ -275,4 +349,26 @@ public class QueryExecutionConvertersUnitTests { Method method = ReflectionUtils.findMethod(javaslang.control.Option.class, "of", Object.class); return (javaslang.control.Option) ReflectionUtils.invokeMethod(method, null, source); } + + @SuppressWarnings("unchecked") + private static javaslang.collection.List javaslangList(T... values) { + + Method method = ReflectionUtils.findMethod(javaslang.collection.List.class, "ofAll", Iterable.class); + return (javaslang.collection.List) ReflectionUtils.invokeMethod(method, null, Arrays.asList(values)); + } + + @SuppressWarnings("unchecked") + private static javaslang.collection.Set javaslangSet(T... values) { + + Method method = ReflectionUtils.findMethod(HashSet.class, "ofAll", Iterable.class); + return (javaslang.collection.Set) ReflectionUtils.invokeMethod(method, null, Arrays.asList(values)); + } + + @SuppressWarnings("unchecked") + private static javaslang.collection.Map javaslangMap(K key, V value) { + + Method method = ReflectionUtils.findMethod(HashMap.class, "ofAll", Map.class); + return (javaslang.collection.Map) ReflectionUtils.invokeMethod(method, null, + Collections.singletonMap(key, value)); + } } diff --git a/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java b/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java index c4a4af0fa..2d1405efa 100644 --- a/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java +++ b/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java @@ -19,6 +19,8 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.springframework.data.util.ClassTypeInformation.*; +import javaslang.collection.Traversable; + import java.lang.reflect.Method; import java.util.Calendar; import java.util.Collection; @@ -106,7 +108,7 @@ public class ClassTypeInformationUnitTests { property = information.getProperty("rawSet"); assertEquals(Set.class, property.getType()); - assertThat(property.getComponentType().getType(), is(Matchers.>equalTo(Object.class))); + assertThat(property.getComponentType().getType(), is(Matchers.> equalTo(Object.class))); assertNull(property.getMapValueType()); } @@ -413,6 +415,29 @@ public class ClassTypeInformationUnitTests { assertThat(information.getProperty("field").getType(), is(typeCompatibleWith(Nested.class))); } + /** + * @see DATACMNS-940 + */ + @Test + public void detectsJavaslangTraversableComponentType() { + + ClassTypeInformation information = ClassTypeInformation.from(SampleTraversable.class); + + assertThat(information.getComponentType().getType(), is(typeCompatibleWith(Integer.class))); + } + + /** + * @see DATACMNS-940 + */ + @Test + public void detectsJavaslangMapComponentAndValueType() { + + ClassTypeInformation information = ClassTypeInformation.from(SampleMap.class); + + assertThat(information.getComponentType().getType(), is(typeCompatibleWith(String.class))); + assertThat(information.getMapValueType().getType(), is(typeCompatibleWith(Integer.class))); + } + static class StringMapContainer extends MapContainer { } @@ -611,4 +636,8 @@ public class ClassTypeInformationUnitTests { static class Nested extends SomeType {} static class Concrete extends SomeType {} + + static interface SampleTraversable extends Traversable {} + + static interface SampleMap extends javaslang.collection.Map {} }