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 c57c7f7ac..d60161f22 100644 --- a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java +++ b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.data.repository.util; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import org.springframework.core.convert.ConversionService; @@ -28,6 +29,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.concurrent.ListenableFuture; import com.google.common.base.Optional; @@ -42,7 +44,7 @@ public abstract class QueryExecutionConverters { private static final boolean GUAVA_PRESENT = ClassUtils.isPresent("com.google.common.base.Optional", QueryExecutionConverters.class.getClassLoader()); - private static final boolean JDK_PRESENT = ClassUtils.isPresent("java.util.Optional", + private static final boolean JDK_8_PRESENT = ClassUtils.isPresent("java.util.Optional", QueryExecutionConverters.class.getClassLoader()); private static final Set> WRAPPER_TYPES = new HashSet>(); @@ -50,13 +52,15 @@ public abstract class QueryExecutionConverters { static { WRAPPER_TYPES.add(Future.class); + WRAPPER_TYPES.add(ListenableFuture.class); if (GUAVA_PRESENT) { WRAPPER_TYPES.add(NullableWrapperToGuavaOptionalConverter.getWrapperType()); } - if (JDK_PRESENT) { + if (JDK_8_PRESENT) { WRAPPER_TYPES.add(NullableWrapperToJdk8OptionalConverter.getWrapperType()); + WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType()); } } @@ -87,8 +91,9 @@ public abstract class QueryExecutionConverters { conversionService.addConverter(new NullableWrapperToGuavaOptionalConverter(conversionService)); } - if (JDK_PRESENT) { + if (JDK_8_PRESENT) { conversionService.addConverter(new NullableWrapperToJdk8OptionalConverter(conversionService)); + conversionService.addConverter(new NullableWrapperToCompletableFutureConverter(conversionService)); } conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService)); @@ -102,23 +107,23 @@ public abstract class QueryExecutionConverters { */ private static abstract class AbstractWrapperTypeConverter implements GenericConverter { - @SuppressWarnings("unused")// + @SuppressWarnings("unused") // private final ConversionService conversionService; - private final Class wrapperType; + private final Class[] wrapperTypes; /** * Creates a new {@link AbstractWrapperTypeConverter} using the given {@link ConversionService} and wrapper type. * * @param conversionService must not be {@literal null}. - * @param wrapperType must not be {@literal null}. + * @param wrapperTypes must not be {@literal null}. */ - protected AbstractWrapperTypeConverter(ConversionService conversionService, Class wrapperType) { + protected AbstractWrapperTypeConverter(ConversionService conversionService, Class... wrapperTypes) { Assert.notNull(conversionService, "ConversionService must not be null!"); - Assert.notNull(wrapperType, "Wrapper type must not be null!"); + Assert.notEmpty(wrapperTypes, "Wrapper type must not be empty!"); this.conversionService = conversionService; - this.wrapperType = wrapperType; + this.wrapperTypes = wrapperTypes; } /* @@ -127,7 +132,14 @@ public abstract class QueryExecutionConverters { */ @Override public Set getConvertibleTypes() { - return Collections.singleton(new ConvertiblePair(NullableWrapper.class, wrapperType)); + + Set pairs = new HashSet(wrapperTypes.length); + + for (Class wrapperType : wrapperTypes) { + pairs.add(new ConvertiblePair(NullableWrapper.class, wrapperType)); + } + + return Collections.unmodifiableSet(pairs); } /* @@ -253,7 +265,7 @@ public abstract class QueryExecutionConverters { * @param conversionService must not be {@literal null}. */ public NullableWrapperToFutureConverter(ConversionService conversionService) { - super(conversionService, Future.class); + super(conversionService, Future.class, ListenableFuture.class); } /* @@ -274,4 +286,45 @@ public abstract class QueryExecutionConverters { return new AsyncResult(source); } } + + /** + * A Spring {@link Converter} to support returning {@link CompletableFuture} instances from repository methods. + * + * @author Oliver Gierke + */ + 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; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object) + */ + @Override + protected Object wrap(Object source) { + return source instanceof CompletableFuture ? source : CompletableFuture.completedFuture(source); + } + + public static Class getWrapperType() { + return CompletableFuture.class; + } + } } 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 cf460bb8b..0017a7022 100644 --- a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,13 @@ package org.springframework.data.repository.util; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; -import org.junit.Assume; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + import org.junit.Before; import org.junit.Test; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.util.ClassUtils; +import org.springframework.util.concurrent.ListenableFuture; import com.google.common.base.Optional; @@ -42,6 +44,19 @@ public class QueryExecutionConvertersUnitTests { QueryExecutionConverters.registerConvertersIn(conversionService); } + /** + * @see DATACMNS-714 + */ + @Test + public void registersWrapperTypes() { + + assertThat(QueryExecutionConverters.supports(Optional.class), is(true)); + 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(CompletableFuture.class), is(true)); + } + /** * @see DATACMNS-483 */ @@ -60,9 +75,22 @@ public class QueryExecutionConvertersUnitTests { @SuppressWarnings("unchecked") public void turnsNullIntoJdk8Optional() { - Assume.assumeThat(ClassUtils.isPresent("java.util.Optional", getClass().getClassLoader()), is(true)); - - java.util.Optional optional = conversionService.convert(new NullableWrapper(null), java.util.Optional.class); + java.util.Optional optional = conversionService.convert(new NullableWrapper(null), + java.util.Optional.class); assertThat(optional, is(java.util.Optional. empty())); } + + /** + * @see DATACMNS-714 + */ + @Test + @SuppressWarnings("unchecked") + public void turnsNullIntoCompletableFutureForNull() throws Exception { + + CompletableFuture result = conversionService.convert(new NullableWrapper(null), CompletableFuture.class); + + assertThat(result, is(notNullValue())); + assertThat(result.isDone(), is(true)); + assertThat(result.get(), is(nullValue())); + } }