From c53378e1a41adcce8c3153d9809ce55b1ea7ebda Mon Sep 17 00:00:00 2001 From: mikereiche Date: Thu, 10 Jun 2021 13:54:37 -0400 Subject: [PATCH 1/3] 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. --- .../SimpleCouchbaseClientFactory.java | 8 +- .../core/ReactiveCouchbaseOperations.java | 10 +- .../core/ReactiveCouchbaseTemplate.java | 10 +- .../couchbase/core/support/PseudoArgs.java | 12 +- .../repository/CouchbaseRepository.java | 11 +- .../repository/DynamicProxyable.java | 80 +++++++++ .../ReactiveCouchbaseRepository.java | 7 +- .../couchbase/repository/ScanConsistency.java | 10 +- .../query/AbstractCouchbaseQuery.java | 13 +- .../query/AbstractReactiveCouchbaseQuery.java | 11 +- .../support/DynamicInvocationHandler.java | 124 +++++++++++++ .../ReactiveCouchbaseRepositoryFactory.java | 20 +-- .../support/SimpleCouchbaseRepository.java | 46 +++-- .../SimpleReactiveCouchbaseRepository.java | 22 ++- .../couchbase/domain/AirportRepository.java | 6 +- ...chbaseRepositoryQueryIntegrationTests.java | 153 +++++++++++++++- ...sitoryQueryCollectionIntegrationTests.java | 169 ++++++++++++++++++ src/test/resources/logback.xml | 2 +- 18 files changed, 628 insertions(+), 86 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.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/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java diff --git a/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java b/src/main/java/org/springframework/data/couchbase/SimpleCouchbaseClientFactory.java index bcb5034bb..b97b57f95 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 @@ /** * The default implementation of a {@link CouchbaseClientFactory}. + * + * @author Michael Nitschinger + * @author Michael Reiche */ public class SimpleCouchbaseClientFactory implements CouchbaseClientFactory { @@ -74,7 +76,7 @@ private SimpleCouchbaseClientFactory(final Supplier cluster, final Stri @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/core/ReactiveCouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseOperations.java index 83e43bbe1..f3b97d35a 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 2aedc9dfc..20a2ce7ba 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -158,11 +158,17 @@ public void setApplicationContext(final ApplicationContext applicationContext) t } /** - * {@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.set(threadLocalArgs); + } + } 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 cb92bbc21..6721f1215 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 @@ -74,42 +74,42 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle } /** - * @@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 + * @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 + * @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 + * @return the collection name from the ThreadLocal field of the template */ private String getThreadLocalCollectionName(ReactiveCouchbaseTemplate template) { return template.getPseudoArgs() == null ? null : template.getPseudoArgs().getCollection(); 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 5544d7e58..61ded5449 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 @@ 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,7 @@ 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 000000000..6c3f616b1 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java @@ -0,0 +1,80 @@ +/* + * 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/ReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java index d76af65f0..91776b9a8 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 @@ * 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 7254e1ae2..810a9b666 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,14 @@ 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/query/AbstractCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractCouchbaseQuery.java index 979de664d..254407afe 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; @@ -84,14 +83,11 @@ protected Object doExecute(CouchbaseQueryMethod method, ResultProcessor processo query = applyAnnotatedConsistencyIfPresent(query); // query = applyAnnotatedCollationIfPresent(query, accessor); // not yet implemented - 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 @@ private CouchbaseQueryExecution getExecution(ParameterAccessor accessor, Convert * @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()); 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 f01e3f6cd..ddb4a6a74 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; @@ -82,15 +81,11 @@ protected Object doExecute(CouchbaseQueryMethod method, ResultProcessor processo query = applyAnnotatedConsistencyIfPresent(query); // query = applyAnnotatedCollationIfPresent(query, accessor); // not yet implemented - 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); } /** 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 000000000..9e50c7877 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java @@ -0,0 +1,124 @@ +/* + * 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 T1 target; + final Class repositoryClass; + final CouchbaseEntityInformation entityInformation; + final ReactiveCouchbaseTemplate reactiveTemplate; + CommonOptions options; + String collection; + String scope;; + + public DynamicInvocationHandler(T1 target, CommonOptions options, String collection, String scope) { + this.target = target; + if (target instanceof CouchbaseRepository) { + reactiveTemplate = ((CouchbaseTemplate) ((CouchbaseRepository) target).getOperations()).reactive(); + } else if (target instanceof ReactiveCouchbaseRepository) { + reactiveTemplate = (ReactiveCouchbaseTemplate) ((ReactiveCouchbaseRepository) target).getOperations(); + } else { + throw new RuntimeException("Unknown target type: " + target.getClass()); + } + this.entityInformation = target instanceof CouchbaseRepository + ? ((CouchbaseRepository) target).getEntityInformation() + : ((ReactiveCouchbaseRepository) target).getEntityInformation(); + 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(this.getClass().getClassLoader(), target.getClass().getInterfaces(), + new DynamicInvocationHandler<>(target, (CommonOptions) args[0], collection, scope)); + } + + if (method.getName().equals("withScope")) { + return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), + new DynamicInvocationHandler<>(target, options, collection, (String) args[0])); + } + + if (method.getName().equals("withCollection")) { + return Proxy.newProxyInstance(this.getClass().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); + } + + Method theMethod = repositoryClass.getMethod(method.getName(), paramTypes); + Object result; + + try { + setThreadLocal(); + result = theMethod.invoke(target, args); + } catch (InvocationTargetException ite) { + throw ite.getCause(); + } finally { + clearThreadLocal(); + } + return result; + } + + private void setThreadLocal() { + reactiveTemplate.setPseudoArgs(new PseudoArgs(this.scope, this.collection, this.options)); + } + + private void clearThreadLocal() { + reactiveTemplate.setPseudoArgs(null); + } +} 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 8ca423c39..6479c372b 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 void setBeanClassLoader(ClassLoader classLoader) { * 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,8 +107,8 @@ public CouchbaseEntityInformation getEntityInformation(Class d */ @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); @@ -150,8 +150,8 @@ public CouchbaseQueryLookupStrategy(QueryMethodEvaluationContextProvider evaluat @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 c91235d4b..7636df13a 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,13 +44,14 @@ * @author Michael Nitschinger * @author Mark Paluch * @author Jens Schauder + * @author Michael Reiche */ public class SimpleCouchbaseRepository implements CouchbaseRepository { /** * Holds the reference to the {@link org.springframework.data.couchbase.core.CouchbaseTemplate}. */ - private final CouchbaseOperations couchbaseOperations; + private final CouchbaseOperations operations; /** * Contains information about the entity being used in this repository. @@ -71,7 +72,8 @@ public SimpleCouchbaseRepository(CouchbaseEntityInformation entityInf Assert.notNull(couchbaseOperations, "CouchbaseOperations must not be null!"); this.entityInformation = entityInformation; - this.couchbaseOperations = couchbaseOperations; + this.operations = couchbaseOperations; + } @Override @@ -79,11 +81,13 @@ public SimpleCouchbaseRepository(CouchbaseEntityInformation entityInf public 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(entityInformation.getJavaType()).one(entity); } else { - return (S) couchbaseOperations.upsertById(entityInformation.getJavaType()).one(entity); + result = (S) operations.upsertById(entityInformation.getJavaType()).one(entity); } + return result; } @Override @@ -95,57 +99,55 @@ public Iterable saveAll(Iterable entities) { @Override public Optional 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(entityInformation.getJavaType()).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(entityInformation.getJavaType()).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().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().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().one(entityInformation.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().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().all(Streamable.of(entities).map(entityInformation::getId).toList()); } @Override public long count() { - return couchbaseOperations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) - .count(); + return operations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()).count(); } @Override public void deleteAll() { - couchbaseOperations.removeByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) - .all(); + operations.removeByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()).all(); } @Override @@ -174,10 +176,16 @@ public Page findAll(Pageable pageable) { * * @return the underlying entity information. */ - protected CouchbaseEntityInformation getEntityInformation() { + @Override + public CouchbaseEntityInformation getEntityInformation() { return entityInformation; } + @Override + public CouchbaseOperations getOperations() { + return operations; + } + /** * Helper method to assemble a n1ql find all query, taking annotations into acocunt. * @@ -185,7 +193,7 @@ protected CouchbaseEntityInformation getEntityInformation() { * @return the list of found entities, already executed. */ private List findAll(Query query) { - return couchbaseOperations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) + return operations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) .matching(query).all(); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java index 7cf8320e5..b4bed1f1b 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. @@ -46,6 +46,7 @@ * @author David Kelly * @author Douglas Six * @author Jens Schauder + * @author Michael Reiche * @since 3.0 */ public class SimpleReactiveCouchbaseRepository implements ReactiveCouchbaseRepository { @@ -81,12 +82,14 @@ public SimpleReactiveCouchbaseRepository(CouchbaseEntityInformation e @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(entityInformation.getJavaType()).one(entity); } else { - return (Mono) operations.upsertById(entityInformation.getJavaType()).one(entity); + result = (Mono) operations.upsertById(entityInformation.getJavaType()).one(entity); } + return result; } @Override @@ -188,7 +191,8 @@ public Mono count() { @Override public Mono deleteAll() { - return operations.removeByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()).all().then(); + return operations.removeByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()).all() + .then(); } /** @@ -196,10 +200,16 @@ public Mono deleteAll() { * * @return the underlying entity information. */ - protected CouchbaseEntityInformation getEntityInformation() { + @Override + public CouchbaseEntityInformation getEntityInformation() { return entityInformation; } + @Override + public ReactiveCouchbaseOperations getOperations() { + return operations; + } + private Flux findAll(Query query) { return operations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) .matching(query).all(); 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 d849d32e3..3531b7b47 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -21,6 +21,7 @@ import org.springframework.data.couchbase.core.RemoveResult; 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.domain.Page; @@ -41,7 +42,8 @@ * @author Michael Reiche */ @Repository -public interface AirportRepository extends CouchbaseRepository { +@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) +public interface AirportRepository extends CouchbaseRepository, DynamicProxyable { @Override @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) @@ -56,6 +58,8 @@ public interface AirportRepository extends CouchbaseRepository @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) Airport findByIata(Iata iata); + Airport iata(String iata); + @Query("#{#n1ql.selectEntity} where iata = $1") @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) List getAllByIata(String iata); 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 4a0918bf4..bd1f057d9 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.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,17 +43,17 @@ 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; @@ -73,8 +75,14 @@ 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 +104,9 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr @Autowired CouchbaseTemplate couchbaseTemplate; + String scopeName = "_default"; + String collectionName = "_default"; + @BeforeEach public void beforeEach() { try { @@ -182,12 +193,17 @@ void findBySimpleProperty() { try { vie = new Airport("airports::vie", "vie", "low6"); vie = airportRepository.save(vie); + Airport airport2 = airportRepository + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.NOT_BOUNDED)) + .findByIata(vie.getIata()); + assertEquals(airport2.getId(), vie.getId()); + 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 +217,9 @@ void findByTypeAlias() { 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,14 +232,123 @@ void findByEnum() { 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); + } + + // save() followed by query with NOT_BOUNDED will result in not finding the document + Airport airport2 = null; + for (int i = 1; i <= 10; 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).parameters(positionalParams)) + .iata(saved.getIata()); + if (airport2 == null) { + break; + } + } catch (DataRetrievalFailureException drfe) { + ; // was expecting this + } 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"); @@ -430,7 +557,9 @@ void findBySimplePropertyAudited() { 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 +592,16 @@ public NaiveAuditorAware testAuditorAware() { 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/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java new file mode 100644 index 000000000..547b69057 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -0,0 +1,169 @@ +/* + * 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 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.data.couchbase.CouchbaseClientFactory; +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.UserRepository; +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.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 CouchbaseClientFactory clientFactory; + + @Autowired AirportRepository airportRepository; + + @Autowired UserRepository userRepository; + + @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(); + ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + // seems that @Autowired is not adequate, so ... + airportRepository = (AirportRepository) ac.getBean("airportRepository"); + } + + @AfterEach + @Override + public void afterEach() { + // first do processing for this class + // no-op + // then call the super method + super.afterEach(); + } + + @Test + public void myTest() { + + Airport vie = new Airport("airports::vie", "vie", "loww"); + try { + airportRepository = airportRepository.withCollection(collectionName); + Airport saved = airportRepository.save(vie); + Airport airport2 = airportRepository.save(saved); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } 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", "loww"); + // create proxy with scope, collection + airportRepository = airportRepository.withScope(scopeName).withCollection(collectionName); + try { + Airport saved = airportRepository.save(vie); + + // valid scope, collection in options + Airport airport2 = airportRepository.withCollection(collectionName) + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + .iata(vie.getIata()); + assertEquals(saved, airport2); + + // given bad collectionName in fluent + assertThrows(IndexFailureException.class, + () -> airportRepository.withCollection("bogusCollection").iata(vie.getIata())); + + // given bad scopeName in fluent + assertThrows(IndexFailureException.class, () -> airportRepository.withScope("bogusScope").iata(vie.getIata())); + + Airport airport6 = airportRepository + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + .iata(vie.getIata()); + assertEquals(saved, airport6); + + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + airportRepository.withScope(scopeName).withCollection(collectionName).deleteAll(); + } + } + + @Test + void findBySimplePropertyWithOptions() { + + Airport vie = new Airport("airports::vie", "vie", "loww"); + JsonArray positionalParams = JsonArray.create().add("\"this parameter will be overridden\""); + try { + Airport saved = airportRepository.withCollection(collectionName).save(vie); + + Airport airport3 = airportRepository.withCollection(collectionName).withOptions( + QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS).parameters(positionalParams)) + .iata(vie.getIata()); + assertEquals(saved, airport3); + + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + airportRepository.withCollection(collectionName).delete(vie); + } + } + +} diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index 57bffc18d..9d05fdc15 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 --> - " + " From 161d70cd832c4c5551d9183092694cb1955c42d1 Mon Sep 17 00:00:00 2001 From: mikereiche Date: Wed, 16 Jun 2021 09:54:39 -0400 Subject: [PATCH 2/3] Scopes and Collections for Repositories. This adds scope and collection support to repositories via the DynamicInvocationHandler and PseudoArgs. It also adds annotations for scopes and collections. Closes #963 --- .../couchbase/core/CouchbaseTemplate.java | 15 +- .../core/ExecutableExistsByIdOperation.java | 7 +- .../ExecutableExistsByIdOperationSupport.java | 23 +- ...utableFindByAnalyticsOperationSupport.java | 2 - .../ExecutableFindByIdOperationSupport.java | 2 - ...ExecutableFindByQueryOperationSupport.java | 5 +- ...eFindFromReplicasByIdOperationSupport.java | 6 +- .../ExecutableInsertByIdOperationSupport.java | 2 - .../core/ExecutableRemoveByIdOperation.java | 7 +- .../ExecutableRemoveByIdOperationSupport.java | 31 +-- ...ecutableRemoveByQueryOperationSupport.java | 7 +- ...ExecutableReplaceByIdOperationSupport.java | 2 - .../ExecutableUpsertByIdOperationSupport.java | 2 - .../core/ReactiveCouchbaseTemplate.java | 17 +- .../core/ReactiveExistsByIdOperation.java | 7 + .../ReactiveExistsByIdOperationSupport.java | 23 +- ...activeFindByAnalyticsOperationSupport.java | 2 - .../ReactiveFindByIdOperationSupport.java | 4 +- .../ReactiveFindByQueryOperationSupport.java | 14 +- ...eFindFromReplicasByIdOperationSupport.java | 4 +- .../ReactiveInsertByIdOperationSupport.java | 5 +- .../core/ReactiveRemoveByIdOperation.java | 7 + .../ReactiveRemoveByIdOperationSupport.java | 31 +-- ...ReactiveRemoveByQueryOperationSupport.java | 11 +- .../ReactiveReplaceByIdOperationSupport.java | 5 +- .../ReactiveUpsertByIdOperationSupport.java | 5 +- .../convert/MappingCouchbaseConverter.java | 5 + .../data/couchbase/core/mapping/Document.java | 19 ++ .../data/couchbase/core/mapping/Expiry.java | 65 ++++++ .../data/couchbase/core/query/Query.java | 1 + .../couchbase/core/support/PseudoArgs.java | 112 ++++++--- .../repository/CouchbaseRepository.java | 1 + .../repository/DynamicProxyable.java | 2 - .../ReactiveCouchbaseRepository.java | 2 +- .../couchbase/repository/ScanConsistency.java | 1 - .../query/CouchbaseQueryMethod.java | 7 +- .../query/StringBasedCouchbaseQuery.java | 4 +- .../support/CouchbaseRepositoryBase.java | 130 +++++++++++ .../support/CouchbaseRepositoryFactory.java | 14 +- .../support/CrudMethodMetadata.java | 5 + .../CrudMethodMetadataPostProcessor.java | 38 ++- .../support/DynamicInvocationHandler.java | 18 +- .../ReactiveCouchbaseRepositoryFactory.java | 2 +- .../support/SimpleCouchbaseRepository.java | 84 +++---- .../SimpleReactiveCouchbaseRepository.java | 86 +++---- ...mplateQueryCollectionIntegrationTests.java | 51 +++- .../data/couchbase/domain/AbstractEntity.java | 6 +- .../data/couchbase/domain/Airport.java | 4 +- .../couchbase/domain/AirportRepository.java | 7 +- .../data/couchbase/domain/Config.java | 12 +- .../data/couchbase/domain/Person.java | 24 +- .../couchbase/domain/PersonRepository.java | 3 +- .../domain/ReactiveAirportRepository.java | 10 +- .../domain/ReactiveUserColRepository.java | 63 +++++ .../data/couchbase/domain/UserCol.java | 37 +++ .../couchbase/domain/UserColRepository.java | 61 +++++ .../data/couchbase/domain/UserRepository.java | 4 + ...chbaseRepositoryQueryIntegrationTests.java | 15 +- ...chbaseRepositoryQueryIntegrationTests.java | 38 ++- ...sitoryQueryCollectionIntegrationTests.java | 55 ++++- ...sitoryQueryCollectionIntegrationTests.java | 219 ++++++++++++++++++ .../couchbase/util/UnmanagedTestCluster.java | 3 +- 62 files changed, 1121 insertions(+), 333 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/Expiry.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/ReactiveUserColRepository.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/UserCol.java 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/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java 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 2f1354dcc..e44a63c1d 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.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; @@ -108,12 +107,22 @@ public ExecutableFindByAnalytics findByAnalytics(Class domainType) { @Override public ExecutableRemoveById removeById() { - return new ExecutableRemoveByIdOperationSupport(this).removeById(); + return removeById(null); + } + + @Override + public ExecutableRemoveById removeById(Class domainType) { + return new ExecutableRemoveByIdOperationSupport(this).removeById(domainType); } @Override 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/ExecutableExistsByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperation.java index 357ba500b..34468efec 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 @@ interface ExistsByIdWithOptions extends TerminatingExistsById, WithExistsOpti } /** - * * 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 5484d9ab5..e441a2dd5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableExistsByIdOperationSupport.java @@ -33,25 +33,32 @@ public class ExecutableExistsByIdOperationSupport implements ExecutableExistsByI @Override 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 +73,18 @@ public Map all(final Collection ids) { @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 54924a80f..ad88bc4de 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 FindByAnalyticsWithQuery withOptions(final AnalyticsOptions options) { @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 b1d07be5a..84db7050d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -77,13 +77,11 @@ public TerminatingFindById withOptions(final GetOptions options) { @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); } 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 86cb58801..c9989c27f 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 @@ static class ExecutableFindByQuerySupport implements ExecutableFindByQuery 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; @@ -154,14 +155,12 @@ public TerminatingFindByQuery withOptions(final QueryOptions options) { @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 5ef12b545..c087b9205 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 @@ 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 @@ static class ExecutableFindFromReplicasByIdSupport implements ExecutableFindF 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 TerminatingFindFromReplicasById withOptions(final GetAnyReplicaOptions @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 908cf4590..52031af21 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 TerminatingInsertById withOptions(final InsertOptions options) { @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 67c5392da..c31bc9b05 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 @@ 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,8 +38,12 @@ public interface ExecutableRemoveByIdOperation { /** * Removes a document. */ + ExecutableRemoveById removeById(Class domainType); + /** + * Removes a document. + */ + @Deprecated ExecutableRemoveById removeById(); - /** * Terminating operations invoking the actual execution. */ 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 a006f1d6e..3bd15e97b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -36,13 +36,19 @@ public ExecutableRemoveByIdOperationSupport(final CouchbaseTemplate template) { @Override 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 +58,19 @@ static class ExecutableRemoveByIdSupport implements ExecutableRemoveById { 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 +86,14 @@ public List all(final Collection ids) { @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 +101,26 @@ public RemoveByIdInCollection withDurability(final DurabilityLevel durabilityLev 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 f85745985..a97c62ba3 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 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 ExecutableRemoveByQueryOperationSupport(final CouchbaseTemplate template) @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 RemoveByQueryConsistentWith withConsistency(final QueryScanConsistency @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 RemoveByQueryWithQuery withOptions(final QueryOptions options) { @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 ba8ab3eb2..ef47eb94a 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 Collection all(Collection objects) { @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 TerminatingReplaceById withOptions(final ReplaceOptions options) { @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 0719082c2..61b7a3945 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 TerminatingUpsertById withOptions(final UpsertOptions options) { @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/ReactiveCouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java index 20a2ce7ba..a7f0587b4 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 ReactiveFindById findById(Class domainType) { @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 ReactiveInsertById insertById(Class domainType) { @Override public ReactiveRemoveById removeById() { - return new ReactiveRemoveByIdOperationSupport(this).removeById(); + return new ReactiveRemoveByIdOperationSupport(this).removeById(null); + } + + @Override + public ReactiveRemoveById removeById(Class domainType) { + return new ReactiveRemoveByIdOperationSupport(this).removeById(domainType); } @Override @@ -168,6 +178,7 @@ public PseudoArgs getPseudoArgs() { * 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/ReactiveExistsByIdOperation.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperation.java index 5415468d7..014cc43ec 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.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 8e03dd576..577f1e7d7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveExistsByIdOperationSupport.java @@ -42,19 +42,26 @@ public class ReactiveExistsByIdOperationSupport implements ReactiveExistsByIdOpe @Override 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; @@ -63,7 +70,7 @@ static class ReactiveExistsByIdSupport implements ReactiveExistsById { @Override public Mono one(final String id) { PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, - options != null ? options : ExistsOptions.existsOptions()); + options != null ? options : ExistsOptions.existsOptions(), null); LOG.trace("statement: {} scope: {} collection: {}", "exitsById", pArgs.getScope(), pArgs.getCollection()); return Mono.just(id) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) @@ -85,20 +92,18 @@ public Mono> all(final Collection ids) { @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 4f64a6b13..53cce70de 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 TerminatingFindByAnalytics withOptions(final AnalyticsOptions options) @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 a9973136e..ec1022b8c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -79,7 +79,7 @@ public Mono one(final String id) { if (fields != null && !fields.isEmpty()) { gOptions.project(fields); } - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, gOptions, null); LOG.trace("statement: {} scope: {} collection: {}", "findById", pArgs.getScope(), pArgs.getCollection()); return template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getCollection(pArgs.getCollection()) .reactive().get(docId, pArgs.getOptions()); @@ -113,13 +113,11 @@ public TerminatingFindById withOptions(final GetOptions options) { @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); } 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 60a504384..4cdf21d99 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -64,9 +64,6 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { private final String collection; private 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 +88,8 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { @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 +107,12 @@ public TerminatingFindByQuery withOptions(final QueryOptions options) { @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); } @@ -161,7 +157,7 @@ public Mono first() { @Override public Flux all() { return Flux.defer(() -> { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); String statement = assembleEntityQuery(false, distinctFields, pArgs.getCollection()); LOG.trace("statement: {} {}", "findByQuery", statement); Mono allResult = pArgs.getScope() == null @@ -208,10 +204,10 @@ public QueryOptions buildOptions(QueryOptions options) { @Override public Mono count() { return Mono.defer(() -> { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); String statement = assembleEntityQuery(true, distinctFields, pArgs.getCollection()); LOG.trace("statement: {} {}", "findByQuery", statement); - Mono countResult = this.collection == null + Mono countResult = pArgs.getScope() == null ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, buildOptions(pArgs.getOptions())) : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, 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 d9a8ed608..179334ce0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -73,7 +73,7 @@ public Mono any(final String id) { if (garOptions.build().transcoder() == null) { garOptions.transcoder(RawJsonTranscoder.INSTANCE); } - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, garOptions, null); LOG.trace("statement: {} scope: {} collection: {}", "getAnyReplica", pArgs.getScope(), pArgs.getCollection()); return template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getCollection(pArgs.getCollection()) .reactive().getAnyReplica(docId, pArgs.getOptions()); @@ -101,14 +101,12 @@ public TerminatingFindFromReplicasById withOptions(final GetAnyReplicaOptions @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/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index 225a593d1..efff38f0b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -78,8 +78,7 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, - options != null ? options : InsertOptions.insertOptions()); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); LOG.trace("statement: {} scope: {} collection: {} options: {}", "insertById", pArgs.getScope(), pArgs.getCollection(), pArgs.getOptions()); return Mono.just(object).flatMap(support::encodeEntity) @@ -127,14 +126,12 @@ public TerminatingInsertById withOptions(final InsertOptions options) { @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 300203fb4..eb55622d3 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 @@ interface RemoveByIdInScope extends RemoveByIdInCollection, InScope { 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 8c7dcc51f..5b95ae2a9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -37,14 +37,21 @@ public ReactiveRemoveByIdOperationSupport(final ReactiveCouchbaseTemplate templa } @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; @@ -53,10 +60,11 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { 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; @@ -68,8 +76,7 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, - options != null ? options : RemoveOptions.removeOptions()); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); return Mono.just(id) .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).reactive().remove(id, buildRemoveOptions(pArgs.getOptions())) @@ -104,7 +111,7 @@ private RemoveOptions buildRemoveOptions(RemoveOptions options) { @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); } @@ -112,34 +119,32 @@ public RemoveByIdInCollection withDurability(final DurabilityLevel durabilityLev 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 a41bd7b91..b49ad3c27 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.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; @@ -45,8 +45,7 @@ public ReactiveRemoveByQueryOperationSupport(final ReactiveCouchbaseTemplate tem @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 { @@ -73,10 +72,10 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery @Override public Flux all() { return Flux.defer(() -> { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); String statement = assembleDeleteQuery(pArgs.getCollection()); LOG.trace("statement: {}", statement); - Mono allResult = pArgs.getCollection() == null + Mono allResult = pArgs.getScope() == null ? template.getCouchbaseClientFactory().getCluster().reactive().query(statement, buildQueryOptions(pArgs.getOptions())) : template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement, @@ -105,7 +104,6 @@ public TerminatingRemoveByQuery matching(final Query query) { @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 +134,6 @@ public RemoveByQueryWithQuery withOptions(final QueryOptions options) { @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 ccd043672..a7b4e579f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -78,8 +78,7 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, - options != null ? options : ReplaceOptions.replaceOptions()); + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); LOG.trace("statement: {} pArgs: {}", "replaceById", pArgs); return Mono.just(object).flatMap(support::encodeEntity).flatMap(converted -> template.getCouchbaseClientFactory() .withScope(pArgs.getScope()).getCollection(pArgs.getCollection()).reactive() @@ -126,14 +125,12 @@ public TerminatingReplaceById withOptions(final ReplaceOptions options) { @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/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index 4ae126860..24495e546 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -75,8 +75,7 @@ static class ReactiveUpsertByIdSupport implements ReactiveUpsertById { @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); return Mono.just(object).flatMap(support::encodeEntity) .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).reactive() @@ -121,14 +120,12 @@ public TerminatingUpsertById withOptions(final UpsertOptions options) { @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/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index 7793be558..306e8a470 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.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 void doWithPersistentProperty(final CouchbasePersistentProperty prop) { 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/Document.java b/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java index 87e93e03b..58d3734bb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java @@ -16,6 +16,9 @@ package org.springframework.data.couchbase.core.mapping; +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.Inherited; import java.lang.annotation.Retention; @@ -25,6 +28,8 @@ import org.springframework.data.annotation.Persistent; +import com.couchbase.client.java.query.QueryScanConsistency; + /** * Identifies a domain object to be persisted to Couchbase. * @@ -68,4 +73,18 @@ */ boolean touchOnRead() default false; + /** + * An optional string indicating the scope name + */ + String scope() default DEFAULT_SCOPE; + + /** + * An optional string indicating the collection name + */ + String collection() default DEFAULT_COLLECTION; + + /** + * An optional string indicating the query scan consistency + */ + 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 000000000..36ad58bb7 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/Expiry.java @@ -0,0 +1,65 @@ +/* + * 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; + +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; + +/** + * Identifies a domain object to be persisted to Couchbase. + * + * @author Michael Nitschinger + * @author Andrey Rubtsov + */ +@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/query/Query.java b/src/main/java/org/springframework/data/couchbase/core/query/Query.java index 62b55aca3..621ba63eb 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 @@ -343,6 +343,7 @@ public QueryOptions buildQueryOptions(QueryOptions options, QueryScanConsistency if (scanConsistency != null) { options.scanConsistency(scanConsistency); } + return options; } 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 6721f1215..168b1e27a 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,8 +15,10 @@ */ package org.springframework.data.couchbase.core.support; -import com.couchbase.client.core.io.CollectionIdentifier; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.mapping.Document; + +import com.couchbase.client.core.io.CollectionIdentifier; public class PseudoArgs { private final OPTS options; @@ -39,21 +41,50 @@ public PseudoArgs(String scopeName, String collectionName, OPTS options) { * @param scope * @param collection * @param options + * @param domainType */ - 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) + // ??? 1) template - values from the args (fluent api) [ or annotations??? ] - String scopeForQuery = scope; - String collectionForQuery = collection; - OPTS optionsForQuery = options; + String scopeForQuery = null; // = scope; + String collectionForQuery = null; // = collection; + OPTS optionsForQuery = null; // = options; + + // 2) repository from DynamicProxy via template threadLocal - has precedence over annotation + + PseudoArgs threadLocal = (PseudoArgs) template.getPseudoArgs(); + template.setPseudoArgs(null); + if (threadLocal != null) { // repository.withScope() + scopeForQuery = /* scopeForQuery != null ? scopeForQuery : */ threadLocal.getScope(); + collectionForQuery = /* collectionForQuery != null ? collectionForQuery : */ threadLocal.getCollection(); + optionsForQuery = /* optionsForQuery != null ? optionsForQuery : */ threadLocal.getOptions(); + } - // 2) from DynamicProxy via template threadLocal + if (scopeForQuery == null) { + if (scope != null /* && !CollectionIdentifier.DEFAULT_SCOPE.equals(scope) */) { // from withScope(scope) + scopeForQuery = scope; + } + } + if (collectionForQuery == null) { + if (collection != null /* && !CollectionIdentifier.DEFAULT_COLLECTION.equals(collection) */) { // withCollection(collection) + collectionForQuery = collection; + } + } + if (optionsForQuery == null) { + if (options != null) { + optionsForQuery = options; + } + } - scopeForQuery = scopeForQuery != null ? scopeForQuery : getThreadLocalScopeName(template); - collectionForQuery = collectionForQuery != null ? collectionForQuery : getThreadLocalCollectionName(template); - optionsForQuery = optionsForQuery != null ? optionsForQuery : getThreadLocalOptions(template); + if (scopeForQuery == null) { // from entity class + scopeForQuery = getScopeAnnotation(domainType); + } + if (collectionForQuery == null) { + collectionForQuery = getCollectionAnnotation(domainType); + } // if a collection was specified but no scope, use the scope from the clientFactory if (collectionForQuery != null && scopeForQuery == null) { @@ -62,15 +93,29 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle // 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; + } + if (collectionForQuery == null || !CollectionIdentifier.DEFAULT_COLLECTION.equals(collectionForQuery)) { // if + // collection + // isn't + // null, + // then + // (maybe) + // use + // template.getScope(), + // otherwise + // null + scopeForQuery = null; + } + } this.scopeName = scopeForQuery; this.collectionName = collectionForQuery; this.options = optionsForQuery; + } /** @@ -94,24 +139,33 @@ 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()); + public String getScopeAnnotation(Class domainType) { + // Document d = AnnotatedElementUtils.findMergedAnnotation(entityInformation.getJavaType(), Document.class); + if (domainType == null) { + return null; + } + Document documentAnnotation = domainType.getAnnotation(Document.class); + if (documentAnnotation != null && documentAnnotation.scope() != null + && !CollectionIdentifier.DEFAULT_SCOPE.equals(documentAnnotation.scope())) { + return documentAnnotation.scope(); + } + return null; } - /** - * @return the scope name from the ThreadLocal field of the template - */ - private String getThreadLocalScopeName(ReactiveCouchbaseTemplate template) { - return template.getPseudoArgs() == null ? null : template.getPseudoArgs().getScope(); + public String getCollectionAnnotation(Class domainType) { + if (domainType == null) { + return null; + } + Document documentAnnotation = domainType.getAnnotation(Document.class); + if (documentAnnotation != null && documentAnnotation.collection() != null + && !CollectionIdentifier.DEFAULT_COLLECTION.equals(documentAnnotation.collection())) { + return documentAnnotation.collection(); + } + return null; } - /** - * @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/CouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java index 61ded5449..f14bca46f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java @@ -50,4 +50,5 @@ public interface CouchbaseRepository extends PagingAndSortingRepository 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 index 6c3f616b1..c97f9b9ed 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java +++ b/src/main/java/org/springframework/data/couchbase/repository/DynamicProxyable.java @@ -29,9 +29,7 @@ * CouchbaseRepository, DynamicProxyable * * @param - * * @author Michael Reiche - * */ public interface DynamicProxyable { 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 91776b9a8..db41820f9 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java @@ -31,5 +31,5 @@ public interface ReactiveCouchbaseRepository extends ReactiveSortingRepository { ReactiveCouchbaseOperations getOperations(); - CouchbaseEntityInformation getEntityInformation(); + 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 810a9b666..961358d9a 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/ScanConsistency.java +++ b/src/main/java/org/springframework/data/couchbase/repository/ScanConsistency.java @@ -28,7 +28,6 @@ * Scan Consistency Annotation * * @author Michael Reiche - * */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE }) 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 862434737..39e704c08 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 @@ -35,7 +35,6 @@ 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; /** @@ -176,7 +175,11 @@ public boolean hasScanConsistencyAnnotation() { * @return the @ScanConsistency annotation */ public ScanConsistency getScanConsistencyAnnotation() { - return method.getAnnotation(ScanConsistency.class); + ScanConsistency sc = method.getAnnotation(ScanConsistency.class); + if (sc == null) { + sc = method.getDeclaringClass().getAnnotation(ScanConsistency.class); + } + return sc; } /** 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 d52a04b3d..b558bf35d 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 @@ protected Query createQuery(ParametersParameterAccessor accessor) { 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; 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 000000000..39bafa61c --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java @@ -0,0 +1,130 @@ +package org.springframework.data.couchbase.repository.support; + +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.repository.ScanConsistency; +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 entityScope = getJavaType().getAnnotation(Document.class) != null + ? getJavaType().getAnnotation(Document.class).scope() + : null; + String interfaceScope = repositoryInterface.getAnnotation(Document.class) != null + ? repositoryInterface.getAnnotation(Document.class).scope() + : null; + return entityScope != null && !CollectionIdentifier.DEFAULT_SCOPE.equals(entityScope) ? entityScope + : (interfaceScope != null && !interfaceScope.equals(CollectionIdentifier.DEFAULT_SCOPE) ? interfaceScope + : null); + } + + /** + * 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 entityCollection = getJavaType().getAnnotation(Document.class) != null + ? getJavaType().getAnnotation(Document.class).collection() + : null; + String interfaceCollection = repositoryInterface.getAnnotation(Document.class) != null + ? repositoryInterface.getAnnotation(Document.class).collection() + : null; + return entityCollection != null && !CollectionIdentifier.DEFAULT_COLLECTION.equals(entityCollection) + ? entityCollection + : (interfaceCollection != null && !interfaceCollection.equals(CollectionIdentifier.DEFAULT_COLLECTION) + ? interfaceCollection + : null); + } + + /** + * 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()
+ * TODO: Where can the annotation on an override in the repository interface of a method in
+ * 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() { + try { + QueryScanConsistency scanConsistency; + ScanConsistency sc = crudMethodMetadata.getScanConsistency(); + scanConsistency = sc != null ? sc.query() : null; + if (scanConsistency != null && scanConsistency != QueryScanConsistency.NOT_BOUNDED) { + return scanConsistency; + } + } catch (Exception e) { + e.printStackTrace(); + } + QueryScanConsistency entityConsistency = getJavaType().getAnnotation(ScanConsistency.class) != null + ? getJavaType().getAnnotation(ScanConsistency.class).query() + : null; + QueryScanConsistency interfaceCollection = repositoryInterface.getAnnotation(ScanConsistency.class) != null + ? repositoryInterface.getAnnotation(ScanConsistency.class).query() + : null; + return entityConsistency != null && entityConsistency != QueryScanConsistency.NOT_BOUNDED ? entityConsistency + : (interfaceCollection != null && interfaceCollection != QueryScanConsistency.NOT_BOUNDED ? interfaceCollection + : null); + } + + /** + * 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 f42d9ff4f..d8bff2d00 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 void setBeanClassLoader(ClassLoader classLoader) { * 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 @@ protected final Object getTargetRepository(final RepositoryInformation metadata) 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 CouchbaseQueryLookupStrategy(QueryMethodEvaluationContextProvider evaluat @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 710c77513..4bd288823 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,9 @@ public interface CrudMethodMetadata { */ ScanConsistency getScanConsistency(); + String getScope(); + + String getCollection(); + + Class repositoryInterface(); } 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 159a5782a..1581df85d 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 @@ -28,6 +28,7 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.NamedThreadLocal; +import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.repository.ScanConsistency; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; @@ -37,6 +38,8 @@ import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; +import com.couchbase.client.core.io.CollectionIdentifier; + /** * {@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 @@ -161,10 +164,10 @@ public Object invoke(MethodInvocation invocation) throws Throwable { try { return invocation.proceed(); } finally { - TransactionSynchronizationManager.unbindResource(method); + // TransactionSynchronizationManager.unbindResource(method); } } finally { - currentInvocation.set(oldInvocation); + // currentInvocation.set(oldInvocation); } } } @@ -179,6 +182,9 @@ private static class DefaultCrudMethodMetadata implements CrudMethodMetadata { private final Method method; private final ScanConsistency scanConsistency; + private String scope; + private String collection; + private Class repositoryInterface; /** * Creates a new {@link DefaultCrudMethodMetadata} for the given {@link Method}. @@ -190,12 +196,25 @@ private static class DefaultCrudMethodMetadata implements CrudMethodMetadata { this.method = method; ScanConsistency scanConsistency = null; + String scope = CollectionIdentifier.DEFAULT_SCOPE; + String collection = CollectionIdentifier.DEFAULT_COLLECTION; + for (Annotation ann : method.getDeclaringClass().getAnnotations()) { + if (ann instanceof ScanConsistency) { + scanConsistency = ((ScanConsistency) ann); + } else if (ann instanceof Document) { + scope = ((Document) ann).scope(); + collection = ((Document) ann).collection(); + } + } for (Annotation ann : method.getAnnotations()) { if (ann instanceof ScanConsistency) { scanConsistency = ((ScanConsistency) ann); } } + this.scanConsistency = scanConsistency; + this.scope = scope; + this.collection = collection; } /* @@ -211,6 +230,21 @@ public Method getMethod() { public ScanConsistency getScanConsistency() { return scanConsistency; } + + @Override + public String getScope() { + return scope; + } + + @Override + public String getCollection() { + return collection; + } + + @Override + public Class repositoryInterface() { + return repositoryInterface; + } } 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 index 9e50c7877..2f0375b4e 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java @@ -33,13 +33,11 @@ /** * Invocation Handler for scope/collection/options proxy for repositories * - * @param - * + * @param * @author Michael Reiche - * */ -public class DynamicInvocationHandler implements InvocationHandler { - final T1 target; +public class DynamicInvocationHandler implements InvocationHandler { + final T target; final Class repositoryClass; final CouchbaseEntityInformation entityInformation; final ReactiveCouchbaseTemplate reactiveTemplate; @@ -47,7 +45,7 @@ public class DynamicInvocationHandler implements InvocationHandler { String collection; String scope;; - public DynamicInvocationHandler(T1 target, 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(); @@ -108,17 +106,15 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl result = theMethod.invoke(target, args); } catch (InvocationTargetException ite) { throw ite.getCause(); - } finally { - clearThreadLocal(); } 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)); } - private void clearThreadLocal() { - reactiveTemplate.setPseudoArgs(null); - } } 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 6479c372b..273fb6568 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 @@ -111,7 +111,7 @@ protected final Object getTargetRepository(final RepositoryInformation metadata) .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; } 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 7636df13a..e2cc0338f 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 @@ -46,34 +46,25 @@ * @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 operations; - /** - * Contains information about the entity being used in this repository. - */ - private final CouchbaseEntityInformation entityInformation; - - private CrudMethodMetadata crudMethodMetadata; - /** * 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; + CouchbaseOperations couchbaseOperations, Class repositoryInterface) { + super(entityInformation, repositoryInterface); this.operations = couchbaseOperations; - } @Override @@ -83,9 +74,9 @@ public S save(S entity) { // if entity has non-null, non-zero version property, then replace() S result; if (hasNonZeroVersionProperty(entity, operations.getConverter())) { - result = (S) operations.replaceById(entityInformation.getJavaType()).one(entity); + result = (S) operations.replaceById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity); } else { - result = (S) operations.upsertById(entityInformation.getJavaType()).one(entity); + result = (S) operations.upsertById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity); } return result; } @@ -99,55 +90,61 @@ public Iterable saveAll(Iterable entities) { @Override public Optional findById(ID id) { Assert.notNull(id, "The given id must not be null!"); - return Optional.ofNullable(operations.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 = operations.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 operations.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!"); - operations.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!"); - operations.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!"); - operations.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!"); - operations.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 operations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()).count(); + return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) + .inCollection(getCollection()).count(); } @Override public void deleteAll() { - operations.removeByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()).all(); + operations.removeByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) + .inCollection(getCollection()).all(); } @Override @@ -171,21 +168,6 @@ public Page findAll(Pageable pageable) { return new PageImpl<>(results, pageable, count()); } - /** - * Returns the information for the underlying template. - * - * @return the underlying entity information. - */ - @Override - public CouchbaseEntityInformation getEntityInformation() { - return entityInformation; - } - - @Override - public CouchbaseOperations getOperations() { - return operations; - } - /** * Helper method to assemble a n1ql find all query, taking annotations into acocunt. * @@ -193,25 +175,13 @@ public CouchbaseOperations getOperations() { * @return the list of found entities, already executed. */ private List findAll(Query query) { - return operations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) - .matching(query).all(); - } - - private QueryScanConsistency buildQueryScanConsistency() { - QueryScanConsistency scanConsistency = QueryScanConsistency.NOT_BOUNDED; - if (crudMethodMetadata.getScanConsistency() != null) { - scanConsistency = crudMethodMetadata.getScanConsistency().query(); - } - return scanConsistency; + return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) + .inCollection(getCollection()).matching(query).all(); } - /** - * 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 b4bed1f1b..2aa7ac6e3 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 @@ -35,8 +35,6 @@ import org.springframework.data.util.Streamable; import org.springframework.util.Assert; -import com.couchbase.client.java.query.QueryScanConsistency; - /** * Reactive repository base implementation for Couchbase. * @@ -49,20 +47,14 @@ * @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. * @@ -70,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; } @@ -85,9 +74,11 @@ public Mono save(S entity) { // if entity has non-null, non-zero version property, then replace() Mono result; if (hasNonZeroVersionProperty(entity, operations.getConverter())) { - result = (Mono) operations.replaceById(entityInformation.getJavaType()).one(entity); + result = (Mono) operations.replaceById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .one(entity); } else { - result = (Mono) operations.upsertById(entityInformation.getJavaType()).one(entity); + result = (Mono) operations.upsertById(getJavaType()).inScope(getScope()).inCollection(getCollection()) + .one(entity); } return result; } @@ -100,7 +91,7 @@ public Flux findAll(Sort sort) { @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 @@ -111,7 +102,7 @@ public Flux saveAll(Publisher entityStream) { @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 @@ -123,7 +114,7 @@ public Mono findById(Publisher publisher) { @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 @@ -142,7 +133,8 @@ public Flux findAll() { 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 @@ -153,7 +145,8 @@ public Flux findAllById(Publisher entityStream) { @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 @@ -165,17 +158,20 @@ public Mono deleteById(Publisher publisher) { @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 @@ -186,23 +182,19 @@ public Mono deleteAll(Publisher entityStream) { @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(); + return operations.removeByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) + .inCollection(getCollection()).all().then(); } - /** - * Returns the information for the underlying template. - * - * @return the underlying entity information. - */ - @Override - public CouchbaseEntityInformation getEntityInformation() { - return entityInformation; + private Flux findAll(Query query) { + return operations.findByQuery(getJavaType()).withConsistency(buildQueryScanConsistency()).inScope(getScope()) + .inCollection(getCollection()).matching(query).all(); } @Override @@ -210,26 +202,4 @@ public ReactiveCouchbaseOperations getOperations() { return operations; } - private Flux findAll(Query query) { - return operations.findByQuery(entityInformation.getJavaType()).withConsistency(buildQueryScanConsistency()) - .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; - } - } 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 3a56b8ffb..586986afe 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.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.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 @@ public void upsertByIdOptions() { // 10 - options .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/domain/AbstractEntity.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java index 6b4c7aa95..f91b9d3f2 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java @@ -28,7 +28,7 @@ @Document public class AbstractEntity { - @Id @GeneratedValue(strategy = GenerationStrategy.UNIQUE) private UUID id; + @Id @GeneratedValue(strategy = GenerationStrategy.UNIQUE) private String id; public AbstractEntity() {} @@ -36,14 +36,14 @@ public AbstractEntity() {} * @return the id */ public UUID getId() { - return id; + return UUID.fromString(id); } /** * set the id */ public void setId(UUID id) { - this.id = id; + this.id = id.toString(); } /* diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airport.java b/src/test/java/org/springframework/data/couchbase/domain/Airport.java index e73a81804..bb7ecae21 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 @@ 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 Airport clearVersion() { 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 3531b7b47..0d99a3fc7 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -20,6 +20,7 @@ import java.util.Optional; import org.springframework.data.couchbase.core.RemoveResult; +import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.couchbase.repository.CouchbaseRepository; import org.springframework.data.couchbase.repository.DynamicProxyable; import org.springframework.data.couchbase.repository.Query; @@ -42,9 +43,11 @@ * @author Michael Reiche */ @Repository -@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) +@Document +// @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(); @@ -58,6 +61,8 @@ public interface AirportRepository extends CouchbaseRepository, @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") 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 1a6a6f468..e1ddf33c3 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -33,7 +33,9 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; +import org.springframework.data.couchbase.repository.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; @@ -48,7 +50,10 @@ */ @Configuration @EnableCouchbaseRepositories +@EnableReactiveCouchbaseRepositories @EnableCouchbaseAuditing(auditorAwareRef = "auditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") +@EnableReactiveCouchbaseAuditing(auditorAwareRef = "reactiveAuditorAwareRef", + dateTimeProviderRef = "dateTimeProviderRef") public class Config extends AbstractCouchbaseConfiguration { String bucketname = "travel-sample"; String username = "Administrator"; @@ -104,6 +109,11 @@ public NaiveAuditorAware testAuditorAware() { return new NaiveAuditorAware(); } + @Bean(name = "reactiveAuditorAwareRef") + public ReactiveNaiveAuditorAware testReactiveAuditorAware() { + return new ReactiveNaiveAuditorAware(); + } + @Bean(name = "dateTimeProviderRef") public DateTimeProvider testDateTimeProvider() { return new AuditingDateTimeProvider(); @@ -177,7 +187,7 @@ public MappingCouchbaseConverter mappingCouchbaseConverter() { } @Override - @Bean(name = "couchbaseMappingConverter") + @Bean(name = "mappingCouchbaseConverter") public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingContext couchbaseMappingContext, CouchbaseCustomConversions couchbaseCustomConversions) { // MappingCouchbaseConverter relies on a SimpleInformationMapper diff --git a/src/test/java/org/springframework/data/couchbase/domain/Person.java b/src/test/java/org/springframework/data/couchbase/domain/Person.java index df64cb829..5578d13ac 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Person.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Person.java @@ -29,8 +29,8 @@ @Document public class Person extends AbstractEntity { - Optional firstname; - @Nullable Optional lastname; + String firstname; + @Nullable String lastname; @CreatedBy private String creator; @@ -71,30 +71,30 @@ static String optional(String name, Optional obj) { return ""; } - public Optional getFirstname() { + public String getFirstname() { return firstname; } public void setFirstname(String firstname) { - this.firstname = firstname == null ? null : (Optional.ofNullable(firstname.equals("") ? null : firstname)); - } - - public void setFirstname(Optional firstname) { this.firstname = firstname; } - public Optional getLastname() { + // public void setFirstname(Optional firstname) { + // this.firstname = firstname; + // } + + public String getLastname() { return lastname; } public void setLastname(String lastname) { - this.lastname = lastname == null ? null : (Optional.ofNullable(lastname.equals("") ? null : lastname)); - } - - public void setLastname(Optional lastname) { this.lastname = lastname; } + // public void setLastname(String lastname) { + // this.lastname = lastname; + // } + public String getMiddlename() { return middlename; } 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 4fdcad520..23e1943d7 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 1c5175906..59426ec27 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 @@ * @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/ReactiveUserColRepository.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveUserColRepository.java new file mode 100644 index 000000000..0b5c742ea --- /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/test/java/org/springframework/data/couchbase/domain/UserCol.java b/src/test/java/org/springframework/data/couchbase/domain/UserCol.java new file mode 100644 index 000000000..241e6b277 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserCol.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.domain; + +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.couchbase.core.mapping.Document; + +/** + * User entity for tests + * + * @author Michael Nitschinger + * @author Michael Reiche + */ + +@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 000000000..e1bbaa370 --- /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 63293b335..18b334d06 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 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 @@ * @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 bd1f057d9..9945d820a 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -317,20 +317,25 @@ void findBySimplePropertyWithOptions() { 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 <= 10; i++) { + 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).parameters(positionalParams)) + airport2 = airportRepository + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.NOT_BOUNDED)) .iata(saved.getIata()); if (airport2 == null) { break; } } catch (DataRetrievalFailureException drfe) { - ; // was expecting this + airport2 = null; // } finally { // airportRepository.delete(vie); // instead of delete, use removeResult to test QueryOptions.consistentWith() @@ -346,13 +351,13 @@ void findBySimplePropertyWithOptions() { } } 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); 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 ed07c651c..a5f015d50 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 @@ void limitTest() { 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 @@ void deleteAll() { } } + @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 index 547b69057..b61230122 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -18,6 +18,8 @@ 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; @@ -26,18 +28,20 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.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.UserRepository; +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; @@ -45,11 +49,8 @@ @IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED) public class CouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests { - @Autowired CouchbaseClientFactory clientFactory; - @Autowired AirportRepository airportRepository; - - @Autowired UserRepository userRepository; + @Autowired UserColRepository userColRepository; @BeforeAll public static void beforeAll() { @@ -73,9 +74,11 @@ public void beforeEach() { 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 @@ -166,4 +169,44 @@ void findBySimplePropertyWithOptions() { } } + @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 000000000..8ffcac46d --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -0,0 +1,219 @@ +/* + * 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() { + + Airport vie = new Airport("airports::vie", "vie", "loww"); + try { + airportRepository = airportRepository.withCollection(collectionName); + Airport saved = airportRepository.save(vie).block(); + Airport airport2 = airportRepository.save(saved).block(); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + airportRepository.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 + airportRepository = airportRepository.withScope(scopeName).withCollection(collectionName); + try { + Airport saved = airportRepository.save(vie).block(); + + // valid scope, collection in options + Airport airport2 = airportRepository.withCollection(collectionName) + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + .iata(vie.getIata()).block(); + assertEquals(saved, airport2); + + // given bad collectionName in fluent + assertThrows(IndexFailureException.class, + () -> airportRepository.withCollection("bogusCollection").iata(vie.getIata()).block()); + + // given bad scopeName in fluent + assertThrows(IndexFailureException.class, + () -> airportRepository.withScope("bogusScope").iata(vie.getIata()).block()); + + Airport airport6 = airportRepository + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + .iata(vie.getIata()).block(); + assertEquals(saved, airport6); + + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + airportRepository.withScope(scopeName).withCollection(collectionName).deleteAll().block(); + } + } + + @Test + void findBySimplePropertyWithOptions() { + + Airport vie = new Airport("airports::vie", "vie", "loww"); + JsonArray positionalParams = JsonArray.create().add("\"this parameter will be overridden\""); + try { + Airport saved = airportRepository.withCollection(collectionName).save(vie).block(); + + Airport airport3 = airportRepository + .withCollection(collectionName).withOptions(QueryOptions.queryOptions() + .scanConsistency(QueryScanConsistency.REQUEST_PLUS).parameters(positionalParams)) + .iata(vie.getIata()).block(); + assertEquals(saved, airport3); + + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + airportRepository.withCollection(collectionName).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/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java b/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java index 1eda812a6..8ec65570b 100644 --- a/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java +++ b/src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java @@ -32,7 +32,6 @@ import java.util.Map; import java.util.Optional; import java.util.Properties; -import java.util.UUID; public class UnmanagedTestCluster extends TestCluster { @@ -60,7 +59,7 @@ ClusterType type() { @Override TestClusterConfig _start() throws Exception { - bucketname = UUID.randomUUID().toString(); + bucketname = "my_bucket"; // UUID.randomUUID().toString(); Response postResponse = httpClient .newCall(new Request.Builder().header("Authorization", Credentials.basic(adminUsername, adminPassword)) From 4f6b67845d4365b9638440a1947ce7478c0533e9 Mon Sep 17 00:00:00 2001 From: mikereiche Date: Thu, 8 Jul 2021 23:36:13 -0400 Subject: [PATCH 3/3] Scopes and Collections for Repositories. This adds scope and collection support to repositories via the DynamicInvocationHandler and PseudoArgs. It also adds annotations for scopes and collections. Closes #963 --- .../AbstractCouchbaseConfiguration.java | 1 - .../core/CouchbaseTemplateSupport.java | 5 +- .../ExecutableFindByIdOperationSupport.java | 2 +- ...ExecutableFindByQueryOperationSupport.java | 6 +- .../core/ExecutableRemoveByIdOperation.java | 2 + .../core/NonReactiveSupportWrapper.java | 4 +- .../ReactiveExistsByIdOperationSupport.java | 13 +- .../ReactiveFindByIdOperationSupport.java | 29 +- .../core/ReactiveFindByQueryOperation.java | 4 +- .../ReactiveFindByQueryOperationSupport.java | 117 +++-- ...eFindFromReplicasByIdOperationSupport.java | 20 +- .../core/ReactiveInsertByIdOperation.java | 3 - .../ReactiveInsertByIdOperationSupport.java | 20 +- .../ReactiveRemoveByIdOperationSupport.java | 18 +- ...ReactiveRemoveByQueryOperationSupport.java | 38 +- .../ReactiveReplaceByIdOperationSupport.java | 21 +- .../core/ReactiveTemplateSupport.java | 18 +- .../ReactiveUpsertByIdOperationSupport.java | 26 +- .../data/couchbase/core/TemplateSupport.java | 14 +- .../BasicCouchbasePersistentEntity.java | 12 +- .../data/couchbase/core/mapping/Document.java | 23 +- .../data/couchbase/core/mapping/Expiry.java | 5 +- .../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 | 63 +-- .../data/couchbase/repository/Collection.java | 46 ++ .../repository/CouchbaseRepository.java | 3 + .../data/couchbase/repository/Options.java | 117 +++++ .../ReactiveCouchbaseRepository.java | 1 + .../repository/{Meta.java => Scope.java} | 20 +- .../ReactiveCouchbaseAuditingRegistrar.java | 15 + .../query/AbstractCouchbaseQuery.java | 24 +- .../query/AbstractCouchbaseQueryBase.java | 16 +- .../query/AbstractReactiveCouchbaseQuery.java | 22 +- .../query/CouchbaseQueryMethod.java | 81 ++-- .../ReactiveStringBasedCouchbaseQuery.java | 2 +- .../query/StringBasedCouchbaseQuery.java | 2 +- .../query/StringBasedN1qlQueryParser.java | 10 +- .../support/CouchbaseRepositoryBase.java | 75 ++-- .../support/CrudMethodMetadata.java | 1 - .../CrudMethodMetadataPostProcessor.java | 64 +-- .../support/DynamicInvocationHandler.java | 16 +- ...hbaseTemplateKeyValueIntegrationTests.java | 6 +- .../core/CustomTypeKeyIntegrationTests.java | 3 +- ...hbaseTemplateKeyValueIntegrationTests.java | 3 +- .../couchbase/domain/AirportRepository.java | 56 ++- .../domain/ReactiveNaiveAuditorAware.java | 3 +- .../data/couchbase/domain/UserCol.java | 6 +- ...chbaseRepositoryQueryIntegrationTests.java | 18 +- ...aseRepositoryKeyValueIntegrationTests.java | 3 +- ...sitoryQueryCollectionIntegrationTests.java | 31 +- ...sitoryQueryCollectionIntegrationTests.java | 36 +- 53 files changed, 1121 insertions(+), 519 deletions(-) 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/Options.java rename src/main/java/org/springframework/data/couchbase/repository/{Meta.java => Scope.java} (66%) 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 225a98d0d..9d6437dac 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.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/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index 140f4d9c4..bf794b9ca 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.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/ExecutableFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java index 84db7050d..087c0cd39 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByIdOperationSupport.java @@ -87,7 +87,7 @@ public FindByIdInCollection inScope(final String scope) { @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 c9989c27f..48d77f02b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -127,8 +127,12 @@ public FindByQueryWithConsistency as(final Class returnType) { @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 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 c31bc9b05..085b451bd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperation.java @@ -39,11 +39,13 @@ public interface ExecutableRemoveByIdOperation { * Removes a document. */ ExecutableRemoveById removeById(Class domainType); + /** * Removes a document. */ @Deprecated ExecutableRemoveById removeById(); + /** * Terminating operations invoking the actual execution. */ 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 6f890a7e7..6e64ecb3b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -15,10 +15,10 @@ */ package org.springframework.data.couchbase.core; -import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import reactor.core.publisher.Mono; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; -import reactor.core.publisher.Mono; /** * Wrapper of {@link TemplateSupport} methods to adapt them to {@link ReactiveTemplateSupport}. 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 2fcbd0133..56b642820 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 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; @@ -70,12 +71,12 @@ static class ReactiveExistsByIdSupport implements ReactiveExistsById { @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, - options != null ? options : ExistsOptions.existsOptions(), domainType); - 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); @@ -85,6 +86,10 @@ public Mono one(final String id) { }); } + 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))) 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 b1600a498..0e722ff52 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 @@ 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 @@ static class ReactiveFindByIdSupport implements ReactiveFindById { @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, domainType); - 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) { @@ -123,7 +122,7 @@ public FindByIdInCollection inScope(final String scope) { @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 de056de02..c7a613b3d 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 @@ interface TerminatingFindByQuery extends OneAndAllReactive { */ Mono exists(); - QueryOptions buildOptions(QueryOptions options); - } /** @@ -170,7 +168,7 @@ interface FindByQueryConsistentWith extends FindByQueryInScope { } /** - * 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 7a5433ad0..d4b88e037 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,7 +61,7 @@ static class ReactiveFindByQuerySupport implements ReactiveFindByQuery { private final Query query; private final QueryScanConsistency scanConsistency; private final String collection; - private String scope; + private final String scope; private final String[] distinctFields; private final QueryOptions options; private final ReactiveTemplateSupport support; @@ -138,10 +137,14 @@ public FindByQueryWithConsistency as(Class returnType) { } @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 @@ -156,73 +159,65 @@ public Mono first() { @Override public Flux all() { - return Flux.defer(() -> { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, - options != null ? options : QueryOptions.queryOptions(), domainType); - 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, domainType); - String statement = assembleEntityQuery(true, distinctFields, pArgs.getCollection()); - LOG.trace("statement: {} {}", "findByQuery", 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 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 7a5983409..88cdcd613 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 @@ static class ReactiveFindFromReplicasByIdSupport implements ReactiveFindFromR @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, domainType); - 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); 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 b7886c1ad..3caf6047f 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 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 @@ interface TerminatingInsertById extends OneAndAllEntityReactive { @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 b30229d2d..7514e8baf 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 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 @@ 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; @@ -79,8 +80,7 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { @Override public Mono one(T object) { PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); - LOG.trace("statement: {} scope: {} collection: {} options: {}", "insertById", pArgs.getScope(), - pArgs.getCollection(), pArgs.getOptions()); + LOG.trace("insertById {}", pArgs); return Mono.just(object).flatMap(support::encodeEntity) .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).reactive() @@ -101,20 +101,8 @@ public Flux all(Collection objects) { 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 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 5b95ae2a9..b4f64ed72 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -20,6 +20,9 @@ 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; @@ -31,6 +34,7 @@ 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; @@ -76,7 +80,8 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, options, domainType); + 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())) @@ -96,16 +101,7 @@ public Flux all(final Collection ids) { } 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 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 e52a9ed87..efcd86583 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByQueryOperationSupport.java @@ -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) { @@ -71,26 +70,23 @@ static class ReactiveRemoveByQuerySupport implements ReactiveRemoveByQuery @Override public Flux all() { - return Flux.defer(() -> { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, - options != null ? options : QueryOptions.queryOptions(), domainType); - String statement = assembleDeleteQuery(pArgs.getCollection()); - LOG.trace("statement: {}", 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 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) { 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 a49939d3c..55aebd4dc 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 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 @@ 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; @@ -79,7 +80,7 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { @Override public Mono one(T object) { PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); - LOG.trace("statement: {} pArgs: {}", "replaceById", pArgs); + LOG.trace("replaceById {}", pArgs); return Mono.just(object).flatMap(support::encodeEntity) .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection()).reactive() @@ -101,20 +102,8 @@ public Flux all(Collection objects) { } 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 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 387045954..5f8977202 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 reactor.core.publisher.Mono; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; -import reactor.core.publisher.Mono; 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 4c7c59e30..b2433e10b 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 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 @@ 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,11 +79,12 @@ static class ReactiveUpsertByIdSupport implements ReactiveUpsertById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, options, domainType); + 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 -> { @@ -97,18 +102,7 @@ public Flux all(Collection objects) { } 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 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 372543c3f..bc42da3f8 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 @@ 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/mapping/BasicCouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java index cb82d2148..3aa10ca53 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.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 @@ protected CouchbasePersistentProperty returnPropertyIfBetterIdPropertyCandidateO @Override public int getExpiry() { - Document annotation = getType().getAnnotation(Document.class); + return getExpiry(AnnotatedElementUtils.findMergedAnnotation(getType(), Expiry.class), environment); + } + + public static int getExpiry(Expiry annotation, Environment environment) { if (annotation == null) return 0; - int expiryValue = getExpiryValue(annotation); + int expiryValue = getExpiryValue(annotation, environment); long secondsShift = annotation.expiryUnit().toSeconds(expiryValue); if (secondsShift > TTL_IN_SECONDS_INCLUSIVE_END) { @@ -121,7 +123,7 @@ public int getExpiry() { } } - private int getExpiryValue(Document annotation) { + private static int getExpiryValue(Expiry annotation, Environment environment) { int expiryValue = annotation.expiry(); String expiryExpressionString = annotation.expiryExpression(); if (StringUtils.hasLength(expiryExpressionString)) { diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java b/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java index 58d3734bb..98f2a3950 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java @@ -16,9 +16,6 @@ package org.springframework.data.couchbase.core.mapping; -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.Inherited; import java.lang.annotation.Retention; @@ -26,7 +23,11 @@ import java.lang.annotation.Target; import java.util.concurrent.TimeUnit; +import org.springframework.core.annotation.AliasFor; import org.springframework.data.annotation.Persistent; +import org.springframework.data.couchbase.repository.Collection; +import org.springframework.data.couchbase.repository.ScanConsistency; +import org.springframework.data.couchbase.repository.Scope; import com.couchbase.client.java.query.QueryScanConsistency; @@ -40,12 +41,15 @@ @Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE }) +@Expiry +@ScanConsistency public @interface Document { /** * 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()} */ + @AliasFor(annotation = Expiry.class, attribute = "expiry") int expiry() default 0; /** @@ -60,11 +64,13 @@ *
* 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; /** @@ -73,18 +79,9 @@ */ boolean touchOnRead() default false; - /** - * An optional string indicating the scope name - */ - String scope() default DEFAULT_SCOPE; - - /** - * An optional string indicating the collection name - */ - String collection() default DEFAULT_COLLECTION; - /** * 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 index 36ad58bb7..92ca7e0bb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/Expiry.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/Expiry.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. @@ -26,10 +26,9 @@ import org.springframework.data.annotation.Persistent; /** - * Identifies a domain object to be persisted to Couchbase. + * Expiry annotation * * @author Michael Nitschinger - * @author Andrey Rubtsov */ @Persistent @Inherited 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 26d165a0e..783838f36 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 @@ */ 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 @@ private enum MetaKey { } } - private final Map values = new LinkedHashMap<>(2); + private final Map values = new LinkedHashMap<>(2); public Meta() {} @@ -68,7 +69,7 @@ public boolean hasValues() { * * @return */ - public Iterable> values() { + public Iterable> values() { return Collections.unmodifiableSet(this.values.entrySet()); } @@ -78,10 +79,26 @@ public Iterable> values() { * @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 @@ void setValue(String key, @Nullable Object value) { @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 000000000..36cd9d355 --- /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 62b55aca3..d6d24c977 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.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 @@ StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTem * @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); + } + + /** + * 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 void setMeta(Meta metaAnnotation) { - Meta meta = metaAnnotation; + 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 d1e99cf7b..fb1bc9daa 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,8 +15,11 @@ */ package org.springframework.data.couchbase.core.support; +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 org.springframework.data.couchbase.core.mapping.Document; import com.couchbase.client.core.io.CollectionIdentifier; @@ -54,35 +57,16 @@ public PseudoArgs(ReactiveCouchbaseTemplate template, String scope, String colle PseudoArgs threadLocal = (PseudoArgs) template.getPseudoArgs(); template.setPseudoArgs(null); - if (threadLocal != null) { // repository.withScope() - scopeForQuery = /* scopeForQuery != null ? scopeForQuery : */ threadLocal.getScope(); - collectionForQuery = /* collectionForQuery != null ? collectionForQuery : */ threadLocal.getCollection(); - optionsForQuery = /* optionsForQuery != null ? optionsForQuery : */ threadLocal.getOptions(); + if (threadLocal != null) { + scopeForQuery = threadLocal.getScope(); + collectionForQuery = threadLocal.getCollection(); + optionsForQuery = threadLocal.getOptions(); } - if (scopeForQuery == null) { - if (scope != null) { // from calling operation - withScope(scope) - scopeForQuery = scope; - } - } - if (collectionForQuery == null) { - if (collection != null) { // from calling operation - withCollection(collection) - collectionForQuery = collection; - } - } - if (optionsForQuery == null) { // from calling operation - withOptions(options) - if (options != null) { - optionsForQuery = options; - } - } - - if (scopeForQuery == null) { // from annotation on entity class - scopeForQuery = getScopeAnnotation(domainType); - } + scopeForQuery = fromFirst(null, scopeForQuery, scope, getScopeFrom(domainType)); + collectionForQuery = fromFirst(null, collectionForQuery, collection, getCollectionFrom(domainType)); + optionsForQuery = fromFirst(null, options, optionsForQuery); - if (collectionForQuery == null) { // from annotation on entity class - collectionForQuery = getCollectionAnnotation(domainType); - } // if a collection was specified but no scope, use the scope from the clientFactory if (collectionForQuery != null && scopeForQuery == null) { @@ -125,31 +109,6 @@ public String getCollection() { return this.collectionName; } - public String getScopeAnnotation(Class domainType) { - // Document d = AnnotatedElementUtils.findMergedAnnotation(entityInformation.getJavaType(), Document.class); - if (domainType == null) { - return null; - } - Document documentAnnotation = domainType.getAnnotation(Document.class); - if (documentAnnotation != null && documentAnnotation.scope() != null - && !CollectionIdentifier.DEFAULT_SCOPE.equals(documentAnnotation.scope())) { - return documentAnnotation.scope(); - } - return null; - } - - public String getCollectionAnnotation(Class domainType) { - if (domainType == null) { - return null; - } - Document documentAnnotation = domainType.getAnnotation(Document.class); - if (documentAnnotation != null && documentAnnotation.collection() != null - && !CollectionIdentifier.DEFAULT_COLLECTION.equals(documentAnnotation.collection())) { - return documentAnnotation.collection(); - } - return null; - } - @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 000000000..6734902b1 --- /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 e820c29a9..f14bca46f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java @@ -19,6 +19,7 @@ import java.util.List; 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; @@ -46,6 +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/Options.java b/src/main/java/org/springframework/data/couchbase/repository/Options.java new file mode 100644 index 000000000..07054c10d --- /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 111c2573a..db41820f9 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepository.java @@ -31,4 +31,5 @@ public interface ReactiveCouchbaseRepository extends ReactiveSortingRepository { ReactiveCouchbaseOperations getOperations(); + CouchbaseEntityInformation getEntityInformation(); } 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 298379ee9..8c146b07e 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 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 088d1643c..25b22026a 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; 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 254407afe..9dc3fe517 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 @@ -59,10 +59,11 @@ public AbstractCouchbaseQuery(CouchbaseQueryMethod method, CouchbaseOperations o 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; } /** @@ -79,9 +80,8 @@ protected Object doExecute(CouchbaseQueryMethod method, ResultProcessor processo 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 = findOperationWithProjection; @@ -118,6 +118,8 @@ private CouchbaseQueryExecution getExecutionToWrap(ParameterAccessor accessor, E 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()) { @@ -135,18 +137,4 @@ private CouchbaseQueryExecution getExecutionToWrap(ParameterAccessor accessor, E } } - /** - * 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 9314371d1..b00101e5c 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 @@ abstract protected Object doExecute(CouchbaseQueryMethod method, ResultProcessor 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 ddb4a6a74..8b04d292c 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 @@ -62,7 +62,9 @@ public AbstractReactiveCouchbaseQuery(ReactiveCouchbaseQueryMethod method, React 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; } /** @@ -78,8 +80,8 @@ protected Object doExecute(CouchbaseQueryMethod method, ResultProcessor processo 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 = findOperationWithProjection; @@ -116,6 +118,8 @@ private ReactiveCouchbaseQueryExecution getExecutionToWrap(ParameterAccessor acc 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()) { @@ -128,18 +132,4 @@ private ReactiveCouchbaseQueryExecution getExecutionToWrap(ParameterAccessor acc } } - /** - * 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 39e704c08..b41419511 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,27 +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.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. @@ -49,13 +55,13 @@ 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; } /** @@ -175,43 +181,46 @@ public boolean hasScanConsistencyAnnotation() { * @return the @ScanConsistency annotation */ public ScanConsistency getScanConsistencyAnnotation() { - ScanConsistency sc = method.getAnnotation(ScanConsistency.class); - if (sc == null) { - sc = method.getDeclaringClass().getAnnotation(ScanConsistency.class); - } - return sc; + 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() { - - 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(); + public A getEntityAnnotation(Class annotationClass) { + return AnnotatedElementUtils.findMergedAnnotation(getEntityInformation().getJavaType(), annotationClass); + } - 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); } /** @@ -267,4 +276,18 @@ public boolean hasReactiveWrapperParameter() { 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 3296397a8..1f29b72d0 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 @@ protected Query createQuery(ParametersParameterAccessor accessor) { @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 b558bf35d..14799bd04 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 @@ -88,7 +88,7 @@ protected Query createQuery(ParametersParameterAccessor accessor) { @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 bba2ef488..3716ac26e 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 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 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 StringBasedN1qlQueryParser(String statement, CouchbaseQueryMethod queryMe 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 index 39bafa61c..992c4d0d0 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryBase.java @@ -1,7 +1,27 @@ +/* + * 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 org.springframework.data.couchbase.core.mapping.Document; +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; @@ -49,15 +69,10 @@ String getId(S entity) { */ String getScope() { - String entityScope = getJavaType().getAnnotation(Document.class) != null - ? getJavaType().getAnnotation(Document.class).scope() - : null; - String interfaceScope = repositoryInterface.getAnnotation(Document.class) != null - ? repositoryInterface.getAnnotation(Document.class).scope() - : null; - return entityScope != null && !CollectionIdentifier.DEFAULT_SCOPE.equals(entityScope) ? entityScope - : (interfaceScope != null && !interfaceScope.equals(CollectionIdentifier.DEFAULT_SCOPE) ? interfaceScope - : null); + 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); } /** @@ -69,17 +84,10 @@ String getScope() { * 1. repository.withCollection() */ String getCollection() { - String entityCollection = getJavaType().getAnnotation(Document.class) != null - ? getJavaType().getAnnotation(Document.class).collection() - : null; - String interfaceCollection = repositoryInterface.getAnnotation(Document.class) != null - ? repositoryInterface.getAnnotation(Document.class).collection() - : null; - return entityCollection != null && !CollectionIdentifier.DEFAULT_COLLECTION.equals(entityCollection) - ? entityCollection - : (interfaceCollection != null && !interfaceCollection.equals(CollectionIdentifier.DEFAULT_COLLECTION) - ? interfaceCollection - : null); + 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); } /** @@ -91,32 +99,17 @@ String getCollection() { * This can be overriden in the operation method by
* 1. Options.scanConsistency (?)
* AbstractCouchbaseQueryBase.applyAnnotatedConsistencyIfPresent()
- * TODO: Where can the annotation on an override in the repository interface of a method in
* 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() { - try { - QueryScanConsistency scanConsistency; - ScanConsistency sc = crudMethodMetadata.getScanConsistency(); - scanConsistency = sc != null ? sc.query() : null; - if (scanConsistency != null && scanConsistency != QueryScanConsistency.NOT_BOUNDED) { - return scanConsistency; - } - } catch (Exception e) { - e.printStackTrace(); - } - QueryScanConsistency entityConsistency = getJavaType().getAnnotation(ScanConsistency.class) != null - ? getJavaType().getAnnotation(ScanConsistency.class).query() - : null; - QueryScanConsistency interfaceCollection = repositoryInterface.getAnnotation(ScanConsistency.class) != null - ? repositoryInterface.getAnnotation(ScanConsistency.class).query() - : null; - return entityConsistency != null && entityConsistency != QueryScanConsistency.NOT_BOUNDED ? entityConsistency - : (interfaceCollection != null && interfaceCollection != QueryScanConsistency.NOT_BOUNDED ? interfaceCollection - : null); + 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); } /** 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 4bd288823..a15f17a84 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 @@ -35,5 +35,4 @@ public interface CrudMethodMetadata { String getCollection(); - Class repositoryInterface(); } 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 1581df85d..836db257f 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,8 +28,10 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.NamedThreadLocal; -import org.springframework.data.couchbase.core.mapping.Document; +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; @@ -39,6 +41,7 @@ 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. @@ -99,8 +102,10 @@ static class CrudMethodMetadataPopulatingMethodInterceptor implements MethodInte 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)); } @@ -151,7 +156,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable { if (methodMetadata == null) { - methodMetadata = new DefaultCrudMethodMetadata(method); + methodMetadata = new DefaultCrudMethodMetadata(method, repositoryInformation); CrudMethodMetadata tmp = metadataCache.putIfAbsent(method, methodMetadata); if (tmp != null) { @@ -182,39 +187,38 @@ private static class DefaultCrudMethodMetadata implements CrudMethodMetadata { private final Method method; private final ScanConsistency scanConsistency; - private String scope; - private String collection; - private Class repositoryInterface; + 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; - String scope = CollectionIdentifier.DEFAULT_SCOPE; - String collection = CollectionIdentifier.DEFAULT_COLLECTION; - for (Annotation ann : method.getDeclaringClass().getAnnotations()) { - if (ann instanceof ScanConsistency) { - scanConsistency = ((ScanConsistency) ann); - } else if (ann instanceof Document) { - scope = ((Document) ann).scope(); - collection = ((Document) ann).collection(); - } - } - 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; - this.scope = scope; - this.collection = collection; + 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); } /* @@ -241,10 +245,6 @@ public String getCollection() { return collection; } - @Override - public Class repositoryInterface() { - return repositoryInterface; - } } 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 index 00a95bbe9..dedba009d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DynamicInvocationHandler.java @@ -39,6 +39,7 @@ 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; @@ -49,18 +50,17 @@ public DynamicInvocationHandler(T target, CommonOptions options, String colle 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.entityInformation = ((CouchbaseRepositoryBase) target).getEntityInformation(); - this.options = options; this.collection = collection; this.scope = scope; this.repositoryClass = target.getClass(); - } @Override @@ -74,17 +74,17 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl */ if (method.getName().equals("withOptions")) { - return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), + return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), new DynamicInvocationHandler<>(target, (CommonOptions) args[0], collection, scope)); } if (method.getName().equals("withScope")) { - return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), + return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), new DynamicInvocationHandler<>(target, options, collection, (String) args[0])); } if (method.getName().equals("withCollection")) { - return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), + return Proxy.newProxyInstance(repositoryClass.getClassLoader(), target.getClass().getInterfaces(), new DynamicInvocationHandler<>(target, options, (String) args[0], scope)); } @@ -95,6 +95,10 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl 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); 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 c09801059..f98cbf7e5 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.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 com.couchbase.client.java.kv.PersistTo; import com.couchbase.client.java.kv.ReplicateTo; +import com.couchbase.client.java.query.QueryScanConsistency; ; @@ -121,7 +121,7 @@ void withDurability() 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 @@ void insertByIdwithDurability() { .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/CustomTypeKeyIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java index 655c4a32f..40adb9c10 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 d4a479719..4fc5e998b 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 @@ void withExpiryAndExpiryAnnotation() } // 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/AirportRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java index 0d99a3fc7..589c55ce7 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportRepository.java @@ -16,20 +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.Document; +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; /** @@ -43,7 +56,7 @@ * @author Michael Reiche */ @Repository -@Document +// @Scope("repositoryScope") // @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) public interface AirportRepository extends CouchbaseRepository, DynamicProxyable { @@ -56,6 +69,7 @@ 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) @@ -106,4 +120,42 @@ Long countFancyExpression(@Param("projectIds") List projectIds, @Param(" @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/ReactiveNaiveAuditorAware.java b/src/test/java/org/springframework/data/couchbase/domain/ReactiveNaiveAuditorAware.java index 0f3b08145..bcdf2677f 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 @@ 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/UserCol.java b/src/test/java/org/springframework/data/couchbase/domain/UserCol.java index 241e6b277..1c38d5695 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserCol.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserCol.java @@ -18,6 +18,8 @@ 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 @@ -26,7 +28,9 @@ * @author Michael Reiche */ -@Document(scope = "other_scope", collection = "other_collection") +@Document +@Scope("other_scope") +@Collection("other_collection") public class UserCol extends User { @PersistenceConstructor 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 9e9631113..5b5eda212 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -58,6 +58,7 @@ 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; @@ -194,9 +195,9 @@ void findBySimpleProperty() { vie = new Airport("airports::vie", "vie", "low6"); vie = airportRepository.save(vie); Airport airport2 = airportRepository - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.NOT_BOUNDED)) + .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) .findByIata(vie.getIata()); - assertEquals(airport2.getId(), vie.getId()); + assertEquals(airport2, vie); List airports = airportRepository.findAllByIata("vie"); assertEquals(1, airports.size()); @@ -365,12 +366,20 @@ public void testCas() { 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())); @@ -517,12 +526,9 @@ void deleteAllById() { 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(); 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 77e6fe2a4..81a6b077a 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 @@ void findByIdAudited() { 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/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java index b61230122..f522671e8 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -93,16 +93,16 @@ public void afterEach() { @Test public void myTest() { + AirportRepository ar = airportRepository.withScope(scopeName).withCollection(collectionName); Airport vie = new Airport("airports::vie", "vie", "loww"); try { - airportRepository = airportRepository.withCollection(collectionName); - Airport saved = airportRepository.save(vie); - Airport airport2 = airportRepository.save(saved); + Airport saved = ar.save(vie); + Airport airport2 = ar.save(saved); } catch (Exception e) { e.printStackTrace(); throw e; } finally { - airportRepository.delete(vie); + ar.delete(vie); } } @@ -118,25 +118,23 @@ void findBySimplePropertyWithCollection() { Airport vie = new Airport("airports::vie", "vie", "loww"); // create proxy with scope, collection - airportRepository = airportRepository.withScope(scopeName).withCollection(collectionName); + AirportRepository ar = airportRepository.withScope(scopeName).withCollection(collectionName); try { - Airport saved = airportRepository.save(vie); + Airport saved = ar.save(vie); // valid scope, collection in options - Airport airport2 = airportRepository.withCollection(collectionName) + 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, - () -> airportRepository.withCollection("bogusCollection").iata(vie.getIata())); + assertThrows(IndexFailureException.class, () -> ar.withCollection("bogusCollection").iata(vie.getIata())); // given bad scopeName in fluent - assertThrows(IndexFailureException.class, () -> airportRepository.withScope("bogusScope").iata(vie.getIata())); + assertThrows(IndexFailureException.class, () -> ar.withScope("bogusScope").iata(vie.getIata())); - Airport airport6 = airportRepository - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + Airport airport6 = ar.withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) .iata(vie.getIata()); assertEquals(saved, airport6); @@ -144,19 +142,20 @@ void findBySimplePropertyWithCollection() { e.printStackTrace(); throw e; } finally { - airportRepository.withScope(scopeName).withCollection(collectionName).deleteAll(); + 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 = airportRepository.withCollection(collectionName).save(vie); + Airport saved = ar.save(vie); - Airport airport3 = airportRepository.withCollection(collectionName).withOptions( + Airport airport3 = ar.withOptions( QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS).parameters(positionalParams)) .iata(vie.getIata()); assertEquals(saved, airport3); @@ -165,7 +164,7 @@ void findBySimplePropertyWithOptions() { e.printStackTrace(); throw e; } finally { - airportRepository.withCollection(collectionName).delete(vie); + ar.delete(vie); } } 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 index 8ffcac46d..51f6a5efe 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -94,16 +94,16 @@ public void afterEach() { @Test public void myTest() { + ReactiveAirportRepository ar = airportRepository.withScope(scopeName).withCollection(collectionName); Airport vie = new Airport("airports::vie", "vie", "loww"); try { - airportRepository = airportRepository.withCollection(collectionName); - Airport saved = airportRepository.save(vie).block(); - Airport airport2 = airportRepository.save(saved).block(); + Airport saved = ar.save(vie).block(); + Airport airport2 = ar.save(saved).block(); } catch (Exception e) { e.printStackTrace(); throw e; } finally { - airportRepository.delete(vie).block(); + ar.delete(vie).block(); } } @@ -119,26 +119,22 @@ void findBySimplePropertyWithCollection() { Airport vie = new Airport("airports::vie", "vie", "loww"); // create proxy with scope, collection - airportRepository = airportRepository.withScope(scopeName).withCollection(collectionName); + ReactiveAirportRepository ar = airportRepository.withScope(scopeName).withCollection(collectionName); try { - Airport saved = airportRepository.save(vie).block(); + Airport saved = ar.save(vie).block(); // valid scope, collection in options - Airport airport2 = airportRepository.withCollection(collectionName) - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + 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, - () -> airportRepository.withCollection("bogusCollection").iata(vie.getIata()).block()); + assertThrows(IndexFailureException.class, () -> ar.withCollection("bogusCollection").iata(vie.getIata()).block()); // given bad scopeName in fluent - assertThrows(IndexFailureException.class, - () -> airportRepository.withScope("bogusScope").iata(vie.getIata()).block()); + assertThrows(IndexFailureException.class, () -> ar.withScope("bogusScope").iata(vie.getIata()).block()); - Airport airport6 = airportRepository - .withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) + Airport airport6 = ar.withOptions(QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)) .iata(vie.getIata()).block(); assertEquals(saved, airport6); @@ -146,7 +142,7 @@ void findBySimplePropertyWithCollection() { e.printStackTrace(); throw e; } finally { - airportRepository.withScope(scopeName).withCollection(collectionName).deleteAll().block(); + ar.deleteAll().block(); } } @@ -154,13 +150,13 @@ void findBySimplePropertyWithCollection() { 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 = airportRepository.withCollection(collectionName).save(vie).block(); + Airport saved = ar.save(vie).block(); - Airport airport3 = airportRepository - .withCollection(collectionName).withOptions(QueryOptions.queryOptions() - .scanConsistency(QueryScanConsistency.REQUEST_PLUS).parameters(positionalParams)) + Airport airport3 = ar.withOptions( + QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS).parameters(positionalParams)) .iata(vie.getIata()).block(); assertEquals(saved, airport3); @@ -168,7 +164,7 @@ void findBySimplePropertyWithOptions() { e.printStackTrace(); throw e; } finally { - airportRepository.withCollection(collectionName).delete(vie).block(); + ar.delete(vie).block(); } }