From 71fc87ad427b54a59d8369ee14c7ce1d9bb92b2f 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 +++++++++++- 3 files changed, 160 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 355d30a49..1816fa18e 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ + 2.0.4 2.11.7 1.4.8 5.0.0.BUILD-SNAPSHOT @@ -175,6 +176,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 aca776c1f..354b551a3 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; *
    *
  • {@code java.util.Optional}
  • *
  • {@code com.google.common.base.Optional}
  • - *
  • {@code scala.Option}
  • + *
  • {@code scala.Option} - as of 1.12
  • *
  • {@code java.util.concurrent.Future}
  • *
  • {@code java.util.concurrent.CompletableFuture}
  • *
  • {@code org.springframework.util.concurrent.ListenableFuture<}
  • + *
  • {@code javaslang.control.Option} - as of 1.13
  • *
  • Reactive wrappers supported by {@link ReactiveWrappers}
  • *
* @@ -70,6 +74,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> UNWRAPPER_TYPES = new HashSet>(); @@ -105,6 +111,11 @@ public abstract class QueryExecutionConverters { UNWRAPPERS.add(ScalOptionUnwrapper.INSTANCE); } + if (JAVASLANG_PRESENT) { + WRAPPER_TYPES.add(NullableWrapperToJavaSlangOptionConverter.getWrapperType()); + UNWRAPPERS.add(JavaSlangOptionUnwrapper.INSTANCE); + } + if (ReactiveWrappers.isAvailable()) { WRAPPER_TYPES.addAll(ReactiveWrappers.getNoValueTypes()); WRAPPER_TYPES.addAll(ReactiveWrappers.getSingleValueTypes()); @@ -174,6 +185,10 @@ public abstract class QueryExecutionConverters { conversionService.addConverter(new NullableWrapperToScalaOptionConverter(conversionService)); } + if (JAVASLANG_PRESENT) { + conversionService.addConverter(new NullableWrapperToJavaSlangOptionConverter(conversionService)); + } + if (ASYNC_RESULT_PRESENT) { conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService)); } @@ -414,6 +429,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. * @@ -486,4 +546,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 02d6518e2..bb19c2be4 100644 --- a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java @@ -26,6 +26,7 @@ import rx.Observable; import rx.Single; import scala.Option; +import java.lang.reflect.Method; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -35,6 +36,7 @@ import org.reactivestreams.Publisher; 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; @@ -70,6 +72,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)); } /** @@ -142,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())); } /** @@ -154,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())); } /** @@ -211,7 +214,7 @@ public class QueryExecutionConvertersUnitTests { public void turnsNullIntoScalaOptionEmpty() { assertThat((Option) conversionService.convert(new NullableWrapper(null), Option.class), - is(Option. empty())); + is(Option.empty())); } /** @@ -229,4 +232,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); + } }