From 1d3af0c128e9406c2f21d57de53f40e05bbe1878 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Sat, 19 Nov 2016 12:45:02 +0100 Subject: [PATCH] DATACMNS-937 - Support for JavaSlang's Option type as nullable wrapper. Repository methods can now return JavaSlang's Option as alternative to JDK 8's Optional or Guavas Optional. --- pom.xml | 8 ++ .../util/QueryExecutionConverters.java | 95 ++++++++++++++++++- .../QueryExecutionConvertersUnitTests.java | 61 +++++++++++- template.mf | 1 + 4 files changed, 161 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 19255544a..e3fdc5362 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ DATACMNS + 2.0.4 2.11.7 1.4.8 @@ -143,6 +144,13 @@ true + + io.javaslang + javaslang + ${javaslang} + true + + javax.el el-api 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 e8f57d04d..9acc89cfe 100644 --- a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java +++ b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java @@ -19,11 +19,13 @@ import scala.Function0; import scala.Option; import scala.runtime.AbstractFunction0; +import java.lang.reflect.Method; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import java.util.function.Supplier; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; @@ -33,6 +35,7 @@ import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.concurrent.ListenableFuture; import com.google.common.base.Optional; @@ -43,10 +46,11 @@ import com.google.common.base.Optional; * * * @author Oliver Gierke @@ -65,6 +69,8 @@ public abstract class QueryExecutionConverters { QueryExecutionConverters.class.getClassLoader()); private static final boolean SCALA_PRESENT = ClassUtils.isPresent("scala.Option", QueryExecutionConverters.class.getClassLoader()); + private static final boolean JAVASLANG_PRESENT = ClassUtils.isPresent("javaslang.control.Option", + QueryExecutionConverters.class.getClassLoader()); private static final Set> WRAPPER_TYPES = new HashSet>(); private static final Set> UNWRAPPERS = new HashSet>(); @@ -92,6 +98,11 @@ public abstract class QueryExecutionConverters { WRAPPER_TYPES.add(NullableWrapperToScalaOptionConverter.getWrapperType()); UNWRAPPERS.add(ScalOptionUnwrapper.INSTANCE); } + + if (JAVASLANG_PRESENT) { + WRAPPER_TYPES.add(NullableWrapperToJavaSlangOptionConverter.getWrapperType()); + UNWRAPPERS.add(JavaSlangOptionUnwrapper.INSTANCE); + } } private QueryExecutionConverters() {} @@ -137,6 +148,10 @@ public abstract class QueryExecutionConverters { conversionService.addConverter(new NullableWrapperToScalaOptionConverter(conversionService)); } + if (JAVASLANG_PRESENT) { + conversionService.addConverter(new NullableWrapperToJavaSlangOptionConverter(conversionService)); + } + conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService)); } @@ -375,6 +390,51 @@ public abstract class QueryExecutionConverters { } } + /** + * Converter to convert from {@link NullableWrapper} into JavaSlang's {@link javaslang.control.Option}. + * + * @author Oliver Gierke + * @since 1.13 + */ + private static class NullableWrapperToJavaSlangOptionConverter extends AbstractWrapperTypeConverter { + + private static final Method OF_METHOD; + private static final Method NONE_METHOD; + + static { + OF_METHOD = ReflectionUtils.findMethod(getWrapperType(), "of", Object.class); + NONE_METHOD = ReflectionUtils.findMethod(getWrapperType(), "none"); + } + + /** + * Creates a new {@link NullableWrapperToJavaSlangOptionConverter} using the given {@link ConversionService}. + * + * @param conversionService must not be {@literal null}. + */ + public NullableWrapperToJavaSlangOptionConverter(ConversionService conversionService) { + super(conversionService, createEmptyOption(), getWrapperType()); + } + + public static Class getWrapperType() { + return javaslang.control.Option.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + protected Object wrap(Object source) { + return (javaslang.control.Option) ReflectionUtils.invokeMethod(OF_METHOD, null, source); + } + + @SuppressWarnings("unchecked") + private static javaslang.control.Option createEmptyOption() { + return (javaslang.control.Option) ReflectionUtils.invokeMethod(NONE_METHOD, null); + } + } + /** * A {@link Converter} to unwrap Guava {@link Optional} instances. * @@ -447,4 +507,37 @@ public abstract class QueryExecutionConverters { return source instanceof Option ? ((Option) source).getOrElse(alternative) : source; } } + + /** + * Converter to unwrap JavaSlang {@link javaslang.control.Option} instances. + * + * @author Oliver Gierke + * @since 1.13 + */ + private static enum JavaSlangOptionUnwrapper 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) { + return source instanceof javaslang.control.Option + ? ((javaslang.control.Option) source).getOrElse(NULL_SUPPLIER) : source; + } + } } 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 228762ccd..9eadf6925 100644 --- a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java @@ -21,6 +21,7 @@ import static org.junit.Assume.*; import scala.Option; +import java.lang.reflect.Method; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -29,6 +30,7 @@ import org.junit.Test; import org.springframework.core.SpringVersion; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.util.Version; +import org.springframework.util.ReflectionUtils; import org.springframework.util.concurrent.ListenableFuture; import com.google.common.base.Optional; @@ -64,6 +66,7 @@ public class QueryExecutionConvertersUnitTests { assertThat(QueryExecutionConverters.supports(Future.class), is(true)); assertThat(QueryExecutionConverters.supports(ListenableFuture.class), is(true)); assertThat(QueryExecutionConverters.supports(Option.class), is(true)); + assertThat(QueryExecutionConverters.supports(javaslang.control.Option.class), is(true)); } /** @@ -85,7 +88,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())); } /** @@ -97,7 +100,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())); } /** @@ -154,7 +157,7 @@ public class QueryExecutionConvertersUnitTests { public void turnsNullIntoScalaOptionEmpty() { assertThat((Option) conversionService.convert(new NullableWrapper(null), Option.class), - is(Option. empty())); + is(Option.empty())); } /** @@ -172,4 +175,56 @@ public class QueryExecutionConvertersUnitTests { public void unwrapsEmptyScalaOption() { assertThat(QueryExecutionConverters.unwrap(Option.empty()), is((Object) null)); } + + /** + * @see DATACMNS-937 + */ + @Test + public void turnsNullIntoJavaSlangOption() { + assertThat(conversionService.convert(new NullableWrapper(null), javaslang.control.Option.class), + is((Object) optionNone())); + } + + /** + * @see DATACMNS-937 + */ + @Test + public void wrapsValueIntoJavaSlangOption() { + + javaslang.control.Option result = conversionService.convert(new NullableWrapper("string"), + javaslang.control.Option.class); + + assertThat(result.isEmpty(), is(false)); + assertThat(result.get(), is((Object) "string")); + } + + /** + * @see DATACMNS-937 + */ + @Test + public void unwrapsEmptyJavaSlangOption() { + assertThat(QueryExecutionConverters.unwrap(optionNone()), is(nullValue())); + } + + /** + * @see DATACMNS-937 + */ + @Test + public void unwrapsJavaSlangOption() { + assertThat(QueryExecutionConverters.unwrap(option("string")), is((Object) "string")); + } + + @SuppressWarnings("unchecked") + private static javaslang.control.Option optionNone() { + + Method method = ReflectionUtils.findMethod(javaslang.control.Option.class, "none"); + return (javaslang.control.Option) ReflectionUtils.invokeMethod(method, null); + } + + @SuppressWarnings("unchecked") + private static javaslang.control.Option option(T source) { + + Method method = ReflectionUtils.findMethod(javaslang.control.Option.class, "of", Object.class); + return (javaslang.control.Option) ReflectionUtils.invokeMethod(method, null, source); + } } diff --git a/template.mf b/template.mf index 2d453512e..605b08db6 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, + 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, javax.servlet.*;version="[2.5.0, 4.0.0)";resolution:=optional,