Skip to content

Commit 1d3af0c

Browse files
committed
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.
1 parent b4518f0 commit 1d3af0c

File tree

4 files changed

+161
-4
lines changed

4 files changed

+161
-4
lines changed

pom.xml

+8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
<properties>
1919
<dist.key>DATACMNS</dist.key>
20+
<javaslang>2.0.4</javaslang>
2021
<scala>2.11.7</scala>
2122
<xmlbeam>1.4.8</xmlbeam>
2223
</properties>
@@ -143,6 +144,13 @@
143144
<optional>true</optional>
144145
</dependency>
145146

147+
<dependency>
148+
<groupId>io.javaslang</groupId>
149+
<artifactId>javaslang</artifactId>
150+
<version>${javaslang}</version>
151+
<optional>true</optional>
152+
</dependency>
153+
146154
<dependency>
147155
<groupId>javax.el</groupId>
148156
<artifactId>el-api</artifactId>

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

+94-1
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
import scala.Option;
2020
import scala.runtime.AbstractFunction0;
2121

22+
import java.lang.reflect.Method;
2223
import java.util.Collections;
2324
import java.util.HashSet;
2425
import java.util.Set;
2526
import java.util.concurrent.CompletableFuture;
2627
import java.util.concurrent.Future;
28+
import java.util.function.Supplier;
2729

2830
import org.springframework.core.convert.ConversionService;
2931
import org.springframework.core.convert.TypeDescriptor;
@@ -33,6 +35,7 @@
3335
import org.springframework.scheduling.annotation.AsyncResult;
3436
import org.springframework.util.Assert;
3537
import org.springframework.util.ClassUtils;
38+
import org.springframework.util.ReflectionUtils;
3639
import org.springframework.util.concurrent.ListenableFuture;
3740

3841
import com.google.common.base.Optional;
@@ -43,10 +46,11 @@
4346
* <ul>
4447
* <li>{@code java.util.Optional}</li>
4548
* <li>{@code com.google.common.base.Optional}</li>
46-
* <li>{@code scala.Option}</li>
49+
* <li>{@code scala.Option} - as of 1.12</li>
4750
* <li>{@code java.util.concurrent.Future}</li>
4851
* <li>{@code java.util.concurrent.CompletableFuture}</li>
4952
* <li>{@code org.springframework.util.concurrent.ListenableFuture<}</li>
53+
* <li>{@code javaslang.control.Option} - as of 1.13</li>
5054
* </ul>
5155
*
5256
* @author Oliver Gierke
@@ -65,6 +69,8 @@ public abstract class QueryExecutionConverters {
6569
QueryExecutionConverters.class.getClassLoader());
6670
private static final boolean SCALA_PRESENT = ClassUtils.isPresent("scala.Option",
6771
QueryExecutionConverters.class.getClassLoader());
72+
private static final boolean JAVASLANG_PRESENT = ClassUtils.isPresent("javaslang.control.Option",
73+
QueryExecutionConverters.class.getClassLoader());
6874

6975
private static final Set<Class<?>> WRAPPER_TYPES = new HashSet<Class<?>>();
7076
private static final Set<Converter<Object, Object>> UNWRAPPERS = new HashSet<Converter<Object, Object>>();
@@ -92,6 +98,11 @@ public abstract class QueryExecutionConverters {
9298
WRAPPER_TYPES.add(NullableWrapperToScalaOptionConverter.getWrapperType());
9399
UNWRAPPERS.add(ScalOptionUnwrapper.INSTANCE);
94100
}
101+
102+
if (JAVASLANG_PRESENT) {
103+
WRAPPER_TYPES.add(NullableWrapperToJavaSlangOptionConverter.getWrapperType());
104+
UNWRAPPERS.add(JavaSlangOptionUnwrapper.INSTANCE);
105+
}
95106
}
96107

97108
private QueryExecutionConverters() {}
@@ -137,6 +148,10 @@ public static void registerConvertersIn(ConfigurableConversionService conversion
137148
conversionService.addConverter(new NullableWrapperToScalaOptionConverter(conversionService));
138149
}
139150

151+
if (JAVASLANG_PRESENT) {
152+
conversionService.addConverter(new NullableWrapperToJavaSlangOptionConverter(conversionService));
153+
}
154+
140155
conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService));
141156
}
142157

@@ -375,6 +390,51 @@ public static Class<?> getWrapperType() {
375390
}
376391
}
377392

393+
/**
394+
* Converter to convert from {@link NullableWrapper} into JavaSlang's {@link javaslang.control.Option}.
395+
*
396+
* @author Oliver Gierke
397+
* @since 1.13
398+
*/
399+
private static class NullableWrapperToJavaSlangOptionConverter extends AbstractWrapperTypeConverter {
400+
401+
private static final Method OF_METHOD;
402+
private static final Method NONE_METHOD;
403+
404+
static {
405+
OF_METHOD = ReflectionUtils.findMethod(getWrapperType(), "of", Object.class);
406+
NONE_METHOD = ReflectionUtils.findMethod(getWrapperType(), "none");
407+
}
408+
409+
/**
410+
* Creates a new {@link NullableWrapperToJavaSlangOptionConverter} using the given {@link ConversionService}.
411+
*
412+
* @param conversionService must not be {@literal null}.
413+
*/
414+
public NullableWrapperToJavaSlangOptionConverter(ConversionService conversionService) {
415+
super(conversionService, createEmptyOption(), getWrapperType());
416+
}
417+
418+
public static Class<?> getWrapperType() {
419+
return javaslang.control.Option.class;
420+
}
421+
422+
/*
423+
* (non-Javadoc)
424+
* @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object)
425+
*/
426+
@Override
427+
@SuppressWarnings("unchecked")
428+
protected Object wrap(Object source) {
429+
return (javaslang.control.Option<Object>) ReflectionUtils.invokeMethod(OF_METHOD, null, source);
430+
}
431+
432+
@SuppressWarnings("unchecked")
433+
private static javaslang.control.Option<Object> createEmptyOption() {
434+
return (javaslang.control.Option<Object>) ReflectionUtils.invokeMethod(NONE_METHOD, null);
435+
}
436+
}
437+
378438
/**
379439
* A {@link Converter} to unwrap Guava {@link Optional} instances.
380440
*
@@ -447,4 +507,37 @@ public Object convert(Object source) {
447507
return source instanceof Option ? ((Option<?>) source).getOrElse(alternative) : source;
448508
}
449509
}
510+
511+
/**
512+
* Converter to unwrap JavaSlang {@link javaslang.control.Option} instances.
513+
*
514+
* @author Oliver Gierke
515+
* @since 1.13
516+
*/
517+
private static enum JavaSlangOptionUnwrapper implements Converter<Object, Object> {
518+
519+
INSTANCE;
520+
521+
private static final Supplier<Object> NULL_SUPPLIER = new Supplier<Object>() {
522+
523+
/*
524+
* (non-Javadoc)
525+
* @see java.util.function.Supplier#get()
526+
*/
527+
public Object get() {
528+
return null;
529+
}
530+
};
531+
532+
/*
533+
* (non-Javadoc)
534+
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
535+
*/
536+
@Override
537+
@SuppressWarnings("unchecked")
538+
public Object convert(Object source) {
539+
return source instanceof javaslang.control.Option
540+
? ((javaslang.control.Option<Object>) source).getOrElse(NULL_SUPPLIER) : source;
541+
}
542+
}
450543
}

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

+58-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import scala.Option;
2323

24+
import java.lang.reflect.Method;
2425
import java.util.concurrent.CompletableFuture;
2526
import java.util.concurrent.Future;
2627

@@ -29,6 +30,7 @@
2930
import org.springframework.core.SpringVersion;
3031
import org.springframework.core.convert.support.DefaultConversionService;
3132
import org.springframework.data.util.Version;
33+
import org.springframework.util.ReflectionUtils;
3234
import org.springframework.util.concurrent.ListenableFuture;
3335

3436
import com.google.common.base.Optional;
@@ -64,6 +66,7 @@ public void registersWrapperTypes() {
6466
assertThat(QueryExecutionConverters.supports(Future.class), is(true));
6567
assertThat(QueryExecutionConverters.supports(ListenableFuture.class), is(true));
6668
assertThat(QueryExecutionConverters.supports(Option.class), is(true));
69+
assertThat(QueryExecutionConverters.supports(javaslang.control.Option.class), is(true));
6770
}
6871

6972
/**
@@ -85,7 +88,7 @@ public void registersCompletableFutureAsWrapperTypeOnSpring42OrBetter() {
8588
public void turnsNullIntoGuavaOptional() {
8689

8790
Optional<Object> optional = conversionService.convert(new NullableWrapper(null), Optional.class);
88-
assertThat(optional, is(Optional.<Object> absent()));
91+
assertThat(optional, is(Optional.<Object>absent()));
8992
}
9093

9194
/**
@@ -97,7 +100,7 @@ public void turnsNullIntoJdk8Optional() {
97100

98101
java.util.Optional<Object> optional = conversionService.convert(new NullableWrapper(null),
99102
java.util.Optional.class);
100-
assertThat(optional, is(java.util.Optional.<Object> empty()));
103+
assertThat(optional, is(java.util.Optional.<Object>empty()));
101104
}
102105

103106
/**
@@ -154,7 +157,7 @@ public void unwrapsNonWrapperTypeToItself() {
154157
public void turnsNullIntoScalaOptionEmpty() {
155158

156159
assertThat((Option<Object>) conversionService.convert(new NullableWrapper(null), Option.class),
157-
is(Option.<Object> empty()));
160+
is(Option.<Object>empty()));
158161
}
159162

160163
/**
@@ -172,4 +175,56 @@ public void unwrapsScalaOption() {
172175
public void unwrapsEmptyScalaOption() {
173176
assertThat(QueryExecutionConverters.unwrap(Option.empty()), is((Object) null));
174177
}
178+
179+
/**
180+
* @see DATACMNS-937
181+
*/
182+
@Test
183+
public void turnsNullIntoJavaSlangOption() {
184+
assertThat(conversionService.convert(new NullableWrapper(null), javaslang.control.Option.class),
185+
is((Object) optionNone()));
186+
}
187+
188+
/**
189+
* @see DATACMNS-937
190+
*/
191+
@Test
192+
public void wrapsValueIntoJavaSlangOption() {
193+
194+
javaslang.control.Option<?> result = conversionService.convert(new NullableWrapper("string"),
195+
javaslang.control.Option.class);
196+
197+
assertThat(result.isEmpty(), is(false));
198+
assertThat(result.get(), is((Object) "string"));
199+
}
200+
201+
/**
202+
* @see DATACMNS-937
203+
*/
204+
@Test
205+
public void unwrapsEmptyJavaSlangOption() {
206+
assertThat(QueryExecutionConverters.unwrap(optionNone()), is(nullValue()));
207+
}
208+
209+
/**
210+
* @see DATACMNS-937
211+
*/
212+
@Test
213+
public void unwrapsJavaSlangOption() {
214+
assertThat(QueryExecutionConverters.unwrap(option("string")), is((Object) "string"));
215+
}
216+
217+
@SuppressWarnings("unchecked")
218+
private static javaslang.control.Option<Object> optionNone() {
219+
220+
Method method = ReflectionUtils.findMethod(javaslang.control.Option.class, "none");
221+
return (javaslang.control.Option<Object>) ReflectionUtils.invokeMethod(method, null);
222+
}
223+
224+
@SuppressWarnings("unchecked")
225+
private static <T> javaslang.control.Option<T> option(T source) {
226+
227+
Method method = ReflectionUtils.findMethod(javaslang.control.Option.class, "of", Object.class);
228+
return (javaslang.control.Option<T>) ReflectionUtils.invokeMethod(method, null, source);
229+
}
175230
}

template.mf

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Import-Template:
1313
com.google.common.*;version="${guava:[=.=.=,+1.0.0)}";resolution:=optional,
1414
com.jayway.jsonpath.*;version="${jsonpath:[=.=.=,+1.0.0]}";resolution:=optional,
1515
com.querydsl.*;version="${querydsl:[=.=.=,+1.0.0)}";resolution:=optional,
16+
javaslang.*;version="${javaslang:[=.=.=,+1.0.0)}";resolution:=optional,
1617
javax.enterprise.*;version="${cdi:[=.=.=,+1.0.0)}";resolution:=optional,
1718
javax.inject.*;version="[1.0.0,2.0.0)";resolution:=optional,
1819
javax.servlet.*;version="[2.5.0, 4.0.0)";resolution:=optional,

0 commit comments

Comments
 (0)