diff --git a/pom.xml b/pom.xml index 8de17af65..2879d3633 100644 --- a/pom.xml +++ b/pom.xml @@ -202,6 +202,31 @@ test + + + org.jetbrains.kotlin + kotlin-stdlib + true + + + + org.jetbrains.kotlin + kotlin-reflect + true + + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + true + + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + true + + org.springframework @@ -215,6 +240,13 @@ + + org.jetbrains.kotlinx + kotlinx-coroutines-test + test + true + + org.slf4j log4j-over-slf4j diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/ReactiveElasticsearchRepository.java b/src/main/java/org/springframework/data/elasticsearch/repository/ReactiveElasticsearchRepository.java index d152c3d34..a94d626dc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/ReactiveElasticsearchRepository.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/ReactiveElasticsearchRepository.java @@ -27,6 +27,4 @@ import org.springframework.data.repository.reactive.ReactiveSortingRepository; */ @NoRepositoryBean public interface ReactiveElasticsearchRepository - extends ReactiveSortingRepository, ReactiveCrudRepository { - -} + extends ReactiveSortingRepository, ReactiveCrudRepository {} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java index 2c417e9ef..9df5c7062 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractElasticsearchRepositoryQuery.java @@ -29,6 +29,7 @@ import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.util.StreamUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -75,7 +76,9 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository public Object execute(Object[] parameters) { ParametersParameterAccessor parameterAccessor = getParameterAccessor(parameters); - Class clazz = getResultClass(); + ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); + Class clazz = resultProcessor.getReturnedType().getDomainType(); + Query query = createQuery(parameters); IndexCoordinates index = elasticsearchOperations.getIndexCoordinatesFor(clazz); @@ -132,8 +135,10 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository public Query createQuery(Object[] parameters) { - Class clazz = getResultClass(); ParametersParameterAccessor parameterAccessor = getParameterAccessor(parameters); + ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); + Class returnedType = resultProcessor.getReturnedType().getDomainType(); + Query query = createQuery(parameterAccessor); Assert.notNull(query, "unsupported query"); @@ -151,10 +156,6 @@ public abstract class AbstractElasticsearchRepositoryQuery implements Repository return query; } - private Class getResultClass() { - return queryMethod.getResultProcessor().getReturnedType().getDomainType(); - } - private ParametersParameterAccessor getParameterAccessor(Object[] parameters) { return new ParametersParameterAccessor(queryMethod.getParameters(), parameters); } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java index 1bd7f652c..2a079ae39 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/AbstractReactiveElasticsearchRepositoryQuery.java @@ -21,6 +21,7 @@ import reactor.core.publisher.Mono; import org.reactivestreams.Publisher; import org.springframework.core.convert.converter.Converter; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHit; import org.springframework.data.elasticsearch.core.SearchHitSupport; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; @@ -81,6 +82,13 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor private Object execute(ElasticsearchParameterAccessor parameterAccessor) { ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); + var returnedType = processor.getReturnedType(); + Class domainType = returnedType.getDomainType(); + Class typeToRead = returnedType.getTypeToRead(); + + if (SearchHit.class.isAssignableFrom(typeToRead)) { + typeToRead = queryMethod.unwrappedReturnType; + } Query query = createQuery(parameterAccessor); @@ -100,8 +108,7 @@ abstract class AbstractReactiveElasticsearchRepositoryQuery implements Repositor ReactiveElasticsearchQueryExecution execution = getExecution(parameterAccessor, new ResultProcessingConverter(processor)); - var returnedType = processor.getReturnedType(); - return execution.execute(query, returnedType.getDomainType(), returnedType.getTypeToRead(), index); + return execution.execute(query, domainType, typeToRead, index); } private ReactiveElasticsearchQueryExecution getExecution(ElasticsearchParameterAccessor accessor, diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java index bad23c8a7..4ab188946 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchQueryMethod.java @@ -43,6 +43,8 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.util.QueryExecutionConverters; +import org.springframework.data.repository.util.ReactiveWrapperConverters; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -62,9 +64,16 @@ import org.springframework.util.ClassUtils; */ public class ElasticsearchQueryMethod extends QueryMethod { + // the following 2 variables exits in the base class, but are private. We need them for + // correct handling of return types (SearchHits), so we have our own values here. + // Alas this means that we have to copy code that initializes these variables and in the + // base class uses them in order to use our variables + protected final Method method; + protected final Class unwrappedReturnType; + private Boolean unwrappedReturnTypeFromSearchHit = null; + private final MappingContext, ElasticsearchPersistentProperty> mappingContext; @Nullable private ElasticsearchEntityMetadata metadata; - protected final Method method; // private in base class, but needed here and in derived classes as well @Nullable private final Query queryAnnotation; @Nullable private final Highlight highlightAnnotation; private final Lazy highlightQueryLazy = Lazy.of(this::createAnnotatedHighlightQuery); @@ -83,6 +92,7 @@ public class ElasticsearchQueryMethod extends QueryMethod { this.queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); this.highlightAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Highlight.class); this.sourceFilters = AnnotatedElementUtils.findMergedAnnotation(method, SourceFilters.class); + this.unwrappedReturnType = potentiallyUnwrapReturnTypeFor(repositoryMetadata, method); verifyCountQueryTypes(); } @@ -188,6 +198,11 @@ public class ElasticsearchQueryMethod extends QueryMethod { * @since 4.0 */ public boolean isSearchHitMethod() { + + if (unwrappedReturnTypeFromSearchHit != null && unwrappedReturnTypeFromSearchHit) { + return true; + } + Class methodReturnType = method.getReturnType(); if (SearchHits.class.isAssignableFrom(methodReturnType)) { @@ -322,4 +337,32 @@ public class ElasticsearchQueryMethod extends QueryMethod { return fieldNames.toArray(new String[0]); } + + // region Copied from QueryMethod base class + /* + * Copied from the QueryMethod class adding support for collections of SearchHit instances. No static method here. + */ + private Class potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) { + TypeInformation returnType = metadata.getReturnType(method); + if (!QueryExecutionConverters.supports(returnType.getType()) + && !ReactiveWrapperConverters.supports(returnType.getType())) { + return returnType.getType(); + } else { + TypeInformation componentType = returnType.getComponentType(); + if (componentType == null) { + throw new IllegalStateException( + String.format("Couldn't find component type for return value of method %s", method)); + } else { + + if (SearchHit.class.isAssignableFrom(componentType.getType())) { + unwrappedReturnTypeFromSearchHit = true; + return componentType.getComponentType().getType(); + } else { + return componentType.getType(); + } + } + } + } + // endregion + } diff --git a/src/main/kotlin/org/springframework/data/elasticsearch/core/DocumentOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/elasticsearch/core/DocumentOperationsExtensions.kt new file mode 100644 index 000000000..c3c1a63b0 --- /dev/null +++ b/src/main/kotlin/org/springframework/data/elasticsearch/core/DocumentOperationsExtensions.kt @@ -0,0 +1,46 @@ +package org.springframework.data.elasticsearch.core + +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates +import org.springframework.data.elasticsearch.core.query.BulkOptions +import org.springframework.data.elasticsearch.core.query.ByQueryResponse +import org.springframework.data.elasticsearch.core.query.IndexQuery +import org.springframework.data.elasticsearch.core.query.Query +import org.springframework.data.elasticsearch.core.query.UpdateQuery + +/** + * Extension functions for [DocumentOperations] methods that take a Class parameter leveraging reified type parameters. + * @author Peter-Josef Meisch + * @since 5.2 + */ + +inline fun DocumentOperations.get(id: String): T? = + get(id, T::class.java) + +inline fun DocumentOperations.get(id: String, index: IndexCoordinates): T? = + get(id, T::class.java, index) + +inline fun DocumentOperations.multiGet(query: Query): List> = + multiGet(query, T::class.java) + +inline fun DocumentOperations.multiGet(query: Query, index: IndexCoordinates): List> = + multiGet(query, T::class.java, index) + +inline fun DocumentOperations.exists(id: String): Boolean = exists(id, T::class.java) + +inline fun DocumentOperations.bulkIndex(queries: List): List = + bulkIndex(queries, T::class.java) + +inline fun DocumentOperations.bulkIndex( + queries: List, + bulkOptions: BulkOptions +): List = + bulkIndex(queries, bulkOptions, T::class.java) + +inline fun DocumentOperations.bulkUpdate(queries: List) = + bulkUpdate(queries, T::class.java) + +inline fun DocumentOperations.delete(id: String): String = + delete(id, T::class.java) + +inline fun DocumentOperations.delete(query: Query): ByQueryResponse = + delete(query, T::class.java) diff --git a/src/main/kotlin/org/springframework/data/elasticsearch/core/ElasticsearchOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/elasticsearch/core/ElasticsearchOperationsExtensions.kt new file mode 100644 index 000000000..20ad9eb35 --- /dev/null +++ b/src/main/kotlin/org/springframework/data/elasticsearch/core/ElasticsearchOperationsExtensions.kt @@ -0,0 +1,15 @@ +package org.springframework.data.elasticsearch.core + +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates + +/** + * Extension functions for [DocumentOperations] methods that take a Class parameter leveraging reified type parameters. + * @author Peter-Josef Meisch + * @since 5.2 + */ + +inline fun ElasticsearchOperations.indexOps(): IndexOperations = + indexOps(T::class.java) + +inline fun ElasticsearchOperations.getIndexCoordinatesFor(): IndexCoordinates = + getIndexCoordinatesFor(T::class.java) diff --git a/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveDocumentOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveDocumentOperationsExtensions.kt new file mode 100644 index 000000000..0900e1a4e --- /dev/null +++ b/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveDocumentOperationsExtensions.kt @@ -0,0 +1,19 @@ +package org.springframework.data.elasticsearch.core + +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates +import org.springframework.data.elasticsearch.core.query.Query +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +/** + * Extension functions for [ReactiveDocumentOperations] methods that take a Class parameter leveraging reified type parameters. + * @author Peter-Josef Meisch + * @since 5.2 + */ + +inline fun ReactiveDocumentOperations.multiGet(query: Query): Flux> = multiGet(query, T::class.java) +inline fun ReactiveDocumentOperations.multiGet(query: Query, index: IndexCoordinates): Flux> = multiGet(query, T::class.java, index) + +inline fun ReactiveDocumentOperations.get(id: String): Mono = get(id, T::class.java) +inline fun ReactiveDocumentOperations.get(id: String, index: IndexCoordinates): Mono = get(id, T::class.java, index) +inline fun ReactiveDocumentOperations.exists(id: String): Mono = exists(id, T::class.java) diff --git a/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperationsExtensions.kt new file mode 100644 index 000000000..ea75056f1 --- /dev/null +++ b/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperationsExtensions.kt @@ -0,0 +1,15 @@ +package org.springframework.data.elasticsearch.core + +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates + +/** + * Extension functions for [ReacctiveElasticsearchOperations] methods that take a Class parameter leveraging reified type parameters. + * @author Peter-Josef Meisch + * @since 5.2 + */ + +inline fun ReactiveElasticsearchOperations.indexOps(): ReactiveIndexOperations = + indexOps(T::class.java) + +inline fun ReactiveElasticsearchOperations.getIndexCoordinatesFor(): IndexCoordinates = + getIndexCoordinatesFor(T::class.java) diff --git a/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveSearchOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveSearchOperationsExtensions.kt new file mode 100644 index 000000000..e4983c1d3 --- /dev/null +++ b/src/main/kotlin/org/springframework/data/elasticsearch/core/ReactiveSearchOperationsExtensions.kt @@ -0,0 +1,26 @@ +package org.springframework.data.elasticsearch.core + +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates +import org.springframework.data.elasticsearch.core.query.Query +import org.springframework.data.elasticsearch.core.suggest.response.Suggest +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +/** + * Extension functions for [SearchOperations] methods that take a Class parameter leveraging reified type parameters. + * @author Peter-Josef Meisch + * @since 5.2 + */ + +inline fun ReactiveSearchOperations.count(): Mono = count(T::class.java) +inline fun ReactiveSearchOperations.count(query: Query): Mono = count(query, T::class.java) +inline fun ReactiveSearchOperations.count(query: Query, index: IndexCoordinates): Mono = count(query, T::class.java, index) +inline fun ReactiveSearchOperations.search(query: Query): Flux> = search(query, T::class.java) +inline fun ReactiveSearchOperations.search(query: Query, index: IndexCoordinates): Flux> = search(query, T::class.java, index) +inline fun ReactiveSearchOperations.searchForPage(query: Query): Mono> = searchForPage(query, T::class.java) +inline fun ReactiveSearchOperations.searchForPage(query: Query, index: IndexCoordinates): Mono> = searchForPage(query, T::class.java, index) +inline fun ReactiveSearchOperations.searchForHits(query: Query): Mono> = searchForHits(query, T::class.java) +inline fun ReactiveSearchOperations.aggregate(query: Query): Flux> = aggregate(query, T::class.java) +inline fun ReactiveSearchOperations.aggregate(query: Query, index: IndexCoordinates): Flux> = aggregate(query, T::class.java, index) +inline fun ReactiveSearchOperations.suggest(query: Query): Mono = suggest(query, T::class.java) +inline fun ReactiveSearchOperations.suggest(query: Query, index: IndexCoordinates): Mono = suggest(query, T::class.java, index) diff --git a/src/main/kotlin/org/springframework/data/elasticsearch/core/SearchOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/elasticsearch/core/SearchOperationsExtensions.kt new file mode 100644 index 000000000..dd797e4cf --- /dev/null +++ b/src/main/kotlin/org/springframework/data/elasticsearch/core/SearchOperationsExtensions.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2023 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core + +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates +import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery +import org.springframework.data.elasticsearch.core.query.Query + +/** + * Extension functions for [SearchOperations] methods that take a Class parameter leveraging reified type parameters. + * @author Peter-Josef Meisch + * @since 5.2 + */ + +inline fun SearchOperations.count(query: Query): Long = count(query, T::class.java) + +inline fun SearchOperations.searchOne(query: Query): SearchHit? = searchOne(query, T::class.java) +inline fun SearchOperations.searchOne(query: Query, index: IndexCoordinates): SearchHit? = searchOne(query, T::class.java, index) +inline fun SearchOperations.multiSearch(queries: List): List> = + multiSearch(queries, T::class.java) + +inline fun SearchOperations.multiSearch(queries: List, index: IndexCoordinates): List> = + multiSearch(queries, T::class.java, index) + +inline fun SearchOperations.search(query: Query): SearchHits = + search(query, T::class.java) + +inline fun SearchOperations.search(query: Query, index: IndexCoordinates): SearchHits = + search(query, T::class.java, index) + +inline fun SearchOperations.search(query: MoreLikeThisQuery): SearchHits = + search(query, T::class.java) + +inline fun SearchOperations.search(query: MoreLikeThisQuery, index: IndexCoordinates): SearchHits = + search(query, T::class.java, index) + +inline fun SearchOperations.searchForStream(query: Query): SearchHitsIterator = + searchForStream(query, T::class.java) + +inline fun SearchOperations.searchForStream(query: Query, index: IndexCoordinates): SearchHitsIterator = + searchForStream(query, T::class.java, index) diff --git a/src/main/kotlin/org/springframework/data/elasticsearch/repository/CoroutineElasticsearchRepository.kt b/src/main/kotlin/org/springframework/data/elasticsearch/repository/CoroutineElasticsearchRepository.kt new file mode 100644 index 000000000..994eb49d5 --- /dev/null +++ b/src/main/kotlin/org/springframework/data/elasticsearch/repository/CoroutineElasticsearchRepository.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2023 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.repository + +import org.springframework.data.repository.NoRepositoryBean +import org.springframework.data.repository.kotlin.CoroutineCrudRepository +import org.springframework.data.repository.kotlin.CoroutineSortingRepository + +/** + * @author Peter-Josef Meisch + * @since 5.2 + */ +@NoRepositoryBean +interface CoroutineElasticsearchRepository : CoroutineCrudRepository, CoroutineSortingRepository diff --git a/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryELCIntegrationTests.kt b/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryELCIntegrationTests.kt new file mode 100644 index 000000000..57c5f6356 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryELCIntegrationTests.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2023 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.repository.query + +import org.springframework.context.annotation.* +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration +import org.springframework.data.elasticsearch.repository.CoroutineElasticsearchRepository +import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories +import org.springframework.data.elasticsearch.utils.IndexNameProvider +import org.springframework.test.context.ContextConfiguration + +/** + * @author Peter-Josef Meisch + * @since 5.2 + */ +@ContextConfiguration(classes = [CoroutineRepositoryELCIntegrationTests.Config::class]) +class CoroutineRepositoryELCIntegrationTests : CoroutineRepositoryIntegrationTests() { + + @Configuration + @Import(ReactiveElasticsearchTemplateConfiguration::class) + @EnableReactiveElasticsearchRepositories( + considerNestedRepositories = true, + includeFilters = [ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + classes = [CoroutineElasticsearchRepository::class] + )] + ) + open class Config { + @Bean + open fun indexNameProvider(): IndexNameProvider { + return IndexNameProvider("coroutine-repository") + } + } +} diff --git a/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryIntegrationTests.kt b/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryIntegrationTests.kt new file mode 100644 index 000000000..99ebe2a51 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/CoroutineRepositoryIntegrationTests.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2023 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.repository.query + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.annotation.Id +import org.springframework.data.elasticsearch.annotations.Document +import org.springframework.data.elasticsearch.annotations.Field +import org.springframework.data.elasticsearch.annotations.FieldType +import org.springframework.data.elasticsearch.core.SearchHit +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest +import org.springframework.data.elasticsearch.repository.CoroutineElasticsearchRepository +import org.springframework.data.elasticsearch.utils.IndexNameProvider + +/** + * Integrationtests for the different methods a [org.springframework.data.elasticsearch.repository.CoroutineElasticsearchRepository] can have. + * @author Peter-Josef Meisch + * @since 5.2 + */ +@Suppress("SpringJavaInjectionPointsAutowiringInspection") +@SpringIntegrationTest +abstract class CoroutineRepositoryIntegrationTests { + + @Autowired + lateinit var indexNameProvider: IndexNameProvider + + @Autowired + lateinit var repository: CoroutineEntityRepository + + val entities = listOf( + Entity("1", "test"), + Entity("2", "test"), + ) + + @BeforeEach + fun setUp() = runTest { + repository.saveAll(entities).last() + } + + @Test + fun `should instantiate repository`() = runTest { + assertThat(repository).isNotNull() + } + + @Test + fun `should run with method returning a list of entities`() = runTest { + + val result = repository.searchByText("test") + + assertThat(result).containsExactlyInAnyOrderElementsOf(entities) + } + + @Test + fun `should run with method returning a flow of entities`() = runTest { + + val result = repository.findByText("test").toList(mutableListOf()) + + assertThat(result).containsExactlyInAnyOrderElementsOf(entities) + } + + @Test + fun `should run with method returning a flow of SearchHit`() = runTest { + + val result = repository.queryByText("test").toList(mutableListOf()) + + assertThat(result.map { it.content }).containsExactlyInAnyOrderElementsOf(entities) + } + + @Document(indexName = "#{@indexNameProvider.indexName()}") + data class Entity( + @Id val id: String?, + @Field(type = FieldType.Text) val text: String?, + ) + + interface CoroutineEntityRepository : CoroutineElasticsearchRepository { + + suspend fun searchByText(text: String): List + suspend fun findByText(text: String): Flow + suspend fun queryByText(text: String): Flow> + } +} diff --git a/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodCoroutineUnitTests.kt b/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodCoroutineUnitTests.kt new file mode 100644 index 000000000..1b89d7c43 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/elasticsearch/repository/query/ReactiveElasticsearchQueryMethodCoroutineUnitTests.kt @@ -0,0 +1,62 @@ +package org.springframework.data.elasticsearch.repository.query + +import kotlinx.coroutines.flow.Flow +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.data.annotation.Id +import org.springframework.data.elasticsearch.annotations.Field +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext +import org.springframework.data.elasticsearch.repository.CoroutineElasticsearchRepository +import org.springframework.data.projection.SpelAwareProxyProjectionFactory +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata +import kotlin.coroutines.Continuation + +/** + * a@author Peter-Josef Meisch + * @since 5.2 + */ +class ReactiveElasticsearchQueryMethodCoroutineUnitTests { + + val projectionFactory = SpelAwareProxyProjectionFactory() + + interface PersonRepository : CoroutineElasticsearchRepository { + + suspend fun findSuspendAllByName(): Flow + + fun findAllByName(): Flow + + suspend fun findSuspendByName(): List + } + + @Test // #2545 + internal fun `should consider methods returning Flow as collection queries`() { + + val method = PersonRepository::class.java.getMethod("findAllByName") + val queryMethod = ReactiveElasticsearchQueryMethod(method, DefaultRepositoryMetadata(PersonRepository::class.java), projectionFactory, SimpleElasticsearchMappingContext()) + + assertThat(queryMethod.isCollectionQuery).isTrue() + } + + @Test // #2545 + internal fun `should consider suspended methods returning Flow as collection queries`() { + + val method = PersonRepository::class.java.getMethod("findSuspendAllByName", Continuation::class.java) + val queryMethod = ReactiveElasticsearchQueryMethod(method, DefaultRepositoryMetadata(PersonRepository::class.java), projectionFactory, SimpleElasticsearchMappingContext()) + + assertThat(queryMethod.isCollectionQuery).isTrue() + } + + @Test // #2545 + internal fun `should consider suspended methods returning List as collection queries`() { + + val method = PersonRepository::class.java.getMethod("findSuspendByName", Continuation::class.java) + val queryMethod = ReactiveElasticsearchQueryMethod(method, DefaultRepositoryMetadata(PersonRepository::class.java), projectionFactory, SimpleElasticsearchMappingContext()) + + assertThat(queryMethod.isCollectionQuery).isTrue() + } + + data class Person( + @Id val id: String?, + @Field val name: String? + ) +}