DATACMNS-940 - Support for Javaslang collections as repository return types.

Javaslang's collection and map types can now be used on repository query methods and get adapted similarly to nullable wrapper types (like JDK's Optional). Also made TypeInformation infrastructure aware of the map type so that a value type lookup gets handle correctly.
This commit is contained in:
Oliver Gierke
2016-12-12 20:07:37 +01:00
parent c52aba358d
commit a9c3b9f66f
5 changed files with 335 additions and 9 deletions

View File

@@ -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<Object, Object> {
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<ConvertiblePair> 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<ConvertiblePair> 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<ConvertiblePair> pairs = new HashSet<ConvertiblePair>();
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);
}
}
}

View File

@@ -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<Object>) source).getOrElse(NULL_SUPPLIER) : source;
if (source instanceof javaslang.control.Option) {
return ((javaslang.control.Option<Object>) source).getOrElse(NULL_SUPPLIER);
}
if (source instanceof Traversable) {
return JavaslangCollections.ToJavaConverter.INSTANCE.convert(source);
}
return source;
}
}
}

View File

@@ -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<S> implements TypeInformation<S> {
private static final Iterable<Class<?>> MAP_TYPES;
static {
ClassLoader classLoader = TypeDiscoverer.class.getClassLoader();
Set<Class<?>> mapTypes = new HashSet<Class<?>>();
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<TypeVariable<?>, Type> typeVariableMap;
private final Map<String, ValueHolder> fieldTypes = new ConcurrentHashMap<String, ValueHolder>();
@@ -329,7 +348,14 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
* @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<S> implements TypeInformation<S> {
protected TypeInformation<?> doGetMapValueType() {
if (isMap()) {
return getTypeArgument(Map.class, 1);
return getTypeArgument(getBaseType(MAP_TYPES), 1);
}
List<TypeInformation<?>> arguments = getTypeArguments();
@@ -399,7 +425,7 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
}
if (isMap()) {
return getTypeArgument(Map.class, 0);
return getTypeArgument(getBaseType(MAP_TYPES), 0);
}
if (Iterable.class.isAssignableFrom(rawType)) {
@@ -525,6 +551,17 @@ class TypeDiscoverer<S> implements TypeInformation<S> {
return createInfo(arguments[index]);
}
private Class<?> getBaseType(Iterable<Class<?>> 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)