From 9a0d17242d96d2f04197dde4588b5cea0befecc9 Mon Sep 17 00:00:00 2001 From: mikereiche Date: Wed, 6 Jan 2021 15:45:51 -0800 Subject: [PATCH] DATACOUCH-588 - Part 2 of framework changes. Add support for projection and distinct. Support for projection is only for properties of the top-level entity. For instance, in UserSubmission, only the properties below can be specified in the projection. Projection support does not provide means of specifying something like address.street - you can only project (or not project) the whole address property. However, the address type in your resultType could have a subset of the properties in Address. If the corresponding submissions in the resultType contained only the userId property public class UserSubmission extends ComparableEntity { private String id; private String username; private List roles; private Address address; private List submissions; Support for Distinct - I have appropriated the MongoDB model for Distinct. It defines a separate DistinctOperationSupport class (within ExecutableFindByQuerySupport) which supports the distinct( distinctFields ) api and execution. The DistinctOperationSupport class has only a distinctFields member, and a 'delegate' member, which is an ExecutableFindByQuerySupport object. TBH, I don't see the advantage over simply adding a distinctFields member to ExecutableFindByQuerySupport Amend #1 - changes as discussed in Pull Request - clean up test entity types Amend #2 - Eliminate DistinctOperationSupport class. In MongoDB, only distinct on a single field is supported, so the returnType from distinct was very different from the returnType of other query operations (all(), one() etc. (but so is count(), and it doesn't need it's own class)). In Couchbase, distinct on any fields in the entity is allowed - so the returned type could be the domainType or resultType. And as(resultType) still allows any resultType to be specified. This makes it unnecessary to have combinations of interfaces such as DistinctWithProjection and DistinctWithQuery. - Clean up the interfaces in ExecutableFindByQuery. There are two types of interfaces (a) the TerminatingFindByQuery which has the one(), oneValue() first(), firstValue(), all(), count(), exists() and stream(); and (b) the option interfaces (FindByQueryWithConsistency etc), which are essentially with-er interfaces. The changes are: 1) make all the with-er interfaces base interfaces instead of chaining them together. (I don't know why there isn't simply one interface with all the with-er methods). 2) make the ExecutableFindByQuery interface extend the Terminating interface and all the with-er interfaces. Amend #3 - Add execution support for collections Amend #4 - Add tests for collections. This includes a new CollectionAwareIntegrationTests class which extends a new JavaIntegratationTests class which extends the existing ClusterAwareIntegrationTests. - Fixed up several issues collections issues that were uncovered by the tests. - Did further cleanup of OperationSupport interfaces. Amend #5 - Revert changes to interfaces in *Operation - Sorted interfaces in same order for consistency (because of the chaining of interfaces, fluent methods must be called in order). --- pom.xml | 8 + .../core/ExecutableExistsByIdOperation.java | 7 +- .../ExecutableFindByAnalyticsOperation.java | 26 +- ...utableFindByAnalyticsOperationSupport.java | 9 +- .../core/ExecutableFindByIdOperation.java | 10 +- .../ExecutableFindByIdOperationSupport.java | 3 +- .../core/ExecutableFindByQueryOperation.java | 83 +++- ...ExecutableFindByQueryOperationSupport.java | 56 ++- ...ecutableFindFromReplicasByIdOperation.java | 7 +- ...eFindFromReplicasByIdOperationSupport.java | 14 +- .../core/ExecutableInsertByIdOperation.java | 7 +- .../core/ExecutableRemoveByIdOperation.java | 9 +- .../ExecutableRemoveByQueryOperation.java | 28 +- ...ecutableRemoveByQueryOperationSupport.java | 14 +- .../core/ExecutableReplaceByIdOperation.java | 8 +- .../core/ExecutableUpsertByIdOperation.java | 7 +- .../core/ReactiveExistsByIdOperation.java | 7 +- .../ReactiveFindByAnalyticsOperation.java | 27 +- ...activeFindByAnalyticsOperationSupport.java | 10 +- .../core/ReactiveFindByIdOperation.java | 10 +- .../ReactiveFindByIdOperationSupport.java | 4 +- .../core/ReactiveFindByQueryOperation.java | 137 +++---- .../ReactiveFindByQueryOperationSupport.java | 125 ++++-- ...ReactiveFindFromReplicasByIdOperation.java | 7 +- ...eFindFromReplicasByIdOperationSupport.java | 13 +- .../core/ReactiveInsertByIdOperation.java | 9 +- .../core/ReactiveRemoveByIdOperation.java | 9 +- .../ReactiveRemoveByIdOperationSupport.java | 12 +- .../core/ReactiveRemoveByQueryOperation.java | 26 +- ...ReactiveRemoveByQueryOperationSupport.java | 39 +- .../core/ReactiveReplaceByIdOperation.java | 7 +- .../core/ReactiveUpsertByIdOperation.java | 7 +- .../data/couchbase/core/query/Query.java | 40 +- .../couchbase/core/query/StringQuery.java | 7 +- .../data/couchbase/core/support/AnyId.java | 31 ++ .../couchbase/core/support/AnyIdReactive.java | 34 ++ .../couchbase/core/support/OneAndAll.java | 46 +++ .../OneAndAllEntity.java} | 6 +- .../OneAndAllEntityReactive.java} | 6 +- .../core/support/OneAndAllExists.java | 34 ++ .../core/support/OneAndAllExistsReactive.java | 34 ++ .../couchbase/core/support/OneAndAllId.java | 32 ++ .../core/support/OneAndAllIdReactive.java | 34 ++ .../core/support/OneAndAllReactive.java | 42 ++ .../support/WithAnalyticsConsistency.java | 28 ++ .../core/support/WithAnalyticsQuery.java | 28 ++ .../core/support/WithCollection.java | 26 ++ .../core/support/WithConsistency.java | 28 ++ .../WithDistinct.java} | 8 +- .../core/support/WithProjection.java | 27 ++ .../core/support/WithProjectionId.java | 27 ++ .../couchbase/core/support/WithQuery.java | 32 ++ .../query/AbstractCouchbaseQuery.java | 4 +- .../query/AbstractCouchbaseQueryBase.java | 7 +- .../query/AbstractReactiveCouchbaseQuery.java | 2 +- .../query/N1qlRepositoryQueryExecutor.java | 5 +- .../query/StringBasedN1qlQueryParser.java | 128 +++++-- .../support/SimpleCouchbaseRepository.java | 6 +- .../SimpleReactiveCouchbaseRepository.java | 4 +- ...hbaseTemplateKeyValueIntegrationTests.java | 56 +-- ...mplateQueryCollectionIntegrationTests.java | 339 +++++++++++++++++ ...ouchbaseTemplateQueryIntegrationTests.java | 185 +++++++-- .../data/couchbase/domain/Address.java | 24 +- .../data/couchbase/domain/Airline.java | 13 +- .../data/couchbase/domain/Airport.java | 40 +- .../couchbase/domain/ComparableEntity.java | 57 +-- .../data/couchbase/domain/ConfigScoped.java | 44 +++ .../data/couchbase/domain/Course.java | 12 - .../data/couchbase/domain/Submission.java | 16 - .../data/couchbase/domain/User.java | 19 +- .../couchbase/domain/UserJustLastName.java | 60 +++ .../domain/UserSubmissionProjected.java | 47 +++ ...chbaseRepositoryQueryIntegrationTests.java | 14 +- ...chbaseRepositoryQueryIntegrationTests.java | 16 +- .../util/ClusterAwareIntegrationTests.java | 81 +++- .../util/CollectionAwareIntegrationTests.java | 76 ++++ .../couchbase/util/JavaIntegrationTests.java | 359 ++++++++++++++++++ .../data/couchbase/util/Util.java | 99 +++++ 78 files changed, 2419 insertions(+), 589 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/AnyId.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/AnyIdReactive.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/OneAndAll.java rename src/main/java/org/springframework/data/couchbase/core/{OneAndAll.java => support/OneAndAllEntity.java} (82%) rename src/main/java/org/springframework/data/couchbase/core/{OneAndAllReactive.java => support/OneAndAllEntityReactive.java} (83%) create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExists.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExistsReactive.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/OneAndAllId.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/OneAndAllIdReactive.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/OneAndAllReactive.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsConsistency.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsQuery.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithCollection.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithConsistency.java rename src/main/java/org/springframework/data/couchbase/core/{InCollection.java => support/WithDistinct.java} (83%) create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithProjection.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithProjectionId.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/support/WithQuery.java create mode 100644 src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/ConfigScoped.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/UserJustLastName.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/UserSubmissionProjected.java create mode 100644 src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/util/Util.java diff --git a/pom.xml b/pom.xml index 5e1661471..0c2a65af4 100644 --- a/pom.xml +++ b/pom.xml @@ -197,6 +197,14 @@ ${kotlin} test + + + org.awaitility + awaitility + 4.0.3 + test + + diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java index a5b44efa3..75185f521 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java @@ -18,6 +18,9 @@ import java.util.Collection; import java.util.Map; +import org.springframework.data.couchbase.core.support.OneAndAllExists; +import org.springframework.data.couchbase.core.support.WithCollection; + public interface ExecutableExistsByIdOperation { /** @@ -25,7 +28,7 @@ public interface ExecutableExistsByIdOperation { */ ExecutableExistsById existsById(); - interface TerminatingExistsById { + interface TerminatingExistsById extends OneAndAllExists { /** * Performs the operation on the ID given. @@ -45,7 +48,7 @@ interface TerminatingExistsById { } - interface ExistsByIdWithCollection extends TerminatingExistsById { + interface ExistsByIdWithCollection extends TerminatingExistsById, WithCollection { /** * Allows to specify a different collection than the default one configured. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java index ebe4e9a73..20962172a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperation.java @@ -19,12 +19,15 @@ import java.util.Optional; import java.util.stream.Stream; -import com.couchbase.client.java.analytics.AnalyticsScanConsistency; -import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.couchbase.core.query.AnalyticsQuery; +import org.springframework.data.couchbase.core.support.OneAndAll; +import org.springframework.data.couchbase.core.support.WithAnalyticsConsistency; +import org.springframework.data.couchbase.core.support.WithAnalyticsQuery; import org.springframework.lang.Nullable; +import com.couchbase.client.java.analytics.AnalyticsScanConsistency; + public interface ExecutableFindByAnalyticsOperation { /** @@ -34,7 +37,7 @@ public interface ExecutableFindByAnalyticsOperation { */ ExecutableFindByAnalytics findByAnalytics(Class domainType); - interface TerminatingFindByAnalytics { + interface TerminatingFindByAnalytics extends OneAndAll { /** * Get exactly zero or one result. @@ -102,7 +105,7 @@ default Optional first() { } - interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics { + interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics, WithAnalyticsQuery { /** * Set the filter for the analytics query to be used. @@ -114,6 +117,7 @@ interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics { } + @Deprecated interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithQuery { /** @@ -121,10 +125,22 @@ interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithQuery { * * @param scanConsistency the custom scan consistency to use for this analytics query. */ + @Deprecated FindByAnalyticsWithQuery consistentWith(AnalyticsScanConsistency scanConsistency); } - interface ExecutableFindByAnalytics extends FindByAnalyticsConsistentWith {} + interface FindByAnalyticsWithConsistency extends FindByAnalyticsConsistentWith, WithAnalyticsConsistency { + + /** + * Allows to override the default scan consistency. + * + * @param scanConsistency the custom scan consistency to use for this analytics query. + */ + FindByAnalyticsConsistentWith withConsistency(AnalyticsScanConsistency scanConsistency); + + } + + interface ExecutableFindByAnalytics extends FindByAnalyticsWithConsistency {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java index 2a3568a7c..d0e517136 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java @@ -15,11 +15,10 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.core.ReactiveFindByAnalyticsOperationSupport.ReactiveFindByAnalyticsSupport; - import java.util.List; import java.util.stream.Stream; +import org.springframework.data.couchbase.core.ReactiveFindByAnalyticsOperationSupport.ReactiveFindByAnalyticsSupport; import org.springframework.data.couchbase.core.query.AnalyticsQuery; import com.couchbase.client.java.analytics.AnalyticsScanConsistency; @@ -79,10 +78,16 @@ public TerminatingFindByAnalytics matching(final AnalyticsQuery query) { } @Override + @Deprecated public FindByAnalyticsWithQuery consistentWith(final AnalyticsScanConsistency scanConsistency) { return new ExecutableFindByAnalyticsSupport<>(template, domainType, query, scanConsistency); } + @Override + public FindByAnalyticsWithConsistency withConsistency(final AnalyticsScanConsistency scanConsistency) { + return new ExecutableFindByAnalyticsSupport<>(template, domainType, query, scanConsistency); + } + @Override public Stream stream() { return reactiveSupport.all().toStream(); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java index 04cd58806..ba6fee1fd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperation.java @@ -17,6 +17,10 @@ import java.util.Collection; +import org.springframework.data.couchbase.core.support.OneAndAllId; +import org.springframework.data.couchbase.core.support.WithCollection; +import org.springframework.data.couchbase.core.support.WithProjectionId; + public interface ExecutableFindByIdOperation { /** @@ -26,7 +30,7 @@ public interface ExecutableFindByIdOperation { */ ExecutableFindById findById(Class domainType); - interface TerminatingFindById { + interface TerminatingFindById extends OneAndAllId { /** * Finds one document based on the given ID. @@ -46,7 +50,7 @@ interface TerminatingFindById { } - interface FindByIdWithCollection extends TerminatingFindById { + interface FindByIdWithCollection extends TerminatingFindById, WithCollection { /** * Allows to specify a different collection than the default one configured. @@ -57,7 +61,7 @@ interface FindByIdWithCollection extends TerminatingFindById { } - interface FindByIdWithProjection extends FindByIdWithCollection { + interface FindByIdWithProjection extends FindByIdWithCollection, WithProjectionId { /** * Load only certain fields for the document. diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java index 8f8ca26a6..caaa7c928 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -15,12 +15,11 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.core.ReactiveFindByIdOperationSupport.ReactiveFindByIdSupport; - import java.util.Arrays; import java.util.Collection; import java.util.List; +import org.springframework.data.couchbase.core.ReactiveFindByIdOperationSupport.ReactiveFindByIdSupport; import org.springframework.util.Assert; public class ExecutableFindByIdOperationSupport implements ExecutableFindByIdOperation { diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java index 883b6e91f..39e6c990e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java @@ -21,6 +21,13 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; +import org.springframework.data.couchbase.core.support.OneAndAll; +import org.springframework.data.couchbase.core.support.WithCollection; +import org.springframework.data.couchbase.core.support.WithConsistency; +import org.springframework.data.couchbase.core.support.WithDistinct; +import org.springframework.data.couchbase.core.support.WithProjection; +import org.springframework.data.couchbase.core.support.WithQuery; import org.springframework.lang.Nullable; import com.couchbase.client.java.query.QueryScanConsistency; @@ -34,7 +41,7 @@ public interface ExecutableFindByQueryOperation { */ ExecutableFindByQuery findByQuery(Class domainType); - interface TerminatingFindByQuery { + interface TerminatingFindByQuery extends OneAndAll { /** * Get exactly zero or one result. * @@ -107,7 +114,7 @@ default Optional first() { * @author Christoph Strobl * @since 2.0 */ - interface FindByQueryWithQuery extends TerminatingFindByQuery { + interface FindByQueryWithQuery extends TerminatingFindByQuery, WithQuery { /** * Set the filter for the query to be used. @@ -117,30 +124,90 @@ interface FindByQueryWithQuery extends TerminatingFindByQuery { */ TerminatingFindByQuery matching(Query query); + /** + * Set the filter {@link QueryCriteriaDefinition criteria} to be used. + * + * @param criteria must not be {@literal null}. + * @return new instance of {@link ExecutableFindByQuery}. + * @throws IllegalArgumentException if criteria is {@literal null}. + */ + default TerminatingFindByQuery matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } + + } + + interface FindByQueryInCollection extends FindByQueryWithQuery, WithCollection { + + /** + * Allows to override the default scan consistency. + * + * @param collection the collection to use for this query. + */ + FindByQueryWithQuery inCollection(String collection); + } - interface FindByQueryConsistentWith extends FindByQueryWithQuery { + @Deprecated + interface FindByQueryConsistentWith extends FindByQueryInCollection { /** * Allows to override the default scan consistency. * * @param scanConsistency the custom scan consistency to use for this query. */ - FindByQueryConsistentWith consistentWith(QueryScanConsistency scanConsistency); + @Deprecated + FindByQueryInCollection consistentWith(QueryScanConsistency scanConsistency); } - interface FindByQueryInCollection extends FindByQueryConsistentWith { + interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { /** * Allows to override the default scan consistency. * - * @param collection the collection to use for this query. + * @param scanConsistency the custom scan consistency to use for this query. + */ + FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + + } + + /** + * Result type override (Optional). + */ + interface FindByQueryWithProjection extends FindByQueryWithConsistency { + + /** + * Define the target type fields should be mapped to.
+ * Skip this step if you are anyway only interested in the original domain type. + * + * @param returnType must not be {@literal null}. + * @return new instance of {@link FindByQueryWithProjection}. + * @throws IllegalArgumentException if returnType is {@literal null}. + */ + FindByQueryWithConsistency as(Class returnType); + } + + /** + * Distinct Find support. + */ + interface FindByQueryWithDistinct extends FindByQueryWithProjection, WithDistinct { + + /** + * Finds the distinct values for a specified {@literal field} across a single collection + * + * @param distinctFields name of the field. Must not be {@literal null}. + * @return new instance of {@link ExecutableFindByQuery}. + * @throws IllegalArgumentException if field is {@literal null}. */ - FindByQueryInCollection inCollection(String collection); + FindByQueryWithProjection distinct(String[] distinctFields); } - interface ExecutableFindByQuery extends FindByQueryInCollection {} + /** + * {@link ExecutableFindByQuery} provides methods for constructing lookup operations in a fluent way. + */ + + interface ExecutableFindByQuery extends FindByQueryWithDistinct {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java index a5c4838c1..ee4908984 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -20,6 +20,7 @@ import org.springframework.data.couchbase.core.ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryScanConsistency; @@ -41,28 +42,33 @@ public ExecutableFindByQueryOperationSupport(final CouchbaseTemplate template) { @Override public ExecutableFindByQuery findByQuery(final Class domainType) { - return new ExecutableFindByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED, - "_default._default"); + return new ExecutableFindByQuerySupport(template, domainType, domainType, ALL_QUERY, + QueryScanConsistency.NOT_BOUNDED, null, null); } static class ExecutableFindByQuerySupport implements ExecutableFindByQuery { private final CouchbaseTemplate template; - private final Class domainType; + private final Class domainType; + private final Class returnType; private final Query query; private final ReactiveFindByQuerySupport reactiveSupport; private final QueryScanConsistency scanConsistency; private final String collection; + private final String[] distinctFields; - ExecutableFindByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, final String collection) { + ExecutableFindByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Class returnType, + final Query query, final QueryScanConsistency scanConsistency, final String collection, + final String[] distinctFields) { this.template = template; this.domainType = domainType; + this.returnType = returnType; this.query = query; - this.reactiveSupport = new ReactiveFindByQuerySupport(template.reactive(), domainType, query, scanConsistency, - collection); + this.reactiveSupport = new ReactiveFindByQuerySupport(template.reactive(), domainType, returnType, query, + scanConsistency, collection, distinctFields); this.scanConsistency = scanConsistency; this.collection = collection; + this.distinctFields = distinctFields; } @Override @@ -88,17 +94,42 @@ public TerminatingFindByQuery matching(final Query query) { } else { scanCons = scanConsistency; } - return new ExecutableFindByQuerySupport<>(template, domainType, query, scanCons, collection); + return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanCons, collection, + distinctFields); } @Override - public FindByQueryConsistentWith consistentWith(final QueryScanConsistency scanConsistency) { - return new ExecutableFindByQuerySupport<>(template, domainType, query, scanConsistency, collection); + @Deprecated + public FindByQueryInCollection consistentWith(final QueryScanConsistency scanConsistency) { + return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, collection, + distinctFields); } @Override - public FindByQueryInCollection inCollection(final String collection) { - return new ExecutableFindByQuerySupport<>(template, domainType, query, scanConsistency, collection); + public FindByQueryConsistentWith withConsistency(final QueryScanConsistency scanConsistency) { + return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, collection, + distinctFields); + } + + @Override + public FindByQueryWithConsistency inCollection(final String collection) { + Assert.hasText(collection, "Collection must not be null nor empty."); + return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, collection, + distinctFields); + } + + @Override + public FindByQueryWithConsistency as(final Class resturnType) { + Assert.notNull(resturnType, "returnType must not be null!"); + return new ExecutableFindByQuerySupport<>(template, domainType, resturnType, query, scanConsistency, collection, + distinctFields); + } + + @Override + public FindByQueryWithProjection distinct(final String[] distinctFields) { + Assert.notNull(distinctFields, "distinctFields must not be null!"); + return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, collection, + distinctFields); } @Override @@ -115,7 +146,6 @@ public long count() { public boolean exists() { return count() > 0; } - } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperation.java index 96897e94e..bc744bceb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperation.java @@ -17,11 +17,14 @@ import java.util.Collection; +import org.springframework.data.couchbase.core.support.AnyId; +import org.springframework.data.couchbase.core.support.WithCollection; + public interface ExecutableFindFromReplicasByIdOperation { ExecutableFindFromReplicasById findFromReplicasById(Class domainType); - interface TerminatingFindFromReplicasById { + interface TerminatingFindFromReplicasById extends AnyId { T any(String id); @@ -29,7 +32,7 @@ interface TerminatingFindFromReplicasById { } - interface FindFromReplicasByIdWithCollection extends TerminatingFindFromReplicasById { + interface FindFromReplicasByIdWithCollection extends TerminatingFindFromReplicasById, WithCollection { TerminatingFindFromReplicasById inCollection(String collection); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperationSupport.java index beefe1046..eccebf35c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperationSupport.java @@ -30,21 +30,25 @@ public class ExecutableFindFromReplicasByIdOperationSupport implements Executabl @Override public ExecutableFindFromReplicasById findFromReplicasById(Class domainType) { - return new ExecutableFindFromReplicasByIdSupport<>(template, domainType, null); + return new ExecutableFindFromReplicasByIdSupport<>(template, domainType, domainType, null); } static class ExecutableFindFromReplicasByIdSupport implements ExecutableFindFromReplicasById { private final CouchbaseTemplate template; - private final Class domainType; + private final Class domainType; + private final Class returnType; private final String collection; private final ReactiveFindFromReplicasByIdSupport reactiveSupport; - ExecutableFindFromReplicasByIdSupport(CouchbaseTemplate template, Class domainType, String collection) { + ExecutableFindFromReplicasByIdSupport(CouchbaseTemplate template, Class domainType, Class returnType, + String collection) { this.template = template; this.domainType = domainType; this.collection = collection; - this.reactiveSupport = new ReactiveFindFromReplicasByIdSupport<>(template.reactive(), domainType, collection); + this.returnType = returnType; + this.reactiveSupport = new ReactiveFindFromReplicasByIdSupport<>(template.reactive(), domainType, returnType, + collection); } @Override @@ -60,7 +64,7 @@ public Collection any(Collection ids) { @Override public TerminatingFindFromReplicasById inCollection(final String collection) { Assert.hasText(collection, "Collection must not be null nor empty."); - return new ExecutableFindFromReplicasByIdSupport<>(template, domainType, collection); + return new ExecutableFindFromReplicasByIdSupport<>(template, domainType, returnType, collection); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java index 761f2e74c..7451b631b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperation.java @@ -18,6 +18,9 @@ import java.time.Duration; import java.util.Collection; +import org.springframework.data.couchbase.core.support.OneAndAllEntity; +import org.springframework.data.couchbase.core.support.WithCollection; + import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; @@ -26,7 +29,7 @@ public interface ExecutableInsertByIdOperation { ExecutableInsertById insertById(Class domainType); - interface TerminatingInsertById extends OneAndAll{ + interface TerminatingInsertById extends OneAndAllEntity { @Override T one(T object); @@ -36,7 +39,7 @@ interface TerminatingInsertById extends OneAndAll{ } - interface InsertByIdWithCollection extends TerminatingInsertById, InCollection { + interface InsertByIdWithCollection extends TerminatingInsertById, WithCollection { TerminatingInsertById inCollection(String collection); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java index 94a6cf682..a53a244ba 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java @@ -18,6 +18,9 @@ import java.util.Collection; import java.util.List; +import org.springframework.data.couchbase.core.support.OneAndAllId; +import org.springframework.data.couchbase.core.support.WithCollection; + import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; @@ -26,7 +29,7 @@ public interface ExecutableRemoveByIdOperation { ExecutableRemoveById removeById(); - interface TerminatingRemoveById { + interface TerminatingRemoveById extends OneAndAllId { RemoveResult one(String id); @@ -34,12 +37,12 @@ interface TerminatingRemoveById { } - interface RemoveByIdWithCollection extends TerminatingRemoveById, InCollection { + interface RemoveByIdWithCollection extends TerminatingRemoveById, WithCollection { TerminatingRemoveById inCollection(String collection); } - interface RemoveByIdWithDurability extends RemoveByIdWithCollection, WithDurability { + interface RemoveByIdWithDurability extends RemoveByIdWithCollection, WithDurability { RemoveByIdWithCollection withDurability(DurabilityLevel durabilityLevel); diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java index 9b50fdedb..788983f44 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java @@ -18,6 +18,10 @@ import java.util.List; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; +import org.springframework.data.couchbase.core.support.WithCollection; +import org.springframework.data.couchbase.core.support.WithConsistency; +import org.springframework.data.couchbase.core.support.WithQuery; import com.couchbase.client.java.query.QueryScanConsistency; @@ -31,24 +35,36 @@ interface TerminatingRemoveByQuery { } - interface RemoveByQueryWithQuery extends TerminatingRemoveByQuery { + interface RemoveByQueryWithQuery extends TerminatingRemoveByQuery, WithQuery { TerminatingRemoveByQuery matching(Query query); + default TerminatingRemoveByQuery matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } + + } + + interface RemoveByQueryInCollection extends RemoveByQueryWithQuery, WithCollection { + + RemoveByQueryWithQuery inCollection(String collection); + } - interface RemoveByQueryConsistentWith extends RemoveByQueryWithQuery { + @Deprecated + interface RemoveByQueryConsistentWith extends RemoveByQueryInCollection { - RemoveByQueryWithQuery consistentWith(QueryScanConsistency scanConsistency); + @Deprecated + RemoveByQueryInCollection consistentWith(QueryScanConsistency scanConsistency); } - interface RemoveByQueryInCollection extends RemoveByQueryConsistentWith { + interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { - RemoveByQueryConsistentWith inCollection(String collection); + RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); } - interface ExecutableRemoveByQuery extends RemoveByQueryInCollection {} + interface ExecutableRemoveByQuery extends RemoveByQueryWithConsistency {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java index 32e98a98a..ff0fb7b5b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java @@ -19,6 +19,7 @@ import org.springframework.data.couchbase.core.ReactiveRemoveByQueryOperationSupport.ReactiveRemoveByQuerySupport; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryScanConsistency; @@ -35,7 +36,7 @@ public ExecutableRemoveByQueryOperationSupport(final CouchbaseTemplate template) @Override public ExecutableRemoveByQuery removeByQuery(Class domainType) { return new ExecutableRemoveByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED, - "_default._default"); + null); } static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuery { @@ -69,12 +70,19 @@ public TerminatingRemoveByQuery matching(final Query query) { } @Override - public RemoveByQueryWithQuery consistentWith(final QueryScanConsistency scanConsistency) { + @Deprecated + public RemoveByQueryInCollection consistentWith(final QueryScanConsistency scanConsistency) { return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, collection); } @Override - public RemoveByQueryInCollection inCollection(final String collection) { + public RemoveByQueryConsistentWith withConsistency(final QueryScanConsistency scanConsistency) { + return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, collection); + } + + @Override + public RemoveByQueryWithConsistency inCollection(final String collection) { + Assert.hasText(collection, "Collection must not be null nor empty."); return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, collection); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java index b89c7ae89..c7aa94e1e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperation.java @@ -18,8 +18,10 @@ import java.time.Duration; import java.util.Collection; +import org.springframework.data.couchbase.core.support.OneAndAllEntity; +import org.springframework.data.couchbase.core.support.WithCollection; + import com.couchbase.client.core.msg.kv.DurabilityLevel; -import com.couchbase.client.java.kv.IncrementOptions; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; @@ -27,7 +29,7 @@ public interface ExecutableReplaceByIdOperation { ExecutableReplaceById replaceById(Class domainType); - interface TerminatingReplaceById extends OneAndAll { + interface TerminatingReplaceById extends OneAndAllEntity { @Override T one(T object); @@ -37,7 +39,7 @@ interface TerminatingReplaceById extends OneAndAll { } - interface ReplaceByIdWithCollection extends TerminatingReplaceById , InCollection { + interface ReplaceByIdWithCollection extends TerminatingReplaceById, WithCollection { TerminatingReplaceById inCollection(String collection); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java index a21ba69d7..eb0d9522e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperation.java @@ -18,6 +18,9 @@ import java.time.Duration; import java.util.Collection; +import org.springframework.data.couchbase.core.support.OneAndAllEntity; +import org.springframework.data.couchbase.core.support.WithCollection; + import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; @@ -26,7 +29,7 @@ public interface ExecutableUpsertByIdOperation { ExecutableUpsertById upsertById(Class domainType); - interface TerminatingUpsertById extends OneAndAll{ + interface TerminatingUpsertById extends OneAndAllEntity { @Override T one(T object); @@ -36,7 +39,7 @@ interface TerminatingUpsertById extends OneAndAll{ } - interface UpsertByIdWithCollection extends TerminatingUpsertById, InCollection { + interface UpsertByIdWithCollection extends TerminatingUpsertById, WithCollection { TerminatingUpsertById inCollection(String collection); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java index cd85162cf..23251d923 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java @@ -20,6 +20,9 @@ import java.util.Collection; import java.util.Map; +import org.springframework.data.couchbase.core.support.OneAndAllExistsReactive; +import org.springframework.data.couchbase.core.support.WithCollection; + public interface ReactiveExistsByIdOperation { /** @@ -27,7 +30,7 @@ public interface ReactiveExistsByIdOperation { */ ReactiveExistsById existsById(); - interface TerminatingExistsById { + interface TerminatingExistsById extends OneAndAllExistsReactive { /** * Performs the operation on the ID given. @@ -47,7 +50,7 @@ interface TerminatingExistsById { } - interface ExistsByIdWithCollection extends TerminatingExistsById { + interface ExistsByIdWithCollection extends TerminatingExistsById, WithCollection { /** * Allows to specify a different collection than the default one configured. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java index ff9f65fb9..8fd47e663 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperation.java @@ -15,14 +15,16 @@ */ package org.springframework.data.couchbase.core; -import com.couchbase.client.java.analytics.AnalyticsScanConsistency; -import org.springframework.dao.IncorrectResultSizeDataAccessException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.couchbase.core.query.AnalyticsQuery; +import org.springframework.data.couchbase.core.support.OneAndAllReactive; +import org.springframework.data.couchbase.core.support.WithAnalyticsConsistency; +import org.springframework.data.couchbase.core.support.WithAnalyticsQuery; -import java.util.Optional; +import com.couchbase.client.java.analytics.AnalyticsScanConsistency; public interface ReactiveFindByAnalyticsOperation { @@ -36,7 +38,7 @@ public interface ReactiveFindByAnalyticsOperation { /** * Compose find execution by calling one of the terminating methods. */ - interface TerminatingFindByAnalytics { + interface TerminatingFindByAnalytics extends OneAndAllReactive { /** * Get exactly zero or one result. @@ -76,7 +78,7 @@ interface TerminatingFindByAnalytics { } - interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics { + interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics, WithAnalyticsQuery { /** * Set the filter for the analytics query to be used. @@ -88,6 +90,7 @@ interface FindByAnalyticsWithQuery extends TerminatingFindByAnalytics { } + @Deprecated interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithQuery { /** @@ -95,10 +98,22 @@ interface FindByAnalyticsConsistentWith extends FindByAnalyticsWithQuery { * * @param scanConsistency the custom scan consistency to use for this analytics query. */ + @Deprecated FindByAnalyticsWithQuery consistentWith(AnalyticsScanConsistency scanConsistency); } - interface ReactiveFindByAnalytics extends FindByAnalyticsConsistentWith {} + interface FindByAnalyticsWithConsistency extends FindByAnalyticsConsistentWith, WithAnalyticsConsistency { + + /** + * Allows to override the default scan consistency. + * + * @param scanConsistency the custom scan consistency to use for this analytics query. + */ + FindByAnalyticsWithQuery withConsistency(AnalyticsScanConsistency scanConsistency); + + } + + interface ReactiveFindByAnalytics extends FindByAnalyticsWithConsistency {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java index 73de94c30..d3205df94 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -15,13 +15,13 @@ */ package org.springframework.data.couchbase.core; -import com.couchbase.client.java.analytics.AnalyticsOptions; -import com.couchbase.client.java.analytics.AnalyticsScanConsistency; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.data.couchbase.core.query.AnalyticsQuery; +import com.couchbase.client.java.analytics.AnalyticsOptions; +import com.couchbase.client.java.analytics.AnalyticsScanConsistency; import com.couchbase.client.java.analytics.ReactiveAnalyticsResult; public class ReactiveFindByAnalyticsOperationSupport implements ReactiveFindByAnalyticsOperation { @@ -60,10 +60,16 @@ public TerminatingFindByAnalytics matching(AnalyticsQuery query) { } @Override + @Deprecated public FindByAnalyticsWithQuery consistentWith(AnalyticsScanConsistency scanConsistency) { return new ReactiveFindByAnalyticsSupport<>(template, domainType, query, scanConsistency); } + @Override + public FindByAnalyticsWithQuery withConsistency(AnalyticsScanConsistency scanConsistency) { + return new ReactiveFindByAnalyticsSupport<>(template, domainType, query, scanConsistency); + } + @Override public Mono one() { return all().singleOrEmpty(); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java index 6d7539558..810496e53 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperation.java @@ -20,6 +20,10 @@ import java.util.Collection; +import org.springframework.data.couchbase.core.support.OneAndAllIdReactive; +import org.springframework.data.couchbase.core.support.WithCollection; +import org.springframework.data.couchbase.core.support.WithProjectionId; + public interface ReactiveFindByIdOperation { /** @@ -29,7 +33,7 @@ public interface ReactiveFindByIdOperation { */ ReactiveFindById findById(Class domainType); - interface TerminatingFindById { + interface TerminatingFindById extends OneAndAllIdReactive { /** * Finds one document based on the given ID. @@ -49,7 +53,7 @@ interface TerminatingFindById { } - interface FindByIdWithCollection extends TerminatingFindById { + interface FindByIdWithCollection extends TerminatingFindById, WithCollection { /** * Allows to specify a different collection than the default one configured. @@ -59,7 +63,7 @@ interface FindByIdWithCollection extends TerminatingFindById { TerminatingFindById inCollection(String collection); } - interface FindByIdWithProjection extends FindByIdWithCollection { + interface FindByIdWithProjection extends FindByIdWithCollection, WithProjectionId { /** * Load only certain fields for the document. diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index a58ffbf10..0cff986be 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -15,9 +15,8 @@ */ package org.springframework.data.couchbase.core; -import static com.couchbase.client.java.kv.GetOptions.*; +import static com.couchbase.client.java.kv.GetOptions.getOptions; -import com.couchbase.client.core.error.DocumentNotFoundException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,6 +26,7 @@ import org.springframework.util.Assert; +import com.couchbase.client.core.error.DocumentNotFoundException; import com.couchbase.client.java.codec.RawJsonTranscoder; import com.couchbase.client.java.kv.GetOptions; diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java index 1b99af3c7..7c670bad1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -20,6 +20,13 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; +import org.springframework.data.couchbase.core.support.OneAndAllReactive; +import org.springframework.data.couchbase.core.support.WithCollection; +import org.springframework.data.couchbase.core.support.WithConsistency; +import org.springframework.data.couchbase.core.support.WithDistinct; +import org.springframework.data.couchbase.core.support.WithProjection; +import org.springframework.data.couchbase.core.support.WithQuery; import com.couchbase.client.java.query.QueryScanConsistency; @@ -41,7 +48,7 @@ public interface ReactiveFindByQueryOperation { /** * Compose find execution by calling one of the terminating methods. */ - interface TerminatingFindByQuery { + interface TerminatingFindByQuery extends OneAndAllReactive { /** * Get exactly zero or one result. @@ -87,7 +94,7 @@ interface TerminatingFindByQuery { * @author Christoph Strobl * @since 2.0 */ - interface FindByQueryWithQuery extends TerminatingFindByQuery { + interface FindByQueryWithQuery extends TerminatingFindByQuery, WithQuery { /** * Set the filter for the query to be used. @@ -97,142 +104,96 @@ interface FindByQueryWithQuery extends TerminatingFindByQuery { */ TerminatingFindByQuery matching(Query query); - } - - interface FindByQueryConsistentWith extends FindByQueryWithQuery { - /** - * Allows to override the default scan consistency. + * Set the filter {@link QueryCriteriaDefinition criteria} to be used. * - * @param scanConsistency the custom scan consistency to use for this query. + * @param criteria must not be {@literal null}. + * @return new instance of {@link ExecutableFindByQueryOperation.ExecutableFindByQuery}. + * @throws IllegalArgumentException if criteria is {@literal null}. */ - FindByQueryConsistentWith consistentWith(QueryScanConsistency scanConsistency); + default TerminatingFindByQuery matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } } /** * Collection override (optional). */ - interface FindByQueryInCollection extends FindByQueryWithQuery { + interface FindByQueryInCollection extends FindByQueryWithQuery, WithCollection { /** * Explicitly set the name of the collection to perform the query on.
* Skip this step to use the default collection derived from the domain type. * * @param collection must not be {@literal null} nor {@literal empty}. - * @return new instance of {@link FindWithProjection}. + * @return new instance of {@link FindByQueryWithProjection}. * @throws IllegalArgumentException if collection is {@literal null}. */ - FindByQueryInCollection inCollection(String collection); + FindByQueryWithQuery inCollection(String collection); } /** - * Result type override (optional). + * @deprecated + * @see FindByQueryWithConsistency */ - interface FindWithProjection extends FindByQueryInCollection, FindDistinct { + @Deprecated + interface FindByQueryConsistentWith extends FindByQueryInCollection { /** - * Define the target type fields should be mapped to.
- * Skip this step if you are anyway only interested in the original domain type. + * Allows to override the default scan consistency. * - * @param resultType must not be {@literal null}. - * @param result type. - * @return new instance of {@link FindWithProjection}. - * @throws IllegalArgumentException if resultType is {@literal null}. + * @param scanConsistency the custom scan consistency to use for this query. */ - FindByQueryWithQuery as(Class resultType); - } - - /** - * Distinct Find support. - * - * @author Christoph Strobl - * @since 2.1 - */ - interface FindDistinct { + @Deprecated + FindByQueryInCollection consistentWith(QueryScanConsistency scanConsistency); - /** - * Finds the distinct values for a specified {@literal field} across a single {@link } or view. - * - * @param field name of the field. Must not be {@literal null}. - * @return new instance of {@link TerminatingDistinct}. - * @throws IllegalArgumentException if field is {@literal null}. - */ - TerminatingDistinct distinct(String field); } - /** - * Result type override. Optional. - * - * @author Christoph Strobl - * @since 2.1 - */ - interface DistinctWithProjection { + interface FindByQueryWithConsistency extends FindByQueryConsistentWith, WithConsistency { /** - * Define the target type the result should be mapped to.
- * Skip this step if you are anyway fine with the default conversion. - *
- *
{@link Object} (the default)
- *
Result is mapped according to the {@link } converting eg. {@link } into plain {@link String}, {@link } to - * {@link Long}, etc. always picking the most concrete type with respect to the domain types property.
- * Any {@link } is run through the {@link org.springframework.data.convert.EntityReader} to obtain the domain type. - *
- * Using {@link Object} also works for non strictly typed fields. Eg. a mixture different types like fields using - * {@link String} in one {@link } while {@link Long} in another.
- *
Any Simple type like {@link String}, {@link Long}, ...
- *
The result is mapped directly by the Couchbase Java driver and the {@link } in place. This works only for - * results where all documents considered for the operation use the very same type for the field.
- *
Any Domain type
- *
Domain types can only be mapped if the if the result of the actual {@code distinct()} operation returns - * {@link }.
- *
{@link }
- *
Using {@link } allows retrieval of the raw driver specific format, which returns eg. {@link }.
- *
+ * Allows to override the default scan consistency. * - * @param resultType must not be {@literal null}. - * @param result type. - * @return new instance of {@link TerminatingDistinct}. - * @throws IllegalArgumentException if resultType is {@literal null}. + * @param scanConsistency the custom scan consistency to use for this query. */ - TerminatingDistinct as(Class resultType); + FindByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); + } /** - * Result restrictions. Optional. - * - * @author Christoph Strobl - * @since 2.1 + * Result type override (optional). */ - interface DistinctWithQuery extends DistinctWithProjection { + interface FindByQueryWithProjection extends FindByQueryWithConsistency { /** - * Set the filter {@link Query criteria} to be used. + * Define the target type fields should be mapped to.
+ * Skip this step if you are anyway only interested in the original domain type. * - * @param query must not be {@literal null}. - * @return new instance of {@link TerminatingDistinct}. - * @throws IllegalArgumentException if criteria is {@literal null}. - * @since 3.0 + * @param returnType must not be {@literal null}. + * @return new instance of {@link FindByQueryWithProjection}. + * @throws IllegalArgumentException if returnType is {@literal null}. */ - TerminatingDistinct matching(Query query); + FindByQueryWithConsistency as(Class returnType); } /** - * Terminating distinct find operations. + * Distinct Find support. * - * @author Christoph Strobl - * @since 2.1 + * @author Michael Reiche */ - interface TerminatingDistinct extends DistinctWithQuery { + interface FindByQueryWithDistinct extends FindByQueryWithProjection, WithDistinct { /** - * Get all matching distinct field values. + * Finds the distinct values for a specified {@literal field} across a single {@link } or view. * - * @return empty {@link Flux} if not match found. Never {@literal null}. + * @param distinctFields name of the field. Must not be {@literal null}. + * @return new instance of {@link ReactiveFindByQuery}. + * @throws IllegalArgumentException if field is {@literal null}. */ - Flux all(); + FindByQueryWithProjection distinct(String[] distinctFields); } - interface ReactiveFindByQuery extends FindByQueryConsistentWith, FindByQueryInCollection, FindDistinct {} + interface ReactiveFindByQuery extends FindByQueryWithDistinct {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index 104c19110..d9cd78af3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -20,6 +20,7 @@ import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.TemplateUtils; +import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; @@ -42,51 +43,79 @@ public ReactiveFindByQueryOperationSupport(final ReactiveCouchbaseTemplate templ @Override public ReactiveFindByQuery findByQuery(final Class domainType) { - return new ReactiveFindByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED, - "_default._default"); + return new ReactiveFindByQuerySupport<>(template, domainType, domainType, ALL_QUERY, + QueryScanConsistency.NOT_BOUNDED, null, null); } static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { private final ReactiveCouchbaseTemplate template; - private final Class domainType; + private final Class domainType; + private final Class returnType; private final Query query; private final QueryScanConsistency scanConsistency; private final String collection; + private final String[] distinctFields; + + ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, + final Class returnType, final Query query, final QueryScanConsistency scanConsistency, + final String collection, final String[] distinctFields) { + Assert.notNull(domainType, "domainType must not be null!"); + Assert.notNull(returnType, "returnType must not be null!"); - ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency, final String collection) { this.template = template; this.domainType = domainType; + this.returnType = returnType; this.query = query; this.scanConsistency = scanConsistency; this.collection = collection; + this.distinctFields = distinctFields; } @Override - public TerminatingFindByQuery matching(Query query) { + public FindByQueryWithQuery matching(Query query) { QueryScanConsistency scanCons; if (query.getScanConsistency() != null) { scanCons = query.getScanConsistency(); } else { scanCons = scanConsistency; } - return new ReactiveFindByQuerySupport<>(template, domainType, query, scanCons, collection); + return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanCons, collection, + distinctFields); + } + + @Override + public FindByQueryInCollection inCollection(String collection) { + Assert.hasText(collection, "Collection must not be null nor empty."); + return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, collection, + distinctFields); } @Override + @Deprecated public FindByQueryConsistentWith consistentWith(QueryScanConsistency scanConsistency) { - return new ReactiveFindByQuerySupport<>(template, domainType, query, scanConsistency, collection); + return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, collection, + distinctFields); } @Override - public FindByQueryInCollection inCollection(String collection) { - return new ReactiveFindByQuerySupport<>(template, domainType, query, scanConsistency, collection); + public FindByQueryWithConsistency withConsistency(QueryScanConsistency scanConsistency) { + return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, collection, + distinctFields); } @Override - public TerminatingDistinct distinct(String field) { - throw new RuntimeException(("not implemented")); + public FindByQueryWithConsistency as(Class returnType) { + Assert.notNull(returnType, "returnType must not be null!"); + return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, collection, + distinctFields); + } + + @Override + public FindByQueryWithDistinct distinct(String[] distinctFields) { + Assert.notNull(distinctFields, "distinctFields must not be null!"); + return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, collection, + distinctFields); } @Override @@ -102,49 +131,61 @@ public Mono first() { @Override public Flux all() { return Flux.defer(() -> { - String statement = assembleEntityQuery(false); - return template.getCouchbaseClientFactory().getCluster().reactive() - .query(statement, query.buildQueryOptions(scanConsistency)).onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> { - String id = row.getString(TemplateUtils.SELECT_ID); - long cas = row.getLong(TemplateUtils.SELECT_CAS); - row.removeKey(TemplateUtils.SELECT_ID); - row.removeKey(TemplateUtils.SELECT_CAS); - return template.support().decodeEntity(id, row.toString(), cas, domainType); - }); + String statement = assembleEntityQuery(false, distinctFields); + Mono allResult = this.collection == null + ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, + query.buildQueryOptions(scanConsistency)) + : template.getCouchbaseClientFactory().getScope().reactive().query(statement, + query.buildQueryOptions(scanConsistency)); + return allResult.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> { + String id = ""; + long cas = 0; + if (distinctFields == null) { + id = row.getString(TemplateUtils.SELECT_ID); + cas = row.getLong(TemplateUtils.SELECT_CAS); + row.removeKey(TemplateUtils.SELECT_ID); + row.removeKey(TemplateUtils.SELECT_CAS); + } + return template.support().decodeEntity(id, row.toString(), cas, returnType); + }); }); } @Override public Mono count() { return Mono.defer(() -> { - String statement = assembleEntityQuery(true); - return template.getCouchbaseClientFactory().getCluster().reactive() - .query(statement, query.buildQueryOptions(scanConsistency)).onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> { - return row.getLong(TemplateUtils.SELECT_COUNT); - }).next(); + String statement = assembleEntityQuery(true, distinctFields); + Mono countResult = this.collection == null + ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, + query.buildQueryOptions(scanConsistency)) + : template.getCouchbaseClientFactory().getScope().reactive().query(statement, + query.buildQueryOptions(scanConsistency)); + return countResult.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> { + return row.getLong(TemplateUtils.SELECT_COUNT); + }).next(); }); } @Override public Mono exists() { return count().map(count -> count > 0); - } + } // not efficient, just need the first one - private String assembleEntityQuery(final boolean count) { - return query.toN1qlSelectString(template, this.domainType, count); + private String assembleEntityQuery(final boolean count, String[] distinctFields) { + return query.toN1qlSelectString(template, this.collection, this.domainType, this.returnType, count, + distinctFields); } } - } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperation.java index 940f98728..9de6e2a20 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperation.java @@ -20,11 +20,14 @@ import java.util.Collection; +import org.springframework.data.couchbase.core.support.AnyIdReactive; +import org.springframework.data.couchbase.core.support.WithCollection; + public interface ReactiveFindFromReplicasByIdOperation { ReactiveFindFromReplicasById findFromReplicasById(Class domainType); - interface TerminatingFindFromReplicasById { + interface TerminatingFindFromReplicasById extends AnyIdReactive { Mono any(String id); @@ -32,7 +35,7 @@ interface TerminatingFindFromReplicasById { } - interface FindFromReplicasByIdWithCollection extends TerminatingFindFromReplicasById { + interface FindFromReplicasByIdWithCollection extends TerminatingFindFromReplicasById, WithCollection { TerminatingFindFromReplicasById inCollection(String collection); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java index 59bb0c54b..bc9e9d7ef 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -37,18 +37,21 @@ public class ReactiveFindFromReplicasByIdOperationSupport implements ReactiveFin @Override public ReactiveFindFromReplicasById findFromReplicasById(Class domainType) { - return new ReactiveFindFromReplicasByIdSupport<>(template, domainType, null); + return new ReactiveFindFromReplicasByIdSupport<>(template, domainType, domainType, null); } static class ReactiveFindFromReplicasByIdSupport implements ReactiveFindFromReplicasById { private final ReactiveCouchbaseTemplate template; - private final Class domainType; + private final Class domainType; + private final Class returnType; private final String collection; - ReactiveFindFromReplicasByIdSupport(ReactiveCouchbaseTemplate template, Class domainType, String collection) { + ReactiveFindFromReplicasByIdSupport(ReactiveCouchbaseTemplate template, Class domainType, Class returnType, + String collection) { this.template = template; this.domainType = domainType; + this.returnType = returnType; this.collection = collection; } @@ -57,7 +60,7 @@ public Mono any(final String id) { return Mono.just(id).flatMap(docId -> { GetAnyReplicaOptions options = getAnyReplicaOptions().transcoder(RawJsonTranscoder.INSTANCE); return template.getCollection(collection).reactive().getAnyReplica(docId, options); - }).map(result -> template.support().decodeEntity(id, result.contentAs(String.class), result.cas(), domainType)) + }).map(result -> template.support().decodeEntity(id, result.contentAs(String.class), result.cas(), returnType)) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); @@ -75,7 +78,7 @@ public Flux any(Collection ids) { @Override public TerminatingFindFromReplicasById inCollection(final String collection) { Assert.hasText(collection, "Collection must not be null nor empty."); - return new ReactiveFindFromReplicasByIdSupport<>(template, domainType, collection); + return new ReactiveFindFromReplicasByIdSupport<>(template, domainType, returnType, collection); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java index 4bc9ffa2f..943a14bf3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java @@ -21,6 +21,9 @@ import java.time.Duration; import java.util.Collection; +import org.springframework.data.couchbase.core.support.OneAndAllEntityReactive; +import org.springframework.data.couchbase.core.support.WithCollection; + import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; @@ -29,7 +32,7 @@ public interface ReactiveInsertByIdOperation { ReactiveInsertById insertById(Class domainType); - interface TerminatingInsertById extends OneAndAllReactive{ + interface TerminatingInsertById extends OneAndAllEntityReactive { Mono one(T object); @@ -37,7 +40,7 @@ interface TerminatingInsertById extends OneAndAllReactive{ } - interface InsertByIdWithCollection extends TerminatingInsertById, InCollection { + interface InsertByIdWithCollection extends TerminatingInsertById, WithCollection { TerminatingInsertById inCollection(String collection); } @@ -50,7 +53,7 @@ interface InsertByIdWithDurability extends InsertByIdWithCollection, WithD } - interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry{ + interface InsertByIdWithExpiry extends InsertByIdWithDurability, WithExpiry { InsertByIdWithDurability withExpiry(Duration expiry); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java index d1cc5fa65..101970acf 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java @@ -20,6 +20,9 @@ import java.util.Collection; +import org.springframework.data.couchbase.core.support.OneAndAllIdReactive; +import org.springframework.data.couchbase.core.support.WithCollection; + import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; @@ -28,7 +31,7 @@ public interface ReactiveRemoveByIdOperation { ReactiveRemoveById removeById(); - interface TerminatingRemoveById { + interface TerminatingRemoveById extends OneAndAllIdReactive { Mono one(String id); @@ -36,12 +39,12 @@ interface TerminatingRemoveById { } - interface RemoveByIdWithCollection extends TerminatingRemoveById, InCollection { + interface RemoveByIdWithCollection extends TerminatingRemoveById, WithCollection { TerminatingRemoveById inCollection(String collection); } - interface RemoveByIdWithDurability extends RemoveByIdWithCollection, WithDurability { + interface RemoveByIdWithDurability extends RemoveByIdWithCollection, WithDurability { RemoveByIdWithCollection withDurability(DurabilityLevel durabilityLevel); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index 4d121e50c..f519d0cea 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -84,12 +84,6 @@ private RemoveOptions buildRemoveOptions() { return options; } - @Override - public TerminatingRemoveById inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); - return new ReactiveRemoveByIdSupport(template, collection, persistTo, replicateTo, durabilityLevel); - } - @Override public RemoveByIdWithCollection withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); @@ -103,6 +97,12 @@ public RemoveByIdWithCollection withDurability(final PersistTo persistTo, final return new ReactiveRemoveByIdSupport(template, collection, persistTo, replicateTo, durabilityLevel); } + @Override + public RemoveByIdWithDurability inCollection(final String collection) { + Assert.hasText(collection, "Collection must not be null nor empty."); + return new ReactiveRemoveByIdSupport(template, collection, persistTo, replicateTo, durabilityLevel); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java index 8518088d9..7eb7b36d9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java @@ -18,6 +18,10 @@ import reactor.core.publisher.Flux; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; +import org.springframework.data.couchbase.core.support.WithCollection; +import org.springframework.data.couchbase.core.support.WithConsistency; +import org.springframework.data.couchbase.core.support.WithQuery; import com.couchbase.client.java.query.QueryScanConsistency; @@ -29,24 +33,34 @@ interface TerminatingRemoveByQuery { Flux all(); } - interface RemoveByQueryWithQuery extends TerminatingRemoveByQuery { + interface RemoveByQueryWithQuery extends TerminatingRemoveByQuery, WithQuery { TerminatingRemoveByQuery matching(Query query); + default TerminatingRemoveByQuery matching(QueryCriteriaDefinition criteria) { + return matching(Query.query(criteria)); + } } - interface RemoveByQueryConsistentWith extends RemoveByQueryWithQuery { + interface RemoveByQueryInCollection extends RemoveByQueryWithQuery, WithCollection { - RemoveByQueryWithQuery consistentWith(QueryScanConsistency scanConsistency); + RemoveByQueryWithQuery inCollection(String collection); } - interface RemoveByQueryInCollection extends RemoveByQueryConsistentWith { + @Deprecated + interface RemoveByQueryConsistentWith extends RemoveByQueryInCollection { + @Deprecated + RemoveByQueryInCollection consistentWith(QueryScanConsistency scanConsistency); - RemoveByQueryConsistentWith inCollection(String collection); + } + + interface RemoveByQueryWithConsistency extends RemoveByQueryConsistentWith, WithConsistency { + + RemoveByQueryConsistentWith withConsistency(QueryScanConsistency scanConsistency); } - interface ReactiveRemoveByQuery extends RemoveByQueryInCollection {} + interface ReactiveRemoveByQuery extends RemoveByQueryWithConsistency {} } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java index ae3b89314..3a76a2817 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -15,12 +15,14 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.core.support.TemplateUtils; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import java.util.Optional; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.support.TemplateUtils; +import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -38,8 +40,7 @@ public ReactiveRemoveByQueryOperationSupport(final ReactiveCouchbaseTemplate tem @Override public ReactiveRemoveByQuery removeByQuery(Class domainType) { - return new ReactiveRemoveByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED, - "_default._default"); + return new ReactiveRemoveByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED, null); } static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery { @@ -63,15 +64,16 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery public Flux all() { return Flux.defer(() -> { String statement = assembleDeleteQuery(); - - return template.getCouchbaseClientFactory().getCluster().reactive().query(statement, buildQueryOptions()) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }).flatMapMany(ReactiveQueryResult::rowsAsObject) + Mono allResult = this.collection == null + ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, buildQueryOptions()) + : template.getCouchbaseClientFactory().getScope().reactive().query(statement, buildQueryOptions()); + return allResult.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }).flatMapMany(ReactiveQueryResult::rowsAsObject) .map(row -> new RemoveResult(row.getString(TemplateUtils.SELECT_ID), row.getLong(TemplateUtils.SELECT_CAS), Optional.empty())); }); @@ -91,17 +93,24 @@ public TerminatingRemoveByQuery matching(final Query query) { } @Override - public RemoveByQueryWithQuery consistentWith(final QueryScanConsistency scanConsistency) { + public RemoveByQueryWithConsistency inCollection(final String collection) { + Assert.hasText(collection, "Collection must not be null nor empty."); + return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, collection); + } + + @Override + @Deprecated + public RemoveByQueryInCollection consistentWith(final QueryScanConsistency scanConsistency) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, collection); } @Override - public RemoveByQueryInCollection inCollection(final String collection) { + public RemoveByQueryConsistentWith withConsistency(final QueryScanConsistency scanConsistency) { return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, collection); } private String assembleDeleteQuery() { - return query.toN1qlRemoveString(template, this.domainType); + return query.toN1qlRemoveString(template, collection, this.domainType); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java index 89bf0ca52..ded3a38ca 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperation.java @@ -21,6 +21,9 @@ import java.time.Duration; import java.util.Collection; +import org.springframework.data.couchbase.core.support.OneAndAllEntityReactive; +import org.springframework.data.couchbase.core.support.WithCollection; + import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; @@ -29,7 +32,7 @@ public interface ReactiveReplaceByIdOperation { ReactiveReplaceById replaceById(Class domainType); - interface TerminatingReplaceById extends OneAndAllReactive { + interface TerminatingReplaceById extends OneAndAllEntityReactive { Mono one(T object); @@ -37,7 +40,7 @@ interface TerminatingReplaceById extends OneAndAllReactive { } - interface ReplaceByIdWithCollection extends TerminatingReplaceById, InCollection { + interface ReplaceByIdWithCollection extends TerminatingReplaceById, WithCollection { TerminatingReplaceById inCollection(String collection); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java index b2c3c700d..72c81800d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperation.java @@ -21,6 +21,9 @@ import java.time.Duration; import java.util.Collection; +import org.springframework.data.couchbase.core.support.OneAndAllEntityReactive; +import org.springframework.data.couchbase.core.support.WithCollection; + import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; @@ -29,7 +32,7 @@ public interface ReactiveUpsertByIdOperation { ReactiveUpsertById upsertById(Class domainType); - interface TerminatingUpsertById extends OneAndAllReactive{ + interface TerminatingUpsertById extends OneAndAllEntityReactive { Mono one(T object); @@ -37,7 +40,7 @@ interface TerminatingUpsertById extends OneAndAllReactive{ } - interface UpsertByIdWithCollection extends TerminatingUpsertById, InCollection { + interface UpsertByIdWithCollection extends TerminatingUpsertById, WithCollection { TerminatingUpsertById inCollection(String collection); } diff --git a/src/main/java/org/springframework/data/couchbase/core/query/Query.java b/src/main/java/org/springframework/data/couchbase/core/query/Query.java index 1f7a2b6a8..82eda0969 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/Query.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Query.java @@ -44,7 +44,7 @@ */ public class Query { - private final List criteria = new ArrayList<>(); + private final List criteria = new ArrayList<>(); private JsonValue parameters = JsonValue.ja(); private long skip; private int limit; @@ -55,11 +55,15 @@ public class Query { public Query() {} - public Query(final QueryCriteria criteriaDefinition) { + public Query(final QueryCriteriaDefinition criteriaDefinition) { addCriteria(criteriaDefinition); } - public Query addCriteria(QueryCriteria criteriaDefinition) { + public static Query query(QueryCriteriaDefinition criteriaDefinition) { + return new Query(criteriaDefinition); + } + + public Query addCriteria(QueryCriteriaDefinition criteriaDefinition) { this.criteria.add(criteriaDefinition); return this; } @@ -194,7 +198,7 @@ public void appendWhere(final StringBuilder sb, int[] paramIndexPtr, CouchbaseCo if (!criteria.isEmpty()) { appendWhereOrAnd(sb); boolean first = true; - for (QueryCriteria c : criteria) { + for (QueryCriteriaDefinition c : criteria) { if (first) { first = false; } else { @@ -262,7 +266,18 @@ public String export(int[]... paramIndexPtrHolder) { // used only by tests } public String toN1qlSelectString(ReactiveCouchbaseTemplate template, Class domainClass, boolean isCount) { - StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, domainClass, isCount); + return toN1qlSelectString(template, null, domainClass, null, isCount, null); + } + + public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass, + boolean isCount) { + return toN1qlSelectString(template, collectionName, domainClass, null, isCount, null); + } + + public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass, + Class returnClass, boolean isCount, String[] distinctFields) { + StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, collectionName, domainClass, + returnClass, isCount, distinctFields); final StringBuilder statement = new StringBuilder(); appendString(statement, n1ql.selectEntity); // select ... appendWhereString(statement, n1ql.filter); // typeKey = typeValue @@ -272,8 +287,9 @@ public String toN1qlSelectString(ReactiveCouchbaseTemplate template, Class domai return statement.toString(); } - public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, Class domainClass) { - StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, domainClass, false); + public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass) { + StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, collectionName, domainClass, null, + false, null); final StringBuilder statement = new StringBuilder(); appendString(statement, n1ql.delete); // delete ... appendWhereString(statement, n1ql.filter); // typeKey = typeValue @@ -282,8 +298,8 @@ public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, Class domai return statement.toString(); } - StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTemplate template, Class domainClass, - boolean isCount) { + StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTemplate template, String collectionName, + Class domainClass, Class returnClass, boolean isCount, String[] distinctFields) { String typeKey = template.getConverter().getTypeKey(); final CouchbasePersistentEntity persistentEntity = template.getConverter().getMappingContext() .getRequiredPersistentEntity(domainClass); @@ -294,7 +310,10 @@ StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTem if (alias != null && alias.isPresent()) { typeValue = alias.toString(); } - return StringBasedN1qlQueryParser.createN1qlSpelValues(template.getBucketName(), typeKey, typeValue, isCount); + + StringBasedN1qlQueryParser sbnqp = new StringBasedN1qlQueryParser(template.getBucketName(), collectionName, + template.getConverter(), domainClass, returnClass, typeKey, typeValue, distinctFields); + return isCount ? sbnqp.getCountContext() : sbnqp.getStatementContext(); } /** @@ -322,4 +341,5 @@ public QueryOptions buildQueryOptions(QueryScanConsistency scanConsistency) { public void setMeta(Meta metaAnnotation) { Meta meta = metaAnnotation; } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java b/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java index fbcd9552f..9b326aa47 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java @@ -15,10 +15,10 @@ */ package org.springframework.data.couchbase.core.query; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; + import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.json.JsonValue; -import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; -import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser; /** * Query created from the string in @Query annotation in the repository interface. @@ -52,7 +52,8 @@ private void appendInlineN1qlStatement(final StringBuilder sb) { } @Override - public String toN1qlSelectString(ReactiveCouchbaseTemplate template, Class domainClass, boolean isCount) { + public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String collection, Class domainClass, + Class resultClass, boolean isCount, String[] distinctFields) { final StringBuilder statement = new StringBuilder(); appendInlineN1qlStatement(statement); // apply the string statement // To use generated parameters for literals diff --git a/src/main/java/org/springframework/data/couchbase/core/support/AnyId.java b/src/main/java/org/springframework/data/couchbase/core/support/AnyId.java new file mode 100644 index 000000000..ea3a7facc --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/AnyId.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +import java.util.Collection; + +/** + * A common interface for those that support one(T), all(Collection) + * + * @author Michael Reiche + * @param - the entity class + */ +public interface AnyId { + + T any(String id); + + Collection any(Collection ids); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/AnyIdReactive.java b/src/main/java/org/springframework/data/couchbase/core/support/AnyIdReactive.java new file mode 100644 index 000000000..9bd97ae15 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/AnyIdReactive.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collection; + +/** + * A common interface for those that support one(T), all(Collection) + * + * @author Michael Reiche + * @param - the entity class + */ + +public interface AnyIdReactive { + Mono any(String id); + + Flux any(Collection ids); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAll.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAll.java new file mode 100644 index 000000000..cacad7ecd --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAll.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +import java.util.Collection; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * A common interface for those that support one(T), all(Collection) + * + * @author Michael Reiche + * + * @param - the entity class + */ +public interface OneAndAll { + + Optional one(); + + Optional first(); + + T oneValue(); + + T firstValue(); + + Collection all(); + + Stream stream(); + + long count(); + + boolean exists(); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/OneAndAll.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntity.java similarity index 82% rename from src/main/java/org/springframework/data/couchbase/core/OneAndAll.java rename to src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntity.java index 7b9292c81..be028fa84 100644 --- a/src/main/java/org/springframework/data/couchbase/core/OneAndAll.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntity.java @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.couchbase.core; +package org.springframework.data.couchbase.core.support; import java.util.Collection; /** - * A common interface for all of Insert, Replace, Upsert + * A common interface for those that support one(T), all(Collection) * * @author Michael Reiche * * @param - the entity class */ -public interface OneAndAll { +public interface OneAndAllEntity { T one(T object); diff --git a/src/main/java/org/springframework/data/couchbase/core/OneAndAllReactive.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntityReactive.java similarity index 83% rename from src/main/java/org/springframework/data/couchbase/core/OneAndAllReactive.java rename to src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntityReactive.java index f486aec00..67fbba7b1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/OneAndAllReactive.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllEntityReactive.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.couchbase.core; +package org.springframework.data.couchbase.core.support; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -21,13 +21,13 @@ import java.util.Collection; /** - * A common interface for all of Insert, Replace, Upsert + * A common interface for those that support one(T), all(Collection) * * @author Michael Reiche * @param - the entity class */ -public interface OneAndAllReactive { +public interface OneAndAllEntityReactive { Mono one(T object); Flux all(Collection objects); diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExists.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExists.java new file mode 100644 index 000000000..1b4041ae6 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExists.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * A common interface for those that support one(T), all(Collection) + * + * @author Michael Reiche + * + * @param - the entity class + */ +public interface OneAndAllExists { + boolean one(String id); + + Map all(Collection ids); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExistsReactive.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExistsReactive.java new file mode 100644 index 000000000..67487c3b5 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllExistsReactive.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.Map; + +/** + * A common interface for those that support one(T), all(Collection) + * + * @author Michael Reiche + * + * @param - the entity class + */ +public interface OneAndAllExistsReactive { + Mono one(String id); + + Mono> all(Collection ids); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllId.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllId.java new file mode 100644 index 000000000..370388c02 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllId.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +import java.util.Collection; + +/** + * A common interface for those that support one(String), all(Collection) + * + * @author Michael Reiche + * + * @param - the entity class + */ +public interface OneAndAllId { + + T one(String id); + + Collection all(Collection ids); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllIdReactive.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllIdReactive.java new file mode 100644 index 000000000..5c8b74f29 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllIdReactive.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collection; + +/** + * A common interface for those that support one(String), all(Collection) + * + * @author Michael Reiche + * @param - the entity class + */ + +public interface OneAndAllIdReactive { + Mono one(String id); + + Flux all(Collection ids); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllReactive.java b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllReactive.java new file mode 100644 index 000000000..7e5414dab --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/OneAndAllReactive.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * A common interface for those that support one(T), all(Collection) + * + * @author Michael Reiche + * @param - the entity class + */ + +public interface OneAndAllReactive { + Mono one(); + + Mono first(); + + Flux all(); + + Mono count(); + + Mono exists(); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsConsistency.java b/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsConsistency.java new file mode 100644 index 000000000..93aa8f825 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsConsistency.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +import com.couchbase.client.java.analytics.AnalyticsScanConsistency; + +/** + * A common interface for all of Insert, Replace, Upsert that take consistency + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithAnalyticsConsistency { + Object withConsistency(AnalyticsScanConsistency scanConsistency); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsQuery.java b/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsQuery.java new file mode 100644 index 000000000..82f18e2ab --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithAnalyticsQuery.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +import org.springframework.data.couchbase.core.query.AnalyticsQuery; + +/** + * A common interface for all of Insert, Replace, Upsert that take Query + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithAnalyticsQuery { + Object matching(AnalyticsQuery query); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithCollection.java b/src/main/java/org/springframework/data/couchbase/core/support/WithCollection.java new file mode 100644 index 000000000..7482365d8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithCollection.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +/** + * A common interface for all of Insert, Replace, Upsert that take Collection + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithCollection { + Object inCollection(String collectionName); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithConsistency.java b/src/main/java/org/springframework/data/couchbase/core/support/WithConsistency.java new file mode 100644 index 000000000..01189f207 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithConsistency.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +import com.couchbase.client.java.query.QueryScanConsistency; + +/** + * A common interface for all of Insert, Replace, Upsert that take consistency + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithConsistency { + Object withConsistency(QueryScanConsistency scanConsistency); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/InCollection.java b/src/main/java/org/springframework/data/couchbase/core/support/WithDistinct.java similarity index 83% rename from src/main/java/org/springframework/data/couchbase/core/InCollection.java rename to src/main/java/org/springframework/data/couchbase/core/support/WithDistinct.java index 7566af480..b7b2f629a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/InCollection.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithDistinct.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.couchbase.core; +package org.springframework.data.couchbase.core.support; /** - * A common interface for all of Insert, Replace, Upsert that take collection + * A common interface for all of Insert, Replace, Upsert that take Distinct * * @author Michael Reiche * @param - the entity class */ -public interface InCollection { - Object inCollection(String collection); +public interface WithDistinct { + Object distinct(String[] distinctFields); } diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithProjection.java b/src/main/java/org/springframework/data/couchbase/core/support/WithProjection.java new file mode 100644 index 000000000..c5b925a51 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithProjection.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +/** + * A common interface for all of Insert, Replace, Upsert that take Projection + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithProjection { + Object as(Class returnType); + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithProjectionId.java b/src/main/java/org/springframework/data/couchbase/core/support/WithProjectionId.java new file mode 100644 index 000000000..df3157a03 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithProjectionId.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +/** + * A common interface for those that support project() + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithProjectionId { + Object project(String[] fields); + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/WithQuery.java b/src/main/java/org/springframework/data/couchbase/core/support/WithQuery.java new file mode 100644 index 000000000..3ef60934f --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/support/WithQuery.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 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.couchbase.core.support; + +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; + +/** + * A common interface for all of Insert, Replace, Upsert that take Query + * + * @author Michael Reiche + * @param - the entity class + */ +public interface WithQuery { + Object matching(Query query); + + Object matching(QueryCriteriaDefinition queryCriteria); +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java index a56c59ffc..eb55e5d49 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java @@ -18,6 +18,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; +import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation.ExecutableFindByQuery; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.repository.query.CouchbaseQueryExecution.DeleteExecution; import org.springframework.data.couchbase.repository.query.CouchbaseQueryExecution.PagedExecution; @@ -82,8 +83,7 @@ protected Object doExecute(CouchbaseQueryMethod method, ResultProcessor processo query = applyAnnotatedConsistencyIfPresent(query); // query = applyAnnotatedCollationIfPresent(query, accessor); // not yet implemented - ExecutableFindByQueryOperation.ExecutableFindByQuery find = typeToRead == null - ? findOperationWithProjection // + ExecutableFindByQuery find = typeToRead == null ? findOperationWithProjection // : findOperationWithProjection; // not yet implemented in core .as(typeToRead); String collection = "_default._default";// method.getEntityInformation().getCollectionName(); // not yet implemented diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQueryBase.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQueryBase.java index a6aec181d..852bd0d3d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQueryBase.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQueryBase.java @@ -36,9 +36,9 @@ import org.springframework.util.Assert; /** - * {@link RepositoryQuery} implementation for Couchbase. - * - * CouchbaseOperationsType is either CouchbaseOperations or ReactiveCouchbaseOperations + * {@link RepositoryQuery} implementation for Couchbase. CouchbaseOperationsType is either CouchbaseOperations or + * ReactiveCouchbaseOperations + * * @author Michael Reiche * @since 4.1 */ @@ -105,6 +105,7 @@ EntityInstantiators getInstantiators() { /** * Execute the query with the provided parameters + * * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[]) */ public Object execute(Object[] parameters) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java index c704ce0d8..2a23615c8 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java @@ -81,7 +81,7 @@ protected Object doExecute(CouchbaseQueryMethod method, ResultProcessor processo query = applyAnnotatedConsistencyIfPresent(query); // query = applyAnnotatedCollationIfPresent(query, accessor); // not yet implemented - ReactiveFindByQueryOperation.FindByQueryWithQuery find = typeToRead == null // + ReactiveFindByQuery find = typeToRead == null // ? findOperationWithProjection // : findOperationWithProjection; // note yet implemented in core .as(typeToRead); diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java index a77e7071d..b1400bbb8 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java @@ -73,11 +73,8 @@ public Object execute(final Object[] parameters) { query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter()).createQuery(); } - // q = (ExecutableFindByQueryOperation.ExecutableFindByQuery) operations.findByQuery(domainClass) - // .consistentWith(buildQueryScanConsistency()).matching(query); - ExecutableFindByQueryOperation.ExecutableFindByQuery operation = (ExecutableFindByQueryOperation.ExecutableFindByQuery) operations - .findByQuery(domainClass).consistentWith(buildQueryScanConsistency()); + .findByQuery(domainClass).withConsistency(buildQueryScanConsistency()); if (queryMethod.isCountQuery()) { return operation.matching(query).count(); } else if (queryMethod.isCollectionQuery()) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java index cc2f9ca68..9d68ee538 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.regex.Matcher; @@ -29,17 +30,24 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.convert.CustomConversions; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.N1QLExpression; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.query.support.N1qlUtils; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.util.TypeInformation; import org.springframework.expression.EvaluationContext; import org.springframework.expression.common.TemplateParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -109,45 +117,117 @@ public class StringBasedN1qlQueryParser { public static final Pattern QUOTE_DETECTION_PATTERN = Pattern.compile("[\"'](?:[^\"'\\\\]*(?:\\\\.)?)*[\"']"); private static final Logger LOGGER = LoggerFactory.getLogger(StringBasedN1qlQueryParser.class); private final String statement; - private final QueryMethod queryMethod; - private PlaceholderType placeHolderType; + private final CouchbaseQueryMethod queryMethod; + private PlaceholderType placeHolderType; private final N1qlSpelValues statementContext; private final N1qlSpelValues countContext; private final CouchbaseConverter couchbaseConverter; private final Collection parameterNames = new HashSet(); public final N1QLExpression parsedExpression; - public StringBasedN1qlQueryParser(String statement, QueryMethod queryMethod, String bucketName, + private GenericConversionService conversionService = new DefaultConversionService(); + private CustomConversions conversions = new CouchbaseCustomConversions(Collections.emptyList()); + + public StringBasedN1qlQueryParser(String statement, CouchbaseQueryMethod queryMethod, String bucketName, CouchbaseConverter couchbaseConverter, String typeField, String typeValue, ParameterAccessor accessor, SpelExpressionParser parser, QueryMethodEvaluationContextProvider evaluationContextProvider) { this.statement = statement; this.queryMethod = queryMethod; - this.statementContext = createN1qlSpelValues(bucketName, typeField, typeValue, false); - this.countContext = createN1qlSpelValues(bucketName, typeField, typeValue, true); this.couchbaseConverter = couchbaseConverter; + this.statementContext = createN1qlSpelValues(bucketName, null, null, null, typeField, typeValue, false, null); + this.countContext = createN1qlSpelValues(bucketName, null, null, null, typeField, typeValue, true, null); this.parsedExpression = getExpression(accessor, getParameters(accessor), null, parser, evaluationContextProvider); - checkPlaceholders( this.parsedExpression.toString() ); + checkPlaceholders(this.parsedExpression.toString()); } - public static N1qlSpelValues createN1qlSpelValues(String bucketName, String typeField, String typeValue, - boolean isCount) { - String b = "`" + bucketName + "`"; - String entity = "META(" + b + ").id AS " + SELECT_ID + ", META(" + b + ").cas AS " + SELECT_CAS; + public StringBasedN1qlQueryParser(String bucketName, String collection, CouchbaseConverter couchbaseConverter, + Class domainClass, Class resultClass, String typeField, String typeValue, String[] distinctFields) { + this.statement = null; + this.queryMethod = null; + this.couchbaseConverter = couchbaseConverter; + this.statementContext = createN1qlSpelValues(bucketName, collection, domainClass, resultClass, typeField, typeValue, + false, distinctFields); + this.countContext = createN1qlSpelValues(bucketName, collection, domainClass, resultClass, typeField, typeValue, + true, distinctFields); + this.parsedExpression = null; + } + + public N1qlSpelValues createN1qlSpelValues(String bucketName, String collection, Class domainClass, Class resultClass, + String typeField, String typeValue, boolean isCount, String[] distinctFields) { + String b = collection != null ? collection : bucketName; + String projectedFields = getProjectedFields(b, resultClass); + String entity = "META(" + i(b) + ").id AS " + SELECT_ID + ", META(" + i(b) + ").cas AS " + SELECT_CAS; String count = "COUNT(*) AS " + CountFragment.COUNT_ALIAS; String selectEntity; - if (isCount) { - selectEntity = "SELECT " + count + " FROM " + b; + if (distinctFields != null) { + String distinctFieldsStr = distinctFields.length == 0 ? projectedFields : getDistinctFields(distinctFields); + if (isCount) { + selectEntity = "SELECT COUNT( DISTINCT {" + distinctFieldsStr + "} ) " + CountFragment.COUNT_ALIAS + " FROM " + + i(b); + } else { + selectEntity = "SELECT DISTINCT " + distinctFieldsStr + " FROM " + i(b); + } + } else if (isCount) { + selectEntity = "SELECT " + count + " FROM " + i(b); } else { - selectEntity = "SELECT " + entity + ", " + b + ".* FROM " + b; + selectEntity = "SELECT " + entity + ", " + projectedFields + " FROM " + i(b); } String typeSelection = "`" + typeField + "` = \"" + typeValue + "\""; - String delete = N1QLExpression.delete().from(i(bucketName)).toString(); - String returning = " returning " + N1qlUtils.createReturningExpressionForDelete(bucketName).toString(); + String delete = N1QLExpression.delete().from(b).toString(); + String returning = " returning " + N1qlUtils.createReturningExpressionForDelete(b).toString(); return new N1qlSpelValues(selectEntity, entity, b, typeSelection, delete, returning); } + private String getDistinctFields(String... distinctFields) { + return i(distinctFields).toString(); + } + + private String getProjectedFields(String b, Class resultClass) { + + String projectedFields = i(b) + ".*"; + if (resultClass != null) { + PersistentEntity persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass); + StringBuilder sb = new StringBuilder(); + getProjectedFieldsInternal(null, sb, persistentEntity.getTypeInformation(), ""); + projectedFields = sb.toString(); + } + return projectedFields; + } + + private void getProjectedFieldsInternal(CouchbasePersistentProperty parent, StringBuilder sb, + TypeInformation resultClass, String path) { + + PersistentEntity persistentEntity = couchbaseConverter.getMappingContext().getPersistentEntity(resultClass); + persistentEntity.doWithProperties(new PropertyHandler() { + @Override + public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { + if (prop.isIdProperty() && parent == null) { + return; + } + // The current limitation is that only top-level properties can be projected + // This traversing of nested data structures would need to replicate the processing done by + // MappingCouchbaseConverter. Either the read or write + // And the n1ql to project lower-level properties is complex + + // if (!conversions.isSimpleType(prop.getType())) { + // getProjectedFieldsInternal(prop, sb, prop.getTypeInformation(), path+prop.getName()+"."); + // } else { + if (sb.length() > 0) { + sb.append(", "); + } + sb.append('`'); + if (path != null && path.length() != 0) { + sb.append(path); + } + sb.append(prop.getName()); + sb.append('`'); + // } + } + }); + } + // this static method can be used to test the parsing behavior for Couchbase specific spel variables // in isolation from the rest of the spel parser initialization chain. public String doParse(SpelExpressionParser parser, EvaluationContext evaluationContext, boolean isCountQuery) { @@ -407,12 +487,14 @@ public N1qlSpelValues(String selectClause, String entityFields, String bucket, S this.returning = returning; } } + // copied from StringN1qlBasedQuery private N1QLExpression getExpression(ParameterAccessor accessor, Object[] runtimeParameters, - ReturnedType returnedType, SpelExpressionParser parser, QueryMethodEvaluationContextProvider evaluationContextProvider) { - boolean isCountQuery = queryMethod.getName().toLowerCase().startsWith("count"); // should be queryMethod.isCountQuery() - EvaluationContext evaluationContext = evaluationContextProvider - .getEvaluationContext(queryMethod.getParameters(), runtimeParameters); + ReturnedType returnedType, SpelExpressionParser parser, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + boolean isCountQuery = queryMethod.isCountQuery(); + EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(queryMethod.getParameters(), + runtimeParameters); N1QLExpression parsedStatement = x(this.doParse(parser, evaluationContext, isCountQuery)); Sort sort = accessor.getSort(); @@ -423,13 +505,11 @@ private N1QLExpression getExpression(ParameterAccessor accessor, Object[] runtim if (queryMethod.isPageQuery()) { Pageable pageable = accessor.getPageable(); Assert.notNull(pageable, "Pageable must not be null!"); - parsedStatement = parsedStatement.limit(pageable.getPageSize()).offset( - Math.toIntExact(pageable.getOffset())); + parsedStatement = parsedStatement.limit(pageable.getPageSize()).offset(Math.toIntExact(pageable.getOffset())); } else if (queryMethod.isSliceQuery()) { Pageable pageable = accessor.getPageable(); Assert.notNull(pageable, "Pageable must not be null!"); - parsedStatement = parsedStatement.limit(pageable.getPageSize() + 1).offset( - Math.toIntExact(pageable.getOffset())); + parsedStatement = parsedStatement.limit(pageable.getPageSize() + 1).offset(Math.toIntExact(pageable.getOffset())); } return parsedStatement; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java index c87906ca3..c91235d4b 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java @@ -138,13 +138,13 @@ public void deleteAll(Iterable entities) { @Override public long count() { - return couchbaseOperations.findByQuery(entityInformation.getJavaType()).consistentWith(buildQueryScanConsistency()) + return couchbaseOperations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) .count(); } @Override public void deleteAll() { - couchbaseOperations.removeByQuery(entityInformation.getJavaType()).consistentWith(buildQueryScanConsistency()) + couchbaseOperations.removeByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) .all(); } @@ -185,7 +185,7 @@ protected CouchbaseEntityInformation getEntityInformation() { * @return the list of found entities, already executed. */ private List findAll(Query query) { - return couchbaseOperations.findByQuery(entityInformation.getJavaType()).consistentWith(buildQueryScanConsistency()) + return couchbaseOperations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) .matching(query).all(); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index 2551f60aa..d6c69ef71 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java @@ -184,7 +184,7 @@ public Mono deleteAll(Publisher entityStream) { @Override public Mono count() { - return operations.findByQuery(entityInformation.getJavaType()).consistentWith(buildQueryScanConsistency()).count(); + return operations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()).count(); } @Override @@ -202,7 +202,7 @@ protected CouchbaseEntityInformation getEntityInformation() { } private Flux findAll(Query query) { - return operations.findByQuery(entityInformation.getJavaType()).consistentWith(buildQueryScanConsistency()) + return operations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) .matching(query).all(); } diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java index 2ee4e469e..868455030 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -23,10 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; -import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE; -import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.time.Duration; @@ -34,32 +31,26 @@ import java.util.Set; import java.util.UUID; -import com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions;; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; -import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; -import org.springframework.data.couchbase.core.ExecutableReplaceByIdOperation.ExecutableReplaceById; import org.springframework.data.couchbase.core.ExecutableRemoveByIdOperation.ExecutableRemoveById; - -import org.springframework.data.couchbase.domain.Config; +import org.springframework.data.couchbase.core.ExecutableReplaceByIdOperation.ExecutableReplaceById; +import org.springframework.data.couchbase.core.support.OneAndAllEntity; import org.springframework.data.couchbase.domain.PersonValue; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserAnnotated; import org.springframework.data.couchbase.domain.UserAnnotated2; import org.springframework.data.couchbase.domain.UserAnnotated3; -import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import org.springframework.data.couchbase.util.JavaIntegrationTests; + +; /** * KV tests Theses tests rely on a cb server running. @@ -68,30 +59,12 @@ * @author Michael Reiche */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) -class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationTests { - - private static CouchbaseClientFactory couchbaseClientFactory; - private CouchbaseTemplate couchbaseTemplate; - private ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; - - @BeforeAll - static void beforeAll() { - couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(), authenticator(), bucketName()); - couchbaseClientFactory.getBucket().waitUntilReady(Duration.ofSeconds(10)); - couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(), - CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions().ignoreIfExists(true)); - } - - @AfterAll - static void afterAll() throws IOException { - couchbaseClientFactory.close(); - } +class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { @BeforeEach - void beforeEach() { - ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); - couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); - reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); + @Override + public void beforeEach() { + super.beforeEach(); couchbaseTemplate.removeByQuery(User.class).all(); couchbaseTemplate.removeByQuery(UserAnnotated.class).all(); couchbaseTemplate.removeByQuery(UserAnnotated2.class).all(); @@ -110,6 +83,7 @@ void upsertAndFindById() { assertThrows(DataIntegrityViolationException.class, () -> couchbaseTemplate.replaceById(User.class).one(user)); User found = couchbaseTemplate.findById(User.class).one(user.getId()); + user.setVersion(found.getVersion()); assertEquals(user, found); couchbaseTemplate.removeById().one(user.getId()); @@ -121,7 +95,7 @@ void withDurability() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Class clazz = User.class; // for now, just User.class. There is no Durability annotation. // insert, replace, upsert - for (OneAndAll operator : new OneAndAll[] { couchbaseTemplate.insertById(clazz), + for (OneAndAllEntity operator : new OneAndAllEntity[] { couchbaseTemplate.insertById(clazz), couchbaseTemplate.replaceById(clazz), couchbaseTemplate.upsertById(clazz) }) { // create an entity of type clazz Constructor cons = clazz.getConstructor(String.class, String.class, String.class); @@ -129,7 +103,7 @@ void withDurability() "firstname", "lastname"); if (clazz.equals(User.class)) { // User.java doesn't have an durability annotation - operator = (OneAndAll) ((WithDurability) operator).withDurability(PersistTo.ACTIVE, ReplicateTo.NONE); + operator = (OneAndAllEntity) ((WithDurability) operator).withDurability(PersistTo.ACTIVE, ReplicateTo.NONE); } // if replace, we need to insert a document to replace @@ -160,7 +134,7 @@ void withExpiryAndExpiryAnnotation() // Entity classes for (Class clazz : new Class[] { User.class, UserAnnotated.class, UserAnnotated2.class, UserAnnotated3.class }) { // insert, replace, upsert - for (OneAndAll operator : new OneAndAll[] { couchbaseTemplate.insertById(clazz), + for (OneAndAllEntity operator : new OneAndAllEntity[] { couchbaseTemplate.insertById(clazz), couchbaseTemplate.replaceById(clazz), couchbaseTemplate.upsertById(clazz) }) { // create an entity of type clazz @@ -169,9 +143,9 @@ void withExpiryAndExpiryAnnotation() "firstname", "lastname"); if (clazz.equals(User.class)) { // User.java doesn't have an expiry annotation - operator = (OneAndAll) ((WithExpiry) operator).withExpiry(Duration.ofSeconds(1)); + operator = (OneAndAllEntity) ((WithExpiry) operator).withExpiry(Duration.ofSeconds(1)); } else if (clazz.equals(UserAnnotated3.class)) { // override the expiry from the annotation with no expiry - operator = (OneAndAll) ((WithExpiry) operator).withExpiry(Duration.ofSeconds(0)); + operator = (OneAndAllEntity) ((WithExpiry) operator).withExpiry(Duration.ofSeconds(0)); } // if replace or remove, we need to insert a document to replace diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java new file mode 100644 index 000000000..cf5fe0154 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java @@ -0,0 +1,339 @@ +/* + * Copyright 2021 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.couchbase.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; +import java.time.temporal.TemporalAccessor; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Address; +import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.Course; +import org.springframework.data.couchbase.domain.NaiveAuditorAware; +import org.springframework.data.couchbase.domain.Submission; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserJustLastName; +import org.springframework.data.couchbase.domain.UserSubmission; +import org.springframework.data.couchbase.domain.UserSubmissionProjected; +import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; +import org.springframework.data.couchbase.util.IgnoreWhen; + +import com.couchbase.client.java.query.QueryScanConsistency; + +/** + * Query tests Theses tests rely on a cb server running This class tests collection support with + * inCollection(collection) It should be identical to CouchbaseTemplateQueryIntegrationTests except for the setup and + * the inCollection(collectionName) calls. Testing without collections could also be done by this class simply by using + * scopeName = null and collectionName = null (except for inCollection() checks that the collectionName is not null) + * + * @author Michael Reiche + */ +@IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { + + @BeforeAll + public static void beforeAll() { + // first call the super method + callSuperBeforeAll(new Object() {}); + // then do processing for this class + // collectionName = null; + // scopeName = null; + } + + @AfterAll + public static void afterAll() { + // first do the processing for this class + // no-op + // then call the super method + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + @Override + public void beforeEach() { + // first call the super method + super.beforeEach(); + // then do processing for this class + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); + } + + @AfterEach + @Override + public void afterEach() { + // first call the super method + super.afterEach(); + // then do processing for this class + // no-op + } + + @Test + void findByQueryAll() { + try { + User user1 = new User(UUID.randomUUID().toString(), "user1", "user1"); + User user2 = new User(UUID.randomUUID().toString(), "user2", "user2"); + + couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2)); + + final List foundUsers = couchbaseTemplate.findByQuery(User.class) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + + for (User u : foundUsers) { + if (!(u.equals(user1) || u.equals(user2))) { + // somebody didn't clean up after themselves. + couchbaseTemplate.removeById().inCollection(collectionName).one(u.getId()); + } + } + assertEquals(2, foundUsers.size()); + TemporalAccessor auditTime = new AuditingDateTimeProvider().getNow().get(); + long auditMillis = Instant.from(auditTime).toEpochMilli(); + String auditUser = new NaiveAuditorAware().getCurrentAuditor().get(); + + for (User u : foundUsers) { + assertTrue(u.equals(user1) || u.equals(user2)); + assertEquals(auditUser, u.getCreator()); + assertEquals(auditMillis, u.getCreatedDate()); + assertEquals(auditUser, u.getLastModifiedBy()); + assertEquals(auditMillis, u.getLastModifiedDate()); + } + couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId()); + reactiveCouchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId()).block(); + } finally { + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); + } + + User usery = couchbaseTemplate.findById(User.class).inCollection(collectionName).one("userx"); + assertNull(usery, "usery should be null"); + User userz = reactiveCouchbaseTemplate.findById(User.class).inCollection(collectionName).one("userx").block(); + assertNull(userz, "userz should be null"); + + } + + @Test + void findByMatchingQuery() { + User user1 = new User(UUID.randomUUID().toString(), "user1", "user1"); + User user2 = new User(UUID.randomUUID().toString(), "user2", "user2"); + User specialUser = new User(UUID.randomUUID().toString(), "special", "special"); + + couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); + + Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); + final List foundUsers = couchbaseTemplate.findByQuery(User.class) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + + assertEquals(1, foundUsers.size()); + } + + @Test + void findByMatchingQueryProjected() { + + UserSubmission user = new UserSubmission(); + user.setId(UUID.randomUUID().toString()); + user.setUsername("dave"); + user.setRoles(Arrays.asList("role1", "role2")); + Address address = new Address(); + address.setStreet("1234 Olcott Street"); + user.setAddress(address); + user.setSubmissions( + Arrays.asList(new Submission(UUID.randomUUID().toString(), user.getId(), "tid", "status", 123))); + user.setCourses(Arrays.asList(new Course(UUID.randomUUID().toString(), user.getId(), "581"), + new Course(UUID.randomUUID().toString(), user.getId(), "777"))); + couchbaseTemplate.upsertById(UserSubmission.class).inCollection(collectionName).one(user); + + Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); + + final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) + .as(UserSubmissionProjected.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) + .inCollection(collectionName).matching(daveUsers).all(); + assertEquals(1, foundUserSubmissions.size()); + assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); + assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); + assertEquals(user.getCourses(), foundUserSubmissions.get(0).getCourses()); + assertEquals(user.getAddress(), foundUserSubmissions.get(0).getAddress()); + + couchbaseTemplate.removeByQuery(UserSubmission.class).inCollection(collectionName).all(); + + User user1 = new User(UUID.randomUUID().toString(), "user1", "user1"); + User user2 = new User(UUID.randomUUID().toString(), "user2", "user2"); + User specialUser = new User(UUID.randomUUID().toString(), "special", "special"); + + couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); + + Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).matching(specialUsers).all(); + assertEquals(1, foundUsers.size()); + + final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) + .as(UserJustLastName.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName) + .matching(specialUsers).all().collectList().block(); + assertEquals(1, foundUsersReactive.size()); + + } + + @Test + void removeByQueryAll() { + User user1 = new User(UUID.randomUUID().toString(), "user1", "user1"); + User user2 = new User(UUID.randomUUID().toString(), "user2", "user2"); + + couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2)); + + assertTrue(couchbaseTemplate.existsById().inCollection(collectionName).one(user1.getId())); + assertTrue(couchbaseTemplate.existsById().inCollection(collectionName).one(user2.getId())); + + couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) + .inCollection(collectionName).all(); + + assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId())); + assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId())); + + } + + @Test + void removeByMatchingQuery() { + User user1 = new User(UUID.randomUUID().toString(), "user1", "user1"); + User user2 = new User(UUID.randomUUID().toString(), "user2", "user2"); + User specialUser = new User(UUID.randomUUID().toString(), "special", "special"); + + couchbaseTemplate.upsertById(User.class).inCollection(collectionName).all(Arrays.asList(user1, user2, specialUser)); + + assertTrue(couchbaseTemplate.existsById().inCollection(collectionName).one(user1.getId())); + assertTrue(couchbaseTemplate.existsById().inCollection(collectionName).one(user2.getId())); + assertTrue(couchbaseTemplate.existsById().inCollection(collectionName).one(specialUser.getId())); + + Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); + + couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) + .inCollection(collectionName).matching(nonSpecialUsers).all(); + + assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user1.getId())); + assertNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(user2.getId())); + assertNotNull(couchbaseTemplate.findById(User.class).inCollection(collectionName).one(specialUser.getId())); + + } + + @Test + void distinct() { + String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" }; + String[] icaos = { "ic0", "ic1", "ic0", "ic1", "ic0", "ic1", "ic0" }; + + try { + for (int i = 0; i < iatas.length; i++) { + Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, icaos[i] /* icao */); + couchbaseTemplate.insertById(Airport.class).inCollection(collectionName).one(airport); + } + + // distinct and count(distinct(...)) calls. use as() and consistentWith to verify fluent api + // as the fluent api for Distinct is tricky + + // distinct icao + List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + assertEquals(2, airports1.size()); + + // distinct all-fields-in-Airport.class + List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all(); + assertEquals(7, airports2.size()); + + // count( distinct { iata, icao } ) + long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) + .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + assertEquals(7, count1); + + // count( distinct (all fields in icaoClass) + Class icaoClass = (new Object() { + String iata; + String icao; + }).getClass(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count(); + assertEquals(7, count2); + + } finally { + couchbaseTemplate.removeById().inCollection(collectionName) + .all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())); + } + } + + @Test + void distinctReactive() { + String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" }; + String[] icaos = { "ic0", "ic1", "ic0", "ic1", "ic0", "ic1", "ic0" }; + + try { + for (int i = 0; i < iatas.length; i++) { + Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, icaos[i] /* icao */); + reactiveCouchbaseTemplate.insertById(Airport.class).inCollection(collectionName).one(airport).block(); + } + + // distinct and count(distinct(...)) calls. use as() and consistentWith to verify fluent api + // as the fluent api for Distinct is tricky + + // distinct icao + List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() + .collectList().block(); + assertEquals(2, airports1.size()); + + // distinct all-fields-in-Airport.class + List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) + .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).all() + .collectList().block(); + assertEquals(7, airports2.size()); + + // count( distinct icao ) + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count() + .block(); + assertEquals(2, count1); + + // count( distinct (all fields in icaoClass) // which only has one field + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + Class icaoClass = (new Object() { + String icao; + }).getClass(); + long count2 = (long) reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).inCollection(collectionName).count().block(); + assertEquals(2, count2); + + } finally { + reactiveCouchbaseTemplate.removeById().inCollection(collectionName) + .all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())).collectList() + .block(); + } + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java index 04595c1b6..30a0a96e1 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java @@ -23,33 +23,35 @@ import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE; -import java.io.IOException; import java.time.Instant; import java.time.temporal.TemporalAccessor; import java.util.Arrays; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.data.couchbase.CouchbaseClientFactory; -import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.domain.Address; +import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.Config; +import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; +import org.springframework.data.couchbase.domain.Submission; import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserJustLastName; +import org.springframework.data.couchbase.domain.UserSubmission; +import org.springframework.data.couchbase.domain.UserSubmissionProjected; import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; import org.springframework.data.couchbase.util.Capabilities; -import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; -import com.couchbase.client.core.error.IndexExistsException; import com.couchbase.client.java.query.QueryScanConsistency; /** @@ -60,30 +62,11 @@ * @author Haris Alesevic */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -class CouchbaseTemplateQueryIntegrationTests extends ClusterAwareIntegrationTests { - - private static CouchbaseClientFactory couchbaseClientFactory; - private CouchbaseTemplate couchbaseTemplate; - private ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; - - @BeforeAll - static void beforeAll() { - couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(), authenticator(), bucketName()); - - try { - couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName()); - } catch (IndexExistsException ex) { - // ignore, all good. - } - } - - @AfterAll - static void afterAll() throws IOException { - couchbaseClientFactory.close(); - } +class CouchbaseTemplateQueryIntegrationTests extends JavaIntegrationTests { @BeforeEach - void beforeEach() { + @Override + public void beforeEach() { ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); @@ -100,7 +83,7 @@ void findByQueryAll() { couchbaseTemplate.upsertById(User.class).all(Arrays.asList(user1, user2)); final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .consistentWith(QueryScanConsistency.REQUEST_PLUS).all(); + .withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); for (User u : foundUsers) { if (!(u.equals(user1) || u.equals(user2))) { @@ -143,11 +126,57 @@ void findByMatchingQuery() { Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .consistentWith(QueryScanConsistency.REQUEST_PLUS).matching(specialUsers).all(); + .withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(specialUsers).all(); assertEquals(1, foundUsers.size()); } + @Test + void findByMatchingQueryProjected() { + + UserSubmission user = new UserSubmission(); + user.setId(UUID.randomUUID().toString()); + user.setUsername("dave"); + user.setRoles(Arrays.asList("role1", "role2")); + Address address = new Address(); + address.setStreet("1234 Olcott Street"); + user.setAddress(address); + user.setSubmissions( + Arrays.asList(new Submission(UUID.randomUUID().toString(), user.getId(), "tid", "status", 123))); + user.setCourses(Arrays.asList(new Course(UUID.randomUUID().toString(), user.getId(), "581"), + new Course(UUID.randomUUID().toString(), user.getId(), "777"))); + couchbaseTemplate.upsertById(UserSubmission.class).one(user); + + Query daveUsers = new Query(QueryCriteria.where("username").like("dave")); + + final List foundUserSubmissions = couchbaseTemplate.findByQuery(UserSubmission.class) + .as(UserSubmissionProjected.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(daveUsers).all(); + assertEquals(1, foundUserSubmissions.size()); + assertEquals(user.getUsername(), foundUserSubmissions.get(0).getUsername()); + assertEquals(user.getId(), foundUserSubmissions.get(0).getId()); + assertEquals(user.getCourses(), foundUserSubmissions.get(0).getCourses()); + assertEquals(user.getAddress(), foundUserSubmissions.get(0).getAddress()); + + couchbaseTemplate.removeByQuery(UserSubmission.class).all(); + + User user1 = new User(UUID.randomUUID().toString(), "user1", "user1"); + User user2 = new User(UUID.randomUUID().toString(), "user2", "user2"); + User specialUser = new User(UUID.randomUUID().toString(), "special", "special"); + + couchbaseTemplate.upsertById(User.class).all(Arrays.asList(user1, user2, specialUser)); + + Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).as(UserJustLastName.class) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(specialUsers).all(); + assertEquals(1, foundUsers.size()); + + final List foundUsersReactive = reactiveCouchbaseTemplate.findByQuery(User.class) + .as(UserJustLastName.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(specialUsers).all() + .collectList().block(); + assertEquals(1, foundUsersReactive.size()); + + } + @Test void removeByQueryAll() { User user1 = new User(UUID.randomUUID().toString(), "user1", "user1"); @@ -158,7 +187,7 @@ void removeByQueryAll() { assertTrue(couchbaseTemplate.existsById().one(user1.getId())); assertTrue(couchbaseTemplate.existsById().one(user2.getId())); - couchbaseTemplate.removeByQuery(User.class).consistentWith(QueryScanConsistency.REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); assertNull(couchbaseTemplate.findById(User.class).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).one(user2.getId())); @@ -179,7 +208,7 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); - couchbaseTemplate.removeByQuery(User.class).consistentWith(QueryScanConsistency.REQUEST_PLUS) + couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) .matching(nonSpecialUsers).all(); assertNull(couchbaseTemplate.findById(User.class).one(user1.getId())); @@ -188,4 +217,94 @@ void removeByMatchingQuery() { } + @Test + void distinct() { + String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" }; + String[] icaos = { "ic0", "ic1", "ic0", "ic1", "ic0", "ic1", "ic0" }; + + try { + for (int i = 0; i < iatas.length; i++) { + Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, icaos[i] /* icao */); + couchbaseTemplate.insertById(Airport.class).one(airport); + } + + // distinct and count(distinct(...)) calls. use as() and consistentWith to verify fluent api + // as the fluent api for Distinct is tricky + + // distinct icao + List airports1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + assertEquals(2, airports1.size()); + + // distinct all-fields-in-Airport.class + List airports2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(Airport.class) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + assertEquals(7, airports2.size()); + + // count( distinct { iata, icao } ) + long count1 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "iata", "icao" }) + .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).count(); + assertEquals(7, count1); + + // count( distinct (all fields in icaoClass) + Class icaoClass = (new Object() { + String iata; + String icao; + }).getClass(); + long count2 = couchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).count(); + assertEquals(7, count2); + + } finally { + couchbaseTemplate.removeById() + .all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())); + } + } + + @Test + void distinctReactive() { + String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" }; + String[] icaos = { "ic0", "ic1", "ic0", "ic1", "ic0", "ic1", "ic0" }; + + try { + for (int i = 0; i < iatas.length; i++) { + Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, icaos[i] /* icao */); + reactiveCouchbaseTemplate.insertById(Airport.class).one(airport).block(); + } + + // distinct and count(distinct(...)) calls. use as() and consistentWith to verify fluent api + // as the fluent api for Distinct is tricky + + // distinct icao + List airports1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all().collectList().block(); + assertEquals(2, airports1.size()); + + // distinct all-fields-in-Airport.class + List airports2 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}) + .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all().collectList().block(); + assertEquals(7, airports2.size()); + + // count( distinct icao ) + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + long count1 = reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] { "icao" }) + .as(Airport.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).count().block(); + assertEquals(2, count1); + + // count( distinct (all fields in icaoClass) // which only has one field + // not currently possible to have multiple fields in COUNT(DISTINCT field1, field2, ... ) due to MB43475 + Class icaoClass = (new Object() { + String icao; + }).getClass(); + long count2 = (long) reactiveCouchbaseTemplate.findByQuery(Airport.class).distinct(new String[] {}).as(icaoClass) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).count().block(); + assertEquals(2, count2); + + } finally { + reactiveCouchbaseTemplate.removeById() + .all(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())).collectList() + .block(); + } + } + } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Address.java b/src/test/java/org/springframework/data/couchbase/domain/Address.java index 43701abd1..54724a569 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Address.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Address.java @@ -1,17 +1,12 @@ package org.springframework.data.couchbase.domain; -import org.springframework.data.annotation.Id; -import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.mapping.Document; -import org.springframework.data.couchbase.core.mapping.id.GeneratedValue; -import org.springframework.data.couchbase.core.mapping.id.GenerationStrategy; - -import java.util.UUID; @Document -public class Address extends AbstractEntity { +public class Address extends ComparableEntity { - private String street; + private String street; + private String city; public Address() {} @@ -23,11 +18,12 @@ public void setStreet(String street) { this.street = street; } - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{\"street\"=\""); - sb.append(getStreet()); - sb.append("\"}"); - return sb.toString(); + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; } + } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airline.java b/src/test/java/org/springframework/data/couchbase/domain/Airline.java index 698b81c8c..402412013 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airline.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airline.java @@ -23,7 +23,7 @@ @Document @CompositeQueryIndex(fields = { "id", "name desc" }) -public class Airline { +public class Airline extends ComparableEntity { @Id String id; @QueryIndexed String name; @@ -42,15 +42,4 @@ public String getName() { return name; } - @Override - public String toString(){ - StringBuilder sb=new StringBuilder(); - sb.append("airline: { "); - sb.append(" id: "); - sb.append(id); - sb.append(" , name: "); - sb.append(name); - sb.append(" }"); - return sb.toString(); - } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airport.java b/src/test/java/org/springframework/data/couchbase/domain/Airport.java index 9f895c823..d6208eeeb 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airport.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airport.java @@ -27,7 +27,7 @@ * @author Michael Reiche */ @Document -public class Airport { +public class Airport extends ComparableEntity { @Id String id; String iata; @@ -53,42 +53,4 @@ public String getIcao() { return icao; } - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{ id: "); - sb.append(getId()); - sb.append(", iata: "); - sb.append(iata); - sb.append(", icao: "); - sb.append(icao); - sb.append(" }"); - return sb.toString(); - } - - public boolean equals(Object o) { - if (o == null) { - return false; - } - if (!(o instanceof Airport)) { - return false; - } - Airport that = (Airport) o; - if (diff(this.id,that.id)) { - return false; - } - if (diff(this.iata,that.iata)) { - return false; - } - if (diff(this.icao,that.icao)) { - return false; - } - return true; - } - - private boolean diff(String s1, String s2){ - if ((s1 == null && s2 != null) || !s1.equals(s2)) { - return true; - } - return false; - } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/ComparableEntity.java b/src/test/java/org/springframework/data/couchbase/domain/ComparableEntity.java index 78cc8831a..d2d44a7cd 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ComparableEntity.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ComparableEntity.java @@ -16,7 +16,8 @@ package org.springframework.data.couchbase.domain; -import java.lang.reflect.Field; +import com.couchbase.mock.deps.com.google.gson.Gson; +import com.couchbase.mock.deps.com.google.gson.GsonBuilder; /** * Comparable entity base class for tests @@ -26,7 +27,7 @@ public class ComparableEntity { /** - * equals() method that recursively calls equals on on fields + * equals() method that relies on toString() * * @param that * @return @@ -41,54 +42,12 @@ public boolean equals(Object that) throws RuntimeException { || !(this.getClass().isAssignableFrom(that.getClass()) || that.getClass().isAssignableFrom(this.getClass()))) { return false; } - // check that all the fields in this have an equal field in that - for (Field f : this.getClass().getFields()) { - if (!same(f, this, that)) { - return false; - } - } - // check that all the fields in that have an equal field in this - for (Field f : that.getClass().getFields()) { - if (!same(f, that, this)) { - return false; - } - } - // check that all the declared fields in this have an equal field in that - for (Field f : this.getClass().getDeclaredFields()) { - if (!same(f, this, that)) { - return false; - } - } - // check that all the declared fields in that have an equal field in this - for (Field f : that.getClass().getDeclaredFields()) { - if (!same(f, that, this)) { - return false; - } - } - return true; - } + return this.toString().equals(that.toString()); - private static boolean same(Field f, Object a, Object b) { - Object thisField = null; - Object thatField = null; + } - try { - thisField = f.get(a); - thatField = f.get(b); - } catch (IllegalAccessException e) { - // assume that the important fields are in toString() - thisField = a.toString(); - thatField = b.toString(); - } - if (thisField == null && thatField == null) { - return true; - } - if (thisField == null && thatField != null) { - return false; - } - if (!thisField.equals(thatField)) { - return false; - } - return true; + public String toString() throws RuntimeException { + Gson gson = new GsonBuilder().create(); + return gson.toJson(this); } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/ConfigScoped.java b/src/test/java/org/springframework/data/couchbase/domain/ConfigScoped.java new file mode 100644 index 000000000..3513d6963 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/ConfigScoped.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 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.couchbase.domain; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; + +/** + * Configuration that uses a scope. This is a separate class as it is difficult to debug if you forget to unset the + * scopeName and the config is used for non-collection operations. + * + * @Author Michael Reiche + */ +@Configuration +@EnableCouchbaseRepositories +@EnableCouchbaseAuditing // this activates auditing +public class ConfigScoped extends Config { + + static String scopeName = null; + + @Override + protected String getScopeName() { + return scopeName; + } + + public static void setScopeName(String scopeName) { + ConfigScoped.scopeName = scopeName; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/Course.java b/src/test/java/org/springframework/data/couchbase/domain/Course.java index 6b7bc71f1..553a10233 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Course.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Course.java @@ -40,16 +40,4 @@ public String getId() { return id; } - public String toString() { - StringBuffer sb = new StringBuffer("Course("); - sb.append("id="); - sb.append(id); - sb.append(", userId="); - sb.append(userId); - sb.append(", room="); - sb.append(room); - sb.append(")"); - return sb.toString(); - } - } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Submission.java b/src/test/java/org/springframework/data/couchbase/domain/Submission.java index fddffde06..2929e3caf 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Submission.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Submission.java @@ -40,20 +40,4 @@ public String getId() { return id; } - public String toString() { - StringBuffer sb = new StringBuffer("Submission("); - sb.append("id="); - sb.append(id); - sb.append(", userId="); - sb.append(userId); - sb.append(", talkId="); - sb.append(talkId); - sb.append(", status="); - sb.append(status); - sb.append(", number="); - sb.append(number); - sb.append(")"); - return sb.toString(); - } - } diff --git a/src/test/java/org/springframework/data/couchbase/domain/User.java b/src/test/java/org/springframework/data/couchbase/domain/User.java index 028202ae2..6fa53f7b2 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/User.java +++ b/src/test/java/org/springframework/data/couchbase/domain/User.java @@ -35,7 +35,7 @@ */ @Document -public class User { +public class User extends ComparableEntity { @Version long version; @Id private String id; @@ -89,26 +89,9 @@ public void setVersion(long version) { this.version = version; } - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - User user = (User) o; - return Objects.equals(id, user.id) && Objects.equals(firstname, user.firstname) - && Objects.equals(lastname, user.lastname); - } - @Override public int hashCode() { return Objects.hash(id, firstname, lastname); } - @Override - public String toString() { - return "User{" + "id='" + id + '\'' + ", firstname='" + firstname + '\'' + ", lastname='" + lastname + '\'' - + ", createdBy='" + createdBy + '\'' + ", createdDate='" + createdDate + '\'' + ", lastModifiedBy='" - + lastModifiedBy + '\'' + ", lastModifiedDate='" + lastModifiedDate + '\'' + '}'; - } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserJustLastName.java b/src/test/java/org/springframework/data/couchbase/domain/UserJustLastName.java new file mode 100644 index 000000000..c5a806ac6 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserJustLastName.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2020 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.couchbase.domain; + +import java.util.Objects; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.couchbase.core.mapping.Document; + +/** + * User entity for tests + * + * @author Michael Nitschinger + * @author Michael Reiche + */ + +@Document +public class UserJustLastName extends ComparableEntity { + + @Id private String id; + private String lastname; + + public User user; + + @PersistenceConstructor + public UserJustLastName(final String id, final String lastname) { + this.id = id; + this.lastname = lastname; + this.user = new User("1", "first", "last"); + } + + public String getId() { + return id; + } + + public String getLastname() { + return lastname; + } + + @Override + public int hashCode() { + return Objects.hash(id, lastname); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionProjected.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionProjected.java new file mode 100644 index 000000000..90bc0810a --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionProjected.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020 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.couchbase.domain; + +import lombok.Data; + +import java.util.List; + +import org.springframework.data.annotation.TypeAlias; +import org.springframework.data.couchbase.core.index.CompositeQueryIndex; +import org.springframework.data.couchbase.core.mapping.Document; + +/** + * UserSubmission entity for tests + * + * @author Michael Reiche + */ +@Data +@Document +@TypeAlias("user") +@CompositeQueryIndex(fields = { "id", "username", "email" }) +public class UserSubmissionProjected extends ComparableEntity { + private String id; + private String username; + private List roles; + private Address address; + private List courses; + + public void setCourses(List courses) { + this.courses = courses; + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index 7a050cacd..a71a4fbe5 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -56,6 +56,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexExistsException; +import reactor.core.publisher.Flux; /** * Repository tests @@ -75,7 +76,7 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr @Autowired UserRepository userRepository; @BeforeEach - void beforeEach() { + public void beforeEach() { try { clientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName()); } catch (IndexExistsException ex) { @@ -171,12 +172,8 @@ void count() { String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" }; try { - for (int i = 0; i < iatas.length; i++) { - Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, - iatas[i].toLowerCase(Locale.ROOT) /* lcao */); - airportRepository.save(airport); - } + airportRepository.saveAll( Arrays.stream(iatas).map((iata) -> new Airport("airports::"+iata, iata, iata.toLowerCase(Locale.ROOT))).collect(Collectors.toSet())); Long count = airportRepository.countFancyExpression(asList("JFK"), asList("jfk"), false); assertEquals(1, count); @@ -201,10 +198,7 @@ void count() { assertEquals(0, airportCount); } finally { - for (int i = 0; i < iatas.length; i++) { - Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, iatas[i] /* lcao */); - airportRepository.delete(airport); - } + airportRepository.deleteAllById(Arrays.stream(iatas).map((iata) -> "airports::"+iata).collect(Collectors.toSet())); } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java index 1b98996c7..d6b582f7a 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java @@ -33,7 +33,6 @@ import java.util.concurrent.Future; import java.util.stream.Collectors; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; @@ -47,14 +46,12 @@ import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; import org.springframework.data.couchbase.util.Capabilities; -import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; import org.springframework.data.domain.PageRequest; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.couchbase.client.core.error.IndexExistsException; - /** * template class for Reactive Couchbase operations * @@ -63,22 +60,13 @@ */ @SpringJUnitConfig(ReactiveCouchbaseRepositoryQueryIntegrationTests.Config.class) @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) -public class ReactiveCouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegrationTests { +public class ReactiveCouchbaseRepositoryQueryIntegrationTests extends JavaIntegrationTests { @Autowired CouchbaseClientFactory clientFactory; @Autowired ReactiveAirportRepository airportRepository; // intellij flags "Could not Autowire", but it runs ok. @Autowired ReactiveUserRepository userRepository; // intellij flags "Could not Autowire", but it runs ok. - @BeforeEach - void beforeEach() { - try { - clientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName()); - } catch (IndexExistsException ex) { - // ignore, all good. - } - } - @Test void shouldSaveAndFindAll() { Airport vie = null; diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java index 5d8bfb963..621453deb 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java @@ -15,11 +15,16 @@ */ package org.springframework.data.couchbase.util; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import com.couchbase.client.core.env.Authenticator; @@ -48,12 +53,18 @@ public static TestClusterConfig config() { return testClusterConfig; } - public static Authenticator authenticator() { + protected static Authenticator authenticator() { return PasswordAuthenticator.create(config().adminUsername(), config().adminPassword()); } - public static String username() { return config().adminUsername(); } - public static String password() { return config().adminPassword(); } + public static String username() { + return config().adminUsername(); + } + + public static String password() { + return config().adminPassword(); + } + public static String bucketName() { return config().bucketname(); } @@ -64,39 +75,71 @@ public static String bucketName() { * @return the connection string to connect. */ public static String connectionString() { - /* - return seedNodes().stream().map(s -> { - if (s.kvPort().isPresent()) { - return s.address() + ":" + s.kvPort().get() + "=" + Services.KV; - } else if (s.clusterManagerPort().isPresent()) { - return s.address() + ":" + s.clusterManagerPort().get() + "=" + Services.MANAGER; - } else { - return s.address() ; - } - }).collect(Collectors.joining(",")); - */ StringBuffer sb = new StringBuffer(); - for(SeedNode s:seedNodes()) { + for (SeedNode s : seedNodes()) { if (s.kvPort().isPresent()) { - if(sb.length() > 0 ) sb.append(","); - sb.append (s.address() + ":" + s.kvPort().get() + "=" + Services.KV); + if (sb.length() > 0) + sb.append(","); + sb.append(s.address() + ":" + s.kvPort().get() + "=" + Services.KV); } if (s.clusterManagerPort().isPresent()) { if (sb.length() > 0) sb.append(","); sb.append(s.address() + ":" + s.clusterManagerPort().get() + "=" + Services.MANAGER); } - if(sb.length() == 0 ){ + if (sb.length() == 0) { sb.append(s.address()); } } return sb.toString(); } - public static Set seedNodes() { + protected static Set seedNodes() { return config().nodes().stream().map(cfg -> SeedNode.create(cfg.hostname(), Optional.ofNullable(cfg.ports().get(Services.KV)), Optional.ofNullable(cfg.ports().get(Services.MANAGER)))) .collect(Collectors.toSet()); } + @BeforeAll() + public static void beforeAll() {} + + @AfterAll + public static void afterAll() {} + + @BeforeEach + public void beforeEach() {} + + @AfterEach + public void afterEach() {} + + /** + * This should probably be the first call in the @BeforeAll method of a test class. + * This will call super.beforeAll() when called as callSuperBeforeAll(new Object() {}); this trickery is necessary + * because super.beforeAll() cannot be used because it is a static method. it is possible and likely that the + * beforeAll() method of should still be called even when a test class defines its own beforeAll() method which would + * hide the beforeAll() of the super class. + * This trickery is not necessary for before/AfterEach, as those are not static methods + * + * @Author Michael Reiche + * + * @param createdHere - an object from a class defined in the calling class + */ + public static void callSuperBeforeAll(Object createdHere) { + callSuper(createdHere, "beforeAll"); + } + + // see comments for callSuperBeforeAll() + public static void callSuperAfterAll(Object createdHere) { + callSuper(createdHere, "afterAll"); + } + + private static void callSuper(Object createdHere, String methodName) { + try { + Method method = createdHere.getClass().getEnclosingClass().getSuperclass().getMethod(methodName); + method.invoke(null); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } diff --git a/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java new file mode 100644 index 000000000..c0e8258c5 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2021 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.couchbase.util; + +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; +import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE; + +import java.time.Duration; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.domain.Config; + +import com.couchbase.client.core.service.ServiceType; +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.ClusterOptions; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.manager.collection.CollectionManager; +import org.springframework.data.couchbase.domain.ConfigScoped; + +/** + * Provides Collection support for integration tests + * + * @Author Michael Reiche + */ +public class CollectionAwareIntegrationTests extends JavaIntegrationTests { + + public static String scopeName = "scope_" + randomString(); + public static String collectionName = "collection_" + randomString(); + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + ClusterEnvironment environment = environment().build(); + Cluster cluster = Cluster.connect(seedNodes(), + ClusterOptions.clusterOptions(authenticator()).environment(environment)); + Bucket bucket = cluster.bucket(config().bucketname()); + bucket.waitUntilReady(Duration.ofSeconds(5)); + waitForService(bucket, ServiceType.QUERY); + waitForQueryIndexerToHaveBucket(cluster, config().bucketname()); + CollectionManager collectionManager = bucket.collections(); + if (scopeName != null || collectionName != null) { + setupScopeCollection(cluster, scopeName, collectionName, collectionManager); + } + + ConfigScoped.setScopeName(scopeName); + ApplicationContext ac = new AnnotationConfigApplicationContext(ConfigScoped.class); + couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); + } + + @AfterAll + public static void afterAll(){ + System.out.println("CollectionAwareIntegrationTests.afterAll()"); + ConfigScoped.setScopeName(null); + callSuperBeforeAll(new Object() {}); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java new file mode 100644 index 000000000..c4589e085 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/util/JavaIntegrationTests.java @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2018 Couchbase, Inc. + * + * 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.couchbase.util; + +import static com.couchbase.client.core.util.CbThrowables.hasCause; +import static com.couchbase.client.core.util.CbThrowables.throwIfUnchecked; +import static com.couchbase.client.java.AsyncUtils.block; +import static com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; +import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE; +import static org.springframework.data.couchbase.util.Util.waitUntilCondition; + +import java.io.IOException; +import java.time.Duration; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Timeout; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; + +import com.couchbase.client.core.diagnostics.PingResult; +import com.couchbase.client.core.diagnostics.PingState; +import com.couchbase.client.core.error.CollectionNotFoundException; +import com.couchbase.client.core.error.CouchbaseException; +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.core.error.IndexExistsException; +import com.couchbase.client.core.error.ParsingFailureException; +import com.couchbase.client.core.error.QueryException; +import com.couchbase.client.core.error.ScopeNotFoundException; +import com.couchbase.client.core.error.UnambiguousTimeoutException; +import com.couchbase.client.core.json.Mapper; +import com.couchbase.client.core.service.ServiceType; +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.ClusterOptions; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.CommonOptions; +import com.couchbase.client.java.Scope; +import com.couchbase.client.java.diagnostics.PingOptions; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.manager.collection.CollectionManager; +import com.couchbase.client.java.manager.collection.CollectionSpec; +import com.couchbase.client.java.manager.collection.ScopeSpec; +import com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions; +import com.couchbase.client.java.manager.search.SearchIndex; +import com.couchbase.client.java.manager.search.UpsertSearchIndexOptions; +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.search.SearchQuery; +import com.couchbase.client.java.search.result.SearchResult; +import org.springframework.data.couchbase.domain.Config; + +/** + * Extends the {@link ClusterAwareIntegrationTests} with java-client specific code. + * + * @Author Michael Reiche + */ +// Temporarily increased timeout to (possibly) workaround MB-37011 when Developer Preview enabled +@Timeout(value = 10, unit = TimeUnit.MINUTES) // Safety timer so tests can't block CI executors +public class JavaIntegrationTests extends ClusterAwareIntegrationTests { + + @Autowired static public CouchbaseTemplate couchbaseTemplate; + @Autowired static public ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + + @BeforeAll + public static void beforeAll() { + callSuperBeforeAll(new Object() {}); + try (CouchbaseClientFactory couchbaseClientFactory = new SimpleCouchbaseClientFactory(connectionString(), + authenticator(), bucketName())) { + couchbaseClientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(), + CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions().ignoreIfExists(true)); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + reactiveCouchbaseTemplate = (ReactiveCouchbaseTemplate) ac.getBean(REACTIVE_COUCHBASE_TEMPLATE); + } + + /** + * Creates a {@link ClusterEnvironment.Builder} which already has the seed nodes and credentials plugged and ready to + * use depending on the environment. + * + * @return the builder, ready to be further modified or used directly. + */ + protected static ClusterEnvironment.Builder environment() { + return ClusterEnvironment.builder(); + } + + /** + * Returns the pre-set cluster options with the environment and authenticator configured. + * + * @return the cluster options ready to be used. + */ + protected static ClusterOptions clusterOptions() { + return ClusterOptions.clusterOptions(authenticator()).environment(environment().build()); + } + + /** + * Helper method to create a primary index if it does not exist. + */ + protected static void createPrimaryIndex(final Cluster cluster, final String bucketName) { + cluster.queryIndexes().createPrimaryIndex(bucketName, createPrimaryQueryIndexOptions().ignoreIfExists(true)); + } + + public static void setupScopeCollection(Cluster cluster, String scopeName, String collectionName, + CollectionManager collectionManager) { + // Create the scope.collection (borrowed from CollectionManagerIntegrationTest ) + ScopeSpec scopeSpec = ScopeSpec.create(scopeName); + CollectionSpec collSpec = CollectionSpec.create(collectionName, scopeName); + + if (!scopeName.equals("_default")) { + collectionManager.createScope(scopeName); + } + + waitUntilCondition(() -> scopeExists(collectionManager, scopeName)); + ScopeSpec found = collectionManager.getScope(scopeName); + assertEquals(scopeSpec, found); + + collectionManager.createCollection(collSpec); + waitUntilCondition(() -> collectionExists(collectionManager, collSpec)); + waitUntilCondition( + () -> collectionReady(cluster.bucket(config().bucketname()).scope(scopeName).collection(collectionName))); + + assertNotEquals(scopeSpec, collectionManager.getScope(scopeName)); + assertTrue(collectionManager.getScope(scopeName).collections().contains(collSpec)); + + waitForQueryIndexerToHaveBucket(cluster, collectionName); + + // the call to createPrimaryIndex takes about 60 seconds + + try { + block(createPrimaryIndex(cluster, config().bucketname(), scopeName, collectionName)); + } catch (Exception e) { + e.printStackTrace(); + } + + waitUntilCondition( + () -> collectionReadyQuery(cluster.bucket(config().bucketname()).scope(scopeName), collectionName)); + } + + protected static void waitForQueryIndexerToHaveBucket(final Cluster cluster, final String bucketName) { + boolean ready = false; + int guard = 100; + + while (!ready && guard != 0) { + guard -= 1; + String statement = "SELECT COUNT(*) > 0 as present FROM system:keyspaces where name = '" + bucketName + "';"; + + QueryResult queryResult = cluster.query(statement); + List rows = queryResult.rowsAsObject(); + if (rows.size() == 1 && rows.get(0).getBoolean("present")) { + ready = true; + } + + if (!ready) { + try { + Thread.sleep(50); + } catch (InterruptedException e) {} + } + } + + if (guard == 0) { + throw new IllegalStateException("Query indexer is still not aware of bucket " + bucketName); + } + } + + /** + * Improve test stability by waiting for a given service to report itself ready. + */ + protected static void waitForService(final Bucket bucket, final ServiceType serviceType) { + bucket.waitUntilReady(Duration.ofSeconds(30)); + + Util.waitUntilCondition(() -> { + PingResult pingResult = bucket.ping(PingOptions.pingOptions().serviceTypes(Collections.singleton(serviceType))); + + return pingResult.endpoints().containsKey(serviceType) && pingResult.endpoints().get(serviceType).size() > 0 + && pingResult.endpoints().get(serviceType).get(0).state() == PingState.OK; + }); + } + + public static boolean collectionExists(CollectionManager mgr, CollectionSpec spec) { + try { + ScopeSpec scope = mgr.getScope(spec.scopeName()); + return scope.collections().contains(spec); + } catch (CollectionNotFoundException e) { + return false; + } + } + + public static boolean collectionReady(Collection collection) { + try { + collection.get("123"); + return true; + } catch (DocumentNotFoundException dnfe) { + return true; + } catch (UnambiguousTimeoutException e) { + if (!e.toString().contains("COLLECTION_NOT_FOUND")) { + throw e; + } + return false; + } + } + + public static boolean collectionReadyQuery(Scope scope, String collectionName) { + try { + scope.query("select * from `" + collectionName + "` where meta().id=\"1\""); + return true; + } catch (DocumentNotFoundException dnfe) { + return true; + } catch (ParsingFailureException e) { + return false; + } + } + + public static boolean scopeExists(CollectionManager mgr, String scopeName) { + try { + mgr.getScope(scopeName); + return true; + } catch (ScopeNotFoundException e) { + return false; + } + } + + public static CompletableFuture createPrimaryIndex(Cluster cluster, String bucketName, String scopeName, + String collectionName) { + CreatePrimaryQueryIndexOptions options = CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions(); + options.timeout(Duration.ofSeconds(300)); + final CreatePrimaryQueryIndexOptions.Built builtOpts = options.build(); + final String indexName = builtOpts.indexName().orElse(null); + + String keyspace = "default:`" + bucketName + "`.`" + scopeName + "`.`" + collectionName + "`"; + String statement = "CREATE PRIMARY INDEX "; + if (indexName != null) { + statement += (indexName) + " "; + } + statement += "ON " + (keyspace); // do not quote, this might be "default:bucketName.scopeName.collectionName" + + return exec(cluster, false, statement, builtOpts.with(), builtOpts).exceptionally(t -> { + if (builtOpts.ignoreIfExists() && hasCause(t, IndexExistsException.class)) { + return null; + } + throwIfUnchecked(t); + throw new RuntimeException(t); + }).thenApply(result -> null); + } + + private static CompletableFuture exec(Cluster cluster, + /*AsyncQueryIndexManager.QueryType queryType*/ boolean queryType, CharSequence statement, + Map with, CommonOptions.BuiltCommonOptions options) { + return with.isEmpty() ? exec(cluster, queryType, statement, options) + : exec(cluster, queryType, statement + " WITH " + Mapper.encodeAsString(with), options); + } + + private static CompletableFuture exec(Cluster cluster, + /*AsyncQueryIndexManager.QueryType queryType,*/ boolean queryType, CharSequence statement, + CommonOptions.BuiltCommonOptions options) { + QueryOptions queryOpts = toQueryOptions(options).readonly(queryType /*requireNonNull(queryType) == READ_ONLY*/); + + return cluster.async().query(statement.toString(), queryOpts).exceptionally(t -> { + throw translateException(t); + }); + } + + private static QueryOptions toQueryOptions(CommonOptions.BuiltCommonOptions options) { + QueryOptions result = QueryOptions.queryOptions(); + options.timeout().ifPresent(result::timeout); + options.retryStrategy().ifPresent(result::retryStrategy); + return result; + } + + private static final Map, Function> errorMessageMap = new LinkedHashMap<>(); + + private static RuntimeException translateException(Throwable t) { + if (t instanceof QueryException) { + final QueryException e = ((QueryException) t); + + for (Map.Entry, Function> entry : errorMessageMap + .entrySet()) { + if (entry.getKey().test(e)) { + return entry.getValue().apply(e); + } + } + } + return (t instanceof RuntimeException) ? (RuntimeException) t : new RuntimeException(t); + } + + public static void createFtsCollectionIndex(Cluster cluster, String indexName, String bucketName, String scopeName, + String collectionName) { + SearchIndex searchIndex = new SearchIndex(indexName, bucketName); + if (scopeName != null) { + // searchIndex = searchIndex.forScopeCollection(scopeName, collectionName); + throw new RuntimeException("forScopeCollection not implemented in current java client version"); + } + + cluster.searchIndexes().upsertIndex(searchIndex, + UpsertSearchIndexOptions.upsertSearchIndexOptions().timeout(Duration.ofSeconds(60))); + + int maxTries = 5; + for (int i = 0; i < maxTries; i++) { + try { + SearchResult result = cluster.searchQuery(indexName, SearchQuery.queryString("junk")); + break; + } catch (CouchbaseException | IllegalStateException ex) { + // this is a pretty dirty hack to avoid a race where we don't know if the index is ready yet + System.out.println("createFtsCollectionIndex: " + i + " " + ex); + if (i < (maxTries - 1) && (ex.getMessage().contains("no planPIndexes for indexName") + || ex.getMessage().contains("pindex_consistency mismatched partition") + || ex.getMessage().contains("pindex not available"))) { + sleepMs(1000); + continue; + } + throw ex; + } + } + } + + public static String randomString() { + return UUID.randomUUID().toString().substring(0, 8); + } + + public static void sleepMs(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException ie) {} + } +} diff --git a/src/test/java/org/springframework/data/couchbase/util/Util.java b/src/test/java/org/springframework/data/couchbase/util/Util.java new file mode 100644 index 000000000..0e9f49074 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/util/Util.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2018 Couchbase, Inc. + * + * 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.couchbase.util; + +import java.io.InputStream; +import java.time.Duration; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.awaitility.Awaitility.with; + +/** + * Provides a bunch of utility APIs that help with testing. + */ +public class Util { + + /** + * Waits and sleeps for a little bit of time until the given condition is met. + * + *

Sleeps 1ms between "false" invocations. It will wait at most one minute to prevent hanging forever in case + * the condition never becomes true.

+ * + * @param supplier return true once it should stop waiting. + */ + public static void waitUntilCondition(final BooleanSupplier supplier) { + waitUntilCondition(supplier, Duration.ofMinutes(1)); + } + + public static void waitUntilCondition(final BooleanSupplier supplier, Duration atMost) { + with().pollInterval(Duration.ofMillis(1)).await().atMost(atMost).until(supplier::getAsBoolean); + } + + public static void waitUntilCondition(final BooleanSupplier supplier, Duration atMost, Duration delay) { + with().pollInterval(delay).await().atMost(atMost).until(supplier::getAsBoolean); + } + + public static void waitUntilThrows(final Class clazz, final Supplier supplier) { + with() + .pollInterval(Duration.ofMillis(1)) + .await() + .atMost(Duration.ofMinutes(1)) + .until(() -> { + try { + supplier.get(); + } catch (final Exception ex) { + return ex.getClass().isAssignableFrom(clazz); + } + return false; + }); + } + + /** + * Returns true if a thread with the given name is currently running. + * + * @param name the name of the thread. + * @return true if running, false otherwise. + */ + public static boolean threadRunning(final String name) { + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + + /** + * Reads a file from the resources folder (in the same path as the requesting test class). + * + *

The class will be automatically loaded relative to the namespace and converted + * to a string.

+ * + * @param filename the filename of the resource. + * @param clazz the reference class. + * @return the loaded string. + */ + public static String readResource(final String filename, final Class clazz) { + String path = "/" + clazz.getPackage().getName().replace(".", "/") + "/" + filename; + InputStream stream = clazz.getResourceAsStream(path); + java.util.Scanner s = new java.util.Scanner(stream, UTF_8.name()).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } + +}