From b294bfce3b4da20d3bb3052eb37eaad085bacf8f Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Mon, 15 Jun 2015 12:57:57 +0200 Subject: [PATCH] DATACMNS-714 - Added support for CompletableFuture and ListenableFuture for query methods. Spring Framework 4.2 is going to introduce support for CompletableFuture as return type for @Async methods. Support for Spring's ListenableFuture has already been supported in the framework 4.1. We've added the necessary infrastructure to allow these types as return types on repository methods, too. Related tickets: SPR-13128. --- .../util/QueryExecutionConverters.java | 77 ++++++++++++++++--- .../QueryExecutionConvertersUnitTests.java | 40 ++++++++-- 2 files changed, 99 insertions(+), 18 deletions(-) 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())); + } }