diff --git a/pom.xml b/pom.xml index f6a6789f3..5e1661471 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,13 @@ test + + io.projectreactor + reactor-test + 3.1.0.RELEASE + test + + com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperationSupport.java index b8a63a9ec..3841fa647 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperationSupport.java @@ -15,6 +15,8 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.core.ReactiveExistsByIdOperationSupport.ReactiveExistsByIdSupport; + import java.util.Collection; import java.util.Map; @@ -36,12 +38,11 @@ public ExecutableExistsById existsById() { static class ExecutableExistsByIdSupport implements ExecutableExistsById { private final CouchbaseTemplate template; - private final ReactiveExistsByIdOperationSupport.ReactiveExistsByIdSupport reactiveSupport; + private final ReactiveExistsByIdSupport reactiveSupport; ExecutableExistsByIdSupport(final CouchbaseTemplate template, final String collection) { this.template = template; - this.reactiveSupport = new ReactiveExistsByIdOperationSupport.ReactiveExistsByIdSupport(template.reactive(), - collection); + this.reactiveSupport = new ReactiveExistsByIdSupport(template.reactive(), collection); } @Override 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 0be1cb59b..2a3568a7c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java @@ -15,13 +15,14 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.core.ReactiveFindByAnalyticsOperationSupport.ReactiveFindByAnalyticsSupport; + import java.util.List; import java.util.stream.Stream; -import com.couchbase.client.java.analytics.AnalyticsScanConsistency; -import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.data.couchbase.core.query.AnalyticsQuery; -import org.springframework.data.couchbase.core.query.Query; + +import com.couchbase.client.java.analytics.AnalyticsScanConsistency; public class ExecutableFindByAnalyticsOperationSupport implements ExecutableFindByAnalyticsOperation { @@ -35,14 +36,15 @@ public ExecutableFindByAnalyticsOperationSupport(final CouchbaseTemplate templat @Override public ExecutableFindByAnalytics findByAnalytics(final Class domainType) { - return new ExecutableFindByAnalyticsSupport<>(template, domainType, ALL_QUERY, AnalyticsScanConsistency.NOT_BOUNDED); + return new ExecutableFindByAnalyticsSupport<>(template, domainType, ALL_QUERY, + AnalyticsScanConsistency.NOT_BOUNDED); } static class ExecutableFindByAnalyticsSupport implements ExecutableFindByAnalytics { private final CouchbaseTemplate template; private final Class domainType; - private final ReactiveFindByAnalyticsOperationSupport.ReactiveFindByAnalyticsSupport reactiveSupport; + private final ReactiveFindByAnalyticsSupport reactiveSupport; private final AnalyticsQuery query; private final AnalyticsScanConsistency scanConsistency; @@ -51,8 +53,8 @@ static class ExecutableFindByAnalyticsSupport implements ExecutableFindByAnal this.template = template; this.domainType = domainType; this.query = query; - this.reactiveSupport = new ReactiveFindByAnalyticsOperationSupport.ReactiveFindByAnalyticsSupport<>( - template.reactive(), domainType, query, scanConsistency); + this.reactiveSupport = new ReactiveFindByAnalyticsSupport<>(template.reactive(), domainType, query, + scanConsistency); this.scanConsistency = scanConsistency; } 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 ad4a333b2..8f8ca26a6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -15,6 +15,8 @@ */ 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; @@ -40,15 +42,14 @@ static class ExecutableFindByIdSupport implements ExecutableFindById { private final Class domainType; private final String collection; private final List fields; - private final ReactiveFindByIdOperationSupport.ReactiveFindByIdSupport reactiveSupport; + private final ReactiveFindByIdSupport reactiveSupport; ExecutableFindByIdSupport(CouchbaseTemplate template, Class domainType, String collection, List fields) { this.template = template; this.domainType = domainType; this.collection = collection; this.fields = fields; - this.reactiveSupport = new ReactiveFindByIdOperationSupport.ReactiveFindByIdSupport<>(template.reactive(), - domainType, collection, fields); + this.reactiveSupport = new ReactiveFindByIdSupport<>(template.reactive(), domainType, collection, fields); } @Override 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 5078d7019..883b6e91f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperation.java @@ -130,6 +130,17 @@ interface FindByQueryConsistentWith extends FindByQueryWithQuery { } - interface ExecutableFindByQuery extends FindByQueryConsistentWith {} + interface FindByQueryInCollection extends FindByQueryConsistentWith { + + /** + * Allows to override the default scan consistency. + * + * @param collection the collection to use for this query. + */ + FindByQueryInCollection inCollection(String collection); + + } + + interface ExecutableFindByQuery extends FindByQueryInCollection {} } 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 7f569501b..a5c4838c1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -18,11 +18,17 @@ import java.util.List; import java.util.stream.Stream; +import org.springframework.data.couchbase.core.ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport; import org.springframework.data.couchbase.core.query.Query; import com.couchbase.client.java.query.QueryScanConsistency; -import org.springframework.data.couchbase.core.ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport; +/** + * {@link ExecutableFindByQueryOperation} implementations for Couchbase. + * + * @author Michael Nitschinger + * @author Michael Reiche + */ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQueryOperation { private static final Query ALL_QUERY = new Query(); @@ -35,7 +41,8 @@ public ExecutableFindByQueryOperationSupport(final CouchbaseTemplate template) { @Override public ExecutableFindByQuery findByQuery(final Class domainType) { - return new ExecutableFindByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED); + return new ExecutableFindByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED, + "_default._default"); } static class ExecutableFindByQuerySupport implements ExecutableFindByQuery { @@ -43,16 +50,19 @@ static class ExecutableFindByQuerySupport implements ExecutableFindByQuery private final CouchbaseTemplate template; private final Class domainType; private final Query query; - private final ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport reactiveSupport; + private final ReactiveFindByQuerySupport reactiveSupport; private final QueryScanConsistency scanConsistency; + private final String collection; ExecutableFindByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency) { + final QueryScanConsistency scanConsistency, final String collection) { this.template = template; this.domainType = domainType; this.query = query; - this.reactiveSupport = new ReactiveFindByQuerySupport(template.reactive(), domainType, query, scanConsistency); + this.reactiveSupport = new ReactiveFindByQuerySupport(template.reactive(), domainType, query, scanConsistency, + collection); this.scanConsistency = scanConsistency; + this.collection = collection; } @Override @@ -78,12 +88,17 @@ public TerminatingFindByQuery matching(final Query query) { } else { scanCons = scanConsistency; } - return new ExecutableFindByQuerySupport<>(template, domainType, query, scanCons); + return new ExecutableFindByQuerySupport<>(template, domainType, query, scanCons, collection); } @Override public FindByQueryConsistentWith consistentWith(final QueryScanConsistency scanConsistency) { - return new ExecutableFindByQuerySupport<>(template, domainType, query, scanConsistency); + return new ExecutableFindByQuerySupport<>(template, domainType, query, scanConsistency, collection); + } + + @Override + public FindByQueryInCollection inCollection(final String collection) { + return new ExecutableFindByQuerySupport<>(template, domainType, query, scanConsistency, collection); } @Override 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 54e7f2c87..beefe1046 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperationSupport.java @@ -17,6 +17,7 @@ import java.util.Collection; +import org.springframework.data.couchbase.core.ReactiveFindFromReplicasByIdOperationSupport.ReactiveFindFromReplicasByIdSupport; import org.springframework.util.Assert; public class ExecutableFindFromReplicasByIdOperationSupport implements ExecutableFindFromReplicasByIdOperation { @@ -37,14 +38,13 @@ static class ExecutableFindFromReplicasByIdSupport implements ExecutableFindF private final CouchbaseTemplate template; private final Class domainType; private final String collection; - private final ReactiveFindFromReplicasByIdOperationSupport.ReactiveFindFromReplicasByIdSupport reactiveSupport; + private final ReactiveFindFromReplicasByIdSupport reactiveSupport; ExecutableFindFromReplicasByIdSupport(CouchbaseTemplate template, Class domainType, String collection) { this.template = template; this.domainType = domainType; this.collection = collection; - this.reactiveSupport = new ReactiveFindFromReplicasByIdOperationSupport.ReactiveFindFromReplicasByIdSupport<>( - template.reactive(), domainType, collection); + this.reactiveSupport = new ReactiveFindFromReplicasByIdSupport<>(template.reactive(), domainType, collection); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java index 18fa5b382..7405fad64 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java @@ -18,6 +18,7 @@ import java.time.Duration; import java.util.Collection; +import org.springframework.data.couchbase.core.ReactiveInsertByIdOperationSupport.ReactiveInsertByIdSupport; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; @@ -48,7 +49,7 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; - private final ReactiveInsertByIdOperationSupport.ReactiveInsertByIdSupport reactiveSupport; + private final ReactiveInsertByIdSupport reactiveSupport; ExecutableInsertByIdSupport(final CouchbaseTemplate template, final Class domainType, final String collection, final PersistTo persistTo, final ReplicateTo replicateTo, final DurabilityLevel durabilityLevel, @@ -60,8 +61,8 @@ static class ExecutableInsertByIdSupport implements ExecutableInsertById { this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.expiry = expiry; - this.reactiveSupport = new ReactiveInsertByIdOperationSupport.ReactiveInsertByIdSupport<>(template.reactive(), - domainType, collection, persistTo, replicateTo, durabilityLevel, expiry); + this.reactiveSupport = new ReactiveInsertByIdSupport<>(template.reactive(), domainType, collection, persistTo, + replicateTo, durabilityLevel, expiry); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java index 27fed458c..3d1f10778 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -18,6 +18,7 @@ import java.util.Collection; import java.util.List; +import org.springframework.data.couchbase.core.ReactiveRemoveByIdOperationSupport.ReactiveRemoveByIdSupport; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.DurabilityLevel; @@ -44,7 +45,7 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { private final PersistTo persistTo; private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; - private final ReactiveRemoveByIdOperationSupport.ReactiveRemoveByIdSupport reactiveRemoveByIdSupport; + private final ReactiveRemoveByIdSupport reactiveRemoveByIdSupport; ExecutableRemoveByIdSupport(final CouchbaseTemplate template, final String collection, final PersistTo persistTo, final ReplicateTo replicateTo, final DurabilityLevel durabilityLevel) { @@ -53,8 +54,8 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { this.persistTo = persistTo; this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; - this.reactiveRemoveByIdSupport = new ReactiveRemoveByIdOperationSupport.ReactiveRemoveByIdSupport( - template.reactive(), collection, persistTo, replicateTo, durabilityLevel); + this.reactiveRemoveByIdSupport = new ReactiveRemoveByIdSupport(template.reactive(), collection, persistTo, + replicateTo, durabilityLevel); } @Override 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 397535e22..9b50fdedb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperation.java @@ -43,6 +43,12 @@ interface RemoveByQueryConsistentWith extends RemoveByQueryWithQuery { } - interface ExecutableRemoveByQuery extends RemoveByQueryConsistentWith {} + interface RemoveByQueryInCollection extends RemoveByQueryConsistentWith { + + RemoveByQueryConsistentWith inCollection(String collection); + + } + + interface ExecutableRemoveByQuery extends RemoveByQueryInCollection {} } 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 f1c4eced5..32e98a98a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java @@ -17,6 +17,7 @@ import java.util.List; +import org.springframework.data.couchbase.core.ReactiveRemoveByQueryOperationSupport.ReactiveRemoveByQuerySupport; import org.springframework.data.couchbase.core.query.Query; import com.couchbase.client.java.query.QueryScanConsistency; @@ -33,7 +34,8 @@ public ExecutableRemoveByQueryOperationSupport(final CouchbaseTemplate template) @Override public ExecutableRemoveByQuery removeByQuery(Class domainType) { - return new ExecutableRemoveByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED); + return new ExecutableRemoveByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED, + "_default._default"); } static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuery { @@ -41,17 +43,19 @@ static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuer private final CouchbaseTemplate template; private final Class domainType; private final Query query; - private final ReactiveRemoveByQueryOperationSupport.ReactiveRemoveByQuerySupport reactiveSupport; + private final ReactiveRemoveByQuerySupport reactiveSupport; private final QueryScanConsistency scanConsistency; + private final String collection; ExecutableRemoveByQuerySupport(final CouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency) { + final QueryScanConsistency scanConsistency, String collection) { this.template = template; this.domainType = domainType; this.query = query; - this.reactiveSupport = new ReactiveRemoveByQueryOperationSupport.ReactiveRemoveByQuerySupport<>( - template.reactive(), domainType, query, scanConsistency); + this.reactiveSupport = new ReactiveRemoveByQuerySupport<>(template.reactive(), domainType, query, scanConsistency, + collection); this.scanConsistency = scanConsistency; + this.collection = collection; } @Override @@ -61,12 +65,17 @@ public List all() { @Override public TerminatingRemoveByQuery matching(final Query query) { - return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency); + return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, collection); } @Override public RemoveByQueryWithQuery consistentWith(final QueryScanConsistency scanConsistency) { - return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency); + return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, collection); + } + + @Override + public RemoveByQueryInCollection inCollection(final String collection) { + return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, collection); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java index 888c998f4..399fa6969 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java @@ -19,6 +19,7 @@ import java.util.Collection; import org.springframework.util.Assert; +import org.springframework.data.couchbase.core.ReactiveReplaceByIdOperationSupport.ReactiveReplaceByIdSupport; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; @@ -48,7 +49,7 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; - private final ReactiveReplaceByIdOperationSupport.ReactiveReplaceByIdSupport reactiveSupport; + private final ReactiveReplaceByIdSupport reactiveSupport; ExecutableReplaceByIdSupport(final CouchbaseTemplate template, final Class domainType, final String collection, final PersistTo persistTo, final ReplicateTo replicateTo, final DurabilityLevel durabilityLevel, @@ -60,7 +61,7 @@ static class ExecutableReplaceByIdSupport implements ExecutableReplaceById this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.expiry = expiry; - this.reactiveSupport = new ReactiveReplaceByIdOperationSupport.ReactiveReplaceByIdSupport<>(template.reactive(), + this.reactiveSupport = new ReactiveReplaceByIdSupport<>(template.reactive(), domainType, collection, persistTo, replicateTo, durabilityLevel, expiry); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java index 3c4c70c67..9ec260ce8 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java @@ -19,6 +19,7 @@ import java.util.Collection; import org.springframework.util.Assert; +import org.springframework.data.couchbase.core.ReactiveUpsertByIdOperationSupport.ReactiveUpsertByIdSupport; import com.couchbase.client.core.msg.kv.DurabilityLevel; import com.couchbase.client.java.kv.PersistTo; @@ -48,7 +49,7 @@ static class ExecutableUpsertByIdSupport implements ExecutableUpsertById { private final ReplicateTo replicateTo; private final DurabilityLevel durabilityLevel; private final Duration expiry; - private final ReactiveUpsertByIdOperationSupport.ReactiveUpsertByIdSupport reactiveSupport; + private final ReactiveUpsertByIdSupport reactiveSupport; ExecutableUpsertByIdSupport(final CouchbaseTemplate template, final Class domainType, final String collection, final PersistTo persistTo, final ReplicateTo replicateTo, final DurabilityLevel durabilityLevel, @@ -60,7 +61,7 @@ static class ExecutableUpsertByIdSupport implements ExecutableUpsertById { this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; this.expiry = expiry; - this.reactiveSupport = new ReactiveUpsertByIdOperationSupport.ReactiveUpsertByIdSupport<>(template.reactive(), + this.reactiveSupport = new ReactiveUpsertByIdSupport<>(template.reactive(), domainType, collection, persistTo, replicateTo, durabilityLevel, expiry); } 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 598048ba5..73de94c30 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -17,7 +17,6 @@ import com.couchbase.client.java.analytics.AnalyticsOptions; import com.couchbase.client.java.analytics.AnalyticsScanConsistency; -import com.couchbase.client.java.query.QueryOptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; 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 309431f9c..1b99af3c7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -15,14 +15,20 @@ */ package org.springframework.data.couchbase.core; -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.Query; import com.couchbase.client.java.query.QueryScanConsistency; +/** + * ReactiveFindByQueryOperation + * + * @author Michael Nitschinger + * @author Michael Reiche + */ public interface ReactiveFindByQueryOperation { /** @@ -104,6 +110,129 @@ interface FindByQueryConsistentWith extends FindByQueryWithQuery { } - interface ReactiveFindByQuery extends FindByQueryConsistentWith {} + /** + * Collection override (optional). + */ + interface FindByQueryInCollection extends FindByQueryWithQuery { + + /** + * 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}. + * @throws IllegalArgumentException if collection is {@literal null}. + */ + FindByQueryInCollection inCollection(String collection); + } + + /** + * Result type override (optional). + */ + interface FindWithProjection extends FindByQueryInCollection, FindDistinct { + + /** + * Define the target type fields should be mapped to.
+ * Skip this step if you are anyway only interested in the original domain type. + * + * @param resultType must not be {@literal null}. + * @param result type. + * @return new instance of {@link FindWithProjection}. + * @throws IllegalArgumentException if resultType is {@literal null}. + */ + FindByQueryWithQuery as(Class resultType); + } + + /** + * Distinct Find support. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface FindDistinct { + + /** + * 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 { + + /** + * 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 }.
+ *
+ * + * @param resultType must not be {@literal null}. + * @param result type. + * @return new instance of {@link TerminatingDistinct}. + * @throws IllegalArgumentException if resultType is {@literal null}. + */ + TerminatingDistinct as(Class resultType); + } + + /** + * Result restrictions. Optional. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface DistinctWithQuery extends DistinctWithProjection { + + /** + * Set the filter {@link Query criteria} to be used. + * + * @param query must not be {@literal null}. + * @return new instance of {@link TerminatingDistinct}. + * @throws IllegalArgumentException if criteria is {@literal null}. + * @since 3.0 + */ + TerminatingDistinct matching(Query query); + } + + /** + * Terminating distinct find operations. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface TerminatingDistinct extends DistinctWithQuery { + + /** + * Get all matching distinct field values. + * + * @return empty {@link Flux} if not match found. Never {@literal null}. + */ + Flux all(); + } + + interface ReactiveFindByQuery extends FindByQueryConsistentWith, FindByQueryInCollection, FindDistinct {} } 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 a538be87d..104c19110 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -15,16 +15,18 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.core.support.TemplateUtils; -import org.springframework.data.couchbase.core.query.Query; - import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.support.TemplateUtils; + import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; /** + * {@link ReactiveFindByQueryOperation} implementations for Couchbase. + * * @author Michael Nitschinger * @author Michael Reiche */ @@ -40,7 +42,8 @@ public ReactiveFindByQueryOperationSupport(final ReactiveCouchbaseTemplate templ @Override public ReactiveFindByQuery findByQuery(final Class domainType) { - return new ReactiveFindByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED); + return new ReactiveFindByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED, + "_default._default"); } static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { @@ -49,13 +52,15 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { private final Class domainType; private final Query query; private final QueryScanConsistency scanConsistency; + private final String collection; ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency) { + final QueryScanConsistency scanConsistency, final String collection) { this.template = template; this.domainType = domainType; this.query = query; this.scanConsistency = scanConsistency; + this.collection = collection; } @Override @@ -66,12 +71,22 @@ public TerminatingFindByQuery matching(Query query) { } else { scanCons = scanConsistency; } - return new ReactiveFindByQuerySupport<>(template, domainType, query, scanCons); + return new ReactiveFindByQuerySupport<>(template, domainType, query, scanCons, collection); } @Override public FindByQueryConsistentWith consistentWith(QueryScanConsistency scanConsistency) { - return new ReactiveFindByQuerySupport<>(template, domainType, query, scanConsistency); + return new ReactiveFindByQuerySupport<>(template, domainType, query, scanConsistency, collection); + } + + @Override + public FindByQueryInCollection inCollection(String collection) { + return new ReactiveFindByQuerySupport<>(template, domainType, query, scanConsistency, collection); + } + + @Override + public TerminatingDistinct distinct(String field) { + throw new RuntimeException(("not implemented")); } @Override @@ -116,8 +131,9 @@ public Mono count() { } else { return throwable; } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> row.getLong(TemplateUtils.SELECT_COUNT)) - .next(); + }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> { + return row.getLong(TemplateUtils.SELECT_COUNT); + }).next(); }); } @@ -129,7 +145,6 @@ public Mono exists() { private String assembleEntityQuery(final boolean count) { return query.toN1qlSelectString(template, this.domainType, count); } - } } 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 7124927ab..59bb0c54b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.core; -import static com.couchbase.client.java.kv.GetAnyReplicaOptions.*; +import static com.couchbase.client.java.kv.GetAnyReplicaOptions.getAnyReplicaOptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index ebf6d0daa..3898fcd82 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -22,6 +22,7 @@ import java.util.Collection; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.util.Assert; import com.couchbase.client.core.msg.kv.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 de837248e..8518088d9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperation.java @@ -41,6 +41,12 @@ interface RemoveByQueryConsistentWith extends RemoveByQueryWithQuery { } - interface ReactiveRemoveByQuery extends RemoveByQueryConsistentWith {} + interface RemoveByQueryInCollection extends RemoveByQueryConsistentWith { + + RemoveByQueryConsistentWith inCollection(String collection); + + } + + interface ReactiveRemoveByQuery extends RemoveByQueryInCollection {} } 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 cb2c277cd..ae3b89314 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -38,7 +38,8 @@ public ReactiveRemoveByQueryOperationSupport(final ReactiveCouchbaseTemplate tem @Override public ReactiveRemoveByQuery removeByQuery(Class domainType) { - return new ReactiveRemoveByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED); + return new ReactiveRemoveByQuerySupport<>(template, domainType, ALL_QUERY, QueryScanConsistency.NOT_BOUNDED, + "_default._default"); } static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery { @@ -47,13 +48,15 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery private final Class domainType; private final Query query; private final QueryScanConsistency scanConsistency; + private final String collection; ReactiveRemoveByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency) { + final QueryScanConsistency scanConsistency, String collection) { this.template = template; this.domainType = domainType; this.query = query; this.scanConsistency = scanConsistency; + this.collection = collection; } @Override @@ -69,7 +72,8 @@ public Flux all() { return throwable; } }).flatMapMany(ReactiveQueryResult::rowsAsObject) - .map(row -> new RemoveResult(row.getString(TemplateUtils.SELECT_ID), row.getLong(TemplateUtils.SELECT_CAS), Optional.empty())); + .map(row -> new RemoveResult(row.getString(TemplateUtils.SELECT_ID), row.getLong(TemplateUtils.SELECT_CAS), + Optional.empty())); }); } @@ -83,12 +87,17 @@ private QueryOptions buildQueryOptions() { @Override public TerminatingRemoveByQuery matching(final Query query) { - return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency); + return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, collection); } @Override public RemoveByQueryWithQuery consistentWith(final QueryScanConsistency scanConsistency) { - return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency); + return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, collection); + } + + @Override + public RemoveByQueryInCollection inCollection(final String collection) { + return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, collection); } private String assembleDeleteQuery() { diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index 4ab824f75..e84055707 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -16,7 +16,8 @@ package org.springframework.data.couchbase.core.convert; -import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.*; +import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.UNIQUE; +import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.USE_ATTRIBUTES; import java.util.ArrayList; import java.util.Collection; @@ -45,8 +46,13 @@ import org.springframework.data.couchbase.core.mapping.id.IdPrefix; import org.springframework.data.couchbase.core.mapping.id.IdSuffix; import org.springframework.data.couchbase.core.query.N1qlJoin; -import org.springframework.data.mapping.*; +import org.springframework.data.mapping.Alias; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.AssociationHandler; +import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; @@ -261,7 +267,7 @@ public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { || prop.isAnnotationPresent(N1qlJoin.class)) { return; } - Object obj = prop.isIdProperty() ? source.getId() : getValueInternal(prop, source, instance); + Object obj = prop.isIdProperty() && parent == null ? source.getId() : getValueInternal(prop, source, instance); accessor.setProperty(prop, obj); } diff --git a/src/main/java/org/springframework/data/couchbase/core/query/Meta.java b/src/main/java/org/springframework/data/couchbase/core/query/Meta.java new file mode 100644 index 000000000..26d165a0e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/query/Meta.java @@ -0,0 +1,133 @@ +/* + * 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.query; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Meta-data for {@link Query} instances. + * + * @author Michael Reiche + */ +public class Meta { + + private enum MetaKey { + EXAMPLE("$example"); + + private String key; + + MetaKey(String key) { + this.key = key; + } + } + + private final Map values = new LinkedHashMap<>(2); + + public Meta() {} + + /** + * Copy a {@link Meta} object. + * + * @since 2.2 + * @param source + */ + Meta(Meta source) { + this.values.putAll(source.values); + } + + /** + * @return + */ + public boolean hasValues() { + return !this.values.isEmpty(); + } + + /** + * Get {@link Iterable} of set meta values. + * + * @return + */ + public Iterable> values() { + return Collections.unmodifiableSet(this.values.entrySet()); + } + + /** + * Sets or removes the value in case of {@literal null} or empty {@link String}. + * + * @param key must not be {@literal null} or empty. + * @param value + */ + void setValue(String key, @Nullable Object value) { + + Assert.hasText(key, "Meta key must not be 'null' or blank."); + + if (value == null || (value instanceof String && !StringUtils.hasText((String) value))) { + this.values.remove(key); + } + this.values.put(key, value); + } + + @Nullable + @SuppressWarnings("unchecked") + private T getValue(String key) { + return (T) this.values.get(key); + } + + private T getValue(String key, T defaultValue) { + + T value = getValue(key); + return value != null ? value : defaultValue; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int hash = ObjectUtils.nullSafeHashCode(this.values); + return hash; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof Meta)) { + return false; + } + + Meta other = (Meta) obj; + return ObjectUtils.nullSafeEquals(this.values, other.values); + } + +} 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 4ad556e85..1f7a2b6a8 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 @@ -20,11 +20,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.couchbase.client.java.json.JsonArray; -import com.couchbase.client.java.json.JsonObject; -import com.couchbase.client.java.json.JsonValue; -import com.couchbase.client.java.query.QueryOptions; -import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; @@ -37,6 +32,12 @@ import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.json.JsonValue; +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryScanConsistency; + /** * @author Michael Nitschinger * @author Michael Reiche @@ -138,7 +139,6 @@ public QueryScanConsistency getScanConsistency() { return queryScanConsistency; } - /** * Sets the given scan consistency on the {@link Query} instance. * @@ -319,4 +319,7 @@ public QueryOptions buildQueryOptions(QueryScanConsistency scanConsistency) { return options; } + public void setMeta(Meta metaAnnotation) { + Meta meta = metaAnnotation; + } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/Meta.java b/src/main/java/org/springframework/data/couchbase/repository/Meta.java new file mode 100644 index 000000000..298379ee9 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/Meta.java @@ -0,0 +1,36 @@ +/* + * 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.repository; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.annotation.QueryAnnotation; + +/** + * @author Michael Reiche + * @since 4.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +@QueryAnnotation +public @interface Meta { + +} 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 new file mode 100644 index 000000000..a56c59ffc --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java @@ -0,0 +1,156 @@ +/* + * 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.repository.query; + +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.query.Query; +import org.springframework.data.couchbase.repository.query.CouchbaseQueryExecution.DeleteExecution; +import org.springframework.data.couchbase.repository.query.CouchbaseQueryExecution.PagedExecution; +import org.springframework.data.repository.core.EntityMetadata; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link RepositoryQuery} implementation for Couchbase. + * + * @author Michael Reiche + * @since 4.1 + */ +public abstract class AbstractCouchbaseQuery extends AbstractCouchbaseQueryBase + implements RepositoryQuery { + + private final ExecutableFindByQueryOperation.ExecutableFindByQuery findOperationWithProjection; + + /** + * Creates a new {@link AbstractCouchbaseQuery} from the given {@link ReactiveCouchbaseQueryMethod} and + * {@link org.springframework.data.couchbase.core.CouchbaseOperations}. + * + * @param method must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + */ + public AbstractCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperations operations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + super(method, operations, expressionParser, evaluationContextProvider); + Assert.notNull(method, "CouchbaseQueryMethod must not be null!"); + Assert.notNull(operations, "ReactiveCouchbaseOperations must not be null!"); + Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); + Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!"); + // this.operations = operations; + EntityMetadata metadata = method.getEntityInformation(); + Class type = metadata.getJavaType(); + this.findOperationWithProjection = operations.findByQuery(type); + } + + /** + * Execute the {@link RepositoryQuery} of the given method with the parameters provided by the + * {@link ParametersParameterAccessor accessor} + * + * @param method the {@link ReactiveCouchbaseQueryMethod} invoked. Never {@literal null}. + * @param processor {@link ResultProcessor} for post procession. Never {@literal null}. + * @param accessor for providing invocation arguments. Never {@literal null}. + * @param typeToRead the desired component target type. Can be {@literal null}. + */ + @Override + protected Object doExecute(CouchbaseQueryMethod method, ResultProcessor processor, + ParametersParameterAccessor accessor, @Nullable Class typeToRead) { + + Query query = createQuery(accessor); + + query = applyAnnotatedConsistencyIfPresent(query); + // query = applyAnnotatedCollationIfPresent(query, accessor); // not yet implemented + + ExecutableFindByQueryOperation.ExecutableFindByQuery find = typeToRead == null + ? findOperationWithProjection // + : findOperationWithProjection; // not yet implemented in core .as(typeToRead); + + String collection = "_default._default";// method.getEntityInformation().getCollectionName(); // not yet implemented + + CouchbaseQueryExecution execution = getExecution(accessor, + new ResultProcessingConverter<>(processor, getOperations(), getInstantiators()), find); + return execution.execute(query, processor.getReturnedType().getDomainType(), collection); + } + + /** + * Returns the execution instance to use. + * + * @param accessor must not be {@literal null}. + * @param resultProcessing must not be {@literal null}. + * @return + */ + private CouchbaseQueryExecution getExecution(ParameterAccessor accessor, Converter resultProcessing, + ExecutableFindByQueryOperation.ExecutableFindByQuery operation) { + return new CouchbaseQueryExecution.ResultProcessingExecution(getExecutionToWrap(accessor, operation), + resultProcessing); + } + + /** + * Returns the execution to wrap + * + * @param accessor must not be {@literal null}. + * @param operation must not be {@literal null}. + * @return + */ + private CouchbaseQueryExecution getExecutionToWrap(ParameterAccessor accessor, + ExecutableFindByQueryOperation.ExecutableFindByQuery operation) { + + if (isDeleteQuery()) { + return new DeleteExecution(getOperations(), getQueryMethod()); + } else if (isTailable(getQueryMethod())) { + return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all(); // s/b tail() instead of all() + } else if (getQueryMethod().isCollectionQuery()) { + return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all(); + } else if (isCountQuery()) { + return (q, t, c) -> operation.matching(q).count(); + } else if (isExistsQuery()) { + return (q, t, c) -> operation.matching(q).exists(); + } else if (getQueryMethod().isPageQuery()) { + return new PagedExecution(operation, accessor.getPageable()); + } else { + return (q, t, c) -> { + ExecutableFindByQueryOperation.TerminatingFindByQuery find = operation.matching(q); + if (isCountQuery()) { + return find.count(); + } + return isLimiting() ? find.first() : find.one(); + }; + } + } + + /** + * Apply Meta annotation to query + * + * @param query must not be {@literal null}. + * @return Query + */ + Query applyQueryMetaAttributesWhenPresent(Query query) { + + if (getQueryMethod().hasQueryMetaAttributes()) { + query.setMeta(getQueryMethod().getQueryMetaAttributes()); + } + + return query; + } +} 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 new file mode 100644 index 000000000..a6aec181d --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQueryBase.java @@ -0,0 +1,247 @@ +/* + * 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.repository.query; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.reactivestreams.Publisher; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.repository.core.EntityMetadata; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link RepositoryQuery} implementation for Couchbase. + * + * CouchbaseOperationsType is either CouchbaseOperations or ReactiveCouchbaseOperations + * @author Michael Reiche + * @since 4.1 + */ +public abstract class AbstractCouchbaseQueryBase implements RepositoryQuery { + + private final CouchbaseQueryMethod method; + private final CouchbaseOperationsType operations; + private final EntityInstantiators instantiators; + private final ExecutableFindByQueryOperation.ExecutableFindByQuery findOperationWithProjection; + private final SpelExpressionParser expressionParser; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + + /** + * Creates a new {@link AbstractCouchbaseQuery} from the given {@link ReactiveCouchbaseQueryMethod} and + * {@link org.springframework.data.couchbase.core.CouchbaseOperations}. + * + * @param method must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + */ + public AbstractCouchbaseQueryBase(CouchbaseQueryMethod method, CouchbaseOperationsType operations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + + Assert.notNull(method, "CouchbaseQueryMethod must not be null!"); + Assert.notNull(operations, "ReactiveCouchbaseOperations must not be null!"); + Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); + Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!"); + + this.method = method; + this.operations = operations; + this.instantiators = new EntityInstantiators(); + this.expressionParser = expressionParser; + this.evaluationContextProvider = evaluationContextProvider; + + EntityMetadata metadata = method.getEntityInformation(); + Class type = metadata.getJavaType(); + this.findOperationWithProjection = operations instanceof CouchbaseOperations + ? ((CouchbaseOperations) operations).findByQuery(type) + : null; // not yet implemented for Reactive + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod() + */ + public CouchbaseQueryMethod getQueryMethod() { + return method; + }; + + /* + * (non-Javadoc) + */ + public CouchbaseOperationsType getOperations() { + return operations; + } + + /* + * (non-Javadoc) + */ + EntityInstantiators getInstantiators() { + return instantiators; + } + + /** + * Execute the query with the provided parameters + * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[]) + */ + public Object execute(Object[] parameters) { + return method.hasReactiveWrapperParameter() ? executeDeferred(parameters) + : execute(new ReactiveCouchbaseParameterAccessor(getQueryMethod(), parameters)); + } + + private Object executeDeferred(Object[] parameters) { + ReactiveCouchbaseParameterAccessor parameterAccessor = new ReactiveCouchbaseParameterAccessor(method, parameters); + if (getQueryMethod().isCollectionQuery()) { + return Flux.defer(() -> (Publisher) execute(parameterAccessor)); + } + return Mono.defer(() -> (Mono) execute(parameterAccessor)); + } + + private Object execute(ParametersParameterAccessor parameterAccessor) { + TypeInformation returnType = ClassTypeInformation + .from(method.getResultProcessor().getReturnedType().getReturnedType()); + ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor); + Class typeToRead = processor.getReturnedType().getTypeToRead(); + + if (typeToRead == null && returnType.getComponentType() != null) { + typeToRead = returnType.getComponentType().getType(); + } + return doExecute(getQueryMethod(), processor, parameterAccessor, typeToRead); + } + + /** + * Execute the {@link RepositoryQuery} of the given method with the parameters provided by the + * {@link ParametersParameterAccessor accessor} + * + * @param method the {@link ReactiveCouchbaseQueryMethod} invoked. Never {@literal null}. + * @param processor {@link ResultProcessor} for post procession. Never {@literal null}. + * @param accessor for providing invocation arguments. Never {@literal null}. + * @param typeToRead the desired component target type. Can be {@literal null}. + */ + abstract protected Object doExecute(CouchbaseQueryMethod method, ResultProcessor processor, + ParametersParameterAccessor accessor, @Nullable Class typeToRead); + + /** + * Add a scan consistency from {@link org.springframework.data.couchbase.repository.ScanConsistency} to the given + * {@link Query} if present. + * + * @param query the {@link Query} to potentially apply the sort to. + * @return the query with potential scan consistency applied. + * @since 4.1 + */ + Query applyAnnotatedConsistencyIfPresent(Query query) { + if (!method.hasScanConsistencyAnnotation()) { + return query; + } + return query.scanConsistency(method.getScanConsistencyAnnotation().query()); + } + + /** + * Creates a {@link Query} instance using the given {@link ParametersParameterAccessor}. Will delegate to + * {@link #createQuery(ParametersParameterAccessor)} by default but allows customization of the count query to be + * triggered. + * + * @param accessor must not be {@literal null}. + * @return + */ + protected abstract Query createCountQuery(ParametersParameterAccessor accessor); + + /** + * Creates a {@link Query} instance using the given {@link ParameterAccessor} + * + * @param accessor must not be {@literal null}. + * @return + */ + protected abstract Query createQuery(ParametersParameterAccessor accessor); + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isCountQuery() + */ + protected boolean isCountQuery() { + return getQueryMethod().isCountQuery(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isExistsQuery() + */ + protected boolean isExistsQuery() { + return getQueryMethod().isExistsQuery(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isDeleteQuery() + */ + protected boolean isDeleteQuery() { + return getQueryMethod().isDeleteQuery(); + } + + /** + * Return whether the query is tailable + * + * @return + */ + boolean isTailable(CouchbaseQueryMethod method) { + return false; // method.getTailableAnnotation() != null; // Not yet implemented + } + + /** + * Return whether the query has an explicit limit set. + * + * @return + */ + protected abstract boolean isLimiting(); + + /** + * Return whether there are ambiguous projection flags + * + * @return + */ + static boolean hasAmbiguousProjectionFlags(boolean isCountQuery, boolean isExistsQuery, boolean isDeleteQuery) { + return multipleOf(isCountQuery, isExistsQuery, isDeleteQuery); + } + + /** + * Count the number of {@literal true} values. + * + * @param values + * @return are there more than one of these true? + */ + static boolean multipleOf(boolean... values) { + int count = 0; + for (boolean value : values) { + if (value) { + if (count != 0) { + return true; + } + count++; + } + } + return false; + } +} 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 new file mode 100644 index 000000000..c704ce0d8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java @@ -0,0 +1,149 @@ +/* + * 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.repository.query; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveFindByQueryOperation; +import org.springframework.data.couchbase.core.ReactiveFindByQueryOperation.ReactiveFindByQuery; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.repository.query.ReactiveCouchbaseQueryExecution.DeleteExecution; +import org.springframework.data.couchbase.repository.query.ReactiveCouchbaseQueryExecution.ResultProcessingExecution; +import org.springframework.data.repository.core.EntityMetadata; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Base class for reactive {@link RepositoryQuery} implementations for Couchbase. + * + * @author Michael Reiche + * @since 4.1 + */ +public abstract class AbstractReactiveCouchbaseQuery extends AbstractCouchbaseQueryBase + implements RepositoryQuery { + + private final ReactiveFindByQuery findOperationWithProjection; + + /** + * Creates a new {@link AbstractReactiveCouchbaseQuery} from the given {@link ReactiveCouchbaseQueryMethod} and + * {@link org.springframework.data.couchbase.core.CouchbaseOperations}. + * + * @param method must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + */ + public AbstractReactiveCouchbaseQuery(ReactiveCouchbaseQueryMethod method, ReactiveCouchbaseOperations operations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + super(method, operations, expressionParser, evaluationContextProvider); + Assert.notNull(method, "CouchbaseQueryMethod must not be null!"); + Assert.notNull(operations, "ReactiveCouchbaseOperations must not be null!"); + Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); + Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null!"); + + EntityMetadata metadata = method.getEntityInformation(); + Class type = metadata.getJavaType(); + this.findOperationWithProjection = operations.findByQuery(type); + } + + /** + * Execute the {@link RepositoryQuery} of the given method with the parameters provided by the + * {@link ParametersParameterAccessor accessor} + * + * @param method the {@link ReactiveCouchbaseQueryMethod} invoked. Never {@literal null}. + * @param processor {@link ResultProcessor} for post procession. Never {@literal null}. + * @param accessor for providing invocation arguments. Never {@literal null}. + * @param typeToRead the desired component target type. Can be {@literal null}. + */ + protected Object doExecute(CouchbaseQueryMethod method, ResultProcessor processor, + ParametersParameterAccessor accessor, @Nullable Class typeToRead) { + + Query query = createQuery(accessor); + query = applyAnnotatedConsistencyIfPresent(query); + // query = applyAnnotatedCollationIfPresent(query, accessor); // not yet implemented + + ReactiveFindByQueryOperation.FindByQueryWithQuery find = typeToRead == null // + ? findOperationWithProjection // + : findOperationWithProjection; // note yet implemented in core .as(typeToRead); + + String collection = "_default._default";// method.getEntityInformation().getCollectionName(); // not yet implemented + + ReactiveCouchbaseQueryExecution execution = getExecution(accessor, + new ResultProcessingConverter<>(processor, getOperations(), getInstantiators()), find); + return execution.execute(query, processor.getReturnedType().getDomainType(), collection); + } + + /** + * Returns the execution instance to use. + * + * @param accessor must not be {@literal null}. + * @param resultProcessing must not be {@literal null}. + * @return + */ + private ReactiveCouchbaseQueryExecution getExecution(ParameterAccessor accessor, + Converter resultProcessing, ReactiveFindByQueryOperation.FindByQueryWithQuery operation) { + return new ResultProcessingExecution(getExecutionToWrap(accessor, operation), resultProcessing); + } + + /** + * Returns the execution to wrap + * + * @param accessor must not be {@literal null}. + * @param operation must not be {@literal null}. + * @return + */ + private ReactiveCouchbaseQueryExecution getExecutionToWrap(ParameterAccessor accessor, + ReactiveFindByQueryOperation.FindByQueryWithQuery operation) { + + if (isDeleteQuery()) { + return new DeleteExecution(getOperations(), getQueryMethod()); + } else if (isTailable(getQueryMethod())) { + return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all(); // s/b tail() instead of all() + } else if (getQueryMethod().isCollectionQuery()) { + return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all(); + } else if (isCountQuery()) { + return (q, t, c) -> operation.matching(q).count(); + } else if (isExistsQuery()) { + return (q, t, c) -> operation.matching(q).exists(); + } else { + return (q, t, c) -> { + ReactiveFindByQueryOperation.TerminatingFindByQuery find = operation.matching(q); + return isLimiting() ? find.first() : find.one(); + }; + } + } + + /** + * Apply Meta annotation to query + * + * @param query must not be {@literal null}. + * @return Query + */ + Query applyQueryMetaAttributesWhenPresent(Query query) { + + if (getQueryMethod().hasQueryMetaAttributes()) { + query.setMeta(getQueryMethod().getQueryMetaAttributes()); + } + + return query; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryExecution.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryExecution.java new file mode 100644 index 000000000..c64054b24 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryExecution.java @@ -0,0 +1,157 @@ +/* + * 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.repository.query; + +import java.util.List; + +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.query.Query; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.support.PageableExecutionUtils; +import org.springframework.util.Assert; + +/** + * Set of classes to contain query execution strategies. Depending (mostly) on the return type of a + * {@link org.springframework.data.repository.query.QueryMethod} a {@link Query} can be executed in various flavors. + * + * @author Michael Reiche + * @since 4.1 + */ +@FunctionalInterface +interface CouchbaseQueryExecution { + + Object execute(Query query, Class type, String collection); + + /** + * {@link CouchbaseQueryExecution} removing documents matching the query. + */ + + final class DeleteExecution implements CouchbaseQueryExecution { + + private final CouchbaseOperations operations; + private final QueryMethod method; + + public DeleteExecution(CouchbaseOperations operations, QueryMethod method) { + this.operations = operations; + this.method = method; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery.Execution#execute(org.springframework.data.couchbase.core.query.Query, java.lang.Class, java.lang.String) + */ + @Override + public Object execute(Query query, Class type, String collection) { + return operations.removeByQuery(type).matching(query).all(); + } + + } + + /** + * An {@link ReactiveCouchbaseQueryExecution} that wraps the results of the given delegate with the given result + * processing. + */ + final class ResultProcessingExecution implements CouchbaseQueryExecution { + + private final CouchbaseQueryExecution delegate; + private final Converter converter; + + public ResultProcessingExecution(CouchbaseQueryExecution delegate, Converter converter) { + Assert.notNull(delegate, "Delegate must not be null!"); + Assert.notNull(converter, "Converter must not be null!"); + this.delegate = delegate; + this.converter = converter; + } + + @Override + public Object execute(Query query, Class type, String collection) { + return converter.convert(delegate.execute(query, type, collection)); + } + } + + /** + * {@link CouchbaseQueryExecution} for {@link Slice} query methods. + */ + final class SlicedExecution implements CouchbaseQueryExecution { + + private final ExecutableFindByQueryOperation.ExecutableFindByQuery find; + private final Pageable pageable; + + public SlicedExecution(ExecutableFindByQueryOperation.ExecutableFindByQuery find, Pageable pageable) { + Assert.notNull(find, "Find must not be null!"); + Assert.notNull(pageable, "Pageable must not be null!"); + this.find = find; + this.pageable = pageable; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.CouchbaseQueryExecution#execute(org.springframework.data.couchbase.core.query.Query) + */ + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Object execute(Query query, Class type, String collection) { + int pageSize = pageable.getPageSize(); + // Apply Pageable but tweak limit to peek into next page + Query modifiedQuery = query.with(pageable).limit(pageSize + 1); + List result = find.matching(modifiedQuery).all(); + boolean hasNext = result.size() > pageSize; + return new SliceImpl(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext); + } + } + + /** + * {@link CouchbaseQueryExecution} for pagination queries. + */ + final class PagedExecution implements CouchbaseQueryExecution { + + private final ExecutableFindByQueryOperation.ExecutableFindByQuery operation; + private final Pageable pageable; + + public PagedExecution(ExecutableFindByQueryOperation.ExecutableFindByQuery operation, Pageable pageable) { + Assert.notNull(operation, "Operation must not be null!"); + Assert.notNull(pageable, "Pageable must not be null!"); + this.operation = operation; + this.pageable = pageable; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.CouchbaseQueryExecution#execute(org.springframework.data.couchbase.core.query.Query) + */ + @Override + public Object execute(Query query, Class type, String collection) { + int overallLimit = 0; // query.getLimit(); + ExecutableFindByQueryOperation.TerminatingFindByQuery matching = operation.matching(query); + // Apply raw pagination + query.with(pageable); + // Adjust limit if page would exceed the overall limit + if (overallLimit != 0 && pageable.getOffset() + pageable.getPageSize() > overallLimit) { + query.limit((int) (overallLimit - pageable.getOffset())); + } + return PageableExecutionUtils.getPage(matching.all(), pageable, () -> { + long count = operation.matching(query.skip(-1).limit(-1)).count(); + return overallLimit != 0 ? Math.min(count, overallLimit) : count; + }); + } + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java index d230572ae..862434737 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java @@ -17,6 +17,7 @@ package org.springframework.data.couchbase.repository.query; import java.lang.reflect.Method; +import java.util.Locale; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; @@ -24,12 +25,17 @@ import org.springframework.data.couchbase.core.query.Dimensional; import org.springframework.data.couchbase.core.query.View; import org.springframework.data.couchbase.core.query.WithConsistency; +import org.springframework.data.couchbase.repository.Meta; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.ScanConsistency; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -39,6 +45,7 @@ * @author Michael Nitschinger * @author Simon Baslé * @author Oliver Gierke + * @author Michael Reiche */ public class CouchbaseQueryMethod extends QueryMethod { @@ -49,6 +56,7 @@ public CouchbaseQueryMethod(Method method, RepositoryMetadata metadata, Projecti super(method, metadata, factory); this.method = method; + } /** @@ -171,6 +179,38 @@ public ScanConsistency getScanConsistencyAnnotation() { return method.getAnnotation(ScanConsistency.class); } + /** + * @return return true if {@link Meta} annotation is available. + */ + public boolean hasQueryMetaAttributes() { + return getMetaAnnotation() != null; + } + + /** + * @return return {@link Meta} annotation + */ + private Meta getMetaAnnotation() { + return method.getAnnotation(Meta.class); + } + + /** + * Returns the {@link org.springframework.data.couchbase.core.query.Meta} attributes to be applied. + * + * @return never {@literal null}. + */ + @Nullable + public org.springframework.data.couchbase.core.query.Meta getQueryMetaAttributes() { + + Meta meta = getMetaAnnotation(); + if (meta == null) { + return new org.springframework.data.couchbase.core.query.Meta(); + } + + org.springframework.data.couchbase.core.query.Meta metaAttributes = new org.springframework.data.couchbase.core.query.Meta(); + + return metaAttributes; + } + /** * Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found * nor the attribute was specified. @@ -182,6 +222,24 @@ public String getInlineN1qlQuery() { return StringUtils.hasText(query) ? query : null; } + /** + * is this a 'delete'? + * + * @return is this a 'delete'? + */ + public boolean isDeleteQuery() { + return getName().toLowerCase(Locale.ROOT).startsWith("delete"); + } + + /** + * is this an 'exists' query? + * + * @return is this an 'exists' query? + */ + public boolean isExistsQuery() { + return getName().toLowerCase(Locale.ROOT).startsWith("exists"); + } + /** * indicates if the method begins with "count" * @@ -189,11 +247,21 @@ public String getInlineN1qlQuery() { * all(). */ public boolean isCountQuery() { - return getName().toLowerCase().startsWith("count"); + return getName().toLowerCase(Locale.ROOT).startsWith("count"); } @Override public String toString() { return super.toString(); } + + public boolean hasReactiveWrapperParameter() { + for (Parameter p : getParameters()) { + if (ReactiveWrapperConverters.supports(p.getType())) { + return true; + } + } + return false; + } + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java index bef5d5161..c7fd87bd4 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java @@ -18,28 +18,35 @@ import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; /** * @author Michael Nitschinger * @author Michael Reiche + * @deprecated */ +@Deprecated public class CouchbaseRepositoryQuery implements RepositoryQuery { private final CouchbaseOperations operations; private final CouchbaseQueryMethod queryMethod; private final NamedQueries namedQueries; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; public CouchbaseRepositoryQuery(final CouchbaseOperations operations, final CouchbaseQueryMethod queryMethod, - final NamedQueries namedQueries) { + final NamedQueries namedQueries, final QueryMethodEvaluationContextProvider evaluationContextProvider) { this.operations = operations; this.queryMethod = queryMethod; this.namedQueries = namedQueries; + this.evaluationContextProvider = evaluationContextProvider; + throw new RuntimeException("Deprecated"); } @Override public Object execute(final Object[] parameters) { - return new N1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries).execute(parameters); + return new N1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries, evaluationContextProvider) + .execute(parameters); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/couchbase/repository/query/DtoInstantiatingConverter.java new file mode 100644 index 000000000..d62767e48 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/DtoInstantiatingConverter.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015-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.repository.query; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.util.Assert; + +/** + * {@link Converter} to instantiate DTOs from fully equipped domain objects. + * + * @author Michael Reiche + * @since 4.1 + */ +class DtoInstantiatingConverter implements Converter { + + private final Class targetType; + private final MappingContext, ? extends PersistentProperty> context; + private final EntityInstantiator instantiator; + + /** + * Creates a new {@link Converter} to instantiate DTOs. + * + * @param dtoType must not be {@literal null}. + * @param context must not be {@literal null}. + * @param entityInstantiators must not be {@literal null}. + */ + public DtoInstantiatingConverter(Class dtoType, + MappingContext, CouchbasePersistentProperty> context, + EntityInstantiators entityInstantiators) { + + Assert.notNull(dtoType, "DTO type must not be null!"); + Assert.notNull(context, "MappingContext must not be null!"); + Assert.notNull(entityInstantiators, "EntityInstantiators must not be null!"); + + this.targetType = dtoType; + this.context = context; + this.instantiator = entityInstantiators.getInstantiatorFor(context.getRequiredPersistentEntity(dtoType)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + if (targetType.isInterface()) { + return source; + } + + final PersistentEntity sourceEntity = context.getRequiredPersistentEntity(source.getClass()); + final PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(source); + final PersistentEntity targetEntity = context.getRequiredPersistentEntity(targetType); + final PreferredConstructor> constructor = targetEntity + .getPersistenceConstructor(); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + Object dto = instantiator.createInstance(targetEntity, new ParameterValueProvider() { + + @Override + public Object getParameterValue(Parameter parameter) { + return sourceAccessor.getProperty(sourceEntity.getPersistentProperty(parameter.getName().toString())); + } + }); + + final PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(dto); + + targetEntity.doWithProperties(new SimplePropertyHandler() { + + @Override + public void doWithPersistentProperty(PersistentProperty property) { + + if (constructor.isConstructorParameter(property)) { + return; + } + + dtoAccessor.setProperty(property, + sourceAccessor.getProperty(sourceEntity.getPersistentProperty(property.getName()))); + } + }); + + return dto; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java index a10ac154d..be4b21375 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.repository.query; -import static org.springframework.data.couchbase.core.query.QueryCriteria.*; +import static org.springframework.data.couchbase.core.query.QueryCriteria.where; import java.util.Iterator; 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 9f19847cd..a77e7071d 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 @@ -15,33 +15,43 @@ */ package org.springframework.data.couchbase.repository.query; -import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import com.couchbase.client.java.query.QueryScanConsistency; /** * @author Michael Nitschinger * @author Michael Reiche + * @deprecated */ +@Deprecated public class N1qlRepositoryQueryExecutor { private final CouchbaseOperations operations; private final CouchbaseQueryMethod queryMethod; private final NamedQueries namedQueries; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; public N1qlRepositoryQueryExecutor(final CouchbaseOperations operations, final CouchbaseQueryMethod queryMethod, - final NamedQueries namedQueries) { + final NamedQueries namedQueries, final QueryMethodEvaluationContextProvider evaluationContextProvider) { this.operations = operations; this.queryMethod = queryMethod; this.namedQueries = namedQueries; + this.evaluationContextProvider = evaluationContextProvider; + throw new RuntimeException("Deprecated"); } + private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser(); + /** * see also {@link ReactiveN1qlRepositoryQueryExecutor#execute(Object[] parameters) execute } * @@ -52,25 +62,31 @@ public Object execute(final Object[] parameters) { final Class domainClass = queryMethod.getResultProcessor().getReturnedType().getDomainType(); final ParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); - // this is identical to ReactiveN1qlRespositoryQueryExecutor, - // except for the type of 'q', and the call to oneValue() vs one() + // counterpart to ReactiveN1qlRespositoryQueryExecutor, Query query; ExecutableFindByQueryOperation.ExecutableFindByQuery q; if (queryMethod.hasN1qlAnnotation()) { query = new StringN1qlQueryCreator(accessor, queryMethod, operations.getConverter(), operations.getBucketName(), - QueryMethodEvaluationContextProvider.DEFAULT, namedQueries).createQuery(); + SPEL_PARSER, evaluationContextProvider, namedQueries).createQuery(); } else { final PartTree tree = new PartTree(queryMethod.getName(), domainClass); query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter()).createQuery(); } - q = (ExecutableFindByQueryOperation.ExecutableFindByQuery) operations.findByQuery(domainClass) - .consistentWith(buildQueryScanConsistency()).matching(query); + + // q = (ExecutableFindByQueryOperation.ExecutableFindByQuery) operations.findByQuery(domainClass) + // .consistentWith(buildQueryScanConsistency()).matching(query); + + ExecutableFindByQueryOperation.ExecutableFindByQuery operation = (ExecutableFindByQueryOperation.ExecutableFindByQuery) operations + .findByQuery(domainClass).consistentWith(buildQueryScanConsistency()); if (queryMethod.isCountQuery()) { - return q.count(); + return operation.matching(query).count(); } else if (queryMethod.isCollectionQuery()) { - return q.all(); + return operation.matching(query).all(); + } else if (queryMethod.isPageQuery()) { + Pageable p = accessor.getPageable(); + return new CouchbaseQueryExecution.PagedExecution(operation, p).execute(query, null, null); } else { - return q.oneValue(); + return operation.matching(query).oneValue(); } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java new file mode 100644 index 000000000..006a4cfa1 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java @@ -0,0 +1,129 @@ +/* + * 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.repository.query; + +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * {@link RepositoryQuery} implementation for Couchbase. Replaces PartTreeN1qlBasedQuery + * + * @author Michael Reiche + * @since 4.1 + */ +public class PartTreeCouchbaseQuery extends AbstractCouchbaseQuery { + + private final PartTree tree; + private final CouchbaseConverter converter; + + /** + * Creates a new {@link PartTreeCouchbaseQuery} from the given {@link QueryMethod} and {@link CouchbaseTemplate}. + * + * @param method must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + */ + public PartTreeCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperations operations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + + super(method, operations, expressionParser, evaluationContextProvider); + + ResultProcessor processor = method.getResultProcessor(); + this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType()); + this.converter = operations.getConverter(); + } + + /** + * Return the {@link PartTree} backing the query. + * + * @return the tree + */ + public PartTree getTree() { + return tree; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#createQuery(org.springframework.data.couchbase.repository.query.ConvertingParameterAccessor, boolean) + */ + @Override + protected Query createQuery(ParametersParameterAccessor accessor) { + + N1qlQueryCreator creator = new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter); + Query query = creator.createQuery(); + + if (tree.isLimiting()) { + query.limit(tree.getMaxResults()); + } + return query; + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#createCountQuery(org.springframework.data.couchbase.repository.query.ConvertingParameterAccessor) + */ + @Override + protected Query createCountQuery(ParametersParameterAccessor accessor) { + return new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter).createQuery(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isCountQuery() + */ + @Override + protected boolean isCountQuery() { + return tree.isCountProjection(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isExistsQuery() + */ + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isDeleteQuery() + */ + @Override + protected boolean isDeleteQuery() { + return tree.isDelete(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isLimiting() + */ + @Override + protected boolean isLimiting() { + return tree.isLimiting(); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java index e3b524cdd..92c80316f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java @@ -16,8 +16,12 @@ package org.springframework.data.couchbase.repository.query; -import static org.springframework.data.couchbase.core.query.N1QLExpression.*; -import static org.springframework.data.couchbase.repository.query.support.N1qlUtils.*; +import static org.springframework.data.couchbase.core.query.N1QLExpression.count; +import static org.springframework.data.couchbase.core.query.N1QLExpression.delete; +import static org.springframework.data.couchbase.core.query.N1QLExpression.i; +import static org.springframework.data.couchbase.core.query.N1QLExpression.select; +import static org.springframework.data.couchbase.core.query.N1QLExpression.x; +import static org.springframework.data.couchbase.repository.query.support.N1qlUtils.createReturningExpressionForDelete; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.query.N1QLExpression; @@ -37,7 +41,9 @@ * @author Simon Baslé * @author Subhashni Balakrishnan * @author Mark Paluch + * @author Michael Reiche */ +@Deprecated public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery { private final PartTree partTree; @@ -46,6 +52,7 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery { public PartTreeN1qlBasedQuery(CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) { super(queryMethod, couchbaseOperations); this.partTree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType()); + throw new RuntimeException("Deprecated"); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryExecution.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryExecution.java new file mode 100644 index 000000000..953c25db7 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryExecution.java @@ -0,0 +1,82 @@ +/* + * 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.repository.query; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.util.Assert; + +/** + * Set of classes to contain query execution strategies. Depending (mostly) on the return type of a + * {@link org.springframework.data.repository.query.QueryMethod} a {@link AbstractReactiveCouchbaseQuery} can be + * executed in various flavors. + * + * @author Michael Reiche + * @since 4.1 + */ +interface ReactiveCouchbaseQueryExecution { + + Object execute(Query query, Class type, String collection); + + /** + * {@link ReactiveCouchbaseQueryExecution} removing documents matching the query. + */ + + final class DeleteExecution implements ReactiveCouchbaseQueryExecution { + + private final ReactiveCouchbaseOperations operations; + private final CouchbaseQueryMethod method; + + public DeleteExecution(ReactiveCouchbaseOperations operations, CouchbaseQueryMethod method) { + this.operations = operations; + this.method = method; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery.Execution#execute(org.springframework.data.couchbase.core.query.Query, java.lang.Class, java.lang.String) + */ + @Override + public Object execute(Query query, Class type, String collection) { + return operations.removeByQuery(type)/*.inCollection(collection)*/.matching(query).all(); + } + + } + + /** + * An {@link ReactiveCouchbaseQueryExecution} that wraps the results of the given delegate with the given result + * processing. + */ + final class ResultProcessingExecution implements ReactiveCouchbaseQueryExecution { + + private final ReactiveCouchbaseQueryExecution delegate; + private final Converter converter; + + public ResultProcessingExecution(ReactiveCouchbaseQueryExecution delegate, Converter converter) { + Assert.notNull(delegate, "Delegate must not be null!"); + Assert.notNull(converter, "Converter must not be null!"); + this.delegate = delegate; + this.converter = converter; + } + + @Override + public Object execute(Query query, Class type, String collection) { + return converter.convert(delegate.execute(query, type, collection)); + } + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryMethod.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryMethod.java new file mode 100644 index 000000000..6e5419976 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseQueryMethod.java @@ -0,0 +1,121 @@ +/* + * 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.repository.query; + +import static org.springframework.data.repository.util.ClassUtils.hasParameterOfType; + +import java.lang.reflect.Method; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.Lazy; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.ClassUtils; + +/** + * Reactive specific implementation of {@link CouchbaseQueryMethod}. + * + * @author Michael Reiche + * @since 4.1 + */ +public class ReactiveCouchbaseQueryMethod extends CouchbaseQueryMethod { + + private static final ClassTypeInformation PAGE_TYPE = ClassTypeInformation.from(Page.class); + private static final ClassTypeInformation SLICE_TYPE = ClassTypeInformation.from(Slice.class); + + private final Method method; + private final Lazy isCollectionQueryCouchbase; // not to be confused with QueryMethod.isCollectionQuery + + /** + * Creates a new {@link ReactiveCouchbaseQueryMethod} from the given {@link Method}. + * + * @param method must not be {@literal null}. + * @param metadata must not be {@literal null}. + * @param projectionFactory must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + public ReactiveCouchbaseQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory projectionFactory, + MappingContext, CouchbasePersistentProperty> mappingContext) { + + super(method, metadata, projectionFactory, mappingContext); + + if (hasParameterOfType(method, Pageable.class)) { + + TypeInformation returnType = ClassTypeInformation.fromReturnTypeOf(method); + + boolean multiWrapper = ReactiveWrappers.isMultiValueType(returnType.getType()); + boolean singleWrapperWithWrappedPageableResult = ReactiveWrappers.isSingleValueType(returnType.getType()) + && (PAGE_TYPE.isAssignableFrom(returnType.getRequiredComponentType()) + || SLICE_TYPE.isAssignableFrom(returnType.getRequiredComponentType())); + + if (singleWrapperWithWrappedPageableResult) { + throw new InvalidDataAccessApiUsageException( + String.format("'%s.%s' must not use sliced or paged execution. Please use Flux.buffer(size, skip).", + ClassUtils.getShortName(method.getDeclaringClass()), method.getName())); + } + + if (!multiWrapper) { + throw new IllegalStateException(String.format( + "Method has to use a either multi-item reactive wrapper return type or a wrapped Page/Slice type. Offending method: %s", + method.toString())); + } + + if (hasParameterOfType(method, Sort.class)) { + throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter. " + + "Use sorting capabilities on Pageable instead! Offending method: %s", method.toString())); + } + } + + this.method = method; + this.isCollectionQueryCouchbase = Lazy.of(() -> { + boolean result = !(isPageQuery() || isSliceQuery()) + && ReactiveWrappers.isMultiValueType(metadata.getReturnType(method).getType()); + return result; + }); + } + + /* + * All reactive query methods are streaming queries. + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#isStreamQuery() + */ + @Override + public boolean isStreamQuery() { + return true; + } + + /* + * does this query return a collection? + * This must override QueryMethod.isCollection() as isCollectionQueryCouchbase is different from isCollectionQuery + * + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#isCollection() + */ + @Override + public boolean isCollectionQuery() { + return (Boolean) this.isCollectionQueryCouchbase.get(); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java index 128a89e62..5e87ab56e 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java @@ -16,34 +16,60 @@ package org.springframework.data.couchbase.repository.query; import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.repository.core.NamedQueries; -import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; /** * @author Michael Nitschinger * @author Michael Reiche + * @deprecated */ -public class ReactiveCouchbaseRepositoryQuery implements RepositoryQuery { +@Deprecated +public class ReactiveCouchbaseRepositoryQuery extends AbstractReactiveCouchbaseQuery { private final ReactiveCouchbaseOperations operations; - private final CouchbaseQueryMethod queryMethod; + private final ReactiveCouchbaseQueryMethod queryMethod; private final NamedQueries namedQueries; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; public ReactiveCouchbaseRepositoryQuery(final ReactiveCouchbaseOperations operations, - final CouchbaseQueryMethod queryMethod, final NamedQueries namedQueries) { + final ReactiveCouchbaseQueryMethod queryMethod, final NamedQueries namedQueries, + final QueryMethodEvaluationContextProvider evaluationContextProvider) { + super(queryMethod, operations, new SpelExpressionParser(), evaluationContextProvider); this.operations = operations; this.queryMethod = queryMethod; this.namedQueries = namedQueries; + this.evaluationContextProvider = evaluationContextProvider; + throw new RuntimeException("Deprecated"); } @Override public Object execute(final Object[] parameters) { - return new ReactiveN1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries).execute(parameters); + return new ReactiveN1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries, evaluationContextProvider) + .execute(parameters); } @Override - public QueryMethod getQueryMethod() { + protected Query createCountQuery(ParametersParameterAccessor accessor) { + return null; + } + + @Override + protected Query createQuery(ParametersParameterAccessor accessor) { + return null; + } + + @Override + protected boolean isLimiting() { + // TODO + return false; + } + + @Override + public ReactiveCouchbaseQueryMethod getQueryMethod() { return queryMethod; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java index bcc11eeb1..084f1684d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java @@ -15,36 +15,32 @@ */ package org.springframework.data.couchbase.repository.query; -import com.couchbase.client.java.query.QueryScanConsistency; -import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; -import org.springframework.data.couchbase.core.ReactiveFindByQueryOperation; -import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; import org.springframework.data.repository.core.NamedQueries; -import org.springframework.data.repository.query.*; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.expression.spel.standard.SpelExpressionParser; -import reactor.core.publisher.Flux; - -import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; -import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.repository.query.parser.PartTree; - -import java.util.List; /** * @author Michael Nitschinger * @author Michael Reiche + * @deprecated */ +@Deprecated public class ReactiveN1qlRepositoryQueryExecutor { private final ReactiveCouchbaseOperations operations; - private final CouchbaseQueryMethod queryMethod; + private final ReactiveCouchbaseQueryMethod queryMethod; private final NamedQueries namedQueries; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; public ReactiveN1qlRepositoryQueryExecutor(final ReactiveCouchbaseOperations operations, - final CouchbaseQueryMethod queryMethod, final NamedQueries namedQueries) { + final ReactiveCouchbaseQueryMethod queryMethod, final NamedQueries namedQueries, + QueryMethodEvaluationContextProvider evaluationContextProvider) { this.operations = operations; this.queryMethod = queryMethod; this.namedQueries = namedQueries; + this.evaluationContextProvider = evaluationContextProvider; + throw new RuntimeException("Deprecated"); } /** @@ -54,41 +50,16 @@ public ReactiveN1qlRepositoryQueryExecutor(final ReactiveCouchbaseOperations ope * @return */ public Object execute(final Object[] parameters) { - final Class domainClass = queryMethod.getResultProcessor().getReturnedType().getDomainType(); - final ParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); - final String namedQueryName = queryMethod.getNamedQueryName(); - - // this is identical to ExecutableN1qlRespositoryQueryExecutor, - // except for the type of 'q', and the call to one() vs oneValue() + // counterpart to N1qlRespositoryQueryExecutor, - Query query; - ReactiveFindByQueryOperation.ReactiveFindByQuery q; if (queryMethod.hasN1qlAnnotation()) { - query = new StringN1qlQueryCreator(accessor, queryMethod, operations.getConverter(), operations.getBucketName(), - QueryMethodEvaluationContextProvider.DEFAULT, namedQueries).createQuery(); + return new ReactiveStringBasedCouchbaseQuery(queryMethod, operations, new SpelExpressionParser(), + evaluationContextProvider, namedQueries).execute(parameters); } else { - final PartTree tree = new PartTree(queryMethod.getName(), domainClass); - query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter()).createQuery(); + return new ReactivePartTreeCouchbaseQuery(queryMethod, operations, new SpelExpressionParser(), + evaluationContextProvider).execute(parameters); } - q = (ReactiveFindByQueryOperation.ReactiveFindByQuery) operations.findByQuery(domainClass) - .consistentWith(buildQueryScanConsistency()).matching(query); - if (queryMethod.isCountQuery()) { - return q.count(); - } else if (queryMethod.isCollectionQuery()) { - return q.all(); - } else { - return q.one(); - } - } - private QueryScanConsistency buildQueryScanConsistency() { - QueryScanConsistency scanConsistency = QueryScanConsistency.NOT_BOUNDED; - if (queryMethod.hasConsistencyAnnotation()) { - scanConsistency = queryMethod.getConsistencyAnnotation().value(); - } else if (queryMethod.hasScanConsistencyAnnotation()) { - scanConsistency = queryMethod.getScanConsistencyAnnotation().query(); - } - return scanConsistency; } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java new file mode 100644 index 000000000..fd60ad402 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java @@ -0,0 +1,109 @@ +/* + * 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.repository.query; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * Reactive PartTree {@link RepositoryQuery} implementation for Couchbase. Replaces ReactivePartN1qlBasedQuery + * + * @author Michael Reiche + * @since 4.1 + */ +public class ReactivePartTreeCouchbaseQuery extends AbstractReactiveCouchbaseQuery { + + private final PartTree tree; + private final CouchbaseConverter converter; + private static final Logger LOG = LoggerFactory.getLogger(ReactivePartTreeCouchbaseQuery.class); + + /** + * Creates a new {@link ReactivePartTreeCouchbaseQuery} from the given {@link QueryMethod} and + * {@link CouchbaseTemplate}. + * + * @param method must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + */ + public ReactivePartTreeCouchbaseQuery(ReactiveCouchbaseQueryMethod method, ReactiveCouchbaseOperations operations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { + + super(method, operations, expressionParser, evaluationContextProvider); + this.tree = new PartTree(method.getName(), method.getResultProcessor().getReturnedType().getDomainType()); + this.converter = operations.getConverter(); + } + + /** + * Return the {@link PartTree} backing the query. + * + * @return the tree + */ + public PartTree getTree() { + return tree; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbease.repository.query.AbstractCouchbaseQuery#createQuery(org.springframework.data.couchbase.repository.query.ConvertingParameterAccessor, boolean) + */ + @Override + protected Query createQuery(ParametersParameterAccessor accessor) { + + N1qlQueryCreator creator = new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter); + Query query = creator.createQuery(); + + if (tree.isLimiting()) { + query.limit(tree.getMaxResults()); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Created query {} for * fields.", query.export()); + } + return query; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#createCountQuery(org.springframework.data.couchbase.repository.query.ConvertingParameterAccessor) + */ + @Override + protected Query createCountQuery(ParametersParameterAccessor accessor) { + Query query = new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter).createQuery(); + if (LOG.isDebugEnabled()) { + LOG.debug("Created query {} for * fields.", query.export()); + } + return query; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isLimiting() + */ + @Override + protected boolean isLimiting() { + return tree.isLimiting(); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeN1qlBasedQuery.java index 3cfd991e8..a452eaec6 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeN1qlBasedQuery.java @@ -15,7 +15,9 @@ */ package org.springframework.data.couchbase.repository.query; -import static org.springframework.data.couchbase.core.query.N1QLExpression.*; +import static org.springframework.data.couchbase.core.query.N1QLExpression.count; +import static org.springframework.data.couchbase.core.query.N1QLExpression.select; +import static org.springframework.data.couchbase.core.query.N1QLExpression.x; import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; import org.springframework.data.couchbase.core.query.N1QLExpression; @@ -31,8 +33,11 @@ * A reactive {@link RepositoryQuery} for Couchbase, based on query derivation * * @author Subhashni Balakrishnan + * @author Michael Reiche * @since 3.0 + * @deprecated */ +@Deprecated public class ReactivePartTreeN1qlBasedQuery extends ReactiveAbstractN1qlBasedQuery { private final PartTree partTree; @@ -41,6 +46,7 @@ public class ReactivePartTreeN1qlBasedQuery extends ReactiveAbstractN1qlBasedQue public ReactivePartTreeN1qlBasedQuery(CouchbaseQueryMethod queryMethod, ReactiveCouchbaseOperations operations) { super(queryMethod, operations); this.partTree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType()); + throw new RuntimeException("deprecated"); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java new file mode 100644 index 000000000..9626e4644 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java @@ -0,0 +1,105 @@ +/* + * 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.repository.query; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +/** + * Query to use a plain JSON String to create the {@link Query} to actually execute. + * + * @author Michael Reiche + * @since 4.1 + */ +public class ReactiveStringBasedCouchbaseQuery extends AbstractReactiveCouchbaseQuery { + + private static final String COUNT_EXISTS_AND_DELETE = "Manually defined query for %s cannot be a count and exists or delete query at the same time!"; + private static final Logger LOG = LoggerFactory.getLogger(ReactiveStringBasedCouchbaseQuery.class); + + private final SpelExpressionParser expressionParser; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final NamedQueries namedQueries; + + /** + * Creates a new {@link ReactiveStringBasedCouchbaseQuery} for the given {@link String}, {@link CouchbaseQueryMethod}, + * {@link ReactiveCouchbaseOperations}, {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}. + * + * @param method must not be {@literal null}. + * @param couchbaseOperations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + * @param namedQueries must not be {@literal null}. + */ + public ReactiveStringBasedCouchbaseQuery(ReactiveCouchbaseQueryMethod method, + ReactiveCouchbaseOperations couchbaseOperations, SpelExpressionParser expressionParser, + QueryMethodEvaluationContextProvider evaluationContextProvider, NamedQueries namedQueries) { + + super(method, couchbaseOperations, expressionParser, evaluationContextProvider); + + Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); + + this.expressionParser = expressionParser; + this.evaluationContextProvider = evaluationContextProvider; + + if (hasAmbiguousProjectionFlags(isCountQuery(), isExistsQuery(), isDeleteQuery())) { + throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method)); + } + + this.namedQueries = namedQueries; + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#createQuery(org.springframework.data.couchbase.repository.query.ConvertingParameterAccessor) + */ + @Override + protected Query createQuery(ParametersParameterAccessor accessor) { + + StringN1qlQueryCreator creator = new StringN1qlQueryCreator(accessor, getQueryMethod(), + getOperations().getConverter(), getOperations().getBucketName(), expressionParser, evaluationContextProvider, + namedQueries); + Query query = creator.createQuery(); + + if (LOG.isDebugEnabled()) { + LOG.debug("Created query " + query.export()); + } + + return query; + } + + @Override + protected Query createCountQuery(ParametersParameterAccessor accessor) { + return applyQueryMetaAttributesWhenPresent(createQuery(accessor)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractReactiveCouchbaseQuery#isLimiting() + */ + @Override + protected boolean isLimiting() { + return false; // not yet implemented + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ResultProcessingConverter.java b/src/main/java/org/springframework/data/couchbase/repository/query/ResultProcessingConverter.java new file mode 100644 index 000000000..dd8e017df --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ResultProcessingConverter.java @@ -0,0 +1,90 @@ +/* + * 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.repository.query; + +import org.reactivestreams.Publisher; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +final class ResultProcessingConverter implements Converter { + + private final ResultProcessor processor; + private final CouchbaseOperationsType operations; + private final EntityInstantiators instantiators; + + public ResultProcessingConverter(ResultProcessor processor, CouchbaseOperationsType operations, + EntityInstantiators instantiators) { + + Assert.notNull(processor, "Processor must not be null!"); + Assert.notNull(operations, "Operations must not be null!"); + Assert.notNull(instantiators, "Instantiators must not be null!"); + + this.processor = processor; + this.operations = operations; + this.instantiators = instantiators; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + ReturnedType returnedType = processor.getReturnedType(); + + if (isVoid(returnedType)) { + + if (source instanceof Mono) { + return ((Mono) source).then(); + } + + if (source instanceof Publisher) { + return Flux.from((Publisher) source).then(); + } + } + + if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType())) { + return source; + } + + CouchbaseConverter cvtr = operations instanceof CouchbaseOperations + ? ((CouchbaseOperations) operations).getConverter() + : ((ReactiveCouchbaseOperations) operations).getConverter(); + if (!cvtr.getMappingContext().hasPersistentEntityFor(returnedType.getReturnedType())) { + return source; + } + + Converter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), + cvtr.getMappingContext(), instantiators); + + return processor.processResult(source, converter); + } + + static boolean isVoid(ReturnedType returnedType) { + return returnedType.getReturnedType().equals(Void.class); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java new file mode 100644 index 000000000..d52a04b3d --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java @@ -0,0 +1,103 @@ +/* + * 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.repository.query; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +/** + * Query to use a plain JSON String to create the {@link Query} to actually execute. + * + * @author Michael Reiche + * @since 4.1 + */ +public class StringBasedCouchbaseQuery extends AbstractCouchbaseQuery { + + private static final String COUNT_EXISTS_AND_DELETE = "Manually defined query for %s cannot be a count and exists or delete query at the same time!"; + private static final Logger LOG = LoggerFactory.getLogger(StringBasedCouchbaseQuery.class); + + private final SpelExpressionParser expressionParser; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final NamedQueries namedQueries; + + /** + * Creates a new {@link StringBasedCouchbaseQuery} for the given {@link String}, {@link CouchbaseQueryMethod}, + * {@link CouchbaseOperations}, {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}. + * + * @param method must not be {@literal null}. + * @param couchbaseOperations must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + * @param namedQueries must not be {@literal null}. + */ + public StringBasedCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperations couchbaseOperations, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider, + NamedQueries namedQueries) { + + super(method, couchbaseOperations, expressionParser, evaluationContextProvider); + + Assert.notNull(expressionParser, "SpelExpressionParser must not be null!"); + + this.expressionParser = expressionParser; + this.evaluationContextProvider = evaluationContextProvider; + + if (hasAmbiguousProjectionFlags(isCountQuery(), isExistsQuery(), isDeleteQuery())) { + throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method)); + } + this.namedQueries = namedQueries; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#createQuery(org.springframework.data.couchbase.repository.query.ConvertingParameterAccessor) + */ + @Override + protected Query createQuery(ParametersParameterAccessor accessor) { + + StringN1qlQueryCreator creator = new StringN1qlQueryCreator(accessor, getQueryMethod(), + getOperations().getConverter(), getOperations().getBucketName(), expressionParser, evaluationContextProvider, + namedQueries); + Query query = creator.createQuery(); + + if (LOG.isDebugEnabled()) { + LOG.debug("Created query " + query.export()); + } + + return query; + } + + @Override + protected Query createCountQuery(ParametersParameterAccessor accessor) { + return applyQueryMetaAttributesWhenPresent(createQuery(accessor)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.query.AbstractCouchbaseQuery#isLimiting() + */ + @Override + protected boolean isLimiting() { + return false; // not yet implemented + } + +} 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 3f3313038..cc2f9ca68 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 @@ -15,8 +15,10 @@ */ package org.springframework.data.couchbase.repository.query; -import static org.springframework.data.couchbase.core.query.N1QLExpression.*; -import static org.springframework.data.couchbase.core.support.TemplateUtils.*; +import static org.springframework.data.couchbase.core.query.N1QLExpression.i; +import static org.springframework.data.couchbase.core.query.N1QLExpression.x; +import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_CAS; +import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_ID; import java.util.ArrayList; import java.util.Collection; @@ -25,7 +27,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.couchbase.client.core.error.InvalidArgumentException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; @@ -42,11 +43,12 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.common.TemplateParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; +import com.couchbase.client.core.error.InvalidArgumentException; import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.json.JsonObject; import com.couchbase.client.java.json.JsonValue; -import org.springframework.util.Assert; /** * @author Subhashni Balakrishnan diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java index df3c4cdaf..d19f185f9 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java @@ -15,12 +15,6 @@ */ package org.springframework.data.couchbase.repository.query; -import static org.springframework.data.couchbase.core.query.N1QLExpression.x; -import static org.springframework.data.couchbase.core.query.QueryCriteria.*; - -import java.util.ArrayList; -import java.util.Iterator; - import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.json.JsonObject; import com.couchbase.client.java.json.JsonValue; @@ -30,19 +24,21 @@ import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.core.query.StringQuery; -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.PersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.core.NamedQueries; -import org.springframework.data.repository.query.*; +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.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.util.Assert; + +import java.util.Iterator; + +import static org.springframework.data.couchbase.core.query.QueryCriteria.where; /** * @author Michael Reiche @@ -56,11 +52,10 @@ public class StringN1qlQueryCreator extends AbstractQueryCreator parameters) { + private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, final QueryCriteria criteria, + final Iterator parameters) { final Part.Type type = part.getType(); switch (type) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java index 747c9d76e..f42d9ff4f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java @@ -26,7 +26,8 @@ import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod; -import org.springframework.data.couchbase.repository.query.CouchbaseRepositoryQuery; +import org.springframework.data.couchbase.repository.query.PartTreeCouchbaseQuery; +import org.springframework.data.couchbase.repository.query.StringBasedCouchbaseQuery; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; @@ -156,25 +157,14 @@ public RepositoryQuery resolveQuery(final Method method, final RepositoryMetadat metadata.getRepositoryInterface(), metadata.getDomainType()); CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext); - return new CouchbaseRepositoryQuery(couchbaseOperations, queryMethod, namedQueries); - /*CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext); - String namedQueryName = queryMethod.getNamedQueryName(); - if (queryMethod.hasN1qlAnnotation()) { - - if (queryMethod.hasInlineN1qlQuery()) { - return new StringN1qlBasedQuery(queryMethod.getInlineN1qlQuery(), queryMethod, couchbaseOperations, - SPEL_PARSER, evaluationContextProvider); - } else if (namedQueries.hasQuery(namedQueryName)) { - String namedQuery = namedQueries.getQuery(namedQueryName); - return new StringN1qlBasedQuery(namedQuery, queryMethod, couchbaseOperations, SPEL_PARSER, - evaluationContextProvider); - } - + return new StringBasedCouchbaseQuery(queryMethod, couchbaseOperations, new SpelExpressionParser(), + evaluationContextProvider, namedQueries); + } else { + return new PartTreeCouchbaseQuery(queryMethod, couchbaseOperations, new SpelExpressionParser(), + evaluationContextProvider); } - - return new PartTreeN1qlBasedQuery(queryMethod, couchbaseOperations);*/ } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java b/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java index 5cc49c1ff..8ca423c39 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java @@ -24,8 +24,9 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; -import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod; -import org.springframework.data.couchbase.repository.query.ReactiveCouchbaseRepositoryQuery; +import org.springframework.data.couchbase.repository.query.ReactiveCouchbaseQueryMethod; +import org.springframework.data.couchbase.repository.query.ReactivePartTreeCouchbaseQuery; +import org.springframework.data.couchbase.repository.query.ReactiveStringBasedCouchbaseQuery; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; @@ -151,8 +152,16 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, NamedQueries namedQueries) { final ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve( metadata.getRepositoryInterface(), metadata.getDomainType()); - return new ReactiveCouchbaseRepositoryQuery(couchbaseOperations, - new CouchbaseQueryMethod(method, metadata, factory, mappingContext), namedQueries); + ReactiveCouchbaseQueryMethod queryMethod = new ReactiveCouchbaseQueryMethod(method, metadata, factory, + mappingContext); + + if (queryMethod.hasN1qlAnnotation()) { + return new ReactiveStringBasedCouchbaseQuery(queryMethod, couchbaseOperations, new SpelExpressionParser(), + evaluationContextProvider, namedQueries); + } else { + return new ReactivePartTreeCouchbaseQuery(queryMethod, couchbaseOperations, new SpelExpressionParser(), + evaluationContextProvider); + } } } 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 bfead97d8..c87906ca3 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 @@ -16,7 +16,7 @@ package org.springframework.data.couchbase.repository.support; -import static org.springframework.data.couchbase.repository.support.Util.*; +import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty; import java.util.Collection; import java.util.List; 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 33c5c0241..04595c1b6 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java @@ -16,8 +16,12 @@ package org.springframework.data.couchbase.core; -import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.data.couchbase.config.BeanNames.*; +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 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; @@ -99,7 +103,6 @@ void findByQueryAll() { .consistentWith(QueryScanConsistency.REQUEST_PLUS).all(); for (User u : foundUsers) { - System.out.println(u); if (!(u.equals(user1) || u.equals(user2))) { // somebody didn't clean up after themselves. couchbaseTemplate.removeById().one(u.getId()); @@ -177,8 +180,7 @@ void removeByMatchingQuery() { Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); couchbaseTemplate.removeByQuery(User.class).consistentWith(QueryScanConsistency.REQUEST_PLUS) - .matching(nonSpecialUsers) - .all(); + .matching(nonSpecialUsers).all(); assertNull(couchbaseTemplate.findById(User.class).one(user1.getId())); assertNull(couchbaseTemplate.findById(User.class).one(user2.getId())); 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 a04bbf3e2..9f895c823 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airport.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airport.java @@ -20,6 +20,12 @@ import org.springframework.data.annotation.PersistenceConstructor; import org.springframework.data.couchbase.core.mapping.Document; +/** + * Airport entity + * + * @author Michael Nitschinger + * @author Michael Reiche + */ @Document public class Airport { @Id String id; diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java index 9d27fc0b7..911aebd70 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -18,13 +18,16 @@ import java.util.List; -import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.ScanConsistency; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import com.couchbase.client.java.query.QueryScanConsistency; + /** * template class for Reactive Couchbase operations * @@ -64,11 +67,12 @@ public interface AirportRepository extends PagingAndSortingRepository projectIds, @Param("planIds") List planIds, @Param("active") Boolean active); + Long countFancyExpression(@Param("projectIds") List projectIds, @Param("planIds") List planIds, + @Param("active") Boolean active); + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + Page findAllByIataNot(String iata, Pageable pageable); } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Config.java b/src/test/java/org/springframework/data/couchbase/domain/Config.java index 6fd052453..b876d00f8 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -16,31 +16,29 @@ package org.springframework.data.couchbase.domain; -import com.couchbase.client.java.Cluster; -import com.couchbase.client.java.ClusterOptions; -import com.couchbase.client.java.env.ClusterEnvironment; +import java.lang.reflect.InvocationTargetException; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.auditing.DateTimeProvider; -import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; -import org.springframework.data.couchbase.config.BeanNames; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; -import org.springframework.data.util.TypeInformation; -import java.lang.reflect.InvocationTargetException; -import java.util.Collections; +import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.DeserializationFeature; +import com.couchbase.client.java.json.JacksonTransformers; /** * @author Michael Nitschinger @@ -190,6 +188,17 @@ public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingConte return converter; } + @Override + @Bean(name = "couchbaseTranslationService") + public TranslationService couchbaseTranslationService() { + final JacksonTranslationService jacksonTranslationService = new JacksonTranslationService(); + jacksonTranslationService.afterPropertiesSet(); + + // for sdk3, we need to ask the mapper _it_ uses to ignore extra fields... + JacksonTransformers.MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + return jacksonTranslationService; + } + @Override public String typeKey() { return "t"; // this will override '_class', is passed in to new CustomMappingCouchbaseConverter diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java index 5d707cf1d..63cb3388d 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java @@ -16,13 +16,19 @@ package org.springframework.data.couchbase.domain; -import org.springframework.data.couchbase.repository.Query; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.ScanConsistency; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.reactive.ReactiveSortingRepository; import org.springframework.stereotype.Repository; -import reactor.core.publisher.Mono; import com.couchbase.client.java.query.QueryScanConsistency; @@ -62,4 +68,25 @@ public interface ReactiveAirportRepository extends ReactiveSortingRepository findById(String var1); + + // use parameter type PageRequest instead of Pageable. Pageable requires a return type of Page<> + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + Flux findAllByIataLike(String iata, final PageRequest page); + + // use parameter type PageRequest instead of Pageable. Pageable requires a return type of Page<> + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + Flux findAllByIataLike(String iata); + + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + Mono findByIata(String iata); + + // This is not efficient. See findAllByIataLike for efficient reactive paging + default public Mono> findAllAirportsPaged(Pageable pageable) { + return count().flatMap(airportCount -> { + return findAll(pageable.getSort()) + .buffer(pageable.getPageSize(), (pageable.getPageNumber() * pageable.getPageSize())) + .elementAt(pageable.getPageNumber(), new ArrayList<>()) + .map(airports -> new PageImpl(airports, pageable, airportCount)); + }); + } } 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 4c14ce1d2..7a050cacd 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -40,17 +40,19 @@ import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.AirportRepository; -import org.springframework.data.couchbase.domain.ReactiveUserRepository; -import org.springframework.data.couchbase.domain.User; -import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.couchbase.domain.Person; import org.springframework.data.couchbase.domain.PersonRepository; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; 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.util.StreamUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexExistsException; @@ -141,13 +143,16 @@ void findBySimpleProperty() { Airport vie = null; try { vie = new Airport("airports::vie", "vie", "loww"); - airportRepository.save(vie); + vie = airportRepository.save(vie); List airports = airportRepository.findAllByIata("vie"); - assertEquals(vie.getId(), airports.get(0).getId()); + assertEquals(1, airports.size()); + Airport airport1 = airportRepository.findById(airports.get(0).getId()).get(); + assertEquals(airport1.getIata(), vie.getIata()); + Airport airport2 = airportRepository.findByIata(airports.get(0).getIata()); + assertEquals(airport1.getId(), vie.getId()); } finally { airportRepository.delete(vie); } - } @Test @@ -164,11 +169,8 @@ public void testCas() { @Test void count() { String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" }; - Future[] future = new Future[iatas.length]; - ExecutorService executorService = Executors.newFixedThreadPool(iatas.length); try { - Callable[] suppliers = new Callable[iatas.length]; for (int i = 0; i < iatas.length; i++) { Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, iatas[i].toLowerCase(Locale.ROOT) /* lcao */); @@ -178,6 +180,11 @@ void count() { Long count = airportRepository.countFancyExpression(asList("JFK"), asList("jfk"), false); assertEquals(1, count); + Pageable pageable = PageRequest.of(0, 2); + Page aPage = airportRepository.findAllByIataNot("JFK", pageable); + assertEquals(iatas.length - 1, aPage.getTotalElements()); + assertEquals(pageable.getPageSize(), aPage.getContent().size()); + long airportCount = airportRepository.count(); assertEquals(7, airportCount); 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 2e0ed241d..1b98996c7 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java @@ -22,10 +22,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import reactor.core.publisher.Flux; import reactor.test.StepVerifier; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -49,6 +50,7 @@ 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.domain.PageRequest; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexExistsException; @@ -80,16 +82,21 @@ void beforeEach() { @Test void shouldSaveAndFindAll() { Airport vie = null; + Airport jfk = null; try { vie = new Airport("airports::vie", "vie", "loww"); airportRepository.save(vie).block(); + jfk = new Airport("airports::jfk", "JFK", "xxxx"); + airportRepository.save(jfk).block(); List all = airportRepository.findAll().toStream().collect(Collectors.toList()); assertFalse(all.isEmpty()); assertTrue(all.stream().anyMatch(a -> a.getId().equals("airports::vie"))); + assertTrue(all.stream().anyMatch(a -> a.getId().equals("airports::jfk"))); } finally { airportRepository.delete(vie).block(); + airportRepository.delete(jfk).block(); } } @@ -103,6 +110,13 @@ void findBySimpleProperty() { assertEquals(1, airports1.size()); List airports2 = airportRepository.findAllByIata("vie").collectList().block(); assertEquals(1, airports2.size()); + vie = airportRepository.save(vie).block(); + List airports = airportRepository.findAllByIata("vie").collectList().block(); + assertEquals(1, airports.size()); + Airport airport1 = airportRepository.findById(airports.get(0).getId()).block(); + assertEquals(airport1.getIata(), vie.getIata()); + Airport airport2 = airportRepository.findByIata(airports.get(0).getIata()).block(); + assertEquals(airport1.getId(), vie.getId()); } finally { airportRepository.delete(vie).block(); } @@ -121,18 +135,35 @@ public void testCas() { @Test void count() { - String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" }; - Future[] future = new Future[iatas.length]; - ExecutorService executorService = Executors.newFixedThreadPool(iatas.length); + Set iatas = new HashSet(); + iatas.add("JFK"); + iatas.add("IAD"); + iatas.add("SFO"); + iatas.add("SJC"); + iatas.add("SEA"); + iatas.add("LAX"); + iatas.add("PHX"); + Future[] future = new Future[iatas.size()]; + ExecutorService executorService = Executors.newFixedThreadPool(iatas.size()); try { - Callable[] suppliers = new Callable[iatas.length]; - for (int i = 0; i < iatas.length; i++) { - Airport airport = new Airport("airports::" + iatas[i], iatas[i] /*iata*/, iatas[i].toLowerCase() /* lcao */); + Callable[] suppliers = new Callable[iatas.size()]; + for (String iata : iatas) { + Airport airport = new Airport("airports::" + iata, iata, iata.toLowerCase() /* lcao */); airportRepository.save(airport).block(); } + int page = 0; + + airportRepository.findAllByIataLike("S%", PageRequest.of(page++, 2)).as(StepVerifier::create) // + .expectNextMatches(a -> { + return iatas.contains(a.getIata()); + }).expectNextMatches(a -> iatas.contains(a.getIata())).verifyComplete(); + + airportRepository.findAllByIataLike("S%", PageRequest.of(page++, 2)).as(StepVerifier::create) // + .expectNextMatches(a -> iatas.contains(a.getIata())).verifyComplete(); + Long airportCount = airportRepository.count().block(); - assertEquals(iatas.length, airportCount); + assertEquals(iatas.size(), airportCount); airportCount = airportRepository.countByIataIn("JFK", "IAD", "SFO").block(); assertEquals(3, airportCount); @@ -144,8 +175,8 @@ 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 */); + for (String iata : iatas) { + Airport airport = new Airport("airports::" + iata, iata, iata.toLowerCase() /* lcao */); try { airportRepository.delete(airport).block(); } catch (DataRetrievalFailureException drfe) { diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java index 4129771ed..a0de49fe0 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorMockedTests.java @@ -15,6 +15,13 @@ */ package org.springframework.data.couchbase.repository.query; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; + +import java.lang.reflect.Method; +import java.util.Properties; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; @@ -37,14 +44,12 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; -import org.springframework.data.repository.query.*; - -import java.lang.reflect.Method; -import java.util.Properties; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; -import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; +import org.springframework.data.repository.query.DefaultParameters; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; /** * @author Michael Nitschinger @@ -75,7 +80,8 @@ void createsQueryCorrectly() throws Exception { converter.getMappingContext()); StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver", "Twist"), - queryMethod, converter, "travel-sample", QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + queryMethod, converter, "travel-sample", new SpelExpressionParser(), + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); Query query = creator.createQuery(); assertEquals( @@ -93,7 +99,8 @@ void createsQueryCorrectly2() throws Exception { converter.getMappingContext()); StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver", "Twist"), - queryMethod, converter, "travel-sample", QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + queryMethod, converter, "travel-sample", new SpelExpressionParser(), + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); Query query = creator.createQuery(); assertEquals( @@ -112,7 +119,8 @@ void wrongNumberArgs() throws Exception { try { StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), - queryMethod, converter, "travel-sample", QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + queryMethod, converter, "travel-sample", new SpelExpressionParser(), + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); } catch (IllegalArgumentException e) { return; } @@ -129,7 +137,8 @@ void doesNotHaveAnnotation() throws Exception { try { StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), - queryMethod, converter, "travel-sample", QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + queryMethod, converter, "travel-sample", new SpelExpressionParser(), + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); } catch (IllegalArgumentException e) { return; } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java index 9681255d4..06968b4fc 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.repository.query; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; import java.lang.reflect.Method; @@ -37,7 +37,8 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.couchbase.domain.*; +import org.springframework.data.couchbase.domain.Airline; +import org.springframework.data.couchbase.domain.AirlineRepository; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; @@ -48,7 +49,12 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; -import org.springframework.data.repository.query.*; +import org.springframework.data.repository.query.DefaultParameters; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** @@ -87,10 +93,10 @@ void findUsingStringNq1l() throws Exception { converter.getMappingContext()); StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Continental"), - queryMethod, converter, config().bucketname(), QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + queryMethod, converter, config().bucketname(), new SpelExpressionParser(), + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); Query query = creator.createQuery(); - System.out.println(query.toN1qlSelectString(couchbaseTemplate.reactive(), Airline.class, false)); try { Thread.sleep(3000); diff --git a/src/test/resources/integration.properties b/src/test/resources/integration.properties index 6c7dcac19..f097d05bd 100644 --- a/src/test/resources/integration.properties +++ b/src/test/resources/integration.properties @@ -12,4 +12,4 @@ cluster.mocked.numReplicas=1 # Entry point configuration if not managed # value of hostname and ns_server port cluster.unmanaged.seed=127.0.0.1:8091 -cluster.unmanaged.numReplicas=0 \ No newline at end of file +cluster.unmanaged.numReplicas=0