diff --git a/pom.xml b/pom.xml index 74b071dfb..071046199 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ DATACMNS + 2.11.7 @@ -190,6 +191,14 @@ test + + + org.scala-lang + scala-library + ${scala} + true + + javax.transaction javax.transaction-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 6d554d901..0461665d7 100644 --- a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java +++ b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java @@ -15,6 +15,8 @@ */ package org.springframework.data.repository.util; +import scala.Option; + import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -35,7 +37,15 @@ import com.google.common.base.Optional; /** * Converters to potentially wrap the execution of a repository method into a variety of wrapper types potentially being - * available on the classpath. + * available on the classpath. Currently supported: + * * * @author Oliver Gierke * @since 1.8 @@ -50,6 +60,8 @@ public abstract class QueryExecutionConverters { QueryExecutionConverters.class.getClassLoader()); private static final boolean JDK_8_PRESENT = ClassUtils.isPresent("java.util.Optional", QueryExecutionConverters.class.getClassLoader()); + private static final boolean SCALA_PRESENT = ClassUtils.isPresent("scala.Option", + QueryExecutionConverters.class.getClassLoader()); private static final Set> WRAPPER_TYPES = new HashSet>(); private static final Set> UNWRAPPERS = new HashSet>(); @@ -72,6 +84,11 @@ public abstract class QueryExecutionConverters { if (JDK_8_PRESENT && SPRING_4_2_PRESENT) { WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType()); } + + if (SCALA_PRESENT) { + WRAPPER_TYPES.add(NullableWrapperToScalaOptionConverter.getWrapperType()); + UNWRAPPERS.add(ScalOptionUnwrapper.INSTANCE); + } } private QueryExecutionConverters() {} @@ -113,6 +130,10 @@ public abstract class QueryExecutionConverters { conversionService.addConverter(new NullableWrapperToCompletableFutureConverter(conversionService)); } + if (SCALA_PRESENT) { + conversionService.addConverter(new NullableWrapperToScalaOptionConverter(conversionService)); + } + conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService)); } @@ -151,6 +172,7 @@ public abstract class QueryExecutionConverters { @SuppressWarnings("unused") // private final ConversionService conversionService; private final Class[] wrapperTypes; + private final Object nullValue; /** * Creates a new {@link AbstractWrapperTypeConverter} using the given {@link ConversionService} and wrapper type. @@ -158,13 +180,15 @@ public abstract class QueryExecutionConverters { * @param conversionService must not be {@literal null}. * @param wrapperTypes must not be {@literal null}. */ - protected AbstractWrapperTypeConverter(ConversionService conversionService, Class... wrapperTypes) { + protected AbstractWrapperTypeConverter(ConversionService conversionService, Object nullValue, + Class... wrapperTypes) { Assert.notNull(conversionService, "ConversionService must not be null!"); Assert.notEmpty(wrapperTypes, "Wrapper type must not be empty!"); this.conversionService = conversionService; this.wrapperTypes = wrapperTypes; + this.nullValue = nullValue; } /* @@ -194,16 +218,9 @@ public abstract class QueryExecutionConverters { Object value = wrapper.getValue(); // TODO: Add Recursive conversion once we move to Spring 4 - return value == null ? getNullValue() : wrap(value); + return value == null ? nullValue : wrap(value); } - /** - * Return the object that shall be used as a replacement for {@literal null}. - * - * @return must not be {@literal null}. - */ - protected abstract Object getNullValue(); - /** * Wrap the given, non-{@literal null} value into the wrapper type. * @@ -226,16 +243,7 @@ public abstract class QueryExecutionConverters { * @param conversionService must not be {@literal null}. */ public NullableWrapperToGuavaOptionalConverter(ConversionService conversionService) { - super(conversionService, Optional.class); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#getNullValue() - */ - @Override - protected Object getNullValue() { - return Optional.absent(); + super(conversionService, Optional.absent(), Optional.class); } /* @@ -265,16 +273,7 @@ public abstract class QueryExecutionConverters { * @param conversionService must not be {@literal null}. */ public NullableWrapperToJdk8OptionalConverter(ConversionService conversionService) { - super(conversionService, java.util.Optional.class); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#getNullValue() - */ - @Override - protected Object getNullValue() { - return java.util.Optional.empty(); + super(conversionService, java.util.Optional.empty(), java.util.Optional.class); } /* @@ -298,24 +297,13 @@ public abstract class QueryExecutionConverters { */ private static class NullableWrapperToFutureConverter extends AbstractWrapperTypeConverter { - private static final AsyncResult NULL_OBJECT = new AsyncResult(null); - /** * Creates a new {@link NullableWrapperToFutureConverter} using the given {@link ConversionService}. * * @param conversionService must not be {@literal null}. */ public NullableWrapperToFutureConverter(ConversionService conversionService) { - super(conversionService, Future.class, ListenableFuture.class); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#getNullValue() - */ - @Override - protected Object getNullValue() { - return NULL_OBJECT; + super(conversionService, new AsyncResult(null), Future.class, ListenableFuture.class); } /* @@ -335,24 +323,13 @@ public abstract class QueryExecutionConverters { */ private static class NullableWrapperToCompletableFutureConverter extends AbstractWrapperTypeConverter { - private static final CompletableFuture NULL_OBJECT = CompletableFuture.completedFuture(null); - /** * Creates a new {@link NullableWrapperToCompletableFutureConverter} using the given {@link ConversionService}. * * @param conversionService must not be {@literal null}. */ public NullableWrapperToCompletableFutureConverter(ConversionService conversionService) { - super(conversionService, CompletableFuture.class); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#getNullValue() - */ - @Override - protected Object getNullValue() { - return NULL_OBJECT; + super(conversionService, CompletableFuture.completedFuture(null), CompletableFuture.class); } /* @@ -369,6 +346,32 @@ public abstract class QueryExecutionConverters { } } + /** + * A Spring {@link Converter} to support Scala's {@link Option}. + * + * @author Oliver Gierke + * @since 1.13 + */ + private static class NullableWrapperToScalaOptionConverter extends AbstractWrapperTypeConverter { + + public NullableWrapperToScalaOptionConverter(ConversionService conversionService) { + super(conversionService, Option.empty(), Option.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object) + */ + @Override + protected Object wrap(Object source) { + return Option.apply(source); + } + + public static Class getWrapperType() { + return Option.class; + } + } + /** * A {@link Converter} to unwrap Guava {@link Optional} instances. * @@ -408,4 +411,24 @@ public abstract class QueryExecutionConverters { return source instanceof java.util.Optional ? ((java.util.Optional) source).orElse(null) : source; } } + + /** + * A {@link Converter} to unwrap a Scala {@link Option} instance. + * + * @author Oliver Gierke + * @author 1.13 + */ + private static enum ScalOptionUnwrapper implements Converter { + + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + return source instanceof Option ? ((Option) source).orNull(null) : 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 88305324a..d098d5eba 100644 --- a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java @@ -19,6 +19,8 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import static org.junit.Assume.*; +import scala.Option; + import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -60,6 +62,7 @@ public class QueryExecutionConvertersUnitTests { assertThat(QueryExecutionConverters.supports(java.util.Optional.class), is(true)); assertThat(QueryExecutionConverters.supports(Future.class), is(true)); assertThat(QueryExecutionConverters.supports(ListenableFuture.class), is(true)); + assertThat(QueryExecutionConverters.supports(Option.class), is(true)); } /** @@ -141,4 +144,23 @@ public class QueryExecutionConvertersUnitTests { public void unwrapsNonWrapperTypeToItself() { assertThat(QueryExecutionConverters.unwrap("Foo"), is((Object) "Foo")); } + + /** + * @see DATACMNS-795 + */ + @Test + @SuppressWarnings("unchecked") + public void turnsNullIntoScalaOptionEmpty() { + + assertThat((Option) conversionService.convert(new NullableWrapper(null), Option.class), + is(Option. empty())); + } + + /** + * @see DATACMNS-795 + */ + @Test + public void unwrapsScalaOption() { + assertThat(QueryExecutionConverters.unwrap(Option.apply("foo")), is((Object) "foo")); + } } diff --git a/template.mf b/template.mf index 808981669..fde9a3cba 100644 --- a/template.mf +++ b/template.mf @@ -14,8 +14,11 @@ Import-Template: com.querydsl.*;version="${querydsl:[=.=.=,+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, javax.xml.bind.*;version="0";resolution:=optional, javax.xml.transform.*;version="0";resolution:=optional, + org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, + org.joda.time.*;version="${jodatime:[=.=.=,+1.0.0)}";resolution:=optional, org.springframework.aop.*;version="${spring:[=.=.=,+1.1.0)}";resolution:=optional, org.springframework.asm.*;version="${spring:[=.=.=,+1.1.0)}", org.springframework.beans.*;version="${spring:[=.=.=,+1.1.0)}", @@ -31,10 +34,8 @@ Import-Template: org.springframework.util.*;version="${spring:[=.=.=,+1.1.0)}", org.springframework.validation.*;version="${spring:[=.=.=,+1.1.0)}";resolution:=optional, org.springframework.web.*;version="${spring:[=.=.=,+1.1.0)}";resolution:=optional, - org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, - org.joda.time.*;version="${jodatime:[=.=.=,+1.0.0)}";resolution:=optional, org.slf4j.*;version="${slf4j:[=.=.=,+1.0.0)}", org.threeten.bp.*;version="${threetenbp:[=.=.=,+1.0.0)}";resolution:=optional, - javax.servlet.*;version="[2.5.0, 4.0.0)";resolution:=optional, - org.w3c.dom.*;version="0" + org.w3c.dom.*;version="0", + scala.*;version="${scala:[=.=.=,+1.0.0)}";resolution:=optional DynamicImport-Package: *