Skip to content

Commit 3067954

Browse files
committed
DATACMNS-795 - Added support for Scala's Option type on repositories.
Just like with Google's or Java 8's optional we now support Scala's Option type as wrapper fro return values on repositories.
1 parent 4af7fe3 commit 3067954

File tree

4 files changed

+109
-54
lines changed

4 files changed

+109
-54
lines changed

pom.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
<properties>
1919
<dist.key>DATACMNS</dist.key>
20+
<scala>2.11.7</scala>
2021
</properties>
2122

2223
<dependencies>
@@ -190,6 +191,14 @@
190191
<scope>test</scope>
191192
</dependency>
192193

194+
<!-- Scala -->
195+
<dependency>
196+
<groupId>org.scala-lang</groupId>
197+
<artifactId>scala-library</artifactId>
198+
<version>${scala}</version>
199+
<optional>true</optional>
200+
</dependency>
201+
193202
<dependency>
194203
<groupId>javax.transaction</groupId>
195204
<artifactId>javax.transaction-api</artifactId>

src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java

Lines changed: 73 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.repository.util;
1717

18+
import scala.Option;
19+
1820
import java.util.Collections;
1921
import java.util.HashSet;
2022
import java.util.Set;
@@ -35,7 +37,15 @@
3537

3638
/**
3739
* Converters to potentially wrap the execution of a repository method into a variety of wrapper types potentially being
38-
* available on the classpath.
40+
* available on the classpath. Currently supported:
41+
* <ul>
42+
* <li>{@code java.util.Optional}</li>
43+
* <li>{@code com.google.common.base.Optional}</li>
44+
* <li>{@code scala.Option}</li>
45+
* <li>{@code java.util.concurrent.Future}</li>
46+
* <li>{@code java.util.concurrent.CompletableFuture}</li>
47+
* <li>{@code org.springframework.util.concurrent.ListenableFuture<}</li>
48+
* </ul>
3949
*
4050
* @author Oliver Gierke
4151
* @since 1.8
@@ -50,6 +60,8 @@ public abstract class QueryExecutionConverters {
5060
QueryExecutionConverters.class.getClassLoader());
5161
private static final boolean JDK_8_PRESENT = ClassUtils.isPresent("java.util.Optional",
5262
QueryExecutionConverters.class.getClassLoader());
63+
private static final boolean SCALA_PRESENT = ClassUtils.isPresent("scala.Option",
64+
QueryExecutionConverters.class.getClassLoader());
5365

5466
private static final Set<Class<?>> WRAPPER_TYPES = new HashSet<Class<?>>();
5567
private static final Set<Converter<Object, Object>> UNWRAPPERS = new HashSet<Converter<Object, Object>>();
@@ -72,6 +84,11 @@ public abstract class QueryExecutionConverters {
7284
if (JDK_8_PRESENT && SPRING_4_2_PRESENT) {
7385
WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType());
7486
}
87+
88+
if (SCALA_PRESENT) {
89+
WRAPPER_TYPES.add(NullableWrapperToScalaOptionConverter.getWrapperType());
90+
UNWRAPPERS.add(ScalOptionUnwrapper.INSTANCE);
91+
}
7592
}
7693

7794
private QueryExecutionConverters() {}
@@ -113,6 +130,10 @@ public static void registerConvertersIn(ConfigurableConversionService conversion
113130
conversionService.addConverter(new NullableWrapperToCompletableFutureConverter(conversionService));
114131
}
115132

133+
if (SCALA_PRESENT) {
134+
conversionService.addConverter(new NullableWrapperToScalaOptionConverter(conversionService));
135+
}
136+
116137
conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService));
117138
}
118139

@@ -151,20 +172,23 @@ private static abstract class AbstractWrapperTypeConverter implements GenericCon
151172
@SuppressWarnings("unused") //
152173
private final ConversionService conversionService;
153174
private final Class<?>[] wrapperTypes;
175+
private final Object nullValue;
154176

155177
/**
156178
* Creates a new {@link AbstractWrapperTypeConverter} using the given {@link ConversionService} and wrapper type.
157179
*
158180
* @param conversionService must not be {@literal null}.
159181
* @param wrapperTypes must not be {@literal null}.
160182
*/
161-
protected AbstractWrapperTypeConverter(ConversionService conversionService, Class<?>... wrapperTypes) {
183+
protected AbstractWrapperTypeConverter(ConversionService conversionService, Object nullValue,
184+
Class<?>... wrapperTypes) {
162185

163186
Assert.notNull(conversionService, "ConversionService must not be null!");
164187
Assert.notEmpty(wrapperTypes, "Wrapper type must not be empty!");
165188

166189
this.conversionService = conversionService;
167190
this.wrapperTypes = wrapperTypes;
191+
this.nullValue = nullValue;
168192
}
169193

170194
/*
@@ -194,16 +218,9 @@ public final Object convert(Object source, TypeDescriptor sourceType, TypeDescri
194218
Object value = wrapper.getValue();
195219

196220
// TODO: Add Recursive conversion once we move to Spring 4
197-
return value == null ? getNullValue() : wrap(value);
221+
return value == null ? nullValue : wrap(value);
198222
}
199223

200-
/**
201-
* Return the object that shall be used as a replacement for {@literal null}.
202-
*
203-
* @return must not be {@literal null}.
204-
*/
205-
protected abstract Object getNullValue();
206-
207224
/**
208225
* Wrap the given, non-{@literal null} value into the wrapper type.
209226
*
@@ -226,16 +243,7 @@ private static class NullableWrapperToGuavaOptionalConverter extends AbstractWra
226243
* @param conversionService must not be {@literal null}.
227244
*/
228245
public NullableWrapperToGuavaOptionalConverter(ConversionService conversionService) {
229-
super(conversionService, Optional.class);
230-
}
231-
232-
/*
233-
* (non-Javadoc)
234-
* @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#getNullValue()
235-
*/
236-
@Override
237-
protected Object getNullValue() {
238-
return Optional.absent();
246+
super(conversionService, Optional.absent(), Optional.class);
239247
}
240248

241249
/*
@@ -265,16 +273,7 @@ private static class NullableWrapperToJdk8OptionalConverter extends AbstractWrap
265273
* @param conversionService must not be {@literal null}.
266274
*/
267275
public NullableWrapperToJdk8OptionalConverter(ConversionService conversionService) {
268-
super(conversionService, java.util.Optional.class);
269-
}
270-
271-
/*
272-
* (non-Javadoc)
273-
* @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#getNullValue()
274-
*/
275-
@Override
276-
protected Object getNullValue() {
277-
return java.util.Optional.empty();
276+
super(conversionService, java.util.Optional.empty(), java.util.Optional.class);
278277
}
279278

280279
/*
@@ -298,24 +297,13 @@ public static Class<?> getWrapperType() {
298297
*/
299298
private static class NullableWrapperToFutureConverter extends AbstractWrapperTypeConverter {
300299

301-
private static final AsyncResult<Object> NULL_OBJECT = new AsyncResult<Object>(null);
302-
303300
/**
304301
* Creates a new {@link NullableWrapperToFutureConverter} using the given {@link ConversionService}.
305302
*
306303
* @param conversionService must not be {@literal null}.
307304
*/
308305
public NullableWrapperToFutureConverter(ConversionService conversionService) {
309-
super(conversionService, Future.class, ListenableFuture.class);
310-
}
311-
312-
/*
313-
* (non-Javadoc)
314-
* @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#getNullValue()
315-
*/
316-
@Override
317-
protected Object getNullValue() {
318-
return NULL_OBJECT;
306+
super(conversionService, new AsyncResult<Object>(null), Future.class, ListenableFuture.class);
319307
}
320308

321309
/*
@@ -335,24 +323,39 @@ protected Object wrap(Object source) {
335323
*/
336324
private static class NullableWrapperToCompletableFutureConverter extends AbstractWrapperTypeConverter {
337325

338-
private static final CompletableFuture<Object> NULL_OBJECT = CompletableFuture.completedFuture(null);
339-
340326
/**
341327
* Creates a new {@link NullableWrapperToCompletableFutureConverter} using the given {@link ConversionService}.
342328
*
343329
* @param conversionService must not be {@literal null}.
344330
*/
345331
public NullableWrapperToCompletableFutureConverter(ConversionService conversionService) {
346-
super(conversionService, CompletableFuture.class);
332+
super(conversionService, CompletableFuture.completedFuture(null), CompletableFuture.class);
347333
}
348334

349335
/*
350336
* (non-Javadoc)
351-
* @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#getNullValue()
337+
* @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object)
352338
*/
353339
@Override
354-
protected Object getNullValue() {
355-
return NULL_OBJECT;
340+
protected Object wrap(Object source) {
341+
return source instanceof CompletableFuture ? source : CompletableFuture.completedFuture(source);
342+
}
343+
344+
public static Class<?> getWrapperType() {
345+
return CompletableFuture.class;
346+
}
347+
}
348+
349+
/**
350+
* A Spring {@link Converter} to support Scala's {@link Option}.
351+
*
352+
* @author Oliver Gierke
353+
* @since 1.13
354+
*/
355+
private static class NullableWrapperToScalaOptionConverter extends AbstractWrapperTypeConverter {
356+
357+
public NullableWrapperToScalaOptionConverter(ConversionService conversionService) {
358+
super(conversionService, Option.empty(), Option.class);
356359
}
357360

358361
/*
@@ -361,11 +364,11 @@ protected Object getNullValue() {
361364
*/
362365
@Override
363366
protected Object wrap(Object source) {
364-
return source instanceof CompletableFuture ? source : CompletableFuture.completedFuture(source);
367+
return Option.apply(source);
365368
}
366369

367370
public static Class<?> getWrapperType() {
368-
return CompletableFuture.class;
371+
return Option.class;
369372
}
370373
}
371374

@@ -408,4 +411,24 @@ public Object convert(Object source) {
408411
return source instanceof java.util.Optional ? ((java.util.Optional<?>) source).orElse(null) : source;
409412
}
410413
}
414+
415+
/**
416+
* A {@link Converter} to unwrap a Scala {@link Option} instance.
417+
*
418+
* @author Oliver Gierke
419+
* @author 1.13
420+
*/
421+
private static enum ScalOptionUnwrapper implements Converter<Object, Object> {
422+
423+
INSTANCE;
424+
425+
/*
426+
* (non-Javadoc)
427+
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
428+
*/
429+
@Override
430+
public Object convert(Object source) {
431+
return source instanceof Option ? ((Option<?>) source).orNull(null) : source;
432+
}
433+
}
411434
}

src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import static org.junit.Assert.*;
2020
import static org.junit.Assume.*;
2121

22+
import scala.Option;
23+
2224
import java.util.concurrent.CompletableFuture;
2325
import java.util.concurrent.Future;
2426

@@ -60,6 +62,7 @@ public void registersWrapperTypes() {
6062
assertThat(QueryExecutionConverters.supports(java.util.Optional.class), is(true));
6163
assertThat(QueryExecutionConverters.supports(Future.class), is(true));
6264
assertThat(QueryExecutionConverters.supports(ListenableFuture.class), is(true));
65+
assertThat(QueryExecutionConverters.supports(Option.class), is(true));
6366
}
6467

6568
/**
@@ -141,4 +144,23 @@ public void unwrapsNullToNull() {
141144
public void unwrapsNonWrapperTypeToItself() {
142145
assertThat(QueryExecutionConverters.unwrap("Foo"), is((Object) "Foo"));
143146
}
147+
148+
/**
149+
* @see DATACMNS-795
150+
*/
151+
@Test
152+
@SuppressWarnings("unchecked")
153+
public void turnsNullIntoScalaOptionEmpty() {
154+
155+
assertThat((Option<Object>) conversionService.convert(new NullableWrapper(null), Option.class),
156+
is(Option.<Object> empty()));
157+
}
158+
159+
/**
160+
* @see DATACMNS-795
161+
*/
162+
@Test
163+
public void unwrapsScalaOption() {
164+
assertThat(QueryExecutionConverters.unwrap(Option.apply("foo")), is((Object) "foo"));
165+
}
144166
}

template.mf

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ Import-Template:
1414
com.querydsl.*;version="${querydsl:[=.=.=,+1.0.0)}";resolution:=optional,
1515
javax.enterprise.*;version="${cdi:[=.=.=,+1.0.0)}";resolution:=optional,
1616
javax.inject.*;version="[1.0.0,2.0.0)";resolution:=optional,
17+
javax.servlet.*;version="[2.5.0, 4.0.0)";resolution:=optional,
1718
javax.xml.bind.*;version="0";resolution:=optional,
1819
javax.xml.transform.*;version="0";resolution:=optional,
20+
org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional,
21+
org.joda.time.*;version="${jodatime:[=.=.=,+1.0.0)}";resolution:=optional,
1922
org.springframework.aop.*;version="${spring:[=.=.=,+1.1.0)}";resolution:=optional,
2023
org.springframework.asm.*;version="${spring:[=.=.=,+1.1.0)}",
2124
org.springframework.beans.*;version="${spring:[=.=.=,+1.1.0)}",
@@ -31,10 +34,8 @@ Import-Template:
3134
org.springframework.util.*;version="${spring:[=.=.=,+1.1.0)}",
3235
org.springframework.validation.*;version="${spring:[=.=.=,+1.1.0)}";resolution:=optional,
3336
org.springframework.web.*;version="${spring:[=.=.=,+1.1.0)}";resolution:=optional,
34-
org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional,
35-
org.joda.time.*;version="${jodatime:[=.=.=,+1.0.0)}";resolution:=optional,
3637
org.slf4j.*;version="${slf4j:[=.=.=,+1.0.0)}",
3738
org.threeten.bp.*;version="${threetenbp:[=.=.=,+1.0.0)}";resolution:=optional,
38-
javax.servlet.*;version="[2.5.0, 4.0.0)";resolution:=optional,
39-
org.w3c.dom.*;version="0"
39+
org.w3c.dom.*;version="0",
40+
scala.*;version="${scala:[=.=.=,+1.0.0)}";resolution:=optional
4041
DynamicImport-Package: *

0 commit comments

Comments
 (0)