From 1581712765afd6d656b7bd3c8ea3169e2734303b Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Mon, 9 Aug 2021 09:57:29 -0700 Subject: [PATCH] Scopes and collections for repositories (#1149) * Add support for scopes and collections for repositories. Adds DynamicProxyable and DynamicInvocationHandler to set scope/collection/options on PseudoArgs when calling operations via repository interfaces. Closes #963. Co-authored-by: mikereiche --- .../SimpleCouchbaseClientFactory.java | 8 +- .../AbstractCouchbaseConfiguration.java | 1 - .../couchbase/core/CouchbaseTemplate.java | 17 +- .../core/CouchbaseTemplateSupport.java | 5 +- .../core/ExecutableExistsByIdOperation.java | 7 +- .../ExecutableExistsByIdOperationSupport.java | 24 +- ...utableFindByAnalyticsOperationSupport.java | 2 - .../ExecutableFindByIdOperationSupport.java | 4 +- ...ExecutableFindByQueryOperationSupport.java | 11 +- ...eFindFromReplicasByIdOperationSupport.java | 6 +- .../ExecutableInsertByIdOperationSupport.java | 2 - .../core/ExecutableRemoveByIdOperation.java | 7 +- .../ExecutableRemoveByIdOperationSupport.java | 32 +- ...ecutableRemoveByQueryOperationSupport.java | 7 +- ...ExecutableReplaceByIdOperationSupport.java | 2 - .../ExecutableUpsertByIdOperationSupport.java | 2 - .../core/NonReactiveSupportWrapper.java | 6 +- .../core/ReactiveCouchbaseOperations.java | 10 +- .../core/ReactiveCouchbaseTemplate.java | 27 +- .../ReactiveCouchbaseTemplateSupport.java | 13 +- .../core/ReactiveExistsByIdOperation.java | 7 + .../ReactiveExistsByIdOperationSupport.java | 35 +- ...activeFindByAnalyticsOperationSupport.java | 2 - .../ReactiveFindByIdOperationSupport.java | 31 +- .../core/ReactiveFindByQueryOperation.java | 4 +- .../ReactiveFindByQueryOperationSupport.java | 124 +++-- ...eFindFromReplicasByIdOperationSupport.java | 22 +- .../core/ReactiveInsertByIdOperation.java | 3 - .../ReactiveInsertByIdOperationSupport.java | 31 +- .../core/ReactiveRemoveByIdOperation.java | 7 + .../ReactiveRemoveByIdOperationSupport.java | 74 ++- ...ReactiveRemoveByQueryOperationSupport.java | 44 +- .../ReactiveReplaceByIdOperationSupport.java | 33 +- .../core/ReactiveTemplateSupport.java | 20 +- .../ReactiveUpsertByIdOperationSupport.java | 29 +- .../data/couchbase/core/TemplateSupport.java | 14 +- .../convert/MappingCouchbaseConverter.java | 5 + .../BasicCouchbasePersistentEntity.java | 12 +- .../data/couchbase/core/mapping/Document.java | 16 + .../data/couchbase/core/mapping/Expiry.java | 64 +++ .../event/AbstractCouchbaseEventListener.java | 26 +- .../mapping/event/AuditingEntityCallback.java | 4 +- .../mapping/event/AuditingEventListener.java | 24 +- .../event/ReactiveAfterDeleteEvent.java | 28 -- .../event/ReactiveAuditingEntityCallback.java | 9 +- .../event/ReactiveAuditingEventListener.java | 87 ---- .../event/ReactiveBeforeConvertEvent.java | 28 -- .../event/ReactiveBeforeDeleteEvent.java | 28 -- .../event/ReactiveBeforeSaveEvent.java | 30 -- .../ValidatingCouchbaseEventListener.java | 1 - .../data/couchbase/core/query/Meta.java | 35 +- .../couchbase/core/query/OptionsBuilder.java | 425 ++++++++++++++++++ .../data/couchbase/core/query/Query.java | 36 +- .../couchbase/core/support/PseudoArgs.java | 79 ++-- .../data/couchbase/repository/Collection.java | 46 ++ .../repository/CouchbaseRepository.java | 12 +- .../repository/DynamicProxyable.java | 78 ++++ .../data/couchbase/repository/Options.java | 117 +++++ .../ReactiveCouchbaseRepository.java | 7 +- .../couchbase/repository/ScanConsistency.java | 9 +- .../repository/{Meta.java => Scope.java} | 20 +- .../ReactiveCouchbaseAuditingRegistrar.java | 27 +- .../query/AbstractCouchbaseQuery.java | 37 +- .../query/AbstractCouchbaseQueryBase.java | 16 +- .../query/AbstractReactiveCouchbaseQuery.java | 33 +- .../query/CouchbaseQueryMethod.java | 78 ++-- .../ReactiveStringBasedCouchbaseQuery.java | 2 +- .../query/StringBasedCouchbaseQuery.java | 6 +- .../query/StringBasedN1qlQueryParser.java | 10 +- .../support/CouchbaseRepositoryBase.java | 123 +++++ .../support/CouchbaseRepositoryFactory.java | 14 +- .../support/CrudMethodMetadata.java | 4 + .../CrudMethodMetadataPostProcessor.java | 62 ++- .../support/DynamicInvocationHandler.java | 123 +++++ .../ReactiveCouchbaseRepositoryFactory.java | 22 +- .../support/SimpleCouchbaseRepository.java | 90 ++-- .../SimpleReactiveCouchbaseRepository.java | 90 ++-- ...hbaseTemplateKeyValueIntegrationTests.java | 6 +- ...mplateQueryCollectionIntegrationTests.java | 51 ++- .../core/CustomTypeKeyIntegrationTests.java | 3 +- ...hbaseTemplateKeyValueIntegrationTests.java | 3 +- .../data/couchbase/domain/Airport.java | 4 +- .../couchbase/domain/AirportRepository.java | 63 ++- .../data/couchbase/domain/Config.java | 7 +- .../couchbase/domain/PersonRepository.java | 3 +- .../domain/ReactiveAirportRepository.java | 10 +- .../domain/ReactiveNaiveAuditorAware.java | 3 +- .../domain/ReactiveUserColRepository.java | 63 +++ .../data/couchbase/domain/UserCol.java} | 21 +- .../couchbase/domain/UserColRepository.java | 61 +++ .../data/couchbase/domain/UserRepository.java | 4 + ...chbaseRepositoryQueryIntegrationTests.java | 172 ++++++- ...aseRepositoryKeyValueIntegrationTests.java | 3 +- ...chbaseRepositoryQueryIntegrationTests.java | 38 +- ...sitoryQueryCollectionIntegrationTests.java | 211 +++++++++ ...sitoryQueryCollectionIntegrationTests.java | 215 +++++++++ src/test/resources/logback.xml | 2 +- 97 files changed, 2617 insertions(+), 962 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/Expiry.java delete mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterDeleteEvent.java delete mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEventListener.java delete mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertEvent.java delete mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeDeleteEvent.java delete mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeSaveEvent.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/Collection.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/Options.java rename src/main/java/org/springframework/data/couchbase/repository/{Meta.java => Scope.java} (66%) create mode 100644 src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/ReactiveUserColRepository.java rename src/{main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterSaveEvent.java => test/java/org/springframework/data/couchbase/domain/UserCol.java} (52%) create mode 100644 src/test/java/org/springframework/data/couchbase/domain/UserColRepository.java create mode 100644 src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java diff --git a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java index bcb5034b..b97b57f9 100644 --- a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java +++ b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2021 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.couchbase; import java.util.function.Supplier; @@ -33,6 +32,9 @@ import com.couchbase.client.java.env.ClusterEnvironment; /** * The default implementation of a {@link CouchbaseClientFactory}. + * + * @author Michael Nitschinger + * @author Michael Reiche */ public class SimpleCouchbaseClientFactory implements CouchbaseClientFactory { @@ -74,7 +76,7 @@ public class SimpleCouchbaseClientFactory implements CouchbaseClientFactory { @Override public CouchbaseClientFactory withScope(final String scopeName) { - return new SimpleCouchbaseClientFactory(cluster, bucket.name(), scopeName); + return new SimpleCouchbaseClientFactory(cluster, bucket.name(), scopeName != null ? scopeName : getScope().name()); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 225a98d0..9d6437da 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -22,7 +22,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java index 2f1354dc..124cab2d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -28,7 +28,6 @@ import org.springframework.data.couchbase.core.index.CouchbasePersistentEntityIn import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; -import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.mapping.context.MappingContext; import org.springframework.lang.Nullable; @@ -107,13 +106,25 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex } @Override + @Deprecated public ExecutableRemoveById removeById() { - return new ExecutableRemoveByIdOperationSupport(this).removeById(); + return removeById(null); } @Override + public ExecutableRemoveById removeById(Class domainType) { + return new ExecutableRemoveByIdOperationSupport(this).removeById(domainType); + } + + @Override + @Deprecated public ExecutableExistsById existsById() { - return new ExecutableExistsByIdOperationSupport(this).existsById(); + return existsById(null); + } + + @Override + public ExecutableExistsById existsById(Class domainType) { + return new ExecutableExistsByIdOperationSupport(this).existsById(domainType); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index 140f4d9c..bf794b9c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -17,6 +17,8 @@ package org.springframework.data.couchbase.core; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -38,9 +40,6 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.util.Assert; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Internal encode/decode support for CouchbaseTemplate. * diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java index 357ba500..34468efe 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java @@ -36,8 +36,14 @@ public interface ExecutableExistsByIdOperation { /** * Checks if the document exists in the bucket. */ + @Deprecated ExecutableExistsById existsById(); + /** + * Checks if the document exists in the bucket. + */ + ExecutableExistsById existsById(Class domainType); + /** * Terminating operations invoking the actual execution. */ @@ -78,7 +84,6 @@ public interface ExecutableExistsByIdOperation { } /** - * * Fluent method to specify the collection. * * @param the entity type to use for the results. 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 5484d9ab..51942a14 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperationSupport.java @@ -32,26 +32,34 @@ public class ExecutableExistsByIdOperationSupport implements ExecutableExistsByI } @Override + @Deprecated public ExecutableExistsById existsById() { - return new ExecutableExistsByIdSupport(template, null, null, null); + return existsById(null); + } + + @Override + public ExecutableExistsById existsById(Class domainType) { + return new ExecutableExistsByIdSupport(template, domainType, null, null, null); } static class ExecutableExistsByIdSupport implements ExecutableExistsById { private final CouchbaseTemplate template; + private final Class domainType; private final String scope; private final String collection; private final ExistsOptions options; private final ReactiveExistsByIdSupport reactiveSupport; - ExecutableExistsByIdSupport(final CouchbaseTemplate template, final String scope, final String collection, - final ExistsOptions options) { + ExecutableExistsByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, + final String collection, final ExistsOptions options) { this.template = template; + this.domainType = domainType; this.scope = scope; this.collection = collection; this.options = options; - this.reactiveSupport = new ReactiveExistsByIdSupport(template.reactive(), scope, collection, options); + this.reactiveSupport = new ReactiveExistsByIdSupport(template.reactive(), domainType, scope, collection, options); } @Override @@ -66,20 +74,18 @@ public class ExecutableExistsByIdOperationSupport implements ExecutableExistsByI @Override public ExistsByIdWithOptions inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); - return new ExecutableExistsByIdSupport(template, scope, collection, options); + return new ExecutableExistsByIdSupport(template, domainType, scope, collection, options); } @Override public TerminatingExistsById withOptions(final ExistsOptions options) { Assert.notNull(options, "Options must not be null."); - return new ExecutableExistsByIdSupport(template, scope, collection, options); + return new ExecutableExistsByIdSupport(template, domainType, scope, collection, options); } @Override public ExistsByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); - return new ExecutableExistsByIdSupport(template, scope, collection, options); + return new ExecutableExistsByIdSupport(template, domainType, scope, collection, options); } } 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 54924a80..ad88bc4d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByAnalyticsOperationSupport.java @@ -97,14 +97,12 @@ public class ExecutableFindByAnalyticsOperationSupport implements ExecutableFind @Override public FindByAnalyticsInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ExecutableFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options); } @Override public FindByAnalyticsWithConsistency inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ExecutableFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options); } 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 b1d07be5..087c0cd3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -77,19 +77,17 @@ public class ExecutableFindByIdOperationSupport implements ExecutableFindByIdOpe @Override public FindByIdWithOptions inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields); } @Override public FindByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, fields); } @Override public FindByIdInScope project(String... fields) { - Assert.notEmpty(fields, "Fields must not be null nor empty."); + Assert.notEmpty(fields, "Fields must not be null."); return new ExecutableFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields)); } 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 86cb5880..48d77f02 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -68,7 +68,8 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu this.returnType = returnType; this.query = query; this.reactiveSupport = new ReactiveFindByQuerySupport(template.reactive(), domainType, returnType, query, - scanConsistency, scope, collection, options, distinctFields, new NonReactiveSupportWrapper(template.support())); + scanConsistency, scope, collection, options, distinctFields, + new NonReactiveSupportWrapper(template.support())); this.scanConsistency = scanConsistency; this.scope = scope; this.collection = collection; @@ -126,8 +127,12 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu @Override public FindByQueryWithProjection distinct(final String[] distinctFields) { Assert.notNull(distinctFields, "distinctFields must not be null!"); + // Coming from an annotation, this cannot be null. + // But a non-null but empty distinctFields means distinct on all fields + // So to indicate do not use distinct, we use {"-"} from the annotation, and here we change it to null. + String[] dFields = distinctFields.length == 1 && "-".equals(distinctFields[0]) ? null : distinctFields; return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields); + collection, options, dFields); } @Override @@ -154,14 +159,12 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu @Override public FindByQueryInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, distinctFields); } @Override public FindByQueryWithConsistency inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ExecutableFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, distinctFields); } 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 5ef12b54..c087b920 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindFromReplicasByIdOperationSupport.java @@ -18,9 +18,9 @@ package org.springframework.data.couchbase.core; import java.util.Collection; import org.springframework.data.couchbase.core.ReactiveFindFromReplicasByIdOperationSupport.ReactiveFindFromReplicasByIdSupport; +import org.springframework.util.Assert; import com.couchbase.client.java.kv.GetAnyReplicaOptions; -import org.springframework.util.Assert; public class ExecutableFindFromReplicasByIdOperationSupport implements ExecutableFindFromReplicasByIdOperation { @@ -54,7 +54,7 @@ public class ExecutableFindFromReplicasByIdOperationSupport implements Executabl this.options = options; this.returnType = returnType; this.reactiveSupport = new ReactiveFindFromReplicasByIdSupport<>(template.reactive(), domainType, returnType, - scope, collection, options, new NonReactiveSupportWrapper(template.support())); + scope, collection, options, new NonReactiveSupportWrapper(template.support())); } @Override @@ -75,13 +75,11 @@ public class ExecutableFindFromReplicasByIdOperationSupport implements Executabl @Override public FindFromReplicasByIdWithOptions inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ExecutableFindFromReplicasByIdSupport<>(template, domainType, returnType, scope, collection, options); } @Override public FindFromReplicasByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ExecutableFindFromReplicasByIdSupport<>(template, domainType, returnType, scope, collection, options); } 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 908cf459..52031af2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java @@ -89,14 +89,12 @@ public class ExecutableInsertByIdOperationSupport implements ExecutableInsertByI @Override public InsertByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry); } @Override public InsertByIdWithOptions inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ExecutableInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java index 67c5392d..085b451b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java @@ -18,7 +18,6 @@ package org.springframework.data.couchbase.core; import java.util.Collection; import java.util.List; -import org.springframework.data.couchbase.core.query.WithConsistency; import org.springframework.data.couchbase.core.support.InCollection; import org.springframework.data.couchbase.core.support.InScope; import org.springframework.data.couchbase.core.support.OneAndAllId; @@ -39,6 +38,12 @@ public interface ExecutableRemoveByIdOperation { /** * Removes a document. */ + ExecutableRemoveById removeById(Class domainType); + + /** + * Removes a document. + */ + @Deprecated ExecutableRemoveById removeById(); /** 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 a006f1d6..9f79dad3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -35,14 +35,21 @@ public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByI } @Override + @Deprecated public ExecutableRemoveById removeById() { - return new ExecutableRemoveByIdSupport(template, null, null, null, PersistTo.NONE, ReplicateTo.NONE, + return removeById(null); + } + + @Override + public ExecutableRemoveById removeById(Class domainType) { + return new ExecutableRemoveByIdSupport(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, DurabilityLevel.NONE, null); } static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { private final CouchbaseTemplate template; + private final Class domainType; private final String scope; private final String collection; private final RemoveOptions options; @@ -52,18 +59,19 @@ public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByI private final Long cas; private final ReactiveRemoveByIdSupport reactiveRemoveByIdSupport; - ExecutableRemoveByIdSupport(final CouchbaseTemplate template, final String scope, final String collection, - final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + ExecutableRemoveByIdSupport(final CouchbaseTemplate template, final Class domainType, final String scope, + final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, final DurabilityLevel durabilityLevel, Long cas) { this.template = template; + this.domainType = domainType; this.scope = scope; this.collection = collection; this.options = options; this.persistTo = persistTo; this.replicateTo = replicateTo; this.durabilityLevel = durabilityLevel; - this.reactiveRemoveByIdSupport = new ReactiveRemoveByIdSupport(template.reactive(), scope, collection, options, - persistTo, replicateTo, durabilityLevel, cas); + this.reactiveRemoveByIdSupport = new ReactiveRemoveByIdSupport(template.reactive(), domainType, scope, collection, + options, persistTo, replicateTo, durabilityLevel, cas); this.cas = cas; } @@ -79,15 +87,14 @@ public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByI @Override public RemoveByIdWithOptions inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); - return new ExecutableRemoveByIdSupport(template, scope, collection, options, persistTo, replicateTo, + return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } @Override public RemoveByIdInCollection withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); - return new ExecutableRemoveByIdSupport(template, scope, collection, options, persistTo, replicateTo, + return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } @@ -95,27 +102,26 @@ public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByI public RemoveByIdInCollection withDurability(final PersistTo persistTo, final ReplicateTo replicateTo) { Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); - return new ExecutableRemoveByIdSupport(template, scope, collection, options, persistTo, replicateTo, + return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } @Override public TerminatingRemoveById withOptions(final RemoveOptions options) { Assert.notNull(options, "Options must not be null."); - return new ExecutableRemoveByIdSupport(template, scope, collection, options, persistTo, replicateTo, + return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } @Override public RemoveByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); - return new ExecutableRemoveByIdSupport(template, scope, collection, options, persistTo, replicateTo, + return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } @Override public RemoveByIdWithDurability withCas(Long cas) { - return new ExecutableRemoveByIdSupport(template, scope, collection, options, persistTo, replicateTo, + return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } } 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 f8574598..a97c62ba 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByQueryOperationSupport.java @@ -19,10 +19,10 @@ import java.util.List; import org.springframework.data.couchbase.core.ReactiveRemoveByQueryOperationSupport.ReactiveRemoveByQuerySupport; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; -import org.springframework.util.Assert; public class ExecutableRemoveByQueryOperationSupport implements ExecutableRemoveByQueryOperation { @@ -36,8 +36,7 @@ public class ExecutableRemoveByQueryOperationSupport implements ExecutableRemove @Override public ExecutableRemoveByQuery removeByQuery(Class domainType) { - return new ExecutableRemoveByQuerySupport<>(template, domainType, ALL_QUERY, null, null, - null, null); + return new ExecutableRemoveByQuerySupport<>(template, domainType, ALL_QUERY, null, null, null, null); } static class ExecutableRemoveByQuerySupport implements ExecutableRemoveByQuery { @@ -90,7 +89,6 @@ public class ExecutableRemoveByQueryOperationSupport implements ExecutableRemove @Override public RemoveByQueryWithConsistency inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, options); } @@ -104,7 +102,6 @@ public class ExecutableRemoveByQueryOperationSupport implements ExecutableRemove @Override public RemoveByQueryInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ExecutableRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, options); } 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 ba8ab3eb..ef47eb94 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java @@ -82,7 +82,6 @@ public class ExecutableReplaceByIdOperationSupport implements ExecutableReplaceB @Override public ReplaceByIdWithOptions inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry); } @@ -118,7 +117,6 @@ public class ExecutableReplaceByIdOperationSupport implements ExecutableReplaceB @Override public ReplaceByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ExecutableReplaceByIdSupport<>(template, domainType, scope, collection, options, 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 0719082c..61b7a394 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java @@ -89,14 +89,12 @@ public class ExecutableUpsertByIdOperationSupport implements ExecutableUpsertByI @Override public UpsertByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ExecutableUpsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry); } @Override public UpsertByIdWithOptions inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ExecutableUpsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry); } diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index 6f890a7e..6e64ecb3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -15,11 +15,11 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; - -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import reactor.core.publisher.Mono; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; + /** * Wrapper of {@link TemplateSupport} methods to adapt them to {@link ReactiveTemplateSupport}. * diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java index 83e43bbe..f3b97d35 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.couchbase.core; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.support.PseudoArgs; /** * Defines common operations on the Couchbase data source, most commonly implemented by * {@link ReactiveCouchbaseTemplate}. + * + * @author Michael Nitschinger + * @author Michael Reiche */ public interface ReactiveCouchbaseOperations extends ReactiveFluentCouchbaseOperations { @@ -46,9 +47,4 @@ public interface ReactiveCouchbaseOperations extends ReactiveFluentCouchbaseOper */ CouchbaseClientFactory getCouchbaseClientFactory(); - /** - * @@return the pseudoArgs from the ThreadLocal field of the CouchbaseOperations - */ - PseudoArgs getPseudoArgs(); - } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java index 2aedc9df..a20e438e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -43,7 +43,7 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A private final CouchbaseConverter converter; private final PersistenceExceptionTranslator exceptionTranslator; private final ReactiveCouchbaseTemplateSupport templateSupport; - private ThreadLocal> threadLocalArgs = new ThreadLocal<>(); + private ThreadLocal> threadLocalArgs = null; public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { this(clientFactory, converter, new JacksonTranslationService()); @@ -64,7 +64,12 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A @Override public ReactiveExistsById existsById() { - return new ReactiveExistsByIdOperationSupport(this).existsById(); + return existsById(null); + } + + @Override + public ReactiveExistsById existsById(Class domainType) { + return new ReactiveExistsByIdOperationSupport(this).existsById(domainType); } @Override @@ -89,7 +94,12 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A @Override public ReactiveRemoveById removeById() { - return new ReactiveRemoveByIdOperationSupport(this).removeById(); + return removeById(null); + } + + @Override + public ReactiveRemoveById removeById(Class domainType) { + return new ReactiveRemoveByIdOperationSupport(this).removeById(domainType); } @Override @@ -158,11 +168,18 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A } /** - * {@inheritDoc} + * @return the pseudoArgs from the ThreadLocal field */ - @Override public PseudoArgs getPseudoArgs() { return threadLocalArgs == null ? null : threadLocalArgs.get(); } + /** + * set the ThreadLocal field + */ + public void setPseudoArgs(PseudoArgs threadLocalArgs) { + this.threadLocalArgs = new ThreadLocal<>(); + this.threadLocalArgs.set(threadLocalArgs); + } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index d1239370..87f22af6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -16,8 +16,6 @@ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; -import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterSaveEvent; import reactor.core.publisher.Mono; import org.slf4j.Logger; @@ -30,11 +28,12 @@ import org.springframework.data.couchbase.core.convert.translation.TranslationSe import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; +import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; +import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterConvertCallback; import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeConvertCallback; -import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeConvertEvent; -import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeSaveEvent; import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.callback.EntityCallbacks; @@ -68,13 +67,13 @@ class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, React @Override public Mono encodeEntity(final Object entityToEncode) { - return Mono.just(entityToEncode).doOnNext(entity -> maybeEmitEvent(new ReactiveBeforeConvertEvent<>(entity))) + return Mono.just(entityToEncode).doOnNext(entity -> maybeEmitEvent(new BeforeConvertEvent<>(entity))) .flatMap(entity -> maybeCallBeforeConvert(entity, "")).map(maybeNewEntity -> { final CouchbaseDocument converted = new CouchbaseDocument(); converter.write(maybeNewEntity, converted); return converted; }).flatMap(converted -> maybeCallAfterConvert(entityToEncode, converted, "").thenReturn(converted)) - .doOnNext(converted -> maybeEmitEvent(new ReactiveBeforeSaveEvent<>(entityToEncode, converted))); + .doOnNext(converted -> maybeEmitEvent(new BeforeSaveEvent<>(entityToEncode, converted))); } @Override @@ -112,7 +111,7 @@ class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, React } else { returnValue = entity; } - maybeEmitEvent(new ReactiveAfterSaveEvent(returnValue, converted)); + maybeEmitEvent(new AfterSaveEvent(returnValue, converted)); return returnValue; }); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java index 5415468d..014cc43e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java @@ -26,6 +26,7 @@ import org.springframework.data.couchbase.core.support.OneAndAllExistsReactive; import org.springframework.data.couchbase.core.support.WithExistsOptions; import com.couchbase.client.java.kv.ExistsOptions; + /** * Exists Operations * @@ -37,8 +38,14 @@ public interface ReactiveExistsByIdOperation { /** * Checks if the document exists in the bucket. */ + @Deprecated ReactiveExistsById existsById(); + /** + * Checks if the document exists in the bucket. + */ + ReactiveExistsById existsById(Class domainType); + /** * Terminating operations invoking the actual execution. */ diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java index 8e03dd57..56b64282 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java @@ -25,6 +25,7 @@ import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.util.Assert; @@ -41,20 +42,28 @@ public class ReactiveExistsByIdOperationSupport implements ReactiveExistsByIdOpe } @Override + @Deprecated public ReactiveExistsById existsById() { - return new ReactiveExistsByIdSupport(template, null, null, null); + return existsById(null); + } + + @Override + public ReactiveExistsById existsById(Class domainType) { + return new ReactiveExistsByIdSupport(template, domainType, null, null, null); } static class ReactiveExistsByIdSupport implements ReactiveExistsById { private final ReactiveCouchbaseTemplate template; + private final Class domainType; private final String scope; private final String collection; private final ExistsOptions options; - ReactiveExistsByIdSupport(final ReactiveCouchbaseTemplate template, final String scope, final String collection, - final ExistsOptions options) { + ReactiveExistsByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, + final String collection, final ExistsOptions options) { this.template = template; + this.domainType = domainType; this.scope = scope; this.collection = collection; this.options = options; @@ -62,12 +71,12 @@ public class ReactiveExistsByIdOperationSupport implements ReactiveExistsByIdOpe @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, - options != null ? options : ExistsOptions.existsOptions()); - LOG.trace("statement: {} scope: {} collection: {}", "exitsById", pArgs.getScope(), pArgs.getCollection()); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + LOG.trace("existsById {}", pArgs); return Mono.just(id) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive().exists(id, pArgs.getOptions()).map(ExistsResult::exists)) + .getCollection(pArgs.getCollection()).reactive().exists(id, buildOptions(pArgs.getOptions())) + .map(ExistsResult::exists)) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); @@ -77,6 +86,10 @@ public class ReactiveExistsByIdOperationSupport implements ReactiveExistsByIdOpe }); } + private ExistsOptions buildOptions(ExistsOptions options) { + return OptionsBuilder.buildExistsOptions(options); + } + @Override public Mono> all(final Collection ids) { return Flux.fromIterable(ids).flatMap(id -> one(id).map(result -> Tuples.of(id, result))) @@ -85,20 +98,18 @@ public class ReactiveExistsByIdOperationSupport implements ReactiveExistsByIdOpe @Override public ExistsByIdWithOptions inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); - return new ReactiveExistsByIdSupport(template, scope, collection, options); + return new ReactiveExistsByIdSupport(template, domainType, scope, collection, options); } @Override public TerminatingExistsById withOptions(final ExistsOptions options) { Assert.notNull(options, "Options must not be null."); - return new ReactiveExistsByIdSupport(template, scope, collection, options); + return new ReactiveExistsByIdSupport(template, domainType, scope, collection, options); } @Override public ExistsByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); - return new ReactiveExistsByIdSupport(template, scope, collection, options); + return new ReactiveExistsByIdSupport(template, domainType, scope, collection, options); } } 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 4f64a6b1..53cce70d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -167,14 +167,12 @@ public class ReactiveFindByAnalyticsOperationSupport implements ReactiveFindByAn @Override public FindByAnalyticsInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ReactiveFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, support); } @Override public FindByAnalyticsWithConsistency inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ReactiveFindByAnalyticsSupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, support); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index a9973136..0e722ff5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -36,7 +36,6 @@ import com.couchbase.client.java.kv.GetOptions; public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperation { private final ReactiveCouchbaseTemplate template; - private static final Logger LOG = LoggerFactory.getLogger(ReactiveFindByIdOperationSupport.class); ReactiveFindByIdOperationSupport(ReactiveCouchbaseTemplate template) { @@ -71,19 +70,19 @@ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperati @Override public Mono one(final String id) { - return Mono.just(id).flatMap(docId -> { - GetOptions gOptions = options != null ? options : getOptions(); - if (gOptions.build().transcoder() == null) { - gOptions.transcoder(RawJsonTranscoder.INSTANCE); - } - if (fields != null && !fields.isEmpty()) { - gOptions.project(fields); - } - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions); - LOG.trace("statement: {} scope: {} collection: {}", "findById", pArgs.getScope(), pArgs.getCollection()); - return template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getCollection(pArgs.getCollection()) - .reactive().get(docId, pArgs.getOptions()); - }).flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType)) + GetOptions gOptions = options != null ? options : getOptions(); + if (gOptions.build().transcoder() == null) { + gOptions.transcoder(RawJsonTranscoder.INSTANCE); + } + if (fields != null && !fields.isEmpty()) { + gOptions.project(fields); + } + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions, domainType); + LOG.trace("findById {}", pArgs); + return Mono.just(id) + .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive().get(docId, pArgs.getOptions())) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType)) .onErrorResume(throwable -> { if (throwable instanceof RuntimeException) { if (throwable instanceof DocumentNotFoundException) { @@ -113,19 +112,17 @@ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperati @Override public FindByIdWithOptions inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, support); } @Override public FindByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, support); } @Override public FindByIdInScope project(String... fields) { - Assert.notEmpty(fields, "Fields must not be null nor empty."); + Assert.notNull(fields, "Fields must not be null"); return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields), support); } 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 de056de0..c7a613b3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperation.java @@ -89,8 +89,6 @@ public interface ReactiveFindByQueryOperation { */ Mono exists(); - QueryOptions buildOptions(QueryOptions options); - } /** @@ -170,7 +168,7 @@ public interface ReactiveFindByQueryOperation { } /** - * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. + * Fluent method to specify scan consistency. Scan consistency may also come from an annotation. * * @param the entity type to use for the results. */ 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 60a50438..d4b88e03 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -41,7 +41,6 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO private static final Query ALL_QUERY = new Query(); private final ReactiveCouchbaseTemplate template; - private static final Logger LOG = LoggerFactory.getLogger(ReactiveFindByQueryOperationSupport.class); public ReactiveFindByQueryOperationSupport(final ReactiveCouchbaseTemplate template) { @@ -62,11 +61,8 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO private final Query query; private final QueryScanConsistency scanConsistency; private final String collection; - private String scope; + private final String scope; private final String[] distinctFields; - // this would hold scanConsistency etc. from the fluent api if they were converted from standalone fields - // withScope(scopeName) could put raw("query_context",default:.) - // this is not the options argument in save( entity, options ). That becomes query.getCouchbaseOptions() private final QueryOptions options; private final ReactiveTemplateSupport support; @@ -91,7 +87,8 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO @Override public FindByQueryWithQuery matching(Query query) { QueryScanConsistency scanCons; - if (query.getScanConsistency() != null) { + if (query.getScanConsistency() != null) { // redundant, since buildQueryOptions() will use + // query.getScanConsistency() scanCons = query.getScanConsistency(); } else { scanCons = scanConsistency; @@ -109,14 +106,12 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO @Override public FindByQueryInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, distinctFields, support); } @Override public FindByQueryWithConsistency inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, collection, options, distinctFields, support); } @@ -142,10 +137,14 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO } @Override - public FindByQueryWithDistinct distinct(String[] distinctFields) { + public FindByQueryWithDistinct distinct(final String[] distinctFields) { Assert.notNull(distinctFields, "distinctFields must not be null!"); + // Coming from an annotation, this cannot be null. + // But a non-null but empty distinctFields means distinct on all fields + // So to indicate do not use distinct, we use {"-"} from the annotation, and here we change it to null. + String[] dFields = distinctFields.length == 1 && "-".equals(distinctFields[0]) ? null : distinctFields; return new ReactiveFindByQuerySupport<>(template, domainType, returnType, query, scanConsistency, scope, - collection, options, distinctFields, support); + collection, options, dFields, support); } @Override @@ -160,72 +159,65 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO @Override public Flux all() { - return Flux.defer(() -> { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options); - String statement = assembleEntityQuery(false, distinctFields, pArgs.getCollection()); - LOG.trace("statement: {} {}", "findByQuery", statement); - Mono allResult = pArgs.getScope() == null - ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, - buildOptions(pArgs.getOptions())) - : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, - buildOptions(pArgs.getOptions())); - return allResult.onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + String statement = assembleEntityQuery(false, distinctFields, pArgs.getCollection()); + LOG.trace("findByQuery {} statement: {}", pArgs, statement); + Mono allResult = pArgs.getScope() == null + ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, + buildOptions(pArgs.getOptions())) + : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, + buildOptions(pArgs.getOptions())); + return Flux.defer(() -> allResult.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }).flatMapMany(ReactiveQueryResult::rowsAsObject).flatMap(row -> { + String id = ""; + long cas = 0; + if (distinctFields == null) { + if (row.getString(TemplateUtils.SELECT_ID) == null) { + return Flux.error(new CouchbaseException( + "query did not project " + TemplateUtils.SELECT_ID + ". Either use #{#n1ql.selectEntity} or project " + + TemplateUtils.SELECT_ID + " and " + TemplateUtils.SELECT_CAS + " : " + statement)); } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).flatMap(row -> { - String id = ""; - long cas = 0; - if (distinctFields == null) { - if (row.getString(TemplateUtils.SELECT_ID) == null) { - return Flux.error(new CouchbaseException( - "query did not project " + TemplateUtils.SELECT_ID + ". Either use #{#n1ql.selectEntity} or project " - + TemplateUtils.SELECT_ID + " and " + TemplateUtils.SELECT_CAS + " : " + statement)); - } - id = row.getString(TemplateUtils.SELECT_ID); - if (row.getLong(TemplateUtils.SELECT_CAS) == null) { - return Flux.error(new CouchbaseException( - "query did not project " + TemplateUtils.SELECT_CAS + ". Either use #{#n1ql.selectEntity} or project " - + TemplateUtils.SELECT_ID + " and " + TemplateUtils.SELECT_CAS + " : " + statement)); - } - cas = row.getLong(TemplateUtils.SELECT_CAS); - row.removeKey(TemplateUtils.SELECT_ID); - row.removeKey(TemplateUtils.SELECT_CAS); + id = row.getString(TemplateUtils.SELECT_ID); + if (row.getLong(TemplateUtils.SELECT_CAS) == null) { + return Flux.error(new CouchbaseException( + "query did not project " + TemplateUtils.SELECT_CAS + ". Either use #{#n1ql.selectEntity} or project " + + TemplateUtils.SELECT_ID + " and " + TemplateUtils.SELECT_CAS + " : " + statement)); } - return support.decodeEntity(id, row.toString(), cas, returnType); - }); - }); + cas = row.getLong(TemplateUtils.SELECT_CAS); + row.removeKey(TemplateUtils.SELECT_ID); + row.removeKey(TemplateUtils.SELECT_CAS); + } + return support.decodeEntity(id, row.toString(), cas, returnType); + })); } - @Override - public QueryOptions buildOptions(QueryOptions options) { + private QueryOptions buildOptions(QueryOptions options) { QueryOptions opts = query.buildQueryOptions(options, scanConsistency); return opts; } @Override public Mono count() { - return Mono.defer(() -> { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options); - String statement = assembleEntityQuery(true, distinctFields, pArgs.getCollection()); - LOG.trace("statement: {} {}", "findByQuery", statement); - Mono countResult = this.collection == null - ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, - buildOptions(pArgs.getOptions())) - : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, - buildOptions(pArgs.getOptions())); - return countResult.onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> { - return row.getLong(TemplateUtils.SELECT_COUNT); - }).next(); - }); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + String statement = assembleEntityQuery(true, distinctFields, pArgs.getCollection()); + LOG.trace("findByQuery {} statement: {}", pArgs, statement); + Mono countResult = pArgs.getScope() == null + ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, + buildOptions(pArgs.getOptions())) + : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, + buildOptions(pArgs.getOptions())); + return Mono.defer(() -> countResult.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> row.getLong(TemplateUtils.SELECT_COUNT)).next()); } @Override 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 d9a8ed60..88cdcd61 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -68,16 +68,16 @@ public class ReactiveFindFromReplicasByIdOperationSupport implements ReactiveFin @Override public Mono any(final String id) { - return Mono.just(id).flatMap(docId -> { - GetAnyReplicaOptions garOptions = options != null ? options : getAnyReplicaOptions(); - if (garOptions.build().transcoder() == null) { - garOptions.transcoder(RawJsonTranscoder.INSTANCE); - } - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions); - LOG.trace("statement: {} scope: {} collection: {}", "getAnyReplica", pArgs.getScope(), pArgs.getCollection()); - return template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getCollection(pArgs.getCollection()) - .reactive().getAnyReplica(docId, pArgs.getOptions()); - }).flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType)) + GetAnyReplicaOptions garOptions = options != null ? options : getAnyReplicaOptions(); + if (garOptions.build().transcoder() == null) { + garOptions.transcoder(RawJsonTranscoder.INSTANCE); + } + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions, domainType); + LOG.trace("getAnyReplica {}", pArgs); + return Mono.just(id) + .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive().getAnyReplica(docId, pArgs.getOptions())) + .flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType)) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); @@ -101,14 +101,12 @@ public class ReactiveFindFromReplicasByIdOperationSupport implements ReactiveFin @Override public FindFromReplicasByIdWithOptions inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ReactiveFindFromReplicasByIdSupport<>(template, domainType, returnType, scope, collection, options, support); } @Override public FindFromReplicasByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ReactiveFindFromReplicasByIdSupport<>(template, domainType, returnType, scope, collection, options, support); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java index b7886c1a..3caf6047 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperation.java @@ -21,7 +21,6 @@ import reactor.core.publisher.Mono; import java.time.Duration; import java.util.Collection; -import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.support.InCollection; import org.springframework.data.couchbase.core.support.InScope; import org.springframework.data.couchbase.core.support.OneAndAllEntityReactive; @@ -68,8 +67,6 @@ public interface ReactiveInsertByIdOperation { @Override Flux all(Collection objects); - InsertOptions buildOptions(InsertOptions options, CouchbaseDocument doc); - } /** 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 53a04794..7514e8ba 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -24,6 +24,7 @@ import java.util.Collection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.util.Assert; @@ -34,8 +35,8 @@ import com.couchbase.client.java.kv.ReplicateTo; public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOperation { - private static final Logger LOG = LoggerFactory.getLogger(ReactiveInsertByIdOperationSupport.class); private final ReactiveCouchbaseTemplate template; + private static final Logger LOG = LoggerFactory.getLogger(ReactiveInsertByIdOperationSupport.class); public ReactiveInsertByIdOperationSupport(final ReactiveCouchbaseTemplate template) { this.template = template; @@ -78,14 +79,12 @@ public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOpe @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, - options != null ? options : InsertOptions.insertOptions()); - LOG.trace("statement: {} scope: {} collection: {} options: {}", "insertById", pArgs.getScope(), - pArgs.getCollection(), pArgs.getOptions()); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + LOG.trace("insertById {}", pArgs); return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) + .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive() + .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) .flatMap(result -> support.applyUpdatedId(object, converted.getId()) .flatMap(updatedObject -> support.applyUpdatedCas(updatedObject, converted, result.cas())))) .onErrorMap(throwable -> { @@ -102,20 +101,8 @@ public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOpe return Flux.fromIterable(objects).flatMap(this::one); } - @Override public InsertOptions buildOptions(InsertOptions options, CouchbaseDocument doc) { // CouchbaseDocument converted - options = options != null ? options : InsertOptions.insertOptions(); - if (persistTo != PersistTo.NONE || replicateTo != ReplicateTo.NONE) { - options.durability(persistTo, replicateTo); - } else if (durabilityLevel != DurabilityLevel.NONE) { - options.durability(durabilityLevel); - } - if (expiry != null) { - options.expiry(expiry); - } else if (doc.getExpiration() != 0) { - options.expiry(Duration.ofSeconds(doc.getExpiration())); - } - return options; + return OptionsBuilder.buildInsertOptions(options, persistTo, replicateTo, durabilityLevel, expiry, doc); } @Override @@ -127,14 +114,12 @@ public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOpe @Override public InsertByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, support); } @Override public InsertByIdWithOptions inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, support); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java index 300203fb..eb55622d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperation.java @@ -40,8 +40,14 @@ public interface ReactiveRemoveByIdOperation { /** * Removes a document. */ + @Deprecated ReactiveRemoveById removeById(); + /** + * Removes a document. + */ + ReactiveRemoveById removeById(Class domainType); + /** * Terminating operations invoking the actual execution. */ @@ -106,6 +112,7 @@ public interface ReactiveRemoveByIdOperation { interface RemoveByIdWithDurability extends RemoveByIdInScope, WithDurability { @Override RemoveByIdInCollection withDurability(DurabilityLevel durabilityLevel); + @Override RemoveByIdInCollection withDurability(PersistTo persistTo, ReplicateTo replicateTo); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index a337c39e..b4f64ed7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -15,13 +15,14 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterDeleteEvent; -import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeDeleteEvent; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Collection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.util.Assert; @@ -33,20 +34,28 @@ import com.couchbase.client.java.kv.ReplicateTo; public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOperation { private final ReactiveCouchbaseTemplate template; + private static final Logger LOG = LoggerFactory.getLogger(ReactiveRemoveByIdOperationSupport.class); public ReactiveRemoveByIdOperationSupport(final ReactiveCouchbaseTemplate template) { this.template = template; } @Override + @Deprecated public ReactiveRemoveById removeById() { - return new ReactiveRemoveByIdSupport(template, null, null, null, PersistTo.NONE, ReplicateTo.NONE, + return removeById(null); + } + + @Override + public ReactiveRemoveById removeById(Class domainType) { + return new ReactiveRemoveByIdSupport(template, domainType, null, null, null, PersistTo.NONE, ReplicateTo.NONE, DurabilityLevel.NONE, null); } static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { private final ReactiveCouchbaseTemplate template; + private final Class domainType; private final String scope; private final String collection; private final RemoveOptions options; @@ -55,10 +64,11 @@ public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOpe private final DurabilityLevel durabilityLevel; private final Long cas; - ReactiveRemoveByIdSupport(final ReactiveCouchbaseTemplate template, final String scope, final String collection, - final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, + ReactiveRemoveByIdSupport(final ReactiveCouchbaseTemplate template, final Class domainType, final String scope, + final String collection, final RemoveOptions options, final PersistTo persistTo, final ReplicateTo replicateTo, final DurabilityLevel durabilityLevel, Long cas) { this.template = template; + this.domainType = domainType; this.scope = scope; this.collection = collection; this.options = options; @@ -70,22 +80,19 @@ public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOpe @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, - options != null ? options : RemoveOptions.removeOptions()); - return Mono.just(id).map(r -> { - template.support().maybeEmitEvent(new ReactiveBeforeDeleteEvent<>(r)); - return r; - }).flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive().remove(id, buildRemoveOptions(pArgs.getOptions())).map(r -> { - template.support().maybeEmitEvent(new ReactiveAfterDeleteEvent<>(r)); - return RemoveResult.from(docId, r); - })).onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + LOG.trace("removeById {}", pArgs); + return Mono.just(id) + .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive().remove(id, buildRemoveOptions(pArgs.getOptions())) + .map(r -> RemoveResult.from(docId, r))) + .onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); } @Override @@ -94,22 +101,13 @@ public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOpe } private RemoveOptions buildRemoveOptions(RemoveOptions options) { - options = options != null ? options : RemoveOptions.removeOptions(); - if (persistTo != PersistTo.NONE || replicateTo != ReplicateTo.NONE) { - options.durability(persistTo, replicateTo); - } else if (durabilityLevel != DurabilityLevel.NONE) { - options.durability(durabilityLevel); - } - if (cas != null) { - options.cas(cas); - } - return options; + return OptionsBuilder.buildRemoveOptions(options, persistTo, replicateTo, durabilityLevel, cas); } @Override public RemoveByIdInCollection withDurability(final DurabilityLevel durabilityLevel) { Assert.notNull(durabilityLevel, "Durability Level must not be null."); - return new ReactiveRemoveByIdSupport(template, scope, collection, options, persistTo, replicateTo, + return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } @@ -117,34 +115,32 @@ public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOpe public RemoveByIdInCollection withDurability(final PersistTo persistTo, final ReplicateTo replicateTo) { Assert.notNull(persistTo, "PersistTo must not be null."); Assert.notNull(replicateTo, "ReplicateTo must not be null."); - return new ReactiveRemoveByIdSupport(template, scope, collection, options, persistTo, replicateTo, + return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } @Override public RemoveByIdWithDurability inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); - return new ReactiveRemoveByIdSupport(template, scope, collection, options, persistTo, replicateTo, + return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } @Override public RemoveByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); - return new ReactiveRemoveByIdSupport(template, scope, collection, options, persistTo, replicateTo, + return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } @Override public TerminatingRemoveById withOptions(final RemoveOptions options) { Assert.notNull(options, "Options must not be null."); - return new ReactiveRemoveByIdSupport(template, scope, collection, options, persistTo, replicateTo, + return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } @Override public RemoveByIdWithDurability withCas(Long cas) { - return new ReactiveRemoveByIdSupport(template, scope, collection, options, persistTo, replicateTo, + return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, cas); } } 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 a41bd7b9..efcd8658 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -15,7 +15,6 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -26,6 +25,7 @@ import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.data.couchbase.core.support.TemplateUtils; +import org.springframework.util.Assert; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -36,7 +36,6 @@ public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQu private static final Query ALL_QUERY = new Query(); private final ReactiveCouchbaseTemplate template; - private static final Logger LOG = LoggerFactory.getLogger(ReactiveRemoveByQueryOperationSupport.class); public ReactiveRemoveByQueryOperationSupport(final ReactiveCouchbaseTemplate template) { @@ -45,8 +44,7 @@ public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQu @Override public ReactiveRemoveByQuery removeByQuery(Class domainType) { - return new ReactiveRemoveByQuerySupport<>(template, domainType, ALL_QUERY,null, null, - null, null); + return new ReactiveRemoveByQuerySupport<>(template, domainType, ALL_QUERY, null, null, null, null); } static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery { @@ -72,25 +70,23 @@ public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQu @Override public Flux all() { - return Flux.defer(() -> { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options); - String statement = assembleDeleteQuery(pArgs.getCollection()); - LOG.trace("statement: {}", statement); - Mono allResult = pArgs.getCollection() == null - ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, - buildQueryOptions(pArgs.getOptions())) - : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, - buildQueryOptions(pArgs.getOptions())); - return allResult.onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }).flatMapMany(ReactiveQueryResult::rowsAsObject) - .map(row -> new RemoveResult(row.getString(TemplateUtils.SELECT_ID), row.getLong(TemplateUtils.SELECT_CAS), - Optional.empty())); - }); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + String statement = assembleDeleteQuery(pArgs.getCollection()); + LOG.trace("removeByQuery {} statement: {}", pArgs, statement); + Mono allResult = pArgs.getScope() == null + ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, + buildQueryOptions(pArgs.getOptions())) + : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, + buildQueryOptions(pArgs.getOptions())); + return Flux.defer(() -> allResult.onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }).flatMapMany(ReactiveQueryResult::rowsAsObject) + .map(row -> new RemoveResult(row.getString(TemplateUtils.SELECT_ID), row.getLong(TemplateUtils.SELECT_CAS), + Optional.empty()))); } private QueryOptions buildQueryOptions(QueryOptions options) { @@ -105,7 +101,6 @@ public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQu @Override public RemoveByQueryWithConsistency inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, options); } @@ -136,7 +131,6 @@ public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQu @Override public RemoveByQueryInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency, scope, collection, options); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index bca6ecd0..55aebd4d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -24,6 +24,7 @@ import java.util.Collection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.util.Assert; @@ -34,8 +35,8 @@ import com.couchbase.client.java.kv.ReplicateTo; public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdOperation { - private static final Logger LOG = LoggerFactory.getLogger(ReactiveReplaceByIdOperationSupport.class); private final ReactiveCouchbaseTemplate template; + private static final Logger LOG = LoggerFactory.getLogger(ReactiveReplaceByIdOperationSupport.class); public ReactiveReplaceByIdOperationSupport(final ReactiveCouchbaseTemplate template) { this.template = template; @@ -78,13 +79,13 @@ public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdO @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, - options != null ? options : ReplaceOptions.replaceOptions()); - LOG.trace("statement: {} pArgs: {}", "replaceById", pArgs); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + LOG.trace("replaceById {}", pArgs); return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .replace(converted.getId(), converted.export(), buildReplaceOptions(pArgs.getOptions(), object, converted)) + .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive() + .replace(converted.getId(), converted.export(), + buildReplaceOptions(pArgs.getOptions(), object, converted)) .flatMap(result -> support.applyUpdatedCas(object, converted, result.cas()))) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { @@ -101,20 +102,8 @@ public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdO } private ReplaceOptions buildReplaceOptions(ReplaceOptions options, T object, CouchbaseDocument doc) { - options = options != null ? options : ReplaceOptions.replaceOptions(); - if (persistTo != PersistTo.NONE || replicateTo != ReplicateTo.NONE) { - options.durability(persistTo, replicateTo); - } else if (durabilityLevel != DurabilityLevel.NONE) { - options.durability(durabilityLevel); - } - if (expiry != null) { - options.expiry(expiry); - } else if (doc.getExpiration() != 0) { - options.expiry(Duration.ofSeconds(doc.getExpiration())); - } - long cas = support.getCas(object); - options.cas(cas); - return options; + return OptionsBuilder.buildReplaceOptions(options, persistTo, replicateTo, durabilityLevel, expiry, + support.getCas(object), doc); } @Override @@ -126,14 +115,12 @@ public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdO @Override public ReplaceByIdWithDurability inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, support); } @Override public ReplaceByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ReactiveReplaceByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, support); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index 38704595..5f897720 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -15,24 +15,24 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; - -import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import reactor.core.publisher.Mono; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; + public interface ReactiveTemplateSupport { - Mono encodeEntity(Object entityToEncode); + Mono encodeEntity(Object entityToEncode); - Mono decodeEntity(String id, String source, long cas, Class entityClass); + Mono decodeEntity(String id, String source, long cas, Class entityClass); - Mono applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); + Mono applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); - Mono applyUpdatedId(T entity, Object id); + Mono applyUpdatedId(T entity, Object id); - Long getCas(Object entity); + Long getCas(Object entity); - String getJavaNameForEntity(Class clazz); + String getJavaNameForEntity(Class clazz); - void maybeEmitEvent(CouchbaseMappingEvent event); + void maybeEmitEvent(CouchbaseMappingEvent event); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index 2c923b5d..b2433e10 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -21,7 +21,10 @@ import reactor.core.publisher.Mono; import java.time.Duration; import java.util.Collection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.util.Assert; @@ -33,6 +36,7 @@ import com.couchbase.client.java.kv.UpsertOptions; public class ReactiveUpsertByIdOperationSupport implements ReactiveUpsertByIdOperation { private final ReactiveCouchbaseTemplate template; + private static final Logger LOG = LoggerFactory.getLogger(ReactiveUpsertByIdOperationSupport.class); public ReactiveUpsertByIdOperationSupport(final ReactiveCouchbaseTemplate template) { this.template = template; @@ -75,12 +79,12 @@ public class ReactiveUpsertByIdOperationSupport implements ReactiveUpsertByIdOpe @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, - options != null ? options : UpsertOptions.upsertOptions()); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + LOG.trace("upsertById {}", pArgs); return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) + .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive() + .upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) .flatMap(result -> support.applyUpdatedId(object, converted.getId()) .flatMap(updatedObject -> support.applyUpdatedCas(updatedObject, converted, result.cas())))) .onErrorMap(throwable -> { @@ -98,18 +102,7 @@ public class ReactiveUpsertByIdOperationSupport implements ReactiveUpsertByIdOpe } private UpsertOptions buildUpsertOptions(UpsertOptions options, CouchbaseDocument doc) { - options = options != null ? options : UpsertOptions.upsertOptions(); - if (persistTo != PersistTo.NONE || replicateTo != ReplicateTo.NONE) { - options.durability(persistTo, replicateTo); - } else if (durabilityLevel != DurabilityLevel.NONE) { - options.durability(durabilityLevel); - } - if (expiry != null) { - options.expiry(expiry); - } else if (doc.getExpiration() != 0) { - options.expiry(Duration.ofSeconds(doc.getExpiration())); - } - return options; + return OptionsBuilder.buildUpsertOptions(options, persistTo, replicateTo, durabilityLevel, expiry, doc); } @Override @@ -121,14 +114,12 @@ public class ReactiveUpsertByIdOperationSupport implements ReactiveUpsertByIdOpe @Override public UpsertByIdWithDurability inCollection(final String collection) { - Assert.hasText(collection, "Collection must not be null nor empty."); return new ReactiveUpsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, support); } @Override public UpsertByIdInCollection inScope(final String scope) { - Assert.hasText(scope, "Scope must not be null nor empty."); return new ReactiveUpsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo, durabilityLevel, expiry, support); } diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index 372543c3..bc42da3f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -20,17 +20,17 @@ import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEve public interface TemplateSupport { - CouchbaseDocument encodeEntity(Object entityToEncode); + CouchbaseDocument encodeEntity(Object entityToEncode); - T decodeEntity(String id, String source, long cas, Class entityClass); + T decodeEntity(String id, String source, long cas, Class entityClass); - T applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); + T applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); - T applyUpdatedId(T entity, Object id); + T applyUpdatedId(T entity, Object id); - long getCas(Object entity); + long getCas(Object entity); - String getJavaNameForEntity(Class clazz); + String getJavaNameForEntity(Class clazz); - void maybeEmitEvent(CouchbaseMappingEvent event); + void maybeEmitEvent(CouchbaseMappingEvent event); } 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 7793be55..306e8a47 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 @@ -34,6 +34,7 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.annotation.Transient; import org.springframework.data.convert.EntityInstantiator; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbaseList; @@ -532,6 +533,10 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem idAttributes.put(order, convertToString(propertyObj)); } + if (prop.isAnnotationPresent(Transient.class)) { + return; + } + if (!conversions.isSimpleType(propertyObj.getClass())) { writePropertyInternal(propertyObj, target, prop, false); } else { diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java index cb82d214..3aa10ca5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java @@ -21,10 +21,9 @@ import java.util.TimeZone; import java.util.concurrent.TimeUnit; import org.springframework.context.EnvironmentAware; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.env.Environment; -import org.springframework.data.annotation.Id; import org.springframework.data.mapping.MappingException; -import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; @@ -98,11 +97,14 @@ public class BasicCouchbasePersistentEntity extends BasicPersistentEntity TTL_IN_SECONDS_INCLUSIVE_END) { @@ -121,7 +123,7 @@ public class BasicCouchbasePersistentEntity extends BasicPersistentEntity * SpEL is NOT supported. */ + @AliasFor(annotation = Expiry.class, attribute = "expiryExpression") String expiryExpression() default ""; /** * An optional time unit for the document's {@link #expiry()}, if set. Default is {@link TimeUnit#SECONDS}. */ + @AliasFor(annotation = Expiry.class, attribute = "expiryUnit") TimeUnit expiryUnit() default TimeUnit.SECONDS; /** @@ -68,4 +79,9 @@ public @interface Document { */ boolean touchOnRead() default false; + /** + * An optional string indicating the query scan consistency + */ + @AliasFor(annotation = ScanConsistency.class, attribute = "query") + QueryScanConsistency queryScanConsistency() default QueryScanConsistency.NOT_BOUNDED; } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/Expiry.java b/src/main/java/org/springframework/data/couchbase/core/mapping/Expiry.java new file mode 100644 index 00000000..92ca7e0b --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/Expiry.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.mapping; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.annotation.Persistent; + +/** + * Expiry annotation + * + * @author Michael Nitschinger + */ +@Persistent +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +public @interface Expiry { + + /** + * An optional expiry time for the document. Default is no expiry. Only one of two might might be set at the same + * time: either {@link #expiry()} or {@link #expiryExpression()} + */ + int expiry() default 0; + + /** + * Same as {@link #expiry} but allows the actual value to be set using standard Spring property sources mechanism. + * Only one might be set at the same time: either {@link #expiry()} or {@link #expiryExpression()}.
+ * Syntax is the same as for {@link org.springframework.core.env.Environment#resolveRequiredPlaceholders(String)}. + *
+ *
+ * The value will be recalculated for every {@link org.springframework.data.couchbase.core.CouchbaseTemplate} + * save/insert/update call, thus allowing actual expiration to reflect changes on-the-fly as soon as property sources + * change.
+ *
+ * SpEL is NOT supported. + */ + String expiryExpression() default ""; + + /** + * An optional time unit for the document's {@link #expiry()}, if set. Default is {@link TimeUnit#SECONDS}. + */ + TimeUnit expiryUnit() default TimeUnit.SECONDS; + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AbstractCouchbaseEventListener.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AbstractCouchbaseEventListener.java index 87f8bcd9..cb45b252 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AbstractCouchbaseEventListener.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AbstractCouchbaseEventListener.java @@ -50,13 +50,9 @@ public class AbstractCouchbaseEventListener implements ApplicationListener implements ApplicationListener, Af */ @Override public Object onBeforeConvert(Object entity, String collection) { - //LOG.debug("onBeforeConvert " + entity); + // LOG.trace("onBeforeConvert " + entity); return entity; // markAudited called in AuditingEventListener.onApplicationEvent() // auditingHandlerFactory.getObject().markAudited(entity); } @@ -65,7 +65,7 @@ public class AuditingEntityCallback implements BeforeConvertCallback, Af */ @Override public Object onAfterConvert(Object entity, CouchbaseDocument document, String collection) { - //LOG.debug("onAfterConvert " + document); + // LOG.trace("onAfterConvert " + document); return entity; } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java index 0ee5aaab..1f267636 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java @@ -65,24 +65,14 @@ public class AuditingEventListener implements ApplicationListener auditingHandlerFactory.getObject().markAudited(it)); - // LOG.info(event.getClass().getSimpleName() + " " + event); } - if (event instanceof BeforeSaveEvent) { - // LOG.info(event.getClass().getSimpleName() + " " + event); - } - if (event instanceof AfterSaveEvent) { - // LOG.info(event.getClass().getSimpleName() + " " + event); - } - if (event instanceof BeforeDeleteEvent) { - // LOG.info(event.getClass().getSimpleName() + " " + event); - } - if (event instanceof AfterDeleteEvent) { - // LOG.info(event.getClass().getSimpleName() + " " + event); - } - if (!event.getClass().getSimpleName().startsWith("Reactive")) { - if (LOG.isDebugEnabled()) { - LOG.debug(event.getClass().getSimpleName() + " " + event.getSource()); - } + if (event instanceof BeforeSaveEvent) {} + if (event instanceof AfterSaveEvent) {} + if (event instanceof BeforeDeleteEvent) {} + if (event instanceof AfterDeleteEvent) {} + + if (LOG.isTraceEnabled()) { + LOG.trace("{} {}", event.getClass().getSimpleName(), event.getSource()); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterDeleteEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterDeleteEvent.java deleted file mode 100644 index 52c79894..00000000 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterDeleteEvent.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.couchbase.core.mapping.event; - -/** - * @author Michael Reiche - */ -public class ReactiveAfterDeleteEvent extends CouchbaseMappingEvent { - - public ReactiveAfterDeleteEvent(E source) { - super(source, null); - } - -} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEntityCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEntityCallback.java index 5f2348da..ec7d2895 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEntityCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEntityCallback.java @@ -63,7 +63,9 @@ public class ReactiveAuditingEntityCallback */ @Override public Publisher onBeforeConvert(final Object entity, final String collection) { - LOG.debug("onBeforeConvert " + entity.toString()); + if (LOG.isTraceEnabled()) { + LOG.trace("onBeforeConvert {}", entity.toString()); + } return this.auditingHandlerFactory.getObject().markAudited(entity); } @@ -76,9 +78,12 @@ public class ReactiveAuditingEntityCallback */ @Override public Publisher onAfterConvert(Object entity, CouchbaseDocument document, String collection) { - LOG.debug("onAfterConvert " + document.toString()); + if (LOG.isTraceEnabled()) { + LOG.trace("onAfterConvert {}", document.toString()); + } return Mono.just(entity); } + /* * (non-Javadoc) * diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEventListener.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEventListener.java deleted file mode 100644 index 7d516dd5..00000000 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEventListener.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.couchbase.core.mapping.event; - -import java.util.Optional; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.ObjectFactory; -import org.springframework.context.ApplicationListener; -import org.springframework.data.auditing.AuditingHandler; -import org.springframework.data.auditing.IsNewAwareAuditingHandler; -import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.util.Assert; - -/** - * Reactive Event listener to populate auditing related fields on an entity about to be saved. - * - * @author Michael Reiche - */ -public class ReactiveAuditingEventListener implements ApplicationListener> { - - private final ObjectFactory auditingHandlerFactory; - - public ReactiveAuditingEventListener() { - this.auditingHandlerFactory = null; - } - - private static final Logger LOG = LoggerFactory.getLogger(ReactiveAuditingEventListener.class); - - /** - * Creates a new {@link ReactiveAuditingEventListener} using the given {@link MappingContext} and - * {@link AuditingHandler} provided by the given {@link ObjectFactory}. Registered in CouchbaseAuditingRegistrar - * - * @param auditingHandlerFactory must not be {@literal null}. - */ - public ReactiveAuditingEventListener(ObjectFactory auditingHandlerFactory) { - Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); - this.auditingHandlerFactory = auditingHandlerFactory; - } - - /* - * (non-Javadoc) - * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) - */ - @Override - public void onApplicationEvent(CouchbaseMappingEvent event) { - if (event instanceof ReactiveBeforeConvertEvent) { - Optional.ofNullable(event.getSource())// - .ifPresent(it -> auditingHandlerFactory.getObject().markAudited(it)); - // LOG.info(event.getClass().getSimpleName() + " " + event); - } - if (event instanceof ReactiveBeforeSaveEvent) { - // LOG.info(event.getClass().getSimpleName() + " " + event); - } - if (event instanceof ReactiveAfterSaveEvent) { - // LOG.info(event.getClass().getSimpleName() + " " + event); - } - if (event instanceof ReactiveBeforeDeleteEvent) { - // LOG.info(event.getClass().getSimpleName() + " " + event); - } - if (event instanceof ReactiveAfterDeleteEvent) { - // LOG.info(event.getClass().getSimpleName() + " " + event); - } - if (event.getClass().getSimpleName().startsWith("Reactive")) { - if (LOG.isDebugEnabled()) { - LOG.debug(event.getClass().getSimpleName() + " " + event.getSource()); - } - } - } - -} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertEvent.java deleted file mode 100644 index 8f3ff6c3..00000000 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertEvent.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.couchbase.core.mapping.event; - -/** - * @author Michael Reiche - */ -public class ReactiveBeforeConvertEvent extends CouchbaseMappingEvent { - - public ReactiveBeforeConvertEvent(E source) { - super(source, null); - } - -} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeDeleteEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeDeleteEvent.java deleted file mode 100644 index 161c2a45..00000000 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeDeleteEvent.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.couchbase.core.mapping.event; - -/** - * @author Michael Reiche - */ -public class ReactiveBeforeDeleteEvent extends CouchbaseMappingEvent { - - public ReactiveBeforeDeleteEvent(E source) { - super(source, null); - } - -} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeSaveEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeSaveEvent.java deleted file mode 100644 index b96e32b6..00000000 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeSaveEvent.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2012-2021 the original author or authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.couchbase.core.mapping.event; - -import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; - -/** - * @author Michael Reiche - */ -public class ReactiveBeforeSaveEvent extends CouchbaseMappingEvent { - - public ReactiveBeforeSaveEvent(E source, CouchbaseDocument document) { - super(source, document); - } - -} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ValidatingCouchbaseEventListener.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ValidatingCouchbaseEventListener.java index 7d08f35e..bdfffa57 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ValidatingCouchbaseEventListener.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ValidatingCouchbaseEventListener.java @@ -60,7 +60,6 @@ public class ValidatingCouchbaseEventListener extends AbstractCouchbaseEventList Set violations = validator.validate(source); if (!violations.isEmpty()) { - LOG.info("During object: {} validation violations found: {}", source, violations); throw new ConstraintViolationException(violations); } 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 index 26d165a0..783838f3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/Meta.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Meta.java @@ -32,8 +32,9 @@ import org.springframework.util.StringUtils; */ public class Meta { - private enum MetaKey { - EXAMPLE("$example"); + public enum MetaKey { + SCAN_CONSISTENCY("scan_consistency"), SCOPE("scope"), COLLECTION("collection"), EXPIRY("expiry"), EXPIRY_UNIT( + "expiry_unit"), EXPIRY_EXPRESSION("expiry_expression"), TIMEOUT("timeout"), RETRY_STRATEGY("retry_strategy"); private String key; @@ -42,7 +43,7 @@ public class Meta { } } - private final Map values = new LinkedHashMap<>(2); + private final Map values = new LinkedHashMap<>(2); public Meta() {} @@ -68,7 +69,7 @@ public class Meta { * * @return */ - public Iterable> values() { + public Iterable> values() { return Collections.unmodifiableSet(this.values.entrySet()); } @@ -78,10 +79,26 @@ public class Meta { * @param key must not be {@literal null} or empty. * @param value */ - void setValue(String key, @Nullable Object value) { + public 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(MetaKey.valueOf(key)); + } + this.values.put(MetaKey.valueOf(key), value); + } + + public void setValue(MetaKey key, @Nullable Object value) { + + if (value == null || (value instanceof String && !StringUtils.hasText((String) value))) { + this.values.remove(key); + } + this.values.put(key, value); + } + + public void set(MetaKey key, @Nullable Object value) { + if (value == null || (value instanceof String && !StringUtils.hasText((String) value))) { this.values.remove(key); } @@ -90,11 +107,15 @@ public class Meta { @Nullable @SuppressWarnings("unchecked") - private T getValue(String key) { + public T getValue(String key) { + return (T) this.values.get(MetaKey.valueOf(key)); + } + + public T get(MetaKey key) { return (T) this.values.get(key); } - private T getValue(String key, T defaultValue) { + public T getValue(String key, T defaultValue) { T value = getValue(key); return value != null ? value : defaultValue; diff --git a/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java b/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java new file mode 100644 index 00000000..36cd9d35 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java @@ -0,0 +1,425 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core.query; + +import static org.springframework.data.couchbase.core.query.Meta.MetaKey.RETRY_STRATEGY; +import static org.springframework.data.couchbase.core.query.Meta.MetaKey.SCAN_CONSISTENCY; +import static org.springframework.data.couchbase.core.query.Meta.MetaKey.TIMEOUT; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.time.Duration; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.repository.Collection; +import org.springframework.data.couchbase.repository.ScanConsistency; +import org.springframework.data.couchbase.repository.Scope; +import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod; + +import com.couchbase.client.core.io.CollectionIdentifier; +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.core.retry.RetryStrategy; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.kv.ExistsOptions; +import com.couchbase.client.java.kv.InsertOptions; +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.client.java.kv.ReplaceOptions; +import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.client.java.kv.UpsertOptions; +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryScanConsistency; + +public class OptionsBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(OptionsBuilder.class); + + static QueryOptions buildQueryOptions(Query query, QueryOptions options, QueryScanConsistency scanConsistency) { + options = options != null ? options : QueryOptions.queryOptions(); + if (query.getParameters() != null) { + if (query.getParameters() instanceof JsonArray) { + options.parameters((JsonArray) query.getParameters()); + } else { + options.parameters((JsonObject) query.getParameters()); + } + } + + Meta meta = query.getMeta() != null ? query.getMeta() : new Meta(); + QueryOptions.Built optsBuilt = options.build(); + JsonObject optsJson = getQueryOpts(optsBuilt); + QueryScanConsistency metaQueryScanConsistency = meta.get(SCAN_CONSISTENCY) != null + ? ((ScanConsistency) meta.get(SCAN_CONSISTENCY)).query() + : null; + QueryScanConsistency qsc = fromFirst(QueryScanConsistency.NOT_BOUNDED, getScanConsistency(optsJson), + scanConsistency, metaQueryScanConsistency); + Duration timeout = fromFirst(Duration.ofSeconds(0), getTimeout(optsBuilt), meta.get(TIMEOUT)); + RetryStrategy retryStrategy = fromFirst(null, getRetryStrategy(optsBuilt), meta.get(RETRY_STRATEGY)); + + if (qsc != null) { + options.scanConsistency(qsc); + } + if (timeout != null) { + options.timeout(timeout); + } + if (retryStrategy != null) { + options.retryStrategy(retryStrategy); + } + if (LOG.isTraceEnabled()) { + LOG.trace("query options: {}", getQueryOpts(options.build())); + } + return options; + } + + public static ExistsOptions buildExistsOptions(ExistsOptions options) { + options = options != null ? options : ExistsOptions.existsOptions(); + return options; + } + + public static InsertOptions buildInsertOptions(InsertOptions options, PersistTo persistTo, ReplicateTo replicateTo, + DurabilityLevel durabilityLevel, Duration expiry, CouchbaseDocument doc) { + options = options != null ? options : InsertOptions.insertOptions(); + if (persistTo != PersistTo.NONE || replicateTo != ReplicateTo.NONE) { + options.durability(persistTo, replicateTo); + } else if (durabilityLevel != DurabilityLevel.NONE) { + options.durability(durabilityLevel); + } + if (expiry != null) { + options.expiry(expiry); + } else if (doc.getExpiration() != 0) { + options.expiry(Duration.ofSeconds(doc.getExpiration())); + } + if (LOG.isTraceEnabled()) { + LOG.trace("insert options: {}" + toString(options)); + } + return options; + } + + public static UpsertOptions buildUpsertOptions(UpsertOptions options, PersistTo persistTo, ReplicateTo replicateTo, + DurabilityLevel durabilityLevel, Duration expiry, CouchbaseDocument doc) { + options = options != null ? options : UpsertOptions.upsertOptions(); + if (persistTo != PersistTo.NONE || replicateTo != ReplicateTo.NONE) { + options.durability(persistTo, replicateTo); + } else if (durabilityLevel != DurabilityLevel.NONE) { + options.durability(durabilityLevel); + } + if (expiry != null) { + options.expiry(expiry); + } else if (doc.getExpiration() != 0) { + options.expiry(Duration.ofSeconds(doc.getExpiration())); + } + if (LOG.isTraceEnabled()) { + LOG.trace("upsert options: {}" + toString(options)); + } + return options; + } + + public static ReplaceOptions buildReplaceOptions(ReplaceOptions options, PersistTo persistTo, ReplicateTo replicateTo, + DurabilityLevel durabilityLevel, Duration expiry, Long cas, CouchbaseDocument doc) { + options = options != null ? options : ReplaceOptions.replaceOptions(); + if (persistTo != PersistTo.NONE || replicateTo != ReplicateTo.NONE) { + options.durability(persistTo, replicateTo); + } else if (durabilityLevel != DurabilityLevel.NONE) { + options.durability(durabilityLevel); + } + if (expiry != null) { + options.expiry(expiry); + } else if (doc.getExpiration() != 0) { + options.expiry(Duration.ofSeconds(doc.getExpiration())); + } + if (cas != null) { + options.cas(cas); + } + if (LOG.isTraceEnabled()) { + LOG.trace("replace options: {}" + toString(options)); + } + return options; + } + + public static RemoveOptions buildRemoveOptions(RemoveOptions options, PersistTo persistTo, ReplicateTo replicateTo, + DurabilityLevel durabilityLevel, Long cas) { + options = options != null ? options : RemoveOptions.removeOptions(); + if (persistTo != PersistTo.NONE || replicateTo != ReplicateTo.NONE) { + options.durability(persistTo, replicateTo); + } else if (durabilityLevel != DurabilityLevel.NONE) { + options.durability(durabilityLevel); + } + RemoveOptions.Built optsBuilt = options.build(); + Duration timeout = fromFirst(Duration.ofSeconds(0), optsBuilt.timeout()); + RetryStrategy retryStrategy = fromFirst(null, optsBuilt.retryStrategy()); + + if (timeout != null) { + options.timeout(timeout); + } + if (retryStrategy != null) { + options.retryStrategy(retryStrategy); + } + if (cas != null) { + options.cas(cas); + } + if (LOG.isTraceEnabled()) { + LOG.trace("remove options: {}" + toString(options)); + } + return options; + } + + /** + * scope annotation could be a + * + * @param domainType + * @return + */ + public static String getScopeFrom(Class domainType) { + if (domainType == null) { + return null; + } + Scope ann = AnnotatedElementUtils.findMergedAnnotation(domainType, Scope.class); + if (ann != null && !CollectionIdentifier.DEFAULT_COLLECTION.equals(ann.value())) { + return ann.value(); + } + return null; + } + + public static String getCollectionFrom(Class domainType) { + if (domainType == null) { + return null; + } + Collection ann = AnnotatedElementUtils.findMergedAnnotation(domainType, Collection.class); + if (ann != null && !CollectionIdentifier.DEFAULT_COLLECTION.equals(ann.value())) { + return ann.value(); + } + return null; + } + + static String toString(InsertOptions o) { + StringBuilder s = new StringBuilder(); + InsertOptions.Built b = o.build(); + s.append("{"); + s.append("durabilityLevel: " + b.durabilityLevel()); + s.append(", persistTo: " + b.persistTo()); + s.append(", replicateTo: " + b.replicateTo()); + s.append(", timeout: " + b.timeout()); + s.append(", retryStrategy: " + b.retryStrategy()); + s.append(", clientContext: " + b.clientContext()); + s.append(", parentSpan: " + b.parentSpan()); + s.append("}"); + return s.toString(); + } + + static String toString(UpsertOptions o) { + StringBuilder s = new StringBuilder(); + UpsertOptions.Built b = o.build(); + s.append("{"); + s.append("durabilityLevel: " + b.durabilityLevel()); + s.append(", persistTo: " + b.persistTo()); + s.append(", replicateTo: " + b.replicateTo()); + s.append(", timeout: " + b.timeout()); + s.append(", retryStrategy: " + b.retryStrategy()); + s.append(", clientContext: " + b.clientContext()); + s.append(", parentSpan: " + b.parentSpan()); + s.append("}"); + return s.toString(); + } + + static String toString(ReplaceOptions o) { + StringBuilder s = new StringBuilder(); + ReplaceOptions.Built b = o.build(); + s.append("{"); + s.append("cas: " + b.cas()); + s.append(", durabilityLevel: " + b.durabilityLevel()); + s.append(", persistTo: " + b.persistTo()); + s.append(", replicateTo: " + b.replicateTo()); + s.append(", timeout: " + b.timeout()); + s.append(", retryStrategy: " + b.retryStrategy()); + s.append(", clientContext: " + b.clientContext()); + s.append(", parentSpan: " + b.parentSpan()); + s.append("}"); + return s.toString(); + } + + static String toString(RemoveOptions o) { + StringBuilder s = new StringBuilder(); + RemoveOptions.Built b = o.build(); + s.append("{"); + s.append("cas: " + b.cas()); + s.append(", durabilityLevel: " + b.durabilityLevel()); + s.append(", persistTo: " + b.persistTo()); + s.append(", replicateTo: " + b.replicateTo()); + s.append(", timeout: " + b.timeout()); + s.append(", retryStrategy: " + b.retryStrategy()); + s.append(", clientContext: " + b.clientContext()); + s.append(", parentSpan: " + b.parentSpan()); + s.append("}"); + return s.toString(); + } + + private static JsonObject getQueryOpts(QueryOptions.Built optsBuilt) { + JsonObject jo = JsonObject.create(); + optsBuilt.injectParams(jo); + return jo; + } + + /** + * Get the most-specific + * + * @param deflt the default value, which we treat as not set + * @param choice array of values or Optional<values>, ordered from most to least specific + * @param + * @return the most specific choice + */ + public static T fromFirst(T deflt, Object... choice) { + T chosen = choice[0] instanceof Optional ? ((Optional) choice[0]).orElse(null) : (T) choice[0]; + for (int i = 1; i < choice.length; i++) { + if (chosen == null || chosen.equals(deflt)) { // overwrite null or default... + if (choice[i] != null) { // ... with non-null + chosen = choice[i] instanceof Optional ? ((Optional) choice[i]).orElse(null) : (T) choice[i]; + } + } + } + return chosen; + } + + private static QueryScanConsistency getScanConsistency(JsonObject opts) { + String str = opts.getString("scan_consistency"); + if ("at_plus".equals(str)) { + return null; + } + return str == null ? null : QueryScanConsistency.valueOf(str.toUpperCase()); + } + + private static JsonObject getScanVectors(JsonObject opts) { + return opts.getObject("scan_vectors"); + } + + private static Duration getTimeout(QueryOptions.Built optsBuilt) { + Optional timeout = optsBuilt.timeout(); + return timeout.isPresent() ? timeout.get() : null; + } + + private static RetryStrategy getRetryStrategy(QueryOptions.Built optsBuilt) { + Optional retryStrategy = optsBuilt.retryStrategy(); + return retryStrategy.isPresent() ? retryStrategy.get() : null; + } + + public static Meta buildMeta(CouchbaseQueryMethod method, Class typeToRead) { + Meta meta = new Meta(); + // Scope and Collection annotations are handled in PseudArgs + // this would include a ScanConsistency in a composed annotation as well. + meta.set(SCAN_CONSISTENCY, method.getScanConsistencyAnnotation()); + return meta; + } + + /** + * return the first merged annotation which does not have attribute with null/defaultValue from the listed elements. + * + * @param + * @param annotation + * @param attributeName + * @param defaultValue + * @param elements + * @return + */ + public static A annotation(Class annotation, String attributeName, V defaultValue, + AnnotatedElement... elements) { + int i = 1; + for (AnnotatedElement el : elements) { + A an = AnnotatedElementUtils.findMergedAnnotation(el, annotation); + if (an != null) { + if (defaultValue != null) { + try { + Method m = an.getClass().getMethod(attributeName); + V value = (V) m.invoke(an); + if (!defaultValue.equals(value)) { + return an; + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } else { + return an; + } + } + } + return null; + } + + public static A annotation(Class annotation, V defaultValue, + AnnotatedElement[] elements) { + return annotation(annotation, "value", defaultValue, elements); + } + + /** + * return the first merged annotation which is not null/defaultValue from the listed elements. + * + * @param + * @param annotation + * @param defaultValue + * @param elements + * @return + */ + public static V annotationAttribute(Class annotation, String attributeName, + V defaultValue, AnnotatedElement[] elements) { + for (AnnotatedElement el : elements) { + A an = AnnotatedElementUtils.findMergedAnnotation(el, annotation); + if (an != null) { + if (defaultValue != null && !defaultValue.equals(an)) { + try { + Method m = an.getClass().getMethod(attributeName); + return (V) m.invoke(an); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } + } + return null; + } + + /** + * return the toString() of the first merged annotation which is not null/defaultValue from the listed elements. + * + * @param annotation + * @param defaultValue + * @param elements + * @param + * @return + */ + public static String annotationString(Class annotation, String attributeName, + Object defaultValue, AnnotatedElement[] elements) { + A result = annotation(annotation, defaultValue, elements); + if (result == null) { + return null; + } + try { + Method m = result.getClass().getMethod(attributeName); + Object value = m.invoke(result); + return value.toString(); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + public static String annotationString(Class annotation, Object defaultValue, + AnnotatedElement[] elements) { + return annotationString(annotation, "value", defaultValue, elements); + } +} 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 62b55aca..d6d24c97 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 @@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod; import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser; import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.domain.Pageable; @@ -52,6 +53,7 @@ public class Query { private int limit; private Sort sort = Sort.unsorted(); private QueryScanConsistency queryScanConsistency; + private Meta meta; static private final Pattern WHERE_PATTERN = Pattern.compile("\\sWHERE\\s"); private static final Logger LOG = LoggerFactory.getLogger(Query.class); @@ -326,28 +328,22 @@ public class Query { * @return QueryOptions */ public QueryOptions buildQueryOptions(QueryOptions options, QueryScanConsistency scanConsistency) { - if (options == null) { // add/override what we got from PseudoArgs - options = QueryOptions.queryOptions(); - } - if (getParameters() != null) { - if (getParameters() instanceof JsonArray) { - options.parameters((JsonArray) getParameters()); - } else { - options.parameters((JsonObject) getParameters()); - } - } - if (scanConsistency == null - || scanConsistency == QueryScanConsistency.NOT_BOUNDED && getScanConsistency() != null) { - scanConsistency = getScanConsistency(); - } - if (scanConsistency != null) { - options.scanConsistency(scanConsistency); - } - return options; + return OptionsBuilder.buildQueryOptions(this, options, scanConsistency); } - public void setMeta(Meta metaAnnotation) { - Meta meta = metaAnnotation; + /** + * this collections annotations from the method, repository class and possibly the entity class to be used as options. + * This will find annotations included in composed annotations as well. Ideally + * + * @param method representing the query. + * @return the query with the annotations applied + */ + public void setMeta(CouchbaseQueryMethod method, Class typeToRead) { + meta = OptionsBuilder.buildMeta(method, typeToRead); + } + + public Meta getMeta() { + return meta; } } diff --git a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java index cb92bbc2..fb1bc9da 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/PseudoArgs.java @@ -15,9 +15,14 @@ */ package org.springframework.data.couchbase.core.support; -import com.couchbase.client.core.io.CollectionIdentifier; +import static org.springframework.data.couchbase.core.query.OptionsBuilder.fromFirst; +import static org.springframework.data.couchbase.core.query.OptionsBuilder.getCollectionFrom; +import static org.springframework.data.couchbase.core.query.OptionsBuilder.getScopeFrom; + import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import com.couchbase.client.core.io.CollectionIdentifier; + public class PseudoArgs { private final OPTS options; private final String scopeName; @@ -35,24 +40,32 @@ public class PseudoArgs { * 2) values from dynamic proxy (via template threadLocal)
* 3) the values from the couchbaseClientFactory
* - * @param template to hold - * @param scope - * @param collection - * @param options + * @param template which holds ThreadLocal pseudo args + * @param scope - from calling operation + * @param collection - from calling operation + * @param options - from calling operation + * @param domainType - entity that may have annotations */ - public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String collection, OPTS options) { + public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String collection, OPTS options, + Class domainType) { - // 1) values from the args (fluent api) + String scopeForQuery = null; + String collectionForQuery = null; + OPTS optionsForQuery = null; - String scopeForQuery = scope; - String collectionForQuery = collection; - OPTS optionsForQuery = options; + // 1) repository from DynamicProxy via template threadLocal - has precedence over annotation - // 2) from DynamicProxy via template threadLocal + PseudoArgs threadLocal = (PseudoArgs) template.getPseudoArgs(); + template.setPseudoArgs(null); + if (threadLocal != null) { + scopeForQuery = threadLocal.getScope(); + collectionForQuery = threadLocal.getCollection(); + optionsForQuery = threadLocal.getOptions(); + } - scopeForQuery = scopeForQuery != null ? scopeForQuery : getThreadLocalScopeName(template); - collectionForQuery = collectionForQuery != null ? collectionForQuery : getThreadLocalCollectionName(template); - optionsForQuery = optionsForQuery != null ? optionsForQuery : getThreadLocalOptions(template); + scopeForQuery = fromFirst(null, scopeForQuery, scope, getScopeFrom(domainType)); + collectionForQuery = fromFirst(null, collectionForQuery, collection, getCollectionFrom(domainType)); + optionsForQuery = fromFirst(null, options, optionsForQuery); // if a collection was specified but no scope, use the scope from the clientFactory @@ -62,56 +75,42 @@ public class PseudoArgs { // specifying scope and collection = _default is not necessary and will fail if server doesn't have collections - if ((scopeForQuery == null || CollectionIdentifier.DEFAULT_SCOPE.equals(scopeForQuery)) - && (collectionForQuery == null || CollectionIdentifier.DEFAULT_COLLECTION.equals(collectionForQuery))) { - scopeForQuery = null; - collectionForQuery = null; + if (scopeForQuery == null || CollectionIdentifier.DEFAULT_SCOPE.equals(scopeForQuery)) { + if (collectionForQuery == null || CollectionIdentifier.DEFAULT_COLLECTION.equals(collectionForQuery)) { + collectionForQuery = null; + scopeForQuery = null; + } } this.scopeName = scopeForQuery; this.collectionName = collectionForQuery; this.options = optionsForQuery; + } /** - * @@return the options + * @return the options */ public OPTS getOptions() { return this.options; } /** - * @@return the scope name + * @return the scope name */ public String getScope() { return this.scopeName; } /** - * @@return the collection name + * @return the collection name */ public String getCollection() { return this.collectionName; } - /** - * @@return the options from the ThreadLocal field of the template - */ - private OPTS getThreadLocalOptions(ReactiveCouchbaseTemplate template) { - return template.getPseudoArgs() == null ? null : (OPTS) (template.getPseudoArgs().getOptions()); - } - - /** - * @@return the scope name from the ThreadLocal field of the template - */ - private String getThreadLocalScopeName(ReactiveCouchbaseTemplate template) { - return template.getPseudoArgs() == null ? null : template.getPseudoArgs().getScope(); - } - - /** - * @@return the collection name from the ThreadLocal field of the template - */ - private String getThreadLocalCollectionName(ReactiveCouchbaseTemplate template) { - return template.getPseudoArgs() == null ? null : template.getPseudoArgs().getCollection(); + @Override + public String toString() { + return "scope: " + getScope() + " collection: " + getCollection() + " options: " + getOptions(); } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/Collection.java b/src/main/java/org/springframework/data/couchbase/repository/Collection.java new file mode 100644 index 00000000..6734902b --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/Collection.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository; + +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_COLLECTION; + +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; + +/** + * Collection Annotation + * + * @author Michael Reiche + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE }) +@Documented +@QueryAnnotation +public @interface Collection { + + /** + * Specifies the collection name + * + * @return the collection name configured, defaults to not DEFAULT_COLLECTION. + */ + String value() default DEFAULT_COLLECTION; + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java index 5544d7e5..f14bca46 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,16 +18,20 @@ package org.springframework.data.couchbase.repository; import java.util.List; -import com.couchbase.client.java.query.QueryScanConsistency; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import org.springframework.data.domain.Sort; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.Repository; +import com.couchbase.client.java.query.QueryScanConsistency; + /** * Couchbase specific {@link Repository} interface. * * @author Michael Nitschinger + * @author Michael Reiche */ @NoRepositoryBean public interface CouchbaseRepository extends PagingAndSortingRepository { @@ -43,4 +47,8 @@ public interface CouchbaseRepository extends PagingAndSortingRepository findAllById(Iterable iterable); + CouchbaseEntityInformation getEntityInformation(); + + CouchbaseOperations getOperations(); + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java new file mode 100644 index 00000000..c97f9b9e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.repository; + +import java.lang.reflect.Proxy; + +import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; +import org.springframework.data.couchbase.repository.support.DynamicInvocationHandler; + +import com.couchbase.client.java.CommonOptions; + +/** + * The generic parameter needs to be REPO which is either a CouchbaseRepository parameterized on T,ID or a + * ReactiveCouchbaseRepository parameterized on T,ID. i.e.: interface AirportRepository extends + * CouchbaseRepository, DynamicProxyable + * + * @param + * @author Michael Reiche + */ +public interface DynamicProxyable { + + CouchbaseEntityInformation getEntityInformation(); + + Object getOperations(); + + /** + * Support for Couchbase-specific options, scope and collections The three "with" methods will return a new proxy + * instance with the specified options, scope, or collections set. The setters are called with the corresponding + * options, scope and collection to set the ThreadLocal fields on the CouchbaseOperations of the repository just + * before the call is made to the repository, and called again with 'null' just after the call is made. The repository + * method will fetch those values to use in the call. + */ + + /** + * @param options - the options to set on the returned repository object + */ + @SuppressWarnings("unchecked") + default REPO withOptions(CommonOptions options) { + REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), + this.getClass().getInterfaces(), new DynamicInvocationHandler(this, options, null, (String) null)); + return proxyInstance; + } + + /** + * @param scope - the scope to set on the returned repository object + */ + @SuppressWarnings("unchecked") + default REPO withScope(String scope) { + REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), + this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, null, scope)); + return proxyInstance; + } + + /** + * @param collection - the collection to set on the returned repository object + */ + @SuppressWarnings("unchecked") + default REPO withCollection(String collection) { + REPO proxyInstance = (REPO) Proxy.newProxyInstance(this.getClass().getClassLoader(), + this.getClass().getInterfaces(), new DynamicInvocationHandler<>(this, null, collection, null)); + return proxyInstance; + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/Options.java b/src/main/java/org/springframework/data/couchbase/repository/Options.java new file mode 100644 index 00000000..07054c10 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/Options.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository; + +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_COLLECTION; +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE; + +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 java.util.concurrent.TimeUnit; + +import org.springframework.data.annotation.QueryAnnotation; + +import com.couchbase.client.java.analytics.AnalyticsScanConsistency; +import com.couchbase.client.java.query.QueryScanConsistency; + +/** + * Scope Annotation + * + * @author Michael Reiche + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE }) +@Documented +@QueryAnnotation +public @interface Options { + + /** + * Specifies the scope name + * + * @return the scope name configured, defaults to not DEFAULT_SCOPE. + */ + String scope() default DEFAULT_SCOPE; + + /** + * Specifies the scope name + * + * @return the scope name configured, defaults to not DEFAULT_SCOPE. + */ + String collection() default DEFAULT_COLLECTION; + + /** + * Specifies a custom scan consistency for N1QL queries. + * + * @return the scan consistency configured, defaults to not bounded. + */ + QueryScanConsistency query() default QueryScanConsistency.NOT_BOUNDED; + + /** + * Specifies a custom scan consistency for analytics queries. + * + * @return the scan consistency configured, defaults to not bounded. + */ + AnalyticsScanConsistency analytics() default AnalyticsScanConsistency.NOT_BOUNDED; + + /** + * Specifies a custom projection. + * + * @return the projection configured, defaults to an empty array (project everything). + */ + String[] project() default {}; + + /** + * Specifies a custom array of distinct fields. + * + * @return the projection configured, we need to do something tricky with the default. We need to default to
+ * no distinct, which is specified by a null array (an empty array means distinct on everything). We'll make + * an array of a single element "-" mean no distinct. + */ + String[] distinct() default { "-" }; + + /** + * An optional expiry time for the document. Default is no expiry. Only one of two might might be set at the same + * time: either {@link #expiry()} or {@link #expiryExpression()} + */ + int expiry() default 0; + + /** + * Same as {@link #expiry} but allows the actual value to be set using standard Spring property sources mechanism. + * Only one might be set at the same time: either {@link #expiry()} or {@link #expiryExpression()}.
+ * Syntax is the same as for {@link org.springframework.core.env.Environment#resolveRequiredPlaceholders(String)}. + *
+ *
+ * The value will be recalculated for every {@link org.springframework.data.couchbase.core.CouchbaseTemplate} + * save/insert/update call, thus allowing actual expiration to reflect changes on-the-fly as soon as property sources + * change.
+ *
+ * SpEL is NOT supported. + */ + String expiryExpression() default ""; + + /** + * An optional time unit for the document's {@link #expiry()}, if set. Default is {@link TimeUnit#SECONDS}. + */ + TimeUnit expiryUnit() default TimeUnit.SECONDS; + + /** + * An timeout for the operation. Default is no timeout. + */ + long timeoutMs() default 0; +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java index d76af65f..db41820f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 the original author or authors. + * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ */ package org.springframework.data.couchbase.repository; +import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.reactive.ReactiveSortingRepository; @@ -22,9 +24,12 @@ import org.springframework.data.repository.reactive.ReactiveSortingRepository; * Couchbase-specific {@link ReactiveSortingRepository} implementation. * * @author Subhashni Balakrishnan + * @author Michael Reiche * @since 3.0 */ @NoRepositoryBean public interface ReactiveCouchbaseRepository extends ReactiveSortingRepository { + ReactiveCouchbaseOperations getOperations(); + CouchbaseEntityInformation getEntityInformation(); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/ScanConsistency.java b/src/main/java/org/springframework/data/couchbase/repository/ScanConsistency.java index 7254e1ae..961358d9 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/ScanConsistency.java +++ b/src/main/java/org/springframework/data/couchbase/repository/ScanConsistency.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2021 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,13 @@ import java.lang.annotation.Target; import com.couchbase.client.java.analytics.AnalyticsScanConsistency; import com.couchbase.client.java.query.QueryScanConsistency; +/** + * Scan Consistency Annotation + * + * @author Michael Reiche + */ @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE }) @Documented public @interface ScanConsistency { diff --git a/src/main/java/org/springframework/data/couchbase/repository/Meta.java b/src/main/java/org/springframework/data/couchbase/repository/Scope.java similarity index 66% rename from src/main/java/org/springframework/data/couchbase/repository/Meta.java rename to src/main/java/org/springframework/data/couchbase/repository/Scope.java index 298379ee..8c146b07 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/Meta.java +++ b/src/main/java/org/springframework/data/couchbase/repository/Scope.java @@ -1,11 +1,11 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2012-2021 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -15,6 +15,8 @@ */ package org.springframework.data.couchbase.repository; +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -24,13 +26,21 @@ import java.lang.annotation.Target; import org.springframework.data.annotation.QueryAnnotation; /** + * Scope Annotation + * * @author Michael Reiche - * @since 4.1 */ @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE }) @Documented @QueryAnnotation -public @interface Meta { +public @interface Scope { + + /** + * Specifies the scope name + * + * @return the scope name configured, defaults to DEFAULT_SCOPE. + */ + String value() default DEFAULT_SCOPE; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/auditing/ReactiveCouchbaseAuditingRegistrar.java b/src/main/java/org/springframework/data/couchbase/repository/auditing/ReactiveCouchbaseAuditingRegistrar.java index a38cbcbf..25b22026 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/auditing/ReactiveCouchbaseAuditingRegistrar.java +++ b/src/main/java/org/springframework/data/couchbase/repository/auditing/ReactiveCouchbaseAuditingRegistrar.java @@ -1,3 +1,18 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.couchbase.repository.auditing; import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_AUDITING_HANDLER; @@ -13,7 +28,6 @@ import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarS import org.springframework.data.auditing.config.AuditingConfiguration; import org.springframework.data.config.ParsingUtils; import org.springframework.data.couchbase.core.mapping.event.ReactiveAuditingEntityCallback; -import org.springframework.data.couchbase.core.mapping.event.ReactiveAuditingEventListener; import org.springframework.util.Assert; /** @@ -82,17 +96,6 @@ class ReactiveCouchbaseAuditingRegistrar extends AuditingBeanDefinitionRegistrar registerInfrastructureBeanWithId(builder.getBeanDefinition(), ReactiveAuditingEntityCallback.class.getName(), registry); - // Register the AuditingEventListener - - BeanDefinitionBuilder builder2 = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEventListener.class); - - builder2 - .addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); - builder.getRawBeanDefinition().setSource(auditingHandlerDefinition.getSource()); - - registerInfrastructureBeanWithId(builder2.getBeanDefinition(), ReactiveAuditingEventListener.class.getName(), - registry); - } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java index 979de664..9dc3fe51 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors + * Copyright 2020-2021 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.springframework.data.couchbase.repository.query; -import com.couchbase.client.core.io.CollectionIdentifier; import org.springframework.core.convert.converter.Converter; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation.ExecutableFindByQuery; @@ -60,10 +59,11 @@ public abstract class AbstractCouchbaseQuery extends AbstractCouchbaseQueryBase< 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); + ExecutableFindByQuery findOp = operations.findByQuery(type); + findOp = (ExecutableFindByQuery) (findOp.inScope(method.getScope()).inCollection(method.getCollection())); + this.findOperationWithProjection = findOp; } /** @@ -80,18 +80,14 @@ public abstract class AbstractCouchbaseQuery extends AbstractCouchbaseQueryBase< ParametersParameterAccessor accessor, @Nullable Class typeToRead) { Query query = createQuery(accessor); - - query = applyAnnotatedConsistencyIfPresent(query); // query = applyAnnotatedCollationIfPresent(query, accessor); // not yet implemented + query = applyQueryMetaAttributesIfPresent(query, typeToRead); - ExecutableFindByQuery find = typeToRead == null ? findOperationWithProjection // - : findOperationWithProjection; // not yet implemented in core .as(typeToRead); - - String collection = null; + ExecutableFindByQuery find = findOperationWithProjection; CouchbaseQueryExecution execution = getExecution(accessor, new ResultProcessingConverter<>(processor, getOperations(), getInstantiators()), find); - return execution.execute(query, processor.getReturnedType().getDomainType(), collection); + return execution.execute(query, processor.getReturnedType().getDomainType(), null); } /** @@ -114,8 +110,7 @@ public abstract class AbstractCouchbaseQuery extends AbstractCouchbaseQueryBase< * @param operation must not be {@literal null}. * @return */ - private CouchbaseQueryExecution getExecutionToWrap(ParameterAccessor accessor, - ExecutableFindByQuery operation) { + private CouchbaseQueryExecution getExecutionToWrap(ParameterAccessor accessor, ExecutableFindByQuery operation) { if (isDeleteQuery()) { return new DeleteExecution(getOperations(), getQueryMethod()); @@ -123,6 +118,8 @@ public abstract class AbstractCouchbaseQuery extends AbstractCouchbaseQueryBase< 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 (getQueryMethod().isStreamQuery()) { + return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).stream(); } else if (isCountQuery()) { return (q, t, c) -> operation.matching(q).count(); } else if (isExistsQuery()) { @@ -140,18 +137,4 @@ public abstract class AbstractCouchbaseQuery extends AbstractCouchbaseQueryBase< } } - /** - * 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 index 9314371d..b00101e5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQueryBase.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQueryBase.java @@ -146,18 +146,14 @@ public abstract class AbstractCouchbaseQueryBase implem ParametersParameterAccessor accessor, @Nullable Class typeToRead); /** - * Add a scan consistency from {@link org.springframework.data.couchbase.repository.ScanConsistency} to the given - * {@link Query} if present. + * Apply Meta annotation to query * - * @param query the {@link Query} to potentially apply the sort to. - * @return the query with potential scan consistency applied. - * @since 4.1 + * @param query must not be {@literal null}. + * @return Query */ - Query applyAnnotatedConsistencyIfPresent(Query query) { - if (!method.hasScanConsistencyAnnotation()) { - return query; - } - return query.scanConsistency(method.getScanConsistencyAnnotation().query()); + Query applyQueryMetaAttributesIfPresent(Query query, Class typeToRead) { + query.setMeta(getQueryMethod(), typeToRead); + return query; } /** diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java index f01e3f6c..8b04d292 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractReactiveCouchbaseQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package org.springframework.data.couchbase.repository.query; -import com.couchbase.client.core.io.CollectionIdentifier; import org.springframework.core.convert.converter.Converter; import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; import org.springframework.data.couchbase.core.ReactiveFindByQueryOperation; @@ -63,7 +62,9 @@ public abstract class AbstractReactiveCouchbaseQuery extends AbstractCouchbaseQu EntityMetadata metadata = method.getEntityInformation(); Class type = metadata.getJavaType(); - this.findOperationWithProjection = operations.findByQuery(type); + ReactiveFindByQuery findOp = operations.findByQuery(type); + findOp = (ReactiveFindByQuery) (findOp.inScope(method.getScope()).inCollection(method.getCollection())); + this.findOperationWithProjection = findOp; } /** @@ -79,18 +80,14 @@ public abstract class AbstractReactiveCouchbaseQuery extends AbstractCouchbaseQu ParametersParameterAccessor accessor, @Nullable Class typeToRead) { Query query = createQuery(accessor); - query = applyAnnotatedConsistencyIfPresent(query); // query = applyAnnotatedCollationIfPresent(query, accessor); // not yet implemented + query = applyQueryMetaAttributesIfPresent(query, typeToRead); - ReactiveFindByQuery find = typeToRead == null // - ? findOperationWithProjection // - : findOperationWithProjection; // note yet implemented in core .as(typeToRead); - - String collection = null; + ReactiveFindByQuery find = findOperationWithProjection; ReactiveCouchbaseQueryExecution execution = getExecution(accessor, new ResultProcessingConverter<>(processor, getOperations(), getInstantiators()), find); - return execution.execute(query, processor.getReturnedType().getDomainType(), collection); + return execution.execute(query, processor.getReturnedType().getDomainType(), null); } /** @@ -121,6 +118,8 @@ public abstract class AbstractReactiveCouchbaseQuery extends AbstractCouchbaseQu 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 (getQueryMethod().isStreamQuery()) { + // return (q, t, c) -> operation.matching(q.with(accessor.getPageable())).all().toStream(); } else if (isCountQuery()) { return (q, t, c) -> operation.matching(q).count(); } else if (isExistsQuery()) { @@ -133,18 +132,4 @@ public abstract class AbstractReactiveCouchbaseQuery extends AbstractCouchbaseQu } } - /** - * 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/CouchbaseQueryMethod.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java index 86243473..b4141951 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,28 +16,33 @@ package org.springframework.data.couchbase.repository.query; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.Locale; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.Dimensional; +import org.springframework.data.couchbase.core.query.OptionsBuilder; 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.Collection; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.ScanConsistency; +import org.springframework.data.couchbase.repository.Scope; 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; +import com.couchbase.client.core.io.CollectionIdentifier; + /** * Represents a query method with couchbase extensions, allowing to discover if View-based query or N1QL-based query * must be used. @@ -50,13 +55,13 @@ import org.springframework.util.StringUtils; public class CouchbaseQueryMethod extends QueryMethod { private final Method method; + private final RepositoryMetadata repositoryMetadata; public CouchbaseQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory, MappingContext, CouchbasePersistentProperty> mappingContext) { super(method, metadata, factory); - this.method = method; - + this.repositoryMetadata = metadata; } /** @@ -176,39 +181,46 @@ public class CouchbaseQueryMethod extends QueryMethod { * @return the @ScanConsistency annotation */ public ScanConsistency getScanConsistencyAnnotation() { - return method.getAnnotation(ScanConsistency.class); + AnnotatedElement[] annotated = new AnnotatedElement[] { method, method.getDeclaringClass(), + repositoryMetadata.getRepositoryInterface(), repositoryMetadata.getDomainType() }; + return OptionsBuilder.annotation(ScanConsistency.class, "query", CollectionIdentifier.DEFAULT_COLLECTION, + annotated); } /** - * @return return true if {@link Meta} annotation is available. + * Caution: findMergedAnnotation() will return the default if there are any annotations but not this annotation + * + * @return annotation */ - public boolean hasQueryMetaAttributes() { - return getMetaAnnotation() != null; + public
A getAnnotation(Class annotationClass) { + return AnnotatedElementUtils.findMergedAnnotation(method, annotationClass); } /** - * @return return {@link Meta} annotation + * Caution: findMergedAnnotation() will return the default if there are any annotations but not this annotation + * + * @return annotation */ - private Meta getMetaAnnotation() { - return method.getAnnotation(Meta.class); + public A getClassAnnotation(Class annotationClass) { + return AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), annotationClass); } /** - * Returns the {@link org.springframework.data.couchbase.core.query.Meta} attributes to be applied. - * - * @return never {@literal null}. + * Caution: findMergedAnnotation() will return the default if there are any annotations but not this annotation + * + * @return annotation */ - @Nullable - public org.springframework.data.couchbase.core.query.Meta getQueryMetaAttributes() { + public A getEntityAnnotation(Class annotationClass) { + return AnnotatedElementUtils.findMergedAnnotation(getEntityInformation().getJavaType(), annotationClass); + } - 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; + /** + * Caution: findMergedAnnotation() will return the default if there are any annotations but not this annotation + * + * @return annotation + */ + public A getRepositoryAnnotation(Class annotationClass) { + return AnnotatedElementUtils.findMergedAnnotation(repositoryMetadata.getRepositoryInterface(), annotationClass); } /** @@ -264,4 +276,18 @@ public class CouchbaseQueryMethod extends QueryMethod { return false; } + public String getCollection() { + // Try the repository method, then the repository class, then the entity class + AnnotatedElement[] annotated = new AnnotatedElement[] { method, method.getDeclaringClass(), + repositoryMetadata.getRepositoryInterface(), repositoryMetadata.getDomainType() }; + return OptionsBuilder.annotationString(Collection.class, CollectionIdentifier.DEFAULT_COLLECTION, annotated); + } + + public String getScope() { + // Try the repository method, then the repository class, then the entity class + AnnotatedElement[] annotated = new AnnotatedElement[] { method, method.getDeclaringClass(), + repositoryMetadata.getRepositoryInterface(), repositoryMetadata.getDomainType() }; + return OptionsBuilder.annotationString(Scope.class, CollectionIdentifier.DEFAULT_SCOPE, annotated); + } + } 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 index 3296397a..1f29b72d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java @@ -91,7 +91,7 @@ public class ReactiveStringBasedCouchbaseQuery extends AbstractReactiveCouchbase @Override protected Query createCountQuery(ParametersParameterAccessor accessor) { - return applyQueryMetaAttributesWhenPresent(createQuery(accessor)); + return applyQueryMetaAttributesIfPresent(createQuery(accessor), null); } /* 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 index d52a04b3..14799bd0 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedCouchbaseQuery.java @@ -79,8 +79,8 @@ public class StringBasedCouchbaseQuery extends AbstractCouchbaseQuery { namedQueries); Query query = creator.createQuery(); - if (LOG.isDebugEnabled()) { - LOG.debug("Created query " + query.export()); + if (LOG.isTraceEnabled()) { + LOG.trace("Created query " + query.export()); } return query; @@ -88,7 +88,7 @@ public class StringBasedCouchbaseQuery extends AbstractCouchbaseQuery { @Override protected Query createCountQuery(ParametersParameterAccessor accessor) { - return applyQueryMetaAttributesWhenPresent(createQuery(accessor)); + return applyQueryMetaAttributesIfPresent(createQuery(accessor), null); } /* 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 bba2ef48..3716ac26 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java @@ -22,7 +22,6 @@ import static org.springframework.data.couchbase.core.support.TemplateUtils.SELE import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.regex.Matcher; @@ -30,11 +29,7 @@ import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.data.convert.CustomConversions; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.N1QLExpression; import org.springframework.data.couchbase.repository.Query; @@ -131,8 +126,9 @@ public class StringBasedN1qlQueryParser { this.statement = statement; this.queryMethod = queryMethod; this.couchbaseConverter = couchbaseConverter; - this.statementContext = createN1qlSpelValues(bucketName, null, null, null, typeField, typeValue, false, null); - this.countContext = createN1qlSpelValues(bucketName, null, null, null, typeField, typeValue, true, null); + String collection = queryMethod.getCollection(); + this.statementContext = createN1qlSpelValues(bucketName, collection, null, null, typeField, typeValue, false, null); + this.countContext = createN1qlSpelValues(bucketName, collection, null, null, typeField, typeValue, true, null); this.parsedExpression = getExpression(accessor, getParameters(accessor), null, parser, evaluationContextProvider); checkPlaceholders(this.parsedExpression.toString()); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java new file mode 100644 index 00000000..992c4d0d --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java @@ -0,0 +1,123 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.repository.support; + +import java.lang.reflect.AnnotatedElement; + +import org.springframework.data.couchbase.core.query.OptionsBuilder; +import org.springframework.data.couchbase.repository.Collection; +import org.springframework.data.couchbase.repository.ScanConsistency; +import org.springframework.data.couchbase.repository.Scope; +import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; + +import com.couchbase.client.core.io.CollectionIdentifier; +import com.couchbase.client.java.query.QueryScanConsistency; + +public class CouchbaseRepositoryBase { + + /** + * Contains information about the entity being used in this repository. + */ + private final CouchbaseEntityInformation entityInformation; + private final Class repositoryInterface; + private CrudMethodMetadata crudMethodMetadata; + + public CouchbaseRepositoryBase(CouchbaseEntityInformation entityInformation, + Class repositoryInterface) { + this.entityInformation = entityInformation; + this.repositoryInterface = repositoryInterface; + } + + /** + * Returns the information for the underlying template. + * + * @return the underlying entity information. + */ + public CouchbaseEntityInformation getEntityInformation() { + return entityInformation; + } + + Class getJavaType() { + return getEntityInformation().getJavaType(); + } + + String getId(S entity) { + return getEntityInformation().getId(entity); + } + + /** + * Get the Scope from
+ * 1. The repository
+ * 2. The entity
+ * 3. otherwise null
+ * This can be overriden in the operation method by
+ * 1. repository.withCollection() 2. Annotation on the method + */ + + String getScope() { + String fromAnnotation = OptionsBuilder.annotationString(Scope.class, CollectionIdentifier.DEFAULT_SCOPE, + new AnnotatedElement[] { getJavaType(), repositoryInterface }); + String fromMetadata = crudMethodMetadata.getScope(); + return OptionsBuilder.fromFirst(CollectionIdentifier.DEFAULT_SCOPE, fromMetadata, fromAnnotation); + } + + /** + * Get the Collection from
+ * 1. The repository
+ * 2. The entity
+ * 3. otherwise null
+ * This can be overriden in the operation method by
+ * 1. repository.withCollection() + */ + String getCollection() { + String fromAnnotation = OptionsBuilder.annotationString(Collection.class, CollectionIdentifier.DEFAULT_COLLECTION, + new AnnotatedElement[] { getJavaType(), repositoryInterface }); + String fromMetadata = crudMethodMetadata.getCollection(); + return OptionsBuilder.fromFirst(CollectionIdentifier.DEFAULT_COLLECTION, fromMetadata, fromAnnotation); + } + + /** + * Get the QueryScanConsistency from
+ * 1. The method annotation (method *could* be available from crudMethodMetadata)
+ * 2. The repository
+ * 3. The entity
+ * 4. otherwise null
+ * This can be overriden in the operation method by
+ * 1. Options.scanConsistency (?)
+ * AbstractCouchbaseQueryBase.applyAnnotatedConsistencyIfPresent()
+ * CouchbaseRepository get picked up? If I have the following, will the annotation be picked up?
+ * Only via crudMethodMetadata
+ * \@ScanConsistency(query=QueryScanConsistency.REQUEST_PLUS)
+ * List findAll();
+ */ + QueryScanConsistency buildQueryScanConsistency() { + ScanConsistency sc = crudMethodMetadata.getScanConsistency(); + QueryScanConsistency fromMeta = sc != null ? sc.query() : null; + QueryScanConsistency fromAnnotation = OptionsBuilder.annotationAttribute(ScanConsistency.class, "query", + QueryScanConsistency.NOT_BOUNDED, new AnnotatedElement[] { getJavaType(), repositoryInterface }); + return OptionsBuilder.fromFirst(QueryScanConsistency.NOT_BOUNDED, fromMeta, fromAnnotation); + } + + /** + * Setter for the repository metadata, contains annotations on the overidden methods. + * + * @param crudMethodMetadata the injected repository metadata. + */ + void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { + this.crudMethodMetadata = crudMethodMetadata; + } +} 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 f42d9ff4..d8bff2d0 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 @@ -90,14 +90,14 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { * Returns entity information based on the domain class. * * @param domainClass the class for the entity. - * @param the value type - * @param the id type. + * @param the value type + * @param the id type. * @return entity information for that domain class. */ @Override public CouchbaseEntityInformation getEntityInformation(Class domainClass) { - CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext.getRequiredPersistentEntity( - domainClass); + CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext + .getRequiredPersistentEntity(domainClass); return new MappingCouchbaseEntityInformation<>(entity); } @@ -116,7 +116,7 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { metadata.getDomainType()); CouchbaseEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); SimpleCouchbaseRepository repository = getTargetRepositoryViaReflection(metadata, entityInformation, - couchbaseOperations); + couchbaseOperations, metadata.getRepositoryInterface()); repository.setRepositoryMethodMetadata(crudMethodMetadataPostProcessor.getCrudMethodMetadata()); return repository; } @@ -153,8 +153,8 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { @Override public RepositoryQuery resolveQuery(final Method method, final RepositoryMetadata metadata, final ProjectionFactory factory, final NamedQueries namedQueries) { - final CouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve( - metadata.getRepositoryInterface(), metadata.getDomainType()); + final CouchbaseOperations couchbaseOperations = couchbaseOperationsMapping + .resolve(metadata.getRepositoryInterface(), metadata.getDomainType()); CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext); diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadata.java b/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadata.java index 710c7751..a15f17a8 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadata.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadata.java @@ -31,4 +31,8 @@ public interface CrudMethodMetadata { */ ScanConsistency getScanConsistency(); + String getScope(); + + String getCollection(); + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java b/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java index 159a5782..836db257 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CrudMethodMetadataPostProcessor.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.repository.support; -import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.HashSet; import java.util.Set; @@ -28,7 +28,10 @@ import org.springframework.aop.TargetSource; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.NamedThreadLocal; +import org.springframework.data.couchbase.core.query.OptionsBuilder; +import org.springframework.data.couchbase.repository.Collection; import org.springframework.data.couchbase.repository.ScanConsistency; +import org.springframework.data.couchbase.repository.Scope; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; import org.springframework.lang.Nullable; @@ -37,6 +40,9 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; +import com.couchbase.client.core.io.CollectionIdentifier; +import com.couchbase.client.java.query.QueryScanConsistency; + /** * {@link RepositoryProxyPostProcessor} that sets up interceptors to read metadata information from the invoked method. * This is necessary to allow redeclaration of CRUD methods in repository interfaces and configure locking information @@ -96,8 +102,10 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B private final ConcurrentMap metadataCache = new ConcurrentHashMap<>(); private final Set implementations = new HashSet<>(); + private final RepositoryInformation repositoryInformation; CrudMethodMetadataPopulatingMethodInterceptor(RepositoryInformation repositoryInformation) { + this.repositoryInformation = repositoryInformation; ReflectionUtils.doWithMethods(repositoryInformation.getRepositoryInterface(), implementations::add, method -> !repositoryInformation.isQueryMethod(method)); } @@ -148,7 +156,7 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B if (methodMetadata == null) { - methodMetadata = new DefaultCrudMethodMetadata(method); + methodMetadata = new DefaultCrudMethodMetadata(method, repositoryInformation); CrudMethodMetadata tmp = metadataCache.putIfAbsent(method, methodMetadata); if (tmp != null) { @@ -161,10 +169,10 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B try { return invocation.proceed(); } finally { - TransactionSynchronizationManager.unbindResource(method); + // TransactionSynchronizationManager.unbindResource(method); } } finally { - currentInvocation.set(oldInvocation); + // currentInvocation.set(oldInvocation); } } } @@ -179,23 +187,38 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B private final Method method; private final ScanConsistency scanConsistency; + private final RepositoryInformation repositoryInformation; + private final String scope; + private final String collection; /** - * Creates a new {@link DefaultCrudMethodMetadata} for the given {@link Method}. - * + * Creates a new {@link DefaultCrudMethodMetadata} for the given {@link Method}. This collects data from implemented + * methods (save(), findById() etc) that would be collected in query.setMeta() for unimplemented methods. There may + * be annotations if the methods were overriden in the repository. + * * @param method must not be {@literal null}. */ - DefaultCrudMethodMetadata(Method method) { + DefaultCrudMethodMetadata(Method method, RepositoryInformation repositoryInformation) { Assert.notNull(method, "Method must not be null!"); this.method = method; - - ScanConsistency scanConsistency = null; - for (Annotation ann : method.getAnnotations()) { - if (ann instanceof ScanConsistency) { - scanConsistency = ((ScanConsistency) ann); - } + this.repositoryInformation = repositoryInformation; + String n = method.getName(); + // internal methods + if (n.equals("getEntityInformation") || n.equals("getOperations") || n.equals("withOptions") + || n.equals("withOptions") || n.equals("withScope")) { + this.scanConsistency = null; + this.scope = null; + this.collection = null; + return; } - this.scanConsistency = scanConsistency; + + AnnotatedElement[] annotated = new AnnotatedElement[] { method, method.getDeclaringClass(), + repositoryInformation.getRepositoryInterface(), repositoryInformation.getDomainType() }; + this.scanConsistency = OptionsBuilder.annotation(ScanConsistency.class, "query", QueryScanConsistency.NOT_BOUNDED, + annotated); + this.scope = OptionsBuilder.annotationString(Scope.class, CollectionIdentifier.DEFAULT_SCOPE, annotated); + this.collection = OptionsBuilder.annotationString(Collection.class, CollectionIdentifier.DEFAULT_COLLECTION, + annotated); } /* @@ -211,6 +234,17 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B public ScanConsistency getScanConsistency() { return scanConsistency; } + + @Override + public String getScope() { + return scope; + } + + @Override + public String getCollection() { + return collection; + } + } private static class ThreadBoundTargetSource implements TargetSource { diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java new file mode 100644 index 00000000..dedba009 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java @@ -0,0 +1,123 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.support; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; + +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.support.PseudoArgs; +import org.springframework.data.couchbase.repository.CouchbaseRepository; +import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository; +import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; + +import com.couchbase.client.java.CommonOptions; + +/** + * Invocation Handler for scope/collection/options proxy for repositories + * + * @param + * @author Michael Reiche + */ +public class DynamicInvocationHandler implements InvocationHandler { + final T target; + final Class repositoryClass; + // needed only to detect parameters of this type to look for methods with parameter of java.lang.Object + final CouchbaseEntityInformation entityInformation; + final ReactiveCouchbaseTemplate reactiveTemplate; + CommonOptions options; + String collection; + String scope;; + + public DynamicInvocationHandler(T target, CommonOptions options, String collection, String scope) { + this.target = target; + if (target instanceof CouchbaseRepository) { + reactiveTemplate = ((CouchbaseTemplate) ((CouchbaseRepository) target).getOperations()).reactive(); + this.entityInformation = ((CouchbaseRepository) target).getEntityInformation(); + } else if (target instanceof ReactiveCouchbaseRepository) { + reactiveTemplate = (ReactiveCouchbaseTemplate) ((ReactiveCouchbaseRepository) target).getOperations(); + this.entityInformation = ((ReactiveCouchbaseRepository) target).getEntityInformation(); + } else { + throw new RuntimeException("Unknown target type: " + target.getClass()); + } + this.options = options; + this.collection = collection; + this.scope = scope; + this.repositoryClass = target.getClass(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + if ("toString".equals(method.getName())) { + return "proxy -> target:" + target; + } + /* Cannot fall-through to use these methods on target, as they will not retain + * the scope, collection and options that may already be set on the proxy + */ + + if (method.getName().equals("withOptions")) { + return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), + new DynamicInvocationHandler<>(target, (CommonOptions) args[0], collection, scope)); + } + + if (method.getName().equals("withScope")) { + return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), + new DynamicInvocationHandler<>(target, options, collection, (String) args[0])); + } + + if (method.getName().equals("withCollection")) { + return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), + new DynamicInvocationHandler<>(target, options, (String) args[0], scope)); + } + + Class[] paramTypes = null; + if (args != null) { + // the CouchbaseRepository methods - save(entity) etc - will have a parameter type of Object instead of entityType + // so change the paramType to match + paramTypes = Arrays.stream(args) + .map(o -> o == null ? null : (o.getClass() == entityInformation.getJavaType() ? Object.class : o.getClass())) + .toArray(Class[]::new); + // the CouchbaseRepository methods - findById(id) etc - will have a parameter type of Object instead of ID + if (method.getName().endsWith("ById") && args.length == 1) { + paramTypes[0] = Object.class; + } + } + + Method theMethod = repositoryClass.getMethod(method.getName(), paramTypes); + Object result; + + try { + setThreadLocal(); + result = theMethod.invoke(target, args); + } catch (InvocationTargetException ite) { + throw ite.getCause(); + } + return result; + } + + private void setThreadLocal() { + if (reactiveTemplate.getPseudoArgs() != null) { + throw new RuntimeException("pseudoArgs not yet consumed by previous caller"); + } + reactiveTemplate.setPseudoArgs(new PseudoArgs(this.scope, this.collection, this.options)); + } + +} 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 8ca423c3..273fb656 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 @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 the original author or authors. + * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,7 +66,7 @@ public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactor * @param couchbaseOperationsMapping the template for the underlying actions. */ public ReactiveCouchbaseRepositoryFactory(final ReactiveRepositoryOperationsMapping couchbaseOperationsMapping) { - Assert.notNull(couchbaseOperationsMapping); + Assert.notNull(couchbaseOperationsMapping, "couchbaseOperationsMapping"); this.couchbaseOperationsMapping = couchbaseOperationsMapping; this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor(); @@ -85,14 +85,14 @@ public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactor * Returns entity information based on the domain class. * * @param domainClass the class for the entity. - * @param the value type - * @param the id type. + * @param the value type + * @param the id type. * @return entity information for that domain class. */ @Override public CouchbaseEntityInformation getEntityInformation(Class domainClass) { - CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext.getRequiredPersistentEntity( - domainClass); + CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext + .getRequiredPersistentEntity(domainClass); return new MappingCouchbaseEntityInformation<>(entity); } @@ -107,11 +107,11 @@ public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactor */ @Override protected final Object getTargetRepository(final RepositoryInformation metadata) { - ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve( - metadata.getRepositoryInterface(), metadata.getDomainType()); + ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping + .resolve(metadata.getRepositoryInterface(), metadata.getDomainType()); CouchbaseEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); SimpleReactiveCouchbaseRepository repository = getTargetRepositoryViaReflection(metadata, entityInformation, - couchbaseOperations); + couchbaseOperations, metadata.getRepositoryInterface()); repository.setRepositoryMethodMetadata(crudMethodMetadataPostProcessor.getCrudMethodMetadata()); return repository; } @@ -150,8 +150,8 @@ public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactor @Override public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { - final ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve( - metadata.getRepositoryInterface(), metadata.getDomainType()); + final ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping + .resolve(metadata.getRepositoryInterface(), metadata.getDomainType()); ReactiveCouchbaseQueryMethod queryMethod = new ReactiveCouchbaseQueryMethod(method, metadata, factory, mappingContext); 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 c91235d4..e2cc0338 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,34 +44,27 @@ import com.couchbase.client.java.query.QueryScanConsistency; * @author Michael Nitschinger * @author Mark Paluch * @author Jens Schauder + * @author Michael Reiche */ -public class SimpleCouchbaseRepository implements CouchbaseRepository { +public class SimpleCouchbaseRepository extends CouchbaseRepositoryBase + implements CouchbaseRepository { /** * Holds the reference to the {@link org.springframework.data.couchbase.core.CouchbaseTemplate}. */ - private final CouchbaseOperations couchbaseOperations; - - /** - * Contains information about the entity being used in this repository. - */ - private final CouchbaseEntityInformation entityInformation; - - private CrudMethodMetadata crudMethodMetadata; + private final CouchbaseOperations operations; /** * Create a new Repository. * * @param entityInformation the Metadata for the entity. * @param couchbaseOperations the reference to the template used. + * @param repositoryInterface the repository interface being fronted */ public SimpleCouchbaseRepository(CouchbaseEntityInformation entityInformation, - CouchbaseOperations couchbaseOperations) { - Assert.notNull(entityInformation, "CouchbaseEntityInformation must not be null!"); - Assert.notNull(couchbaseOperations, "CouchbaseOperations must not be null!"); - - this.entityInformation = entityInformation; - this.couchbaseOperations = couchbaseOperations; + CouchbaseOperations couchbaseOperations, Class repositoryInterface) { + super(entityInformation, repositoryInterface); + this.operations = couchbaseOperations; } @Override @@ -79,11 +72,13 @@ public class SimpleCouchbaseRepository implements CouchbaseRepository S save(S entity) { Assert.notNull(entity, "Entity must not be null!"); // if entity has non-null, non-zero version property, then replace() - if (hasNonZeroVersionProperty(entity, couchbaseOperations.getConverter())) { - return (S) couchbaseOperations.replaceById(entityInformation.getJavaType()).one(entity); + S result; + if (hasNonZeroVersionProperty(entity, operations.getConverter())) { + result = (S) operations.replaceById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity); } else { - return (S) couchbaseOperations.upsertById(entityInformation.getJavaType()).one(entity); + result = (S) operations.upsertById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity); } + return result; } @Override @@ -95,57 +90,61 @@ public class SimpleCouchbaseRepository implements CouchbaseRepository findById(ID id) { Assert.notNull(id, "The given id must not be null!"); - return Optional.ofNullable(couchbaseOperations.findById(entityInformation.getJavaType()).one(id.toString())); + return Optional.ofNullable( + operations.findById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(id.toString())); } @Override public List findAllById(Iterable ids) { Assert.notNull(ids, "The given Iterable of ids must not be null!"); List convertedIds = Streamable.of(ids).stream().map(Objects::toString).collect(Collectors.toList()); - Collection all = couchbaseOperations.findById(entityInformation.getJavaType()).all(convertedIds); + Collection all = operations.findById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .all(convertedIds); return Streamable.of(all).stream().collect(StreamUtils.toUnmodifiableList()); } @Override public boolean existsById(ID id) { Assert.notNull(id, "The given id must not be null!"); - return couchbaseOperations.existsById().one(id.toString()); + return operations.existsById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(id.toString()); } @Override public void deleteById(ID id) { Assert.notNull(id, "The given id must not be null!"); - couchbaseOperations.removeById().one(id.toString()); + operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(id.toString()); } @Override public void delete(T entity) { Assert.notNull(entity, "Entity must not be null!"); - couchbaseOperations.removeById().one(entityInformation.getId(entity)); + operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(getId(entity)); } @Override public void deleteAllById(Iterable ids) { Assert.notNull(ids, "The given Iterable of ids must not be null!"); - couchbaseOperations.removeById().all(Streamable.of(ids).map(Objects::toString).toList()); + operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .all(Streamable.of(ids).map(Objects::toString).toList()); } @Override public void deleteAll(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities must not be null!"); - couchbaseOperations.removeById().all(Streamable.of(entities).map(entityInformation::getId).toList()); + operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .all(Streamable.of(entities).map(this::getId).toList()); } @Override public long count() { - return couchbaseOperations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) - .count(); + return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) + .inCollection(getCollection()).count(); } @Override public void deleteAll() { - couchbaseOperations.removeByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) - .all(); + operations.removeByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) + .inCollection(getCollection()).all(); } @Override @@ -169,15 +168,6 @@ public class SimpleCouchbaseRepository implements CouchbaseRepository(results, pageable, count()); } - /** - * Returns the information for the underlying template. - * - * @return the underlying entity information. - */ - protected CouchbaseEntityInformation getEntityInformation() { - return entityInformation; - } - /** * Helper method to assemble a n1ql find all query, taking annotations into acocunt. * @@ -185,25 +175,13 @@ public class SimpleCouchbaseRepository implements CouchbaseRepository findAll(Query query) { - return couchbaseOperations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) - .matching(query).all(); + return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) + .inCollection(getCollection()).matching(query).all(); } - private QueryScanConsistency buildQueryScanConsistency() { - QueryScanConsistency scanConsistency = QueryScanConsistency.NOT_BOUNDED; - if (crudMethodMetadata.getScanConsistency() != null) { - scanConsistency = crudMethodMetadata.getScanConsistency().query(); - } - return scanConsistency; - } - - /** - * Setter for the repository metadata, contains annotations on the overidden methods. - * - * @param crudMethodMetadata the injected repository metadata. - */ - void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { - this.crudMethodMetadata = crudMethodMetadata; + @Override + public CouchbaseOperations getOperations() { + return operations; } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index 7cf8320e..2aa7ac6e 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 the original author or authors. + * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,8 +35,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; -import com.couchbase.client.java.query.QueryScanConsistency; - /** * Reactive repository base implementation for Couchbase. * @@ -46,22 +44,17 @@ import com.couchbase.client.java.query.QueryScanConsistency; * @author David Kelly * @author Douglas Six * @author Jens Schauder + * @author Michael Reiche * @since 3.0 */ -public class SimpleReactiveCouchbaseRepository implements ReactiveCouchbaseRepository { +public class SimpleReactiveCouchbaseRepository extends CouchbaseRepositoryBase + implements ReactiveCouchbaseRepository { /** * Holds the reference to the {@link CouchbaseOperations}. */ private final ReactiveCouchbaseOperations operations; - /** - * Contains information about the entity being used in this repository. - */ - private final CouchbaseEntityInformation entityInformation; - - private CrudMethodMetadata crudMethodMetadata; - /** * Create a new Repository. * @@ -69,11 +62,8 @@ public class SimpleReactiveCouchbaseRepository implements ReactiveCouchba * @param operations the reference to the reactive template used. */ public SimpleReactiveCouchbaseRepository(CouchbaseEntityInformation entityInformation, - ReactiveCouchbaseOperations operations) { - Assert.notNull(operations, "ReactiveCouchbaseOperations must not be null!"); - Assert.notNull(entityInformation, "CouchbaseEntityInformation must not be null!"); - - this.entityInformation = entityInformation; + ReactiveCouchbaseOperations operations, Class repositoryInterface) { + super(entityInformation, repositoryInterface); this.operations = operations; } @@ -81,12 +71,16 @@ public class SimpleReactiveCouchbaseRepository implements ReactiveCouchba @Override public Mono save(S entity) { Assert.notNull(entity, "Entity must not be null!"); - // if entity has non-null version property, then replace() + // if entity has non-null, non-zero version property, then replace() + Mono result; if (hasNonZeroVersionProperty(entity, operations.getConverter())) { - return (Mono) operations.replaceById(entityInformation.getJavaType()).one(entity); + result = (Mono) operations.replaceById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .one(entity); } else { - return (Mono) operations.upsertById(entityInformation.getJavaType()).one(entity); + result = (Mono) operations.upsertById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .one(entity); } + return result; } @Override @@ -97,7 +91,7 @@ public class SimpleReactiveCouchbaseRepository implements ReactiveCouchba @Override public Flux saveAll(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities must not be null!"); - return Flux.fromIterable(entities).flatMap(this::save); + return Flux.fromIterable(entities).flatMap(e -> save(e)); } @Override @@ -108,7 +102,7 @@ public class SimpleReactiveCouchbaseRepository implements ReactiveCouchba @Override public Mono findById(ID id) { - return operations.findById(entityInformation.getJavaType()).one(id.toString()); + return operations.findById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(id.toString()); } @Override @@ -120,7 +114,7 @@ public class SimpleReactiveCouchbaseRepository implements ReactiveCouchba @Override public Mono existsById(ID id) { Assert.notNull(id, "The given id must not be null!"); - return operations.existsById().one(id.toString()); + return operations.existsById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(id.toString()); } @Override @@ -139,7 +133,8 @@ public class SimpleReactiveCouchbaseRepository implements ReactiveCouchba public Flux findAllById(Iterable ids) { Assert.notNull(ids, "The given Iterable of ids must not be null!"); List convertedIds = Streamable.of(ids).stream().map(Objects::toString).collect(Collectors.toList()); - return (Flux) operations.findById(entityInformation.getJavaType()).all(convertedIds); + return (Flux) operations.findById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .all(convertedIds); } @Override @@ -150,7 +145,8 @@ public class SimpleReactiveCouchbaseRepository implements ReactiveCouchba @Override public Mono deleteById(ID id) { - return operations.removeById().one(id.toString()).then(); + return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(id.toString()) + .then(); } @Override @@ -162,17 +158,20 @@ public class SimpleReactiveCouchbaseRepository implements ReactiveCouchba @Override public Mono delete(T entity) { Assert.notNull(entity, "Entity must not be null!"); - return operations.removeById().one(entityInformation.getId(entity)).then(); + return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(getId(entity)) + .then(); } @Override public Mono deleteAllById(Iterable ids) { - return operations.removeById().all(Streamable.of(ids).map(Object::toString).toList()).then(); + return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .all(Streamable.of(ids).map(Object::toString).toList()).then(); } @Override public Mono deleteAll(Iterable entities) { - return operations.removeById().all(Streamable.of(entities).map(entityInformation::getId).toList()).then(); + return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .all(Streamable.of(entities).map(this::getId).toList()).then(); } @Override @@ -183,43 +182,24 @@ public class SimpleReactiveCouchbaseRepository implements ReactiveCouchba @Override public Mono count() { - return operations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()).count(); + return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) + .inCollection(getCollection()).count(); } @Override public Mono deleteAll() { - return operations.removeByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()).all().then(); - } - - /** - * Returns the information for the underlying template. - * - * @return the underlying entity information. - */ - protected CouchbaseEntityInformation getEntityInformation() { - return entityInformation; + return operations.removeByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) + .inCollection(getCollection()).all().then(); } private Flux findAll(Query query) { - return operations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) - .matching(query).all(); + return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) + .inCollection(getCollection()).matching(query).all(); } - private QueryScanConsistency buildQueryScanConsistency() { - QueryScanConsistency scanConsistency = QueryScanConsistency.NOT_BOUNDED; - if (crudMethodMetadata.getScanConsistency() != null) { - scanConsistency = crudMethodMetadata.getScanConsistency().query(); - } - return scanConsistency; - } - - /** - * Setter for the repository metadata, contains annotations on the overidden methods. - * - * @param crudMethodMetadata the injected repository metadata. - */ - void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { - this.crudMethodMetadata = crudMethodMetadata; + @Override + public ReactiveCouchbaseOperations getOperations() { + return operations; } } diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java index c0980105..f98cbf7e 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -31,7 +31,6 @@ import java.util.HashSet; import java.util.Set; import java.util.UUID; -import com.couchbase.client.java.query.QueryScanConsistency; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.dao.DataIntegrityViolationException; @@ -50,6 +49,7 @@ import org.springframework.data.couchbase.util.JavaIntegrationTests; import com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.client.java.query.QueryScanConsistency; ; @@ -121,7 +121,7 @@ class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { returned = (User) operator.one(user); break; } catch (Exception ofe) { - System.out.println(""+i+" caught: "+ofe); + System.out.println("" + i + " caught: " + ofe); couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); if (i == 4) { throw ofe; @@ -259,7 +259,7 @@ class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { .one(user); break; } catch (Exception ofe) { - System.out.println(""+i+" caught: "+ofe); + System.out.println("" + i + " caught: " + ofe); couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); if (i == 4) { throw ofe; diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java index 3a56b8ff..586986af 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryCollectionIntegrationTests.java @@ -36,6 +36,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; @@ -44,18 +45,19 @@ import org.springframework.data.couchbase.domain.Course; import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.Submission; import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserCol; import org.springframework.data.couchbase.domain.UserJustLastName; import org.springframework.data.couchbase.domain.UserSubmission; import org.springframework.data.couchbase.domain.UserSubmissionProjected; import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; import org.springframework.data.couchbase.util.Capabilities; -import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; import org.springframework.data.couchbase.util.IgnoreWhen; import com.couchbase.client.core.error.AmbiguousTimeoutException; import com.couchbase.client.core.error.UnambiguousTimeoutException; +import com.couchbase.client.core.io.CollectionIdentifier; import com.couchbase.client.java.analytics.AnalyticsOptions; import com.couchbase.client.java.kv.ExistsOptions; import com.couchbase.client.java.kv.GetAnyReplicaOptions; @@ -752,4 +754,51 @@ class CouchbaseTemplateQueryCollectionIntegrationTests extends CollectionAwareIn .inCollection(otherCollection).withOptions(options).one(vie)); } + @Test + public void testScopeCollectionAnnotation() { + UserCol user = new UserCol("1", "Dave", "Wilson"); + Query query = Query.query(QueryCriteria.where("firstname").is(user.getFirstname())); + try { + UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName) + .one(user); + List found = couchbaseTemplate.findByQuery(UserCol.class) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) + .matching(query).all(); + assertEquals(saved, found.get(0), "should have found what was saved"); + List notfound = couchbaseTemplate.findByQuery(UserCol.class).inScope(CollectionIdentifier.DEFAULT_SCOPE) + .inCollection(CollectionIdentifier.DEFAULT_COLLECTION).matching(query).all(); + assertEquals(0, notfound.size(), "should not have found what was saved"); + couchbaseTemplate.removeByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName).matching(query) + .all(); + } finally { + try { + couchbaseTemplate.removeByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName).matching(query) + .all(); + } catch (DataRetrievalFailureException drfe) {} + } + } + + @Test + public void testScopeCollectionRepoWith() { + UserCol user = new UserCol("1", "Dave", "Wilson"); + Query query = Query.query(QueryCriteria.where("firstname").is(user.getFirstname())); + try { + UserCol saved = couchbaseTemplate.insertById(UserCol.class).inScope(scopeName).inCollection(collectionName) + .one(user); + List found = couchbaseTemplate.findByQuery(UserCol.class) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).inScope(scopeName).inCollection(collectionName) + .matching(query).all(); + assertEquals(saved, found.get(0), "should have found what was saved"); + List notfound = couchbaseTemplate.findByQuery(UserCol.class).inScope(CollectionIdentifier.DEFAULT_SCOPE) + .inCollection(CollectionIdentifier.DEFAULT_COLLECTION).matching(query).all(); + assertEquals(0, notfound.size(), "should not have found what was saved"); + couchbaseTemplate.removeByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName).matching(query) + .all(); + } finally { + try { + couchbaseTemplate.removeByQuery(UserCol.class).inScope(scopeName).inCollection(collectionName).matching(query) + .all(); + } catch (DataRetrievalFailureException drfe) {} + } + } } diff --git a/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java index 655c4a32..40adb9c1 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java @@ -16,7 +16,8 @@ package org.springframework.data.couchbase.core; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import java.time.Duration; import java.util.UUID; diff --git a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java index d4a47971..4fc5e998 100644 --- a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java @@ -150,7 +150,8 @@ class ReactiveCouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationT } // if replace or remove, we need to insert a document to replace - if (operator instanceof ReactiveReplaceByIdOperation.ReactiveReplaceById || operator instanceof ExecutableRemoveById) { + if (operator instanceof ReactiveReplaceByIdOperation.ReactiveReplaceById + || operator instanceof ExecutableRemoveById) { reactiveCouchbaseTemplate.insertById(User.class).one(user).block(); } // call to insert/replace/update 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 e73a8180..bb7ecae2 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airport.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airport.java @@ -19,8 +19,8 @@ package org.springframework.data.couchbase.domain; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceConstructor; -import org.springframework.data.annotation.Version; import org.springframework.data.annotation.TypeAlias; +import org.springframework.data.annotation.Version; import org.springframework.data.couchbase.core.mapping.Document; /** @@ -42,7 +42,6 @@ public class Airport extends ComparableEntity { @CreatedBy private String createdBy; - @PersistenceConstructor public Airport(String id, String iata, String icao) { this.id = id; @@ -78,6 +77,7 @@ public class Airport extends ComparableEntity { version = Long.valueOf(0); return this; } + public String getCreatedBy() { return createdBy; } 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 d849d32e..589c55ce 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -16,18 +16,33 @@ package org.springframework.data.couchbase.domain; +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_COLLECTION; +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.List; import java.util.Optional; +import java.util.concurrent.TimeUnit; +import org.springframework.core.annotation.AliasFor; import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.core.mapping.Expiry; +import org.springframework.data.couchbase.repository.Collection; import org.springframework.data.couchbase.repository.CouchbaseRepository; +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.data.couchbase.repository.Options; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.ScanConsistency; +import org.springframework.data.couchbase.repository.Scope; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import com.couchbase.client.java.analytics.AnalyticsScanConsistency; import com.couchbase.client.java.query.QueryScanConsistency; /** @@ -41,8 +56,11 @@ import com.couchbase.client.java.query.QueryScanConsistency; * @author Michael Reiche */ @Repository -public interface AirportRepository extends CouchbaseRepository { +// @Scope("repositoryScope") +// @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) +public interface AirportRepository extends CouchbaseRepository, DynamicProxyable { + // override an annotate with REQUEST_PLUS @Override @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) List findAll(); @@ -51,11 +69,16 @@ public interface AirportRepository extends CouchbaseRepository List findAllByIata(String iata); @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + @ComposedMetaAnnotation(collection = "_default", timeoutMs = 1000) Airport findByIata(String iata); @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) Airport findByIata(Iata iata); + // NOT_BOUNDED to test ScanConsistency + // @ScanConsistency(query = QueryScanConsistency.NOT_BOUNDED) + Airport iata(String iata); + @Query("#{#n1ql.selectEntity} where iata = $1") @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) List getAllByIata(String iata); @@ -97,4 +120,42 @@ public interface AirportRepository extends CouchbaseRepository @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) Optional findByIdAndIata(String id, String iata); + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD, ElementType.TYPE }) + // @Meta + @Scope + @Collection + @ScanConsistency + @Expiry + @Options + public @interface ComposedMetaAnnotation { + + // @AliasFor(annotation = Meta.class, attribute = "maxExecutionTimeMs") + // long execTime() default -1; + + @AliasFor(annotation = ScanConsistency.class, attribute = "query") + QueryScanConsistency query() default QueryScanConsistency.NOT_BOUNDED; + + @AliasFor(annotation = ScanConsistency.class, attribute = "analytics") + AnalyticsScanConsistency analytics() default AnalyticsScanConsistency.NOT_BOUNDED; + + @AliasFor(annotation = Scope.class, attribute = "value") + String scope() default DEFAULT_SCOPE; + + @AliasFor(annotation = Collection.class, attribute = "value") + String collection() default DEFAULT_COLLECTION; + + @AliasFor(annotation = Expiry.class, attribute = "expiry") + int expiry() default 0; + + @AliasFor(annotation = Expiry.class, attribute = "expiryUnit") + TimeUnit expiryUnit() default TimeUnit.SECONDS; + + @AliasFor(annotation = Expiry.class, attribute = "expiryExpression") + String expiryExpression() default ""; + + @AliasFor(annotation = Options.class, attribute = "timeoutMs") + long timeoutMs() default 0; + + } } 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 4e2bc5c4..78b78077 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -38,6 +38,7 @@ import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; import org.springframework.data.couchbase.repository.auditing.EnableReactiveCouchbaseAuditing; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.config.EnableReactiveCouchbaseRepositories; import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; @@ -52,8 +53,10 @@ import com.couchbase.client.java.json.JacksonTransformers; */ @Configuration @EnableCouchbaseRepositories -@EnableCouchbaseAuditing(auditorAwareRef="auditorAwareRef", dateTimeProviderRef="dateTimeProviderRef") // this activates auditing -@EnableReactiveCouchbaseAuditing(auditorAwareRef="reactiveAuditorAwareRef", dateTimeProviderRef="dateTimeProviderRef") // this activates auditing +@EnableReactiveCouchbaseRepositories +@EnableCouchbaseAuditing(auditorAwareRef = "auditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") +@EnableReactiveCouchbaseAuditing(auditorAwareRef = "reactiveAuditorAwareRef", + dateTimeProviderRef = "dateTimeProviderRef") public class Config extends AbstractCouchbaseConfiguration { String bucketname = "travel-sample"; diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java index 4fdcad52..23e1943d 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java @@ -16,7 +16,6 @@ package org.springframework.data.couchbase.domain; import java.util.List; -import java.util.Optional; import java.util.UUID; import org.springframework.data.couchbase.repository.Query; @@ -92,7 +91,7 @@ public interface PersonRepository extends CrudRepository { Iterable saveAll(Iterable var1); - Optional findById(UUID var1); + Person findById(UUID var1); boolean existsById(UUID var1); 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 1c517590..59426ec2 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveAirportRepository.java @@ -16,19 +16,19 @@ package org.springframework.data.couchbase.domain; -import org.springframework.data.couchbase.core.RemoveResult; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.ArrayList; +import org.springframework.data.couchbase.repository.DynamicProxyable; import org.springframework.data.couchbase.repository.Query; +import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository; 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 com.couchbase.client.java.json.JsonArray; @@ -41,7 +41,8 @@ import com.couchbase.client.java.query.QueryScanConsistency; * @author Michael Reiche */ @Repository -public interface ReactiveAirportRepository extends ReactiveSortingRepository { +public interface ReactiveAirportRepository + extends ReactiveCouchbaseRepository, DynamicProxyable { @Override @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) @@ -57,6 +58,9 @@ public interface ReactiveAirportRepository extends ReactiveSortingRepository findAllByIata(String iata); + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + Mono iata(String iata); + @Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter}") Flux findAllPoliciesByApplicableTypes(String state, JsonArray applicableTypes); diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveNaiveAuditorAware.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveNaiveAuditorAware.java index 0f3b0814..bcdf2677 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveNaiveAuditorAware.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveNaiveAuditorAware.java @@ -15,9 +15,9 @@ */ package org.springframework.data.couchbase.domain; -import org.springframework.data.domain.ReactiveAuditorAware; import reactor.core.publisher.Mono; +import org.springframework.data.domain.ReactiveAuditorAware; /** * This class returns a string that represents the current user @@ -28,6 +28,7 @@ import reactor.core.publisher.Mono; public class ReactiveNaiveAuditorAware implements ReactiveAuditorAware { public static final String AUDITOR = "reactive_auditor"; + @Override public Mono getCurrentAuditor() { return Mono.just(AUDITOR); diff --git a/src/test/java/org/springframework/data/couchbase/domain/ReactiveUserColRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveUserColRepository.java new file mode 100644 index 00000000..0b5c742e --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveUserColRepository.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2020 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.domain; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.data.couchbase.repository.Query; +import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository; +import org.springframework.data.couchbase.repository.ScanConsistency; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.query.QueryScanConsistency; + +/** + * User Repository for tests + * + * @author Michael Nitschinger + * @author Michael Reiche + */ +@Repository +@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) +public interface ReactiveUserColRepository + extends ReactiveCouchbaseRepository, DynamicProxyable { + + Mono save(S var1); + + Flux findByFirstname(String firstname); + + Flux findByFirstnameIn(String... firstnames); + + Flux findByFirstnameIn(JsonArray firstnames); + + Flux findByFirstnameAndLastname(String firstname, String lastname); + + @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and firstname = $1 and lastname = $2") + Flux getByFirstnameAndLastname(String firstname, String lastname); + + @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (firstname = $first or lastname = $last)") + Flux getByFirstnameOrLastname(@Param("first") String firstname, @Param("last") String lastname); + + Flux findByIdIsNotNullAndFirstnameEquals(String firstname); + + Flux findByVersionEqualsAndFirstnameEquals(Long version, String firstname); + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterSaveEvent.java b/src/test/java/org/springframework/data/couchbase/domain/UserCol.java similarity index 52% rename from src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterSaveEvent.java rename to src/test/java/org/springframework/data/couchbase/domain/UserCol.java index 3c0a97a3..1c38d569 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterSaveEvent.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserCol.java @@ -14,17 +14,28 @@ * limitations under the License. */ -package org.springframework.data.couchbase.core.mapping.event; +package org.springframework.data.couchbase.domain; -import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.repository.Collection; +import org.springframework.data.couchbase.repository.Scope; /** + * User entity for tests + * + * @author Michael Nitschinger * @author Michael Reiche */ -public class ReactiveAfterSaveEvent extends CouchbaseMappingEvent { - public ReactiveAfterSaveEvent(E source, CouchbaseDocument document) { - super(source, document); +@Document +@Scope("other_scope") +@Collection("other_collection") +public class UserCol extends User { + + @PersistenceConstructor + public UserCol(final String id, final String firstname, final String lastname) { + super(id, firstname, lastname); } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserColRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserColRepository.java new file mode 100644 index 00000000..e1bbaa37 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserColRepository.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2020 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.domain; + +import java.util.List; + +import org.springframework.data.couchbase.repository.CouchbaseRepository; +import org.springframework.data.couchbase.repository.DynamicProxyable; +import org.springframework.data.couchbase.repository.Query; +import org.springframework.data.couchbase.repository.ScanConsistency; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.query.QueryScanConsistency; + +/** + * User Repository for tests + * + * @author Michael Nitschinger + * @author Michael Reiche + */ +@Repository +@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) +public interface UserColRepository extends CouchbaseRepository, DynamicProxyable { + + S save(S var1); + + List findByFirstname(String firstname); + + List findByFirstnameIn(String... firstnames); + + List findByFirstnameIn(JsonArray firstnames); + + List findByFirstnameAndLastname(String firstname, String lastname); + + @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and firstname = $1 and lastname = $2") + List getByFirstnameAndLastname(String firstname, String lastname); + + @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (firstname = $first or lastname = $last)") + List getByFirstnameOrLastname(@Param("first") String firstname, @Param("last") String lastname); + + List findByIdIsNotNullAndFirstnameEquals(String firstname); + + List findByVersionEqualsAndFirstnameEquals(Long version, String firstname); + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java index 63293b33..18b334d0 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java @@ -20,10 +20,12 @@ import java.util.List; import org.springframework.data.couchbase.repository.CouchbaseRepository; import org.springframework.data.couchbase.repository.Query; +import org.springframework.data.couchbase.repository.ScanConsistency; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.query.QueryScanConsistency; /** * User Repository for tests @@ -32,6 +34,7 @@ import com.couchbase.client.java.json.JsonArray; * @author Michael Reiche */ @Repository +@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) public interface UserRepository extends CouchbaseRepository { List findByFirstname(String firstname); @@ -51,4 +54,5 @@ public interface UserRepository extends CouchbaseRepository { List findByIdIsNotNullAndFirstnameEquals(String firstname); List findByVersionEqualsAndFirstnameEquals(Long version, String firstname); + } 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 49ddbcee..5b5eda21 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.lang.reflect.Method; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -41,21 +43,22 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.auditing.DateTimeProvider; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.RemoveResult; import org.springframework.data.couchbase.core.query.N1QLExpression; -import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.AirportRepository; -import org.springframework.data.couchbase.domain.Iata; import org.springframework.data.couchbase.domain.NaiveAuditorAware; 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.UserAnnotated; import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; @@ -73,8 +76,14 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import com.couchbase.client.core.error.AmbiguousTimeoutException; import com.couchbase.client.core.error.CouchbaseException; import com.couchbase.client.core.error.IndexExistsException; +import com.couchbase.client.core.error.IndexFailureException; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.kv.MutationState; +import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; /** @@ -96,6 +105,9 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr @Autowired CouchbaseTemplate couchbaseTemplate; + String scopeName = "_default"; + String collectionName = "_default"; + @BeforeEach public void beforeEach() { try { @@ -182,12 +194,17 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr try { vie = new Airport("airports::vie", "vie", "low6"); vie = airportRepository.save(vie); + Airport airport2 = airportRepository + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + .findByIata(vie.getIata()); + assertEquals(airport2, vie); + List airports = airportRepository.findAllByIata("vie"); 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()); + airport2 = airportRepository.findByIata(airports.get(0).getIata()); + assertEquals(airport2.getId(), vie.getId()); } finally { airportRepository.delete(vie); } @@ -201,7 +218,9 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr vie = airportRepository.save(vie); List airports = couchbaseTemplate.findByQuery(Airport.class) .withConsistency(QueryScanConsistency.REQUEST_PLUS) - .matching(new Query(QueryCriteria.where(N1QLExpression.x("_class")).is("airport"))).all(); + .matching(org.springframework.data.couchbase.core.query.Query + .query(QueryCriteria.where(N1QLExpression.x("_class")).is("airport"))) + .all(); assertFalse(airports.isEmpty(), "should have found aiport"); } finally { airportRepository.delete(vie); @@ -214,18 +233,132 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr try { vie = new Airport("airports::vie", "vie", "loww"); vie = airportRepository.save(vie); - Airport airport2 = airportRepository.findByIata(Iata.vie); + Airport airport2 = airportRepository.findByIata(vie.getIata()); assertNotNull(airport2, "should have found " + vie); assertEquals(airport2.getId(), vie.getId()); + + } finally { + airportRepository.delete(vie); + } + } + + /** + * can test against _default._default without setting up additional scope/collection and also test for collections and + * scopes that do not exist These same tests should be repeated on non-default scope and collection in a test that + * supports collections + */ + @Test + @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) + void findBySimplePropertyWithCollection() { + + Airport vie = new Airport("airports::vie", "vie", "low7"); + try { + Airport saved = airportRepository.withScope(scopeName).withCollection(collectionName).save(vie); + // given collection (on scope used by template) + Airport airport2 = airportRepository.withCollection(collectionName) + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + .iata(vie.getIata()); + assertEquals(saved, airport2); + + // given scope and collection + + Airport airport3 = airportRepository.withScope(scopeName).withCollection(collectionName) + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + .iata(vie.getIata()); + assertEquals(saved, airport3); + + // given bad collection + assertThrows(IndexFailureException.class, + () -> airportRepository.withCollection("bogusCollection").iata(vie.getIata())); + + // given bad scope + assertThrows(IndexFailureException.class, () -> airportRepository.withScope("bogusScope").iata(vie.getIata())); + } finally { airportRepository.delete(vie); } } + @Test + @IgnoreWhen(hasCapabilities = { Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) + void findBySimplePropertyWithCollectionFail() { + // can test against _default._default without setting up additional scope/collection + // the server will throw an exception if it doesn't support COLLECTIONS + Airport vie = new Airport("airports::vie", "vie", "low8"); + try { + + Airport saved = airportRepository.save(vie); + + assertThrows(CouchbaseException.class, () -> airportRepository.withScope("non_default_scope_name") + .withCollection(collectionName).iata(vie.getIata())); + + } finally { + airportRepository.delete(vie); + } + } + + @Test + void findBySimplePropertyWithOptions() { + + Airport vie = new Airport("airports::vie", "vie", "low9"); + JsonArray positionalParams = JsonArray.create().add("this parameter will be overridden"); + // JsonObject namedParams = JsonObject.create().put("$1", vie.getIata()); + try { + Airport saved = airportRepository.save(vie); + // Duration of 1 nano-second will cause timeout + assertThrows(AmbiguousTimeoutException.class, () -> airportRepository + .withOptions(QueryOptions.queryOptions().timeout(Duration.ofNanos(1))).iata(vie.getIata())); + + Airport airport3 = airportRepository.withOptions( + QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS).parameters(positionalParams)) + .iata(vie.getIata()); + assertEquals(saved, airport3); + + } finally { + airportRepository.delete(vie); + } + + } + + @Test + public void saveNotBounded() { + // save() followed by query with NOT_BOUNDED will result in not finding the document + Airport vie = new Airport("airports::vie", "vie", "low9"); + Airport airport2 = null; + for (int i = 1; i <= 100; i++) { + // set version == 0 so save() will be an upsert, not a replace + Airport saved = airportRepository.save(vie.clearVersion()); + try { + airport2 = airportRepository + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.NOT_BOUNDED)) + .iata(saved.getIata()); + if (airport2 == null) { + break; + } + } catch (DataRetrievalFailureException drfe) { + airport2 = null; // + } finally { + // airportRepository.delete(vie); + // instead of delete, use removeResult to test QueryOptions.consistentWith() + RemoveResult removeResult = couchbaseTemplate.removeById().one(vie.getId()); + assertEquals(vie.getId(), removeResult.getId()); + assertTrue(removeResult.getCas() != 0); + assertTrue(removeResult.getMutationToken().isPresent()); + Airport airport3 = airportRepository + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS) + .consistentWith(MutationState.from(removeResult.getMutationToken().get()))) + .iata(vie.getIata()); + assertNull(airport3, "should have been removed"); + } + } + assertNull(airport2, "airport2 should have likely been null at least once"); + } + @Test public void testCas() { User user = new User("1", "Dave", "Wilson"); userRepository.save(user); + userRepository.findByFirstname("Dave"); user.setVersion(user.getVersion() - 1); assertThrows(DataIntegrityViolationException.class, () -> userRepository.save(user)); user.setVersion(0); @@ -233,12 +366,20 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr userRepository.delete(user); } + @Test + public void testExpiryAnnotation() { + UserAnnotated user = new UserAnnotated("1", "Dave", "Wilson"); + userRepository.save(user); + userRepository.findByFirstname("Dave"); + sleep(2000); + assertThrows(DataRetrievalFailureException.class, () -> userRepository.delete(user)); + } + @Test void count() { String[] iatas = { "JFK", "IAD", "SFO", "SJC", "SEA", "LAX", "PHX" }; try { - airportRepository.saveAll( Arrays.stream(iatas).map((iata) -> new Airport("airports::" + iata, iata, iata.toLowerCase(Locale.ROOT))) .collect(Collectors.toSet())); @@ -385,12 +526,9 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr Airport vienna = new Airport("airports::vie", "vie", "LOWW"); Airport frankfurt = new Airport("airports::fra", "fra", "EDDF"); Airport losAngeles = new Airport("airports::lax", "lax", "KLAX"); - try { airportRepository.saveAll(asList(vienna, frankfurt, losAngeles)); - airportRepository.deleteAllById(asList(vienna.getId(), losAngeles.getId())); - assertThat(airportRepository.findAll()).containsExactly(frankfurt); } finally { airportRepository.deleteAll(); @@ -430,7 +568,9 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr private void sleep(int millis) { try { Thread.sleep(millis); // so they are executed out-of-order - } catch (InterruptedException ie) {} + } catch (InterruptedException ie) { + ; + } } @Configuration @@ -463,10 +603,16 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr return new NaiveAuditorAware(); } + @Override + public void configureEnvironment(final ClusterEnvironment.Builder builder) { + builder.ioConfig().maxHttpConnections(11).idleHttpConnectionTimeout(Duration.ofSeconds(4)); + return; + } + @Bean(name = "dateTimeProviderRef") public DateTimeProvider testDateTimeProvider() { return new AuditingDateTimeProvider(); } - } + } } diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java index 77e6fe2a..81a6b077 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java @@ -73,7 +73,8 @@ public class ReactiveCouchbaseRepositoryKeyValueIntegrationTests extends Cluster Airport saved = airportRepository.save(vie).block(); Airport airport1 = airportRepository.findById(saved.getId()).block(); assertEquals(airport1, saved); - assertEquals(saved.getCreatedBy(), ReactiveNaiveAuditorAware.AUDITOR); // ReactiveNaiveAuditorAware will provide this + assertEquals(saved.getCreatedBy(), ReactiveNaiveAuditorAware.AUDITOR); // ReactiveNaiveAuditorAware will provide + // this } finally { airportRepository.delete(vie).block(); } 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 ed07c651..a5f015d5 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java @@ -130,17 +130,17 @@ public class ReactiveCouchbaseRepositoryQueryIntegrationTests extends JavaIntegr Airport vie = new Airport("airports::vie", "vie", "low3"); Airport saved1 = airportRepository.save(vie).block(); Airport saved2 = airportRepository.save(vie.withId(UUID.randomUUID().toString())).block(); - try { - airportRepository.findAll().collectList().block(); // findAll has QueryScanConsistency; - Mono airport = airportRepository.findPolicySnapshotByPolicyIdAndEffectiveDateTime("any", 0); - System.out.println("------------------------------"); - System.out.println(airport.block()); - System.out.println("------------------------------"); - Flux airports = airportRepository.findPolicySnapshotAll(); - System.out.println(airports.collectList().block()); - System.out.println("------------------------------"); - Mono ap = getPolicyByIdAndEffectiveDateTime("x", Instant.now()); - System.out.println(ap.block()); + try { + airportRepository.findAll().collectList().block(); // findAll has QueryScanConsistency; + Mono airport = airportRepository.findPolicySnapshotByPolicyIdAndEffectiveDateTime("any", 0); + System.out.println("------------------------------"); + System.out.println(airport.block()); + System.out.println("------------------------------"); + Flux airports = airportRepository.findPolicySnapshotAll(); + System.out.println(airports.collectList().block()); + System.out.println("------------------------------"); + Mono ap = getPolicyByIdAndEffectiveDateTime("x", Instant.now()); + System.out.println(ap.block()); } finally { airportRepository.delete(saved1).block(); airportRepository.delete(saved2).block(); @@ -249,6 +249,22 @@ public class ReactiveCouchbaseRepositoryQueryIntegrationTests extends JavaIntegr } } + @Test + void deleteOne() { + + Airport vienna = new Airport("airports::vie", "vie", "LOWW"); + + try { + Airport ap = airportRepository.save(vienna).block(); + assertEquals(vienna.getId(), ap.getId(), "should have saved what was provided"); + airportRepository.delete(vienna).as(StepVerifier::create).verifyComplete(); + + airportRepository.findAll().as(StepVerifier::create).verifyComplete(); + } finally { + airportRepository.deleteAll().block(); + } + } + @Configuration @EnableReactiveCouchbaseRepositories("org.springframework.data.couchbase") static class Config extends AbstractCouchbaseConfiguration { diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java new file mode 100644 index 00000000..f522671e --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -0,0 +1,211 @@ +/* + * Copyright 2017-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.query; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.AirportRepository; +import org.springframework.data.couchbase.domain.Config; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserCol; +import org.springframework.data.couchbase.domain.UserColRepository; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; +import org.springframework.data.couchbase.util.IgnoreWhen; + +import com.couchbase.client.core.error.IndexFailureException; +import com.couchbase.client.core.io.CollectionIdentifier; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryScanConsistency; + +@IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +public class CouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { + + @Autowired AirportRepository airportRepository; + @Autowired UserColRepository userColRepository; + + @BeforeAll + public static void beforeAll() { + // first call the super method + callSuperBeforeAll(new Object() {}); + // then do processing for this class + } + + @AfterAll + public static void afterAll() { + // first do the processing for this class + // no-op + // then call the super method + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + @Override + public void beforeEach() { + // first call the super method + super.beforeEach(); + // then do processing for this class + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); + couchbaseTemplate.removeByQuery(UserCol.class).inScope(otherScope).inCollection(otherCollection).all(); + ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + // seems that @Autowired is not adequate, so ... + airportRepository = (AirportRepository) ac.getBean("airportRepository"); + userColRepository = (UserColRepository) ac.getBean("userColRepository"); + } + + @AfterEach + @Override + public void afterEach() { + // first do processing for this class + // no-op + // then call the super method + super.afterEach(); + } + + @Test + public void myTest() { + + AirportRepository ar = airportRepository.withScope(scopeName).withCollection(collectionName); + Airport vie = new Airport("airports::vie", "vie", "loww"); + try { + Airport saved = ar.save(vie); + Airport airport2 = ar.save(saved); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + ar.delete(vie); + } + + } + + /** + * can test against _default._default without setting up additional scope/collection and also test for collections and + * scopes that do not exist These same tests should be repeated on non-default scope and collection in a test that + * supports collections + */ + @Test + @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) + void findBySimplePropertyWithCollection() { + + Airport vie = new Airport("airports::vie", "vie", "loww"); + // create proxy with scope, collection + AirportRepository ar = airportRepository.withScope(scopeName).withCollection(collectionName); + try { + Airport saved = ar.save(vie); + + // valid scope, collection in options + Airport airport2 = ar.withCollection(collectionName) + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + .iata(vie.getIata()); + assertEquals(saved, airport2); + + // given bad collectionName in fluent + assertThrows(IndexFailureException.class, () -> ar.withCollection("bogusCollection").iata(vie.getIata())); + + // given bad scopeName in fluent + assertThrows(IndexFailureException.class, () -> ar.withScope("bogusScope").iata(vie.getIata())); + + Airport airport6 = ar.withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + .iata(vie.getIata()); + assertEquals(saved, airport6); + + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + ar.deleteAll(); + } + } + + @Test + void findBySimplePropertyWithOptions() { + + AirportRepository ar = airportRepository.withScope(scopeName).withCollection(collectionName); + Airport vie = new Airport("airports::vie", "vie", "loww"); + JsonArray positionalParams = JsonArray.create().add("\"this parameter will be overridden\""); + try { + Airport saved = ar.save(vie); + + Airport airport3 = ar.withOptions( + QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS).parameters(positionalParams)) + .iata(vie.getIata()); + assertEquals(saved, airport3); + + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + ar.delete(vie); + } + } + + @Test + public void testScopeCollectionAnnotation() { + // template default scope is my_scope + // UserCol annotation scope is other_scope + UserCol user = new UserCol("1", "Dave", "Wilson"); + try { + UserCol saved = userColRepository.withCollection(otherCollection).save(user); // should use UserCol annotation + // scope + List found = userColRepository.withCollection(otherCollection).findByFirstname(user.getFirstname()); + assertEquals(saved, found.get(0), "should have found what was saved"); + List notfound = userColRepository.withScope(CollectionIdentifier.DEFAULT_SCOPE) + .withCollection(CollectionIdentifier.DEFAULT_COLLECTION).findByFirstname(user.getFirstname()); + assertEquals(0, notfound.size(), "should not have found what was saved"); + } finally { + try { + userColRepository.withScope(otherScope).withCollection(otherCollection).delete(user); + } catch (DataRetrievalFailureException drfe) {} + } + } + + // template default scope is my_scope + // UserCol annotation scope is other_scope + @Test + public void testScopeCollectionRepoWith() { + UserCol user = new UserCol("1", "Dave", "Wilson"); + try { + UserCol saved = userColRepository.withScope(scopeName).withCollection(collectionName).save(user); + List found = userColRepository.withScope(scopeName).withCollection(collectionName) + .findByFirstname(user.getFirstname()); + assertEquals(saved, found.get(0), "should have found what was saved"); + List notfound = userColRepository.withScope(CollectionIdentifier.DEFAULT_SCOPE) + .withCollection(CollectionIdentifier.DEFAULT_COLLECTION).findByFirstname(user.getFirstname()); + assertEquals(0, notfound.size(), "should not have found what was saved"); + userColRepository.withScope(scopeName).withCollection(collectionName).delete(user); + } finally { + try { + userColRepository.withScope(scopeName).withCollection(collectionName).delete(user); + } catch (DataRetrievalFailureException drfe) {} + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java new file mode 100644 index 00000000..51f6a5ef --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -0,0 +1,215 @@ +/* + * Copyright 2017-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.repository.query; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.data.couchbase.domain.Airport; +import org.springframework.data.couchbase.domain.Config; +import org.springframework.data.couchbase.domain.ReactiveAirportRepository; +import org.springframework.data.couchbase.domain.ReactiveUserColRepository; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserCol; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests; +import org.springframework.data.couchbase.util.IgnoreWhen; + +import com.couchbase.client.core.error.IndexFailureException; +import com.couchbase.client.core.io.CollectionIdentifier; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryScanConsistency; + +@IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) +public class ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { + + @Autowired ReactiveAirportRepository airportRepository; + @Autowired ReactiveUserColRepository userColRepository; + + @BeforeAll + public static void beforeAll() { + // first call the super method + callSuperBeforeAll(new Object() {}); + // then do processing for this class + } + + @AfterAll + public static void afterAll() { + // first do the processing for this class + // no-op + // then call the super method + callSuperAfterAll(new Object() {}); + } + + @BeforeEach + @Override + public void beforeEach() { + // first call the super method + super.beforeEach(); + // then do processing for this class + couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all(); + couchbaseTemplate.removeByQuery(UserCol.class).inScope(otherScope).inCollection(otherCollection).all(); + + ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + // seems that @Autowired is not adequate, so ... + airportRepository = (ReactiveAirportRepository) ac.getBean("reactiveAirportRepository"); + userColRepository = (ReactiveUserColRepository) ac.getBean("reactiveUserColRepository"); + } + + @AfterEach + @Override + public void afterEach() { + // first do processing for this class + // no-op + // then call the super method + super.afterEach(); + } + + @Test + public void myTest() { + + ReactiveAirportRepository ar = airportRepository.withScope(scopeName).withCollection(collectionName); + Airport vie = new Airport("airports::vie", "vie", "loww"); + try { + Airport saved = ar.save(vie).block(); + Airport airport2 = ar.save(saved).block(); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + ar.delete(vie).block(); + } + + } + + /** + * can test against _default._default without setting up additional scope/collection and also test for collections and + * scopes that do not exist These same tests should be repeated on non-default scope and collection in a test that + * supports collections + */ + @Test + @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) + void findBySimplePropertyWithCollection() { + + Airport vie = new Airport("airports::vie", "vie", "loww"); + // create proxy with scope, collection + ReactiveAirportRepository ar = airportRepository.withScope(scopeName).withCollection(collectionName); + try { + Airport saved = ar.save(vie).block(); + + // valid scope, collection in options + Airport airport2 = ar.withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + .iata(vie.getIata()).block(); + assertEquals(saved, airport2); + + // given bad collectionName in fluent + assertThrows(IndexFailureException.class, () -> ar.withCollection("bogusCollection").iata(vie.getIata()).block()); + + // given bad scopeName in fluent + assertThrows(IndexFailureException.class, () -> ar.withScope("bogusScope").iata(vie.getIata()).block()); + + Airport airport6 = ar.withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + .iata(vie.getIata()).block(); + assertEquals(saved, airport6); + + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + ar.deleteAll().block(); + } + } + + @Test + void findBySimplePropertyWithOptions() { + + Airport vie = new Airport("airports::vie", "vie", "loww"); + ReactiveAirportRepository ar = airportRepository.withScope(scopeName).withCollection(collectionName); + JsonArray positionalParams = JsonArray.create().add("\"this parameter will be overridden\""); + try { + Airport saved = ar.save(vie).block(); + + Airport airport3 = ar.withOptions( + QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS).parameters(positionalParams)) + .iata(vie.getIata()).block(); + assertEquals(saved, airport3); + + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + ar.delete(vie).block(); + } + } + + @Test + public void testScopeCollectionAnnotation() { + // template default scope is my_scope + // UserCol annotation scope is other_scope + UserCol user = new UserCol("1", "Dave", "Wilson"); + try { + UserCol saved = userColRepository.withCollection(otherCollection).save(user).block(); // should use UserCol + // annotation + // scope + List found = userColRepository.withCollection(otherCollection).findByFirstname(user.getFirstname()) + .collectList().block(); + assertEquals(saved, found.get(0), "should have found what was saved"); + List notfound = userColRepository.withScope(CollectionIdentifier.DEFAULT_SCOPE) + .withCollection(CollectionIdentifier.DEFAULT_COLLECTION).findByFirstname(user.getFirstname()).collectList() + .block(); + assertEquals(0, notfound.size(), "should not have found what was saved"); + } finally { + try { + userColRepository.withScope(otherScope).withCollection(otherCollection).delete(user); + } catch (DataRetrievalFailureException drfe) {} + } + } + + // template default scope is my_scope + // UserCol annotation scope is other_scope + @Test + public void testScopeCollectionRepoWith() { + UserCol user = new UserCol("1", "Dave", "Wilson"); + try { + UserCol saved = userColRepository.withScope(scopeName).withCollection(collectionName).save(user).block(); + List found = userColRepository.withScope(scopeName).withCollection(collectionName) + .findByFirstname(user.getFirstname()).collectList().block(); + assertEquals(saved, found.get(0), "should have found what was saved"); + List notfound = userColRepository.withScope(CollectionIdentifier.DEFAULT_SCOPE) + .withCollection(CollectionIdentifier.DEFAULT_COLLECTION).findByFirstname(user.getFirstname()).collectList() + .block(); + assertEquals(0, notfound.size(), "should not have found what was saved"); + userColRepository.withScope(scopeName).withCollection(collectionName).delete(user).block(); + } finally { + try { + userColRepository.withScope(scopeName).withCollection(collectionName).delete(user).block(); + } catch (DataRetrievalFailureException drfe) {} + } + } +} diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index 57bffc18..9d05fdc1 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -22,7 +22,7 @@ - log additional debug info during automatic index creation --> - " + "