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() : ""; + } + +}