From 5a36f5e1e81f264e1798a7a14d29dcd7106a8ba1 Mon Sep 17 00:00:00 2001 From: ezequielantunez Date: Wed, 11 Jan 2023 21:50:19 +0100 Subject: [PATCH] Add Query by Example feature. Original Pull Request #2422 Closes #2418 --- .../client/elc/CriteriaQueryProcessor.java | 8 + .../client/erhlc/CriteriaQueryProcessor.java | 4 + .../elasticsearch/core/query/Criteria.java | 22 +- .../ElasticsearchRepositoryFactory.java | 28 +- ...eactiveElasticsearchRepositoryFactory.java | 17 + .../querybyexample/ExampleCriteriaMapper.java | 159 +++++ .../QueryByExampleElasticsearchExecutor.java | 104 +++ ...veQueryByExampleElasticsearchExecutor.java | 96 +++ .../elc/CriteriaQueryProcessorUnitTests.java | 27 + .../CriteriaQueryProcessorUnitTests.java | 26 + ...sticsearchExecutorELCIntegrationTests.java | 44 ++ ...icsearchExecutorERHLCIntegrationTests.java | 44 ++ ...ElasticsearchExecutorIntegrationTests.java | 638 +++++++++++++++++ ...sticsearchExecutorELCIntegrationTests.java | 44 ++ ...icsearchExecutorERHLCIntegrationTests.java | 44 ++ ...ElasticsearchExecutorIntegrationTests.java | 652 ++++++++++++++++++ 16 files changed, 1951 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorERHLCIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java index cb01bf783..5e01e2f81 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessor.java @@ -39,6 +39,7 @@ import org.springframework.util.Assert; * query. * * @author Peter-Josef Meisch + * @author Ezequiel Antúnez Camacho * @since 4.4 */ class CriteriaQueryProcessor { @@ -329,6 +330,13 @@ class CriteriaQueryProcessor { throw new CriteriaQueryException("value for " + fieldName + " is not an Iterable"); } break; + case REGEXP: + queryBuilder // + .regexp(rb -> rb // + .field(fieldName) // + .value(value.toString()) // + .boost(boost)); // + break; default: throw new CriteriaQueryException("Could not build query for " + entry); } diff --git a/src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java b/src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java index 5726f669c..bd001c267 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessor.java @@ -44,6 +44,7 @@ import org.springframework.util.Assert; * @author Rasmus Faber-Espensen * @author James Bodkin * @author Peter-Josef Meisch + * @author Ezequiel Antúnez Camacho * @deprecated since 5.0 */ @Deprecated @@ -248,6 +249,9 @@ class CriteriaQueryProcessor { } } break; + case REGEXP: + query = regexpQuery(fieldName, value.toString()); + break; } return query; } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java index 056135fab..0fab5c5e9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java @@ -50,6 +50,7 @@ import org.springframework.util.StringUtils; * @author Mohsin Husen * @author Franck Marchand * @author Peter-Josef Meisch + * @author Ezequiel Antúnez Camacho */ public class Criteria { @@ -611,6 +612,21 @@ public class Criteria { return this; } + /** + * Add a {@link OperationKey#REGEXP} entry to the {@link #queryCriteriaEntries}. + * + * @param value the regexp value to match + * @return this object + * @since 5.1 + */ + public Criteria regexp(String value) { + + Assert.notNull(value, "value must not be null"); + + queryCriteriaEntries.add(new CriteriaEntry(OperationKey.REGEXP, value)); + return this; + } + // endregion // region criteria entries - filter @@ -954,7 +970,11 @@ public class Criteria { /** * @since 4.3 */ - NOT_EMPTY; + NOT_EMPTY, // + /** + * @since 5.1 + */ + REGEXP; /** * @return true if this key does not have an associated value diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java index 2bf315afd..a8e56c053 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ElasticsearchRepositoryFactory.java @@ -15,22 +15,21 @@ */ package org.springframework.data.elasticsearch.repository.support; -import static org.springframework.data.querydsl.QuerydslUtils.*; - -import java.lang.reflect.Method; -import java.util.Optional; - import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.data.elasticsearch.repository.query.ElasticsearchPartQuery; import org.springframework.data.elasticsearch.repository.query.ElasticsearchQueryMethod; import org.springframework.data.elasticsearch.repository.query.ElasticsearchStringQuery; +import org.springframework.data.elasticsearch.repository.support.querybyexample.QueryByExampleElasticsearchExecutor; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; @@ -38,6 +37,11 @@ import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import java.lang.reflect.Method; +import java.util.Optional; + +import static org.springframework.data.querydsl.QuerydslUtils.QUERY_DSL_PRESENT; + /** * Factory to create {@link ElasticsearchRepository} * @@ -49,6 +53,7 @@ import org.springframework.util.Assert; * @author Christoph Strobl * @author Sascha Woo * @author Peter-Josef Meisch + * @author Ezequiel Antúnez Camacho */ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport { @@ -122,4 +127,17 @@ public class ElasticsearchRepositoryFactory extends RepositoryFactorySupport { protected RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) { return new ElasticsearchRepositoryMetadata(repositoryInterface); } + + @Override + protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { + RepositoryComposition.RepositoryFragments fragments = RepositoryComposition.RepositoryFragments.empty(); + + if (QueryByExampleExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { + fragments = fragments.append(RepositoryFragment.implemented(QueryByExampleExecutor.class, + instantiateClass(QueryByExampleElasticsearchExecutor.class, elasticsearchOperations))); + } + + return fragments; + } + } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java index 2f04de92a..226f2e2c9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/ReactiveElasticsearchRepositoryFactory.java @@ -25,15 +25,19 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersiste import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchQueryMethod; import org.springframework.data.elasticsearch.repository.query.ReactiveElasticsearchStringQuery; import org.springframework.data.elasticsearch.repository.query.ReactivePartTreeElasticsearchQuery; +import org.springframework.data.elasticsearch.repository.support.querybyexample.ReactiveQueryByExampleElasticsearchExecutor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; @@ -45,6 +49,7 @@ import org.springframework.util.Assert; * * @author Christoph Strobl * @author Ivan Greene + * @author Ezequiel Antúnez Camacho * @since 3.2 */ public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFactorySupport { @@ -168,4 +173,16 @@ public class ReactiveElasticsearchRepositoryFactory extends ReactiveRepositoryFa } } } + + @Override + protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { + RepositoryComposition.RepositoryFragments fragments = RepositoryComposition.RepositoryFragments.empty(); + + if (ReactiveQueryByExampleExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { + fragments = fragments.append(RepositoryFragment.implemented(ReactiveQueryByExampleExecutor.class, + instantiateClass(ReactiveQueryByExampleElasticsearchExecutor.class, operations))); + } + + return fragments; + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java new file mode 100644 index 000000000..45ad8d0ba --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ExampleCriteriaMapper.java @@ -0,0 +1,159 @@ +/* + * 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.support.querybyexample; + +import java.util.Map; +import java.util.Optional; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.support.ExampleMatcherAccessor; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Maps a {@link Example} to a {@link org.springframework.data.elasticsearch.core.query.Criteria} + * + * @author Ezequiel Antúnez Camacho + * @since 5.1 + */ +class ExampleCriteriaMapper { + + private final MappingContext, ElasticsearchPersistentProperty> mappingContext; + + /** + * Builds a {@link ExampleCriteriaMapper} + * + * @param mappingContext mappingContext to use + */ + ExampleCriteriaMapper( + MappingContext, ElasticsearchPersistentProperty> mappingContext) { + this.mappingContext = mappingContext; + } + + Criteria criteria(Example example) { + return buildCriteria(example); + } + + private Criteria buildCriteria(Example example) { + final ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(example.getMatcher()); + + return applyPropertySpecs(new Criteria(), "", example.getProbe(), + mappingContext.getRequiredPersistentEntity(example.getProbeType()), matcherAccessor, + example.getMatcher().getMatchMode()); + } + + private Criteria applyPropertySpecs(Criteria criteria, String path, @Nullable Object probe, + ElasticsearchPersistentEntity persistentEntity, ExampleMatcherAccessor exampleSpecAccessor, + ExampleMatcher.MatchMode matchMode) { + + if (probe == null) { + return criteria; + } + + PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(probe); + + for (ElasticsearchPersistentProperty property : persistentEntity) { + final String propertyName = property.getName(); + String propertyPath = StringUtils.hasText(path) ? (path + "." + propertyName) : propertyName; + if (exampleSpecAccessor.isIgnoredPath(propertyPath) || property.isCollectionLike() + || property.isVersionProperty()) { + continue; + } + + Object propertyValue = propertyAccessor.getProperty(property); + if (property.isMap() && propertyValue != null) { + for (Map.Entry entry : ((Map) propertyValue).entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + criteria = applyPropertySpec(propertyPath + "." + key, value, exampleSpecAccessor, property, matchMode, + criteria); + } + continue; + } + + criteria = applyPropertySpec(propertyPath, propertyValue, exampleSpecAccessor, property, matchMode, criteria); + } + return criteria; + } + + private Criteria applyPropertySpec(String path, Object propertyValue, ExampleMatcherAccessor exampleSpecAccessor, + ElasticsearchPersistentProperty property, ExampleMatcher.MatchMode matchMode, Criteria criteria) { + + if (exampleSpecAccessor.isIgnoreCaseForPath(path)) { + throw new InvalidDataAccessApiUsageException( + "Current implementation of Query-by-Example supports only case-sensitive matching."); + } + + final Object transformedValue = exampleSpecAccessor.getValueTransformerForPath(path) + .apply(Optional.ofNullable(propertyValue)).orElse(null); + + if (transformedValue == null) { + criteria = tryToAppendMustNotSentence(criteria, path, exampleSpecAccessor); + } else { + if (property.isEntity()) { + return applyPropertySpecs(criteria, path, transformedValue, + mappingContext.getRequiredPersistentEntity(property), exampleSpecAccessor, matchMode); + } else { + return applyStringMatcher(applyMatchMode(criteria, path, matchMode), transformedValue, + exampleSpecAccessor.getStringMatcherForPath(path)); + } + } + return criteria; + } + + private Criteria tryToAppendMustNotSentence(Criteria criteria, String path, + ExampleMatcherAccessor exampleSpecAccessor) { + if (ExampleMatcher.NullHandler.INCLUDE.equals(exampleSpecAccessor.getNullHandler()) + || exampleSpecAccessor.hasPropertySpecifier(path)) { + return criteria.and(path).not().exists(); + } + return criteria; + } + + private Criteria applyMatchMode(Criteria criteria, String path, ExampleMatcher.MatchMode matchMode) { + if (matchMode == ExampleMatcher.MatchMode.ALL) { + return criteria.and(path); + } else { + return criteria.or(path); + } + } + + private Criteria applyStringMatcher(Criteria criteria, Object value, ExampleMatcher.StringMatcher stringMatcher) { + return switch (stringMatcher) { + case DEFAULT -> criteria.is(value); + case EXACT -> criteria.matchesAll(value); + case STARTING -> criteria.startsWith(validateString(value)); + case ENDING -> criteria.endsWith(validateString(value)); + case CONTAINING -> criteria.contains(validateString(value)); + case REGEX -> criteria.regexp(validateString(value)); + }; + } + + private String validateString(Object value) { + if (value instanceof String) { + return value.toString(); + } + throw new IllegalArgumentException("This operation requires a String but got " + value.getClass()); + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java new file mode 100644 index 000000000..c64c8faf3 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutor.java @@ -0,0 +1,104 @@ +/* + * 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.support.querybyexample; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHitSupport; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.SearchPage; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.data.repository.query.QueryByExampleExecutor; + +/** + * @author Ezequiel Antúnez Camacho + * @since 5.1 + */ +public class QueryByExampleElasticsearchExecutor implements QueryByExampleExecutor { + + protected ElasticsearchOperations operations; + protected ExampleCriteriaMapper exampleCriteriaMapper; + + public QueryByExampleElasticsearchExecutor(ElasticsearchOperations operations) { + this.operations = operations; + this.exampleCriteriaMapper = new ExampleCriteriaMapper(operations.getElasticsearchConverter().getMappingContext()); + } + + @Override + public Optional findOne(Example example) { + CriteriaQuery criteriaQuery = CriteriaQuery.builder(exampleCriteriaMapper.criteria(example)).withMaxResults(2).build(); + SearchHits searchHits = operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType())); + if (searchHits.getTotalHits() > 1) { + throw new org.springframework.dao.IncorrectResultSizeDataAccessException(1); + } + return Optional.ofNullable(searchHits).filter(SearchHits::hasSearchHits) + .map(result -> (List) SearchHitSupport.unwrapSearchHits(result)).map(s -> s.get(0)); + } + + @Override + public Iterable findAll(Example example) { + CriteriaQuery criteriaQuery = new CriteriaQuery(exampleCriteriaMapper.criteria(example)); + SearchHits searchHits = operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType())); + return (List) SearchHitSupport.unwrapSearchHits(searchHits); + } + + @Override + public Iterable findAll(Example example, Sort sort) { + CriteriaQuery criteriaQuery = CriteriaQuery.builder(exampleCriteriaMapper.criteria(example)).withSort(sort).build(); + SearchHits searchHits = operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType())); + return (List) SearchHitSupport.unwrapSearchHits(searchHits); + } + + @Override + public Page findAll(Example example, Pageable pageable) { + CriteriaQuery criteriaQuery = CriteriaQuery.builder(exampleCriteriaMapper.criteria(example)).withPageable(pageable) + .build(); + SearchHits searchHits = operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType())); + SearchPage page = SearchHitSupport.searchPageFor(searchHits, criteriaQuery.getPageable()); + return (Page) SearchHitSupport.unwrapSearchHits(page); + + } + + @Override + public long count(Example example) { + final CriteriaQuery criteriaQuery = new CriteriaQuery(exampleCriteriaMapper.criteria(example)); + return operations.count(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType())); + } + + @Override + public boolean exists(Example example) { + return count(example) > 0L; + } + + @Override + public R findBy(Example example, Function, R> queryFunction) { + throw new UnsupportedOperationException("findBy example and queryFunction is not supported"); + } + +} diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java new file mode 100644 index 000000000..37eade7b3 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutor.java @@ -0,0 +1,96 @@ +/* + * 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.support.querybyexample; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; + +/** + * @author Ezequiel Antúnez Camacho + * @since 5.1 + */ +public class ReactiveQueryByExampleElasticsearchExecutor implements ReactiveQueryByExampleExecutor { + + protected ReactiveElasticsearchOperations operations; + protected ExampleCriteriaMapper exampleCriteriaMapper; + + public ReactiveQueryByExampleElasticsearchExecutor(ReactiveElasticsearchOperations operations) { + this.operations = operations; + this.exampleCriteriaMapper = new ExampleCriteriaMapper(operations.getElasticsearchConverter().getMappingContext()); + } + + @Override + public Mono findOne(Example example) { + return Mono.just(example) + .map(e -> CriteriaQuery.builder(exampleCriteriaMapper.criteria(e)).withMaxResults(2).build()) + .flatMapMany(criteriaQuery -> operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType()))) + .buffer(2).map(searchHitList -> { + if (searchHitList.size() > 1) { + throw new IncorrectResultSizeDataAccessException(1); + } + return searchHitList.iterator().next(); + }).map(SearchHit::getContent).next(); + } + + @Override + public Flux findAll(Example example) { + return Mono.just(example).map(e -> new CriteriaQuery(exampleCriteriaMapper.criteria(e))) + .flatMapMany(criteriaQuery -> operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType()))) + .map(SearchHit::getContent); + } + + @Override + public Flux findAll(Example example, Sort sort) { + return Mono.just(example).map(e -> CriteriaQuery.builder(exampleCriteriaMapper.criteria(e)).withSort(sort).build()) + .flatMapMany(criteriaQuery -> operations.search(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType()))) + .map(SearchHit::getContent); + } + + @Override + public Mono count(Example example) { + return Mono.just(example).map(e -> new CriteriaQuery(exampleCriteriaMapper.criteria(e))) + .flatMap(criteriaQuery -> operations.count(criteriaQuery, example.getProbeType(), + operations.getIndexCoordinatesFor(example.getProbeType()))); + } + + @Override + public Mono exists(Example example) { + return count(example).map(count -> count > 0); + + } + + @Override + public > P findBy(Example example, + Function, P> queryFunction) { + throw new UnsupportedOperationException("findBy example and queryFunction is not supported"); + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java index a40f8a807..7307fdb09 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/CriteriaQueryProcessorUnitTests.java @@ -28,6 +28,7 @@ import org.springframework.data.elasticsearch.core.query.Criteria; /** * @author Peter-Josef Meisch + * @author Ezequiel Antúnez Camacho */ @SuppressWarnings("ConstantConditions") class CriteriaQueryProcessorUnitTests { @@ -456,4 +457,30 @@ class CriteriaQueryProcessorUnitTests { assertEquals(expected, queryString, false); } + + @Test // #2418 + void shouldBuildRegexpQuery() throws JSONException { + String expected = """ + { + "bool": { + "must": [ + { + "regexp": { + "field1": { + "value": "[^abc]" + } + } + } + ] + } + } + """; + + Criteria criteria = new Criteria("field1").regexp("[^abc]"); + + var queryString = queryToJson(CriteriaQueryProcessor.createQuery(criteria), mapper); + + assertEquals(expected, queryString, false); + } + } diff --git a/src/test/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessorUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessorUnitTests.java index 82f36a427..114387cf0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessorUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/erhlc/CriteriaQueryProcessorUnitTests.java @@ -24,6 +24,7 @@ import org.springframework.data.elasticsearch.core.query.Criteria; /** * @author Peter-Josef Meisch + * @author Ezequiel Antúnez Camacho */ @SuppressWarnings("ConstantConditions") class CriteriaQueryProcessorUnitTests { @@ -447,4 +448,29 @@ class CriteriaQueryProcessorUnitTests { assertEquals(expected, query, false); } + + @Test // #2418 + void shouldBuildRegexpQuery() throws JSONException { + String expected = """ + { + "bool": { + "must": [ + { + "regexp": { + "field1": { + "value": "[^abc]" + } + } + } + ] + } + } + """; + + Criteria criteria = new Criteria("field1").regexp("[^abc]"); + + String queryString = queryProcessor.createQuery(criteria).toString(); + + assertEquals(expected, queryString, false); + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java new file mode 100644 index 000000000..e9358052d --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorELCIntegrationTests.java @@ -0,0 +1,44 @@ +/* + * 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.support.querybyexample; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Ezequiel Antúnez Camacho + * @since 5.1 + */ +@ContextConfiguration(classes = { QueryByExampleElasticsearchExecutorELCIntegrationTests.Config.class }) +public class QueryByExampleElasticsearchExecutorELCIntegrationTests + extends QueryByExampleElasticsearchExecutorIntegrationTests { + + @Configuration + @Import({ ElasticsearchTemplateConfiguration.class }) + @EnableElasticsearchRepositories(basePackages = { "org.springframework.data.elasticsearch.repository.support" }, + considerNestedRepositories = true) + static class Config { + @Bean + IndexNameProvider indexNameProvider() { + return new IndexNameProvider("query-by-example-repository"); + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorERHLCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorERHLCIntegrationTests.java new file mode 100644 index 000000000..4001d1347 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorERHLCIntegrationTests.java @@ -0,0 +1,44 @@ +/* + * 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.support.querybyexample; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Ezequiel Antúnez Camacho + * @since 5.1 + */ +@ContextConfiguration(classes = { QueryByExampleElasticsearchExecutorERHLCIntegrationTests.Config.class }) +public class QueryByExampleElasticsearchExecutorERHLCIntegrationTests + extends QueryByExampleElasticsearchExecutorIntegrationTests { + + @Configuration + @Import({ ElasticsearchRestTemplateConfiguration.class }) + @EnableElasticsearchRepositories(basePackages = { "org.springframework.data.elasticsearch.repository.support" }, + considerNestedRepositories = true) + static class Config { + @Bean + IndexNameProvider indexNameProvider() { + return new IndexNameProvider("query-by-example-repository-es7"); + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java new file mode 100644 index 000000000..c6fb55373 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/QueryByExampleElasticsearchExecutorIntegrationTests.java @@ -0,0 +1,638 @@ +/* + * 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.support.querybyexample; + +import org.assertj.core.api.AbstractThrowableAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +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.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.data.repository.query.QueryByExampleExecutor; +import org.springframework.lang.Nullable; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.data.elasticsearch.utils.IdGenerator.nextIdAsString; + +/** + * @author Ezequiel Antúnez Camacho + * @since 5.1 + */ +@SpringIntegrationTest +abstract class QueryByExampleElasticsearchExecutorIntegrationTests { + + @Autowired private SampleElasticsearchRepository repository; + @Autowired private ElasticsearchOperations operations; + @Autowired private IndexNameProvider indexNameProvider; + + @BeforeEach + void before() { + indexNameProvider.increment(); + operations.indexOps(SampleEntity.class).createWithMapping(); + } + + @Test // #2418 + @org.junit.jupiter.api.Order(Integer.MAX_VALUE) + void cleanup() { + operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); + } + + @Nested + @DisplayName("All QueryByExampleExecutor operations should work") + class QueryByExampleExecutorOperations { + @Test // #2418 + void shouldFindOne() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setDocumentId(documentId2); + Optional entityFromElasticSearch = repository.findOne(Example.of(probe)); + + // then + assertThat(entityFromElasticSearch).contains(sampleEntity2); + + } + + @Test // #2418 + void shouldThrowExceptionIfMoreThanOneResultInFindOne() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("some message"); + final Example example = Example.of(probe); + AbstractThrowableAssert assertThatThrownBy = assertThatThrownBy( + () -> repository.findOne(example)); + + // then + assertThatThrownBy.isInstanceOf(IncorrectResultSizeDataAccessException.class); + + } + + @Test // #2418 + void shouldFindOneWithNestedField() { + // given + SampleEntity.SampleNestedEntity sampleNestedEntity = new SampleEntity.SampleNestedEntity(); + sampleNestedEntity.setNestedData("sampleNestedData"); + sampleNestedEntity.setAnotherNestedData("sampleAnotherNestedData"); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("some message"); + sampleEntity.setSampleNestedEntity(sampleNestedEntity); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity.SampleNestedEntity sampleNestedEntity2 = new SampleEntity.SampleNestedEntity(); + sampleNestedEntity2.setNestedData("sampleNestedData2"); + sampleNestedEntity2.setAnotherNestedData("sampleAnotherNestedData2"); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("some message"); + sampleEntity2.setSampleNestedEntity(sampleNestedEntity2); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)); + + // when + SampleEntity.SampleNestedEntity sampleNestedEntityProbe = new SampleEntity.SampleNestedEntity(); + sampleNestedEntityProbe.setNestedData("sampleNestedData"); + SampleEntity probe = new SampleEntity(); + probe.setSampleNestedEntity(sampleNestedEntityProbe); + + Optional entityFromElasticSearch = repository.findOne(Example.of(probe)); + + // then + assertThat(entityFromElasticSearch).contains(sampleEntity); + + } + + @Test // #2418 + void shouldFindAll() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("hello world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("bye world"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("hello world"); + Iterable sampleEntities = repository.findAll(Example.of(probe)); + + // then + assertThat(sampleEntities).isNotNull().hasSize(2); + } + + @Test // #2418 + void shouldFindAllWithSort() { + // given + SampleEntity sampleEntityWithRate11 = new SampleEntity(); + sampleEntityWithRate11.setDocumentId(nextIdAsString()); + sampleEntityWithRate11.setMessage("hello world"); + sampleEntityWithRate11.setRate(11); + sampleEntityWithRate11.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntityWithRate13 = new SampleEntity(); + sampleEntityWithRate13.setDocumentId(nextIdAsString()); + sampleEntityWithRate13.setMessage("hello world"); + sampleEntityWithRate13.setRate(13); + sampleEntityWithRate13.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntityWithRate22 = new SampleEntity(); + sampleEntityWithRate22.setDocumentId(nextIdAsString()); + sampleEntityWithRate22.setMessage("hello world"); + sampleEntityWithRate22.setRate(22); + sampleEntityWithRate22.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntityWithRate11, sampleEntityWithRate13, sampleEntityWithRate22)); + + // when + SampleEntity probe = new SampleEntity(); + final Iterable all = repository.findAll(); + Iterable sampleEntities = repository.findAll(Example.of(probe), + Sort.by(Sort.Direction.DESC, "rate")); + + // then + assertThat(sampleEntities).isNotNull().hasSize(3).containsExactly(sampleEntityWithRate22, sampleEntityWithRate13, + sampleEntityWithRate11); + } + + @Test // #2418 + void shouldFindAllWithPageable() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setRate(1); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("hello world"); + sampleEntity2.setRate(3); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hello world"); + sampleEntity3.setRate(2); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + Iterable page1 = repository.findAll(Example.of(probe), + PageRequest.of(0, 2, Sort.Direction.DESC, "rate")); + Iterable page2 = repository.findAll(Example.of(probe), + PageRequest.of(1, 2, Sort.Direction.DESC, "rate")); + + // then + assertThat(page1).isNotNull().hasSize(2).containsExactly(sampleEntity2, sampleEntity3); + assertThat(page2).isNotNull().hasSize(1).containsExactly(sampleEntity); + } + + @Test // #2418 + void shouldCount() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setDocumentId(documentId2); + final long count = repository.count(Example.of(probe)); + + // then + assertThat(count).isPositive(); + } + + @Test // #2418 + void shouldExists() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setDocumentId(documentId2); + boolean exists = repository.exists(Example.of(probe)); + + // then + assertThat(exists).isTrue(); + } + + } + + @Nested + @DisplayName("All ExampleMatchers should work") + class AllExampleMatchersShouldWork { + + @Test // #2418 + void defaultStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("hello world"); + Iterable sampleEntities = repository.findAll(Example.of(probe, ExampleMatcher.matching() + .withMatcher("message", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.DEFAULT)))); + + // then + assertThat(sampleEntities).isNotNull().hasSize(1); + } + + @Test // #2418 + void exactStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("bye world"); + Iterable sampleEntities = repository.findAll(Example.of(probe, ExampleMatcher.matching() + .withMatcher("message", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.EXACT)))); + + // then + assertThat(sampleEntities).isNotNull().hasSize(1); + } + + @Test // #2418 + void startingStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("h"); + Iterable sampleEntities = repository.findAll(Example.of(probe, ExampleMatcher.matching() + .withMatcher("message", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.STARTING)))); + + // then + assertThat(sampleEntities).isNotNull().hasSize(2); + } + + @Test // #2418 + void endingStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("world"); + Iterable sampleEntities = repository.findAll(Example.of(probe, ExampleMatcher.matching() + .withMatcher("message", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.ENDING)))); + + // then + assertThat(sampleEntities).isNotNull().hasSize(2); + } + + @Test // #2418 + void regexStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("[(hello)(hola)].*"); + Iterable sampleEntities = repository.findAll(Example.of(probe, ExampleMatcher.matching() + .withMatcher("message", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.REGEX)))); + + // then + assertThat(sampleEntities).isNotNull().hasSize(2); + } + + } + + @Document(indexName = "#{@indexNameProvider.indexName()}") + static class SampleEntity { + @Nullable + @Id private String documentId; + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String type; + @Nullable + @Field(type = FieldType.Keyword, store = true) private String message; + @Nullable private Integer rate; + @Nullable private Boolean available; + @Nullable + @Field(type = FieldType.Nested, store = true, fielddata = true) private SampleNestedEntity sampleNestedEntity; + @Nullable + @Version private Long version; + + @Nullable + public String getDocumentId() { + return documentId; + } + + public void setDocumentId(@Nullable String documentId) { + this.documentId = documentId; + } + + @Nullable + public String getType() { + return type; + } + + public void setType(@Nullable String type) { + this.type = type; + } + + @Nullable + public String getMessage() { + return message; + } + + public void setMessage(@Nullable String message) { + this.message = message; + } + + @Nullable + public Integer getRate() { + return rate; + } + + public void setRate(Integer rate) { + this.rate = rate; + } + + @Nullable + public Boolean isAvailable() { + return available; + } + + public void setAvailable(Boolean available) { + this.available = available; + } + + @Nullable + public SampleNestedEntity getSampleNestedEntity() { + return sampleNestedEntity; + } + + public void setSampleNestedEntity(SampleNestedEntity sampleNestedEntity) { + this.sampleNestedEntity = sampleNestedEntity; + } + + @Nullable + public java.lang.Long getVersion() { + return version; + } + + public void setVersion(@Nullable java.lang.Long version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SampleEntity that = (SampleEntity) o; + + if (!Objects.equals(rate, that.rate)) + return false; + if (available != that.available) + return false; + if (!Objects.equals(documentId, that.documentId)) + return false; + if (!Objects.equals(type, that.type)) + return false; + if (!Objects.equals(message, that.message)) + return false; + if (!Objects.equals(sampleNestedEntity, that.sampleNestedEntity)) + return false; + return Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + int result = documentId != null ? documentId.hashCode() : 0; + result = 31 * result + (type != null ? type.hashCode() : 0); + result = 31 * result + (message != null ? message.hashCode() : 0); + result = 31 * result + (rate != null ? rate.hashCode() : 0); + result = 31 * result + (available != null ? available.hashCode() : 0); + result = 31 * result + (sampleNestedEntity != null ? sampleNestedEntity.hashCode() : 0); + result = 31 * result + (version != null ? version.hashCode() : 0); + return result; + } + + static class SampleNestedEntity { + + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String nestedData; + + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String anotherNestedData; + + @Nullable + public String getNestedData() { + return nestedData; + } + + public void setNestedData(@Nullable String nestedData) { + this.nestedData = nestedData; + } + + @Nullable + public String getAnotherNestedData() { + return anotherNestedData; + } + + public void setAnotherNestedData(@Nullable String anotherNestedData) { + this.anotherNestedData = anotherNestedData; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SampleNestedEntity that = (SampleNestedEntity) o; + + return Objects.equals(nestedData, that.nestedData) && Objects.equals(anotherNestedData, that.anotherNestedData); + } + + @Override + public int hashCode() { + int result = nestedData != null ? nestedData.hashCode() : 0; + result = 31 * result + (anotherNestedData != null ? anotherNestedData.hashCode() : 0); + return result; + } + } + } + + interface SampleElasticsearchRepository + extends ElasticsearchRepository, QueryByExampleExecutor {} + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java new file mode 100644 index 000000000..68227cca4 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.java @@ -0,0 +1,44 @@ +/* + * 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.support.querybyexample; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Ezequiel Antúnez Camacho + * @since 5.1 + */ +@ContextConfiguration(classes = { ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests.Config.class }) +public class ReactiveQueryByExampleElasticsearchExecutorELCIntegrationTests + extends ReactiveQueryByExampleElasticsearchExecutorIntegrationTests { + + @Configuration + @Import({ ReactiveElasticsearchTemplateConfiguration.class }) + @EnableReactiveElasticsearchRepositories(considerNestedRepositories = true) + static class Config { + @Bean + IndexNameProvider indexNameProvider() { + return new IndexNameProvider("reactive-query-by-example-repository"); + } + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.java new file mode 100644 index 000000000..96b904a63 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.java @@ -0,0 +1,44 @@ +/* + * 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.support.querybyexample; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Ezequiel Antúnez Camacho + * @since 5.1 + */ +@ContextConfiguration(classes = { ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests.Config.class }) +public class ReactiveQueryByExampleElasticsearchExecutorERHLCIntegrationTests + extends ReactiveQueryByExampleElasticsearchExecutorIntegrationTests { + + @Configuration + @Import({ ReactiveElasticsearchRestTemplateConfiguration.class }) + @EnableReactiveElasticsearchRepositories(considerNestedRepositories = true) + static class Config { + @Bean + IndexNameProvider indexNameProvider() { + return new IndexNameProvider("reactive-query-by-example-repository-es7"); + } + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java new file mode 100644 index 000000000..ff3f33ce9 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/repository/support/querybyexample/ReactiveQueryByExampleElasticsearchExecutorIntegrationTests.java @@ -0,0 +1,652 @@ +/* + * 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.support.querybyexample; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.Sort; +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.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.data.elasticsearch.repository.ReactiveElasticsearchRepository; +import org.springframework.data.elasticsearch.utils.IndexNameProvider; +import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; +import org.springframework.lang.Nullable; +import reactor.test.StepVerifier; + +import java.util.List; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.elasticsearch.utils.IdGenerator.nextIdAsString; + +/** + * @author Ezequiel Antúnez Camacho + * @since 5.1 + */ +@SpringIntegrationTest +abstract class ReactiveQueryByExampleElasticsearchExecutorIntegrationTests { + + @Autowired private SampleReactiveElasticsearchRepository repository; + @Autowired private ReactiveElasticsearchOperations operations; + @Autowired private IndexNameProvider indexNameProvider; + + @BeforeEach + void before() { + indexNameProvider.increment(); + operations.indexOps(SampleEntity.class).createWithMapping() + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + } + + @Test // #2418 + @org.junit.jupiter.api.Order(Integer.MAX_VALUE) + void cleanup() { + operations.indexOps(IndexCoordinates.of(indexNameProvider.getPrefix() + "*")).delete(); + } + + @Nested + @DisplayName("All QueryByExampleExecutor operations should work") + class ReactiveQueryByExampleExecutorOperations { + @Test + void shouldFindOne() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)) // + .as(StepVerifier::create) // + .expectNextCount(2L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setDocumentId(documentId2); + repository.findOne(Example.of(probe)) + // then + .as(StepVerifier::create) // + .consumeNextWith(entityFromElasticSearch -> assertThat(entityFromElasticSearch).isEqualTo(sampleEntity2)) // + .verifyComplete(); + } + + @Test + void shouldThrowExceptionIfMoreThanOneResultInFindOne() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)) // + .as(StepVerifier::create) // + .expectNextCount(2L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + repository.findOne(Example.of(probe)) + // then + .as(StepVerifier::create) // + .expectError(IncorrectResultSizeDataAccessException.class) // + .verify(); + } + + @Test + void shouldFindOneWithNestedField() { + // given + SampleEntity.SampleNestedEntity sampleNestedEntity = new SampleEntity.SampleNestedEntity(); + sampleNestedEntity.setNestedData("sampleNestedData"); + sampleNestedEntity.setAnotherNestedData("sampleAnotherNestedData"); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("some message"); + sampleEntity.setSampleNestedEntity(sampleNestedEntity); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity.SampleNestedEntity sampleNestedEntity2 = new SampleEntity.SampleNestedEntity(); + sampleNestedEntity2.setNestedData("sampleNestedData2"); + sampleNestedEntity2.setAnotherNestedData("sampleAnotherNestedData2"); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("some message"); + sampleEntity2.setSampleNestedEntity(sampleNestedEntity2); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)) // + .as(StepVerifier::create) // + .expectNextCount(2L) // + .verifyComplete(); + + // when + SampleEntity.SampleNestedEntity sampleNestedEntityProbe = new SampleEntity.SampleNestedEntity(); + sampleNestedEntityProbe.setNestedData("sampleNestedData"); + SampleEntity probe = new SampleEntity(); + probe.setSampleNestedEntity(sampleNestedEntityProbe); + repository.findOne(Example.of(probe)) + // then + .as(StepVerifier::create) // + .expectNext(sampleEntity) // + .verifyComplete(); + } + + @Test + void shouldFindAll() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("hello world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("bye world"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("hello world"); + repository.findAll(Example.of(probe)) + // then + .as(StepVerifier::create) // + .expectNextSequence(List.of(sampleEntity, sampleEntity2)) // + .verifyComplete(); + } + + @Test + void shouldFindAllWithSort() { + // given + SampleEntity sampleEntityWithRate11 = new SampleEntity(); + sampleEntityWithRate11.setDocumentId(nextIdAsString()); + sampleEntityWithRate11.setMessage("hello world"); + sampleEntityWithRate11.setRate(11); + sampleEntityWithRate11.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntityWithRate13 = new SampleEntity(); + sampleEntityWithRate13.setDocumentId(nextIdAsString()); + sampleEntityWithRate13.setMessage("hello world"); + sampleEntityWithRate13.setRate(13); + sampleEntityWithRate13.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntityWithRate22 = new SampleEntity(); + sampleEntityWithRate22.setDocumentId(nextIdAsString()); + sampleEntityWithRate22.setMessage("hello world"); + sampleEntityWithRate22.setRate(22); + sampleEntityWithRate22.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntityWithRate11, sampleEntityWithRate13, sampleEntityWithRate22)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + repository.findAll(Example.of(probe), Sort.by(Sort.Direction.DESC, "rate")) + // then + .as(StepVerifier::create) // + .expectNextSequence(List.of(sampleEntityWithRate22, sampleEntityWithRate13, sampleEntityWithRate11)) // + .verifyComplete(); + } + + @Test + void shouldCount() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)) // + .as(StepVerifier::create) // + .expectNextCount(2L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setDocumentId(documentId2); + repository.count(Example.of(probe)) + // then + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + @Test + void shouldExists() { + // given + String documentId = nextIdAsString(); + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(documentId); + sampleEntity.setMessage("some message"); + sampleEntity.setVersion(System.currentTimeMillis()); + + String documentId2 = nextIdAsString(); + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(documentId2); + sampleEntity2.setMessage("some message"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2)) // + .as(StepVerifier::create) // + .expectNextCount(2L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setDocumentId(documentId2); + repository.exists(Example.of(probe)) + // then + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + } + + } + + @Nested + @DisplayName("All ExampleMatchers should work") + class AllExampleMatchersShouldWork { + + @Test // #2418 + void defaultStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("hello world"); + repository + .findAll(Example.of(probe, + ExampleMatcher.matching().withMatcher("message", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.DEFAULT)))) + // then + .as(StepVerifier::create) // + .expectNext(sampleEntity) // + .verifyComplete(); + } + + @Test // #2418 + void exactStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("bye world"); + repository + .findAll(Example.of(probe, + ExampleMatcher.matching().withMatcher("message", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.EXACT)))) + // then + .as(StepVerifier::create) // + .expectNext(sampleEntity2) // + .verifyComplete(); + } + + @Test // #2418 + void startingStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("h"); + repository + .findAll(Example.of(probe, + ExampleMatcher.matching().withMatcher("message", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.STARTING)))) + // then + .as(StepVerifier::create) // + .expectNextSequence(List.of(sampleEntity, sampleEntity3)) // + .verifyComplete(); + } + + @Test // #2418 + void endingStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("world"); + repository + .findAll(Example.of(probe, + ExampleMatcher.matching().withMatcher("message", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.ENDING)))) + // then + .as(StepVerifier::create) // + .expectNextSequence(List.of(sampleEntity, sampleEntity2)) // + .verifyComplete(); + } + + @Test // #2418 + void regexStringMatcherShouldWork() { + // given + SampleEntity sampleEntity = new SampleEntity(); + sampleEntity.setDocumentId(nextIdAsString()); + sampleEntity.setMessage("hello world"); + sampleEntity.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity2 = new SampleEntity(); + sampleEntity2.setDocumentId(nextIdAsString()); + sampleEntity2.setMessage("bye world"); + sampleEntity2.setVersion(System.currentTimeMillis()); + + SampleEntity sampleEntity3 = new SampleEntity(); + sampleEntity3.setDocumentId(nextIdAsString()); + sampleEntity3.setMessage("hola mundo"); + sampleEntity3.setVersion(System.currentTimeMillis()); + + repository.saveAll(List.of(sampleEntity, sampleEntity2, sampleEntity3)) // + .as(StepVerifier::create) // + .expectNextCount(3L) // + .verifyComplete(); + + // when + SampleEntity probe = new SampleEntity(); + probe.setMessage("[(hello)(hola)].*"); + repository + .findAll(Example.of(probe, + ExampleMatcher.matching().withMatcher("message", + ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.REGEX)))) + // then + .as(StepVerifier::create) // + .expectNextSequence(List.of(sampleEntity, sampleEntity3)) // + .verifyComplete(); + } + + } + + @Document(indexName = "#{@indexNameProvider.indexName()}") + static class SampleEntity { + @Nullable + @Id private String documentId; + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String type; + @Nullable + @Field(type = FieldType.Keyword, store = true) private String message; + @Nullable private Integer rate; + @Nullable private Boolean available; + @Nullable + @Field(type = FieldType.Nested, store = true, + fielddata = true) private SampleEntity.SampleNestedEntity sampleNestedEntity; + @Nullable + @Version private Long version; + + @Nullable + public String getDocumentId() { + return documentId; + } + + public void setDocumentId(@Nullable String documentId) { + this.documentId = documentId; + } + + @Nullable + public String getType() { + return type; + } + + public void setType(@Nullable String type) { + this.type = type; + } + + @Nullable + public String getMessage() { + return message; + } + + public void setMessage(@Nullable String message) { + this.message = message; + } + + @Nullable + public Integer getRate() { + return rate; + } + + public void setRate(Integer rate) { + this.rate = rate; + } + + @Nullable + public Boolean isAvailable() { + return available; + } + + public void setAvailable(Boolean available) { + this.available = available; + } + + @Nullable + public SampleEntity.SampleNestedEntity getSampleNestedEntity() { + return sampleNestedEntity; + } + + public void setSampleNestedEntity(SampleEntity.SampleNestedEntity sampleNestedEntity) { + this.sampleNestedEntity = sampleNestedEntity; + } + + @Nullable + public java.lang.Long getVersion() { + return version; + } + + public void setVersion(@Nullable java.lang.Long version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SampleEntity that = (SampleEntity) o; + + if (!Objects.equals(rate, that.rate)) + return false; + if (available != that.available) + return false; + if (!Objects.equals(documentId, that.documentId)) + return false; + if (!Objects.equals(type, that.type)) + return false; + if (!Objects.equals(message, that.message)) + return false; + if (!Objects.equals(sampleNestedEntity, that.sampleNestedEntity)) + return false; + return Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + int result = documentId != null ? documentId.hashCode() : 0; + result = 31 * result + (type != null ? type.hashCode() : 0); + result = 31 * result + (message != null ? message.hashCode() : 0); + result = 31 * result + (rate != null ? rate.hashCode() : 0); + result = 31 * result + (available != null ? available.hashCode() : 0); + result = 31 * result + (sampleNestedEntity != null ? sampleNestedEntity.hashCode() : 0); + result = 31 * result + (version != null ? version.hashCode() : 0); + return result; + } + + static class SampleNestedEntity { + + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String nestedData; + + @Nullable + @Field(type = FieldType.Text, store = true, fielddata = true) private String anotherNestedData; + + @Nullable + public String getNestedData() { + return nestedData; + } + + public void setNestedData(@Nullable String nestedData) { + this.nestedData = nestedData; + } + + @Nullable + public String getAnotherNestedData() { + return anotherNestedData; + } + + public void setAnotherNestedData(@Nullable String anotherNestedData) { + this.anotherNestedData = anotherNestedData; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SampleEntity.SampleNestedEntity that = (SampleEntity.SampleNestedEntity) o; + + return Objects.equals(nestedData, that.nestedData) && Objects.equals(anotherNestedData, that.anotherNestedData); + } + + @Override + public int hashCode() { + int result = nestedData != null ? nestedData.hashCode() : 0; + result = 31 * result + (anotherNestedData != null ? anotherNestedData.hashCode() : 0); + return result; + } + } + } + + interface SampleReactiveElasticsearchRepository + extends ReactiveElasticsearchRepository, ReactiveQueryByExampleExecutor {} + +}