diff --git a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java index 7152b7e91..fa31b9e04 100644 --- a/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/AbstractTemplateSupport.java @@ -15,11 +15,8 @@ */ package org.springframework.data.couchbase.core; -import java.lang.reflect.InaccessibleObjectException; -import java.util.Map; -import java.util.Set; - import com.couchbase.client.core.annotation.Stability; +import com.couchbase.client.core.error.CouchbaseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; @@ -29,7 +26,6 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; -import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.core.support.TemplateUtils; import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; @@ -39,13 +35,16 @@ import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.util.ClassUtils; -import com.couchbase.client.core.error.CouchbaseException; +import java.lang.reflect.InaccessibleObjectException; +import java.util.Map; +import java.util.Set; /** * Base shared by Reactive and non-Reactive TemplateSupport * * @author Michael Reiche + * @author Mico Piira */ @Stability.Internal public abstract class AbstractTemplateSupport { @@ -68,7 +67,7 @@ public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConv abstract ReactiveCouchbaseTemplate getReactiveTemplate(); public T decodeEntityBase(Object id, String source, Long cas, Class entityClass, String scope, - String collection, Object txResultHolder, CouchbaseResourceHolder holder) { + String collection, Object txResultHolder, CouchbaseResourceHolder holder, CouchbaseDocument converted) { // this is the entity class defined for the repository. It may not be the class of the document that was read // we will reset it after reading the document @@ -88,7 +87,6 @@ public T decodeEntityBase(Object id, String source, Long cas, Class entit // to unwrap. This results in List being unwrapped past String[] to String, so this may also be a // Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told. // if this is a Collection or array, only the first element will be returned. - final CouchbaseDocument converted = new CouchbaseDocument(id); Set> set = ((CouchbaseDocument) translationService.decode(source, converted)) .getContent().entrySet(); return (T) set.iterator().next().getValue(); @@ -99,8 +97,6 @@ public T decodeEntityBase(Object id, String source, Long cas, Class entit + TemplateUtils.SELECT_ID); } - final CouchbaseDocument converted = new CouchbaseDocument(id); - // if possible, set the version property in the source so that if the constructor has a long version argument, // it will have a value and not fail (as null is not a valid argument for a long argument). This possible failure // can be avoid by defining the argument as Long instead of long. @@ -148,7 +144,7 @@ CouchbasePersistentEntity couldBePersistentEntity(Class entityClass) { return null; } - public T applyResultBase(T entity, CouchbaseDocument converted, Object id, long cas, + public T applyResultBase(T entity, Object id, long cas, Object txResultHolder, CouchbaseResourceHolder holder) { ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); @@ -168,7 +164,6 @@ public T applyResultBase(T entity, CouchbaseDocument converted, Object id, l if (holder != null) { holder.transactionResultHolder(txResultHolder, (T) accessor.getBean()); } - maybeEmitEvent(new AfterSaveEvent(accessor.getBean(), converted)); return (T) accessor.getBean(); } 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 8714296df..18290da4e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -22,10 +22,7 @@ import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.event.AfterConvertCallback; -import org.springframework.data.couchbase.core.mapping.event.BeforeConvertCallback; -import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; -import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; +import org.springframework.data.couchbase.core.mapping.event.*; import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.util.Assert; @@ -37,6 +34,7 @@ * @author Michael Reiche * @author Jorge Rodriguez Martin * @author Carlos Espinaco + * @author Mico Piira * @since 3.0 */ class CouchbaseTemplateSupport extends AbstractTemplateSupport implements ApplicationContextAware, TemplateSupport { @@ -51,26 +49,30 @@ public CouchbaseTemplateSupport(final CouchbaseTemplate template, final Couchbas } @Override - public CouchbaseDocument encodeEntity(final Object entityToEncode) { + public EncodedEntity encodeEntity(final T entityToEncode) { maybeEmitEvent(new BeforeConvertEvent<>(entityToEncode)); Object maybeNewEntity = maybeCallBeforeConvert(entityToEncode, ""); final CouchbaseDocument converted = new CouchbaseDocument(); converter.write(maybeNewEntity, converted); - maybeCallAfterConvert(entityToEncode, converted, ""); maybeEmitEvent(new BeforeSaveEvent<>(entityToEncode, converted)); - return converted; + return new EncodedEntity<>(maybeCallBeforeSave(entityToEncode, converted, ""), converted); } @Override public T decodeEntity(Object id, String source, Long cas, Class entityClass, String scope, String collection, Object txHolder, CouchbaseResourceHolder holder) { - return decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder, holder); + CouchbaseDocument converted = new CouchbaseDocument(id); + T decoded = decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder, holder, converted); + maybeEmitEvent(new AfterConvertEvent<>(decoded, converted)); + return maybeCallAfterConvert(decoded, converted, ""); } @Override public T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, Object txResultHolder, CouchbaseResourceHolder holder) { - return applyResultBase(entity, converted, id, cas, txResultHolder, holder); + T applied = applyResultBase(entity, id, cas, txResultHolder, holder); + maybeEmitEvent(new AfterSaveEvent<>(applied, converted)); + return maybeCallAfterSave(applied, converted, ""); } @Override @@ -119,6 +121,24 @@ protected T maybeCallAfterConvert(T object, CouchbaseDocument document, Stri return object; } + protected T maybeCallAfterSave(T object, CouchbaseDocument document, String collection) { + if (null != entityCallbacks) { + return entityCallbacks.callback(AfterSaveCallback.class, object, document, collection); + } else { + LOG.info("maybeCallAfterSave called, but CouchbaseTemplate not initialized with applicationContext"); + } + return object; + } + + protected T maybeCallBeforeSave(T object, CouchbaseDocument document, String collection) { + if (null != entityCallbacks) { + return entityCallbacks.callback(BeforeSaveCallback.class, object, document, collection); + } else { + LOG.info("maybeCallBeforeSave called, but CouchbaseTemplate not initialized with applicationContext"); + } + return object; + } + @Override ReactiveCouchbaseTemplate getReactiveTemplate() { return template.reactive(); diff --git a/src/main/java/org/springframework/data/couchbase/core/EncodedEntity.java b/src/main/java/org/springframework/data/couchbase/core/EncodedEntity.java new file mode 100644 index 000000000..09f888919 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/EncodedEntity.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2023 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core; + +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; + +/** + * @author Mico Piira + */ +public record EncodedEntity(T entity, CouchbaseDocument document) { +} 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 4d90f591a..b824d5a35 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -26,6 +26,7 @@ * * @author Carlos Espinaco * @author Michael Reiche + * @author Mico Piira * @since 4.2 */ public class NonReactiveSupportWrapper implements ReactiveTemplateSupport { @@ -37,7 +38,7 @@ public NonReactiveSupportWrapper(TemplateSupport support) { } @Override - public Mono encodeEntity(Object entityToEncode) { + public Mono> encodeEntity(T entityToEncode) { return Mono.fromSupplier(() -> support.encodeEntity(entityToEncode)); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index 5b9843536..72b86aacc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -16,28 +16,25 @@ package org.springframework.data.couchbase.core; -import reactor.core.publisher.Mono; - import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; -import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; -import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterConvertCallback; -import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeConvertCallback; +import org.springframework.data.couchbase.core.mapping.event.*; import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; import org.springframework.util.Assert; +import reactor.core.publisher.Mono; /** * Internal encode/decode support for {@link ReactiveCouchbaseTemplate}. * * @author Carlos Espinaco * @author Michael Reiche + * @author Mico Piira * @since 4.2 */ class ReactiveCouchbaseTemplateSupport extends AbstractTemplateSupport @@ -53,14 +50,19 @@ public ReactiveCouchbaseTemplateSupport(final ReactiveCouchbaseTemplate template } @Override - public Mono encodeEntity(final Object entityToEncode) { - return Mono.just(entityToEncode).doOnNext(entity -> maybeEmitEvent(new BeforeConvertEvent<>(entity))) - .flatMap(entity -> maybeCallBeforeConvert(entity, "")).map(maybeNewEntity -> { + public Mono> encodeEntity(final T entityToEncode) { + maybeEmitEvent(new BeforeConvertEvent<>(entityToEncode)); + return maybeCallBeforeConvert(entityToEncode, "") + .map(maybeNewEntity -> { final CouchbaseDocument converted = new CouchbaseDocument(); converter.write(maybeNewEntity, converted); return converted; - }).flatMap(converted -> maybeCallAfterConvert(entityToEncode, converted, "").thenReturn(converted)) - .doOnNext(converted -> maybeEmitEvent(new BeforeSaveEvent<>(entityToEncode, converted))); + }) + .flatMap(converted -> { + maybeEmitEvent(new BeforeSaveEvent<>(entityToEncode, converted)); + return maybeCallBeforeSave(entityToEncode, converted, "") + .map(potentiallyModified -> new EncodedEntity<>(potentiallyModified, converted)); + }); } @Override @@ -71,14 +73,23 @@ ReactiveCouchbaseTemplate getReactiveTemplate() { @Override public Mono decodeEntity(Object id, String source, Long cas, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder) { + CouchbaseDocument converted = new CouchbaseDocument(id); return Mono - .fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder, holder)); + .fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder, holder, converted)) + .flatMap(entity -> { + maybeEmitEvent(new AfterConvertEvent<>(entity, converted)); + return maybeCallAfterConvert(entity, converted, ""); + }); } @Override public Mono applyResult(T entity, CouchbaseDocument converted, Object id, Long cas, Object txResultHolder, CouchbaseResourceHolder holder) { - return Mono.fromSupplier(() -> applyResultBase(entity, converted, id, cas, txResultHolder, holder)); + return Mono.fromSupplier(() -> applyResultBase(entity, id, cas, txResultHolder, holder)) + .flatMap(saved -> { + maybeEmitEvent(new AfterSaveEvent<>(saved, converted)); + return maybeCallAfterSave(saved, converted, ""); + }); } @Override @@ -104,6 +115,25 @@ public void setReactiveEntityCallbacks(ReactiveEntityCallbacks reactiveEntityCal this.reactiveEntityCallbacks = reactiveEntityCallbacks; } + protected Mono maybeCallBeforeSave(T object, CouchbaseDocument document, String collection) { + if (reactiveEntityCallbacks != null) { + return reactiveEntityCallbacks.callback(ReactiveBeforeSaveCallback.class, object, document, collection); + } else { + LOG.info("maybeCallBeforeSave called, but ReactiveCouchbaseTemplate not initialized with applicationContext"); + } + return Mono.just(object); + } + + protected Mono maybeCallAfterSave(T object, CouchbaseDocument document, String collection) { + if (reactiveEntityCallbacks != null) { + return reactiveEntityCallbacks.callback(ReactiveAfterSaveCallback.class, object, document, collection); + } else { + LOG.info("maybeCallAfterSave called, but ReactiveCouchbaseTemplate not initialized with applicationContext"); + } + return Mono.just(object); + } + + protected Mono maybeCallBeforeConvert(T object, String collection) { if (reactiveEntityCallbacks != null) { return reactiveEntityCallbacks.callback(ReactiveBeforeConvertCallback.class, object, collection); 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 c6dfb5924..bd0f868d3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -46,6 +46,7 @@ * * @author Michael Reiche * @author Tigran Babloyan + * @author Mico Piira */ public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOperation { @@ -102,12 +103,15 @@ public Mono one(T object) { return Mono .just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getCollection(pArgs.getCollection())) .flatMap(collection -> support.encodeEntity(object) - .flatMap(converted -> TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> { + .flatMap(encodedEntity -> TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> { + T potentiallyModified = encodedEntity.entity(); + CouchbaseDocument converted = encodedEntity.document(); + if (!ctxOpt.isPresent()) { return collection.reactive() .insert(converted.getId().toString(), converted.export(), buildOptions(pArgs.getOptions(), converted)) - .flatMap(result -> this.support.applyResult(object, converted, converted.getId(), result.cas(), + .flatMap(result -> this.support.applyResult(potentiallyModified, converted, converted.getId(), result.cas(), null, null)); } else { rejectInvalidTransactionalOptions(); @@ -120,7 +124,7 @@ public Mono one(T object) { template.getCouchbaseClientFactory().getCluster().environment().transcoder() .encode(converted.export()).encoded(), new SpanWrapper(span)) - .flatMap(result -> this.support.applyResult(object, converted, converted.getId(), result.cas(), + .flatMap(result -> this.support.applyResult(potentiallyModified, converted, converted.getId(), result.cas(), null, null)); } })).onErrorMap(throwable -> { diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java index 4d09819d7..54de9a5f3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java @@ -23,7 +23,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.CouchbaseList; import org.springframework.data.couchbase.core.query.OptionsBuilder; import org.springframework.data.couchbase.core.support.PseudoArgs; import org.springframework.util.Assert; @@ -37,6 +36,7 @@ * {@link ReactiveMutateInByIdOperation} implementations for Couchbase. * * @author Tigran Babloyan + * @author Mico Piira */ public class ReactiveMutateInByIdOperationSupport implements ReactiveMutateInByIdOperation { @@ -105,14 +105,16 @@ public Mono one(T object) { } Mono reactiveEntity = TransactionalSupport.verifyNotInTransaction("mutateInById") - .then(support.encodeEntity(object)).flatMap(converted -> { + .then(support.encodeEntity(object)).flatMap(encodedEntity -> { + T potentiallyModified = encodedEntity.entity(); + CouchbaseDocument converted = encodedEntity.document(); return Mono .just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection())) .flatMap(collection -> collection.reactive() - .mutateIn(converted.getId().toString(), getMutations(converted), buildMutateInOptions(pArgs.getOptions(), object, converted)) + .mutateIn(converted.getId().toString(), getMutations(converted), buildMutateInOptions(pArgs.getOptions(), potentiallyModified, converted)) .flatMap( - result -> support.applyResult(object, converted, converted.getId(), result.cas(), null, null))); + result -> support.applyResult(potentiallyModified, converted, converted.getId(), result.cas(), null, null))); }); return reactiveEntity.onErrorMap(throwable -> { 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 fa176074c..0c2f10195 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -49,6 +49,7 @@ * * @author Michael Reiche * @author Tigran Babloyan + * @author Mico Piira */ public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdOperation { @@ -105,20 +106,22 @@ public Mono one(T object) { return Mono .just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getCollection(pArgs.getCollection())) .flatMap(collection -> support.encodeEntity(object) - .flatMap(converted -> TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> { + .flatMap(encodedEntity -> TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> { + T potentiallyModified = encodedEntity.entity(); + CouchbaseDocument converted = encodedEntity.document(); if (!ctxOpt.isPresent()) { return collection.reactive() .replace(converted.getId().toString(), converted.export(), - buildReplaceOptions(pArgs.getOptions(), object, converted)) - .flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null, + buildReplaceOptions(pArgs.getOptions(), potentiallyModified, converted)) + .flatMap(result -> support.applyResult(potentiallyModified, converted, converted.getId(), result.cas(), null, null)); } else { rejectInvalidTransactionalOptions(); - Long cas = support.getCas(object); + Long cas = support.getCas(potentiallyModified); if (cas == null || cas == 0) { throw new IllegalArgumentException( - "cas must be supplied in object for tx replace. object=" + object); + "cas must be supplied in object for tx replace. object=" + potentiallyModified); } CollectionIdentifier collId = makeCollectionIdentifier(collection.async()); @@ -138,7 +141,7 @@ public Mono one(T object) { return ctx.replace(getResult, template.getCouchbaseClientFactory().getCluster().environment() .transcoder().encode(converted.export()).encoded(), new SpanWrapper(span)); }).flatMap( - result -> support.applyResult(object, converted, converted.getId(), result.cas(), null, null)); + result -> support.applyResult(potentiallyModified, converted, converted.getId(), result.cas(), null, null)); } })).onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { 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 a128ac09f..294aa3fb5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -15,20 +15,20 @@ */ package org.springframework.data.couchbase.core; -import reactor.core.publisher.Mono; - import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder; +import reactor.core.publisher.Mono; /** * ReactiveTemplateSupport * * @author Michael Reiche + * @author Mico Piira */ public interface ReactiveTemplateSupport { - Mono encodeEntity(Object entityToEncode); + Mono> encodeEntity(T entityToEncode); Mono decodeEntity(Object id, String source, Long cas, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder); 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 ea0c4e62e..61e6b2f64 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -38,6 +38,7 @@ * * @author Michael Reiche * @author Tigran Babloyan + * @author Mico Piira */ public class ReactiveUpsertByIdOperationSupport implements ReactiveUpsertByIdOperation { @@ -92,14 +93,16 @@ public Mono one(T object) { LOG.debug("upsertById object={} {}", object, pArgs); } Mono reactiveEntity = TransactionalSupport.verifyNotInTransaction("upsertById") - .then(support.encodeEntity(object)).flatMap(converted -> { + .then(support.encodeEntity(object)).flatMap(encodedEntity -> { + T potentiallyModified = encodedEntity.entity(); + CouchbaseDocument converted = encodedEntity.document(); return Mono .just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()) .getCollection(pArgs.getCollection())) .flatMap(collection -> collection.reactive() .upsert(converted.getId().toString(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted)) .flatMap( - result -> support.applyResult(object, converted, converted.getId(), result.cas(), null, null))); + result -> support.applyResult(potentiallyModified, converted, converted.getId(), result.cas(), null, null))); }); return reactiveEntity.onErrorMap(throwable -> { 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 8e176a815..b3b3a5c2f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -21,10 +21,11 @@ /** * @author Michael Reiche + * @author Mico Piira */ public interface TemplateSupport { - CouchbaseDocument encodeEntity(Object entityToEncode); + EncodedEntity encodeEntity(T entityToEncode); T decodeEntity(Object id, String source, Long cas, Class entityClass, String scope, String collection, Object txResultHolder, CouchbaseResourceHolder holder); diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterConvertCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterConvertCallback.java index a923790e4..7bc66fa0d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterConvertCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterConvertCallback.java @@ -19,23 +19,22 @@ import org.springframework.data.mapping.callback.EntityCallback; /** - * Callback being invoked after a domain object is materialized from a {@link CouchbaseDocument} when reading results. + * Callback being invoked after a domain object is materialized from a document when reading results. * * @author Michael Reiche + * @author Mico Piira * @see org.springframework.data.mapping.callback.EntityCallbacks * @since 4.2 */ @FunctionalInterface public interface AfterConvertCallback extends EntityCallback { - /** - * Entity callback method invoked after a domain object is materialized from a {@link CouchbaseDocument}. Can return - * either the same or a modified instance of the domain object. + * Entity callback method invoked after a domain object is materialized from a document. * * @param entity the domain object (the result of the conversion). - * @param document must not be {@literal null}. - * @param collection name of the collection. - * @return the domain object that is the result of reading it from the {@link CouchbaseDocument}. + * @param document must not be null. + * @param collection name of the document. + * @return the domain object that is the result of reading it from the document. */ T onAfterConvert(T entity, CouchbaseDocument document, String collection); } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterConvertEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterConvertEvent.java new file mode 100644 index 000000000..41fc9566c --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterConvertEvent.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core.mapping.event; + +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; + +/** + * @author Mico Piira + */ +public class AfterConvertEvent extends CouchbaseMappingEvent { + public AfterConvertEvent(E source, CouchbaseDocument document) { + super(source, document); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterSaveCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterSaveCallback.java new file mode 100644 index 000000000..33a341e2e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterSaveCallback.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core.mapping.event; + +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * Entity callback triggered after save of a CouchbaseDocument. + * + * @author Mico Piira + */ +@FunctionalInterface +public interface AfterSaveCallback extends EntityCallback { + /** + * Entity callback method invoked after a domain object is saved. Can return either the same or a modified instance of the domain object. + * + * @param entity the domain object that was saved. + * @param document CouchbaseDocument representing the entity. + * @param collection name of the collection. + * @return the domain object that was persisted. + */ + T onAfterSave(T entity, CouchbaseDocument document, String collection); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEntityCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEntityCallback.java deleted file mode 100644 index 5c82c0e95..000000000 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEntityCallback.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2012-2024 the original author or authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.couchbase.core.mapping.event; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.ObjectFactory; -import org.springframework.core.Ordered; -import org.springframework.data.auditing.AuditingHandler; -import org.springframework.data.auditing.IsNewAwareAuditingHandler; -import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.mapping.callback.EntityCallback; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.util.Assert; - -/** - * {@link EntityCallback} to populate auditing related fields on an entity about to be saved. - * - * @author Jorge Rodríguez Martín - * @since 4.2 - */ -public class AuditingEntityCallback implements BeforeConvertCallback, AfterConvertCallback, Ordered { - - private final ObjectFactory auditingHandlerFactory; - private static final Logger LOG = LoggerFactory.getLogger(AuditingEntityCallback.class); - - /** - * Creates a new {@link AuditingEntityCallback} using the given {@link MappingContext} and {@link AuditingHandler} - * provided by the given {@link ObjectFactory}. - * - * @param auditingHandlerFactory must not be {@literal null}. - */ - public AuditingEntityCallback(ObjectFactory auditingHandlerFactory) { - Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); - this.auditingHandlerFactory = auditingHandlerFactory; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.couchbase.core.mapping.event.BeforeConvertCallback#onBeforeConvert(java.lang.Object, java.lang.String) - */ - @Override - public Object onBeforeConvert(Object entity, String collection) { - // LOG.trace("onBeforeConvert " + entity); - return entity; // markAudited called in AuditingEventListener.onApplicationEvent() - // auditingHandlerFactory.getObject().markAudited(entity); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.couchbase.core.mapping.event.AfterConvertCallback#onAfterConvert(java.lang.Object, CouchbaseDocument, java.lang.String) - */ - @Override - public Object onAfterConvert(Object entity, CouchbaseDocument document, String collection) { - // LOG.trace("onAfterConvert " + document); - return entity; - } - - /* - * (non-Javadoc) - * @see org.springframework.core.Ordered#getOrder() - */ - @Override - public int getOrder() { - return 100; - } - -} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertCallback.java index f8cd1423a..3efa2a052 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertCallback.java @@ -22,15 +22,14 @@ * * @author Mark Paluch * @author Michael Reiche + * @author Mico Piira * @see org.springframework.data.mapping.callback.EntityCallbacks * @since 2.2 */ @FunctionalInterface public interface BeforeConvertCallback extends EntityCallback { - /** - * Entity callback method invoked before a domain object is converted to be persisted. Can return either the same or a - * modified instance of the domain object. + * Entity callback method invoked before a domain object is converted to be persisted. * * @param entity the domain object to save. * @param collection name of the collection. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeSaveCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeSaveCallback.java new file mode 100644 index 000000000..162e023be --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeSaveCallback.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core.mapping.event; + +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * Entity callback triggered before save of a CouchbaseDocument. + * + * @author Mico Piira + */ +@FunctionalInterface +public interface BeforeSaveCallback extends EntityCallback { + /** + * Entity callback method invoked before a domain object is saved. + * Can return either the same or a modified instance of the domain object and can modify CouchbaseDocument contents. + * This method is called after converting the entity to a CouchbaseDocument so effectively the document is used as + * outcome of invoking this callback. + * Changes to the domain object are not taken into account for saving, only changes to the document. + * Only transient fields of the entity should be changed in this callback. + * To change persistent the entity before being converted, use the {@link BeforeConvertCallback}. + * + * @param entity the domain object to save. + * @param document CouchbaseDocument representing the entity. + * @param collection name of the collection. + * @return the domain object to be persisted. + */ + T onBeforeSave(T entity, CouchbaseDocument document, String collection); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterConvertCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterConvertCallback.java index cacb59991..e90322e86 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterConvertCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterConvertCallback.java @@ -20,9 +20,10 @@ import org.springframework.data.mapping.callback.EntityCallback; /** - * Callback being invoked after a domain object is materialized from a {@link org.springframework.data.couchbase.core.mapping.CouchbaseDocument} when reading results. + * Callback being invoked after a domain object is materialized from a document when reading results. * * @author Jorge Rodríguez Martín + * @author Mico Piira * @see org.springframework.data.mapping.callback.EntityCallbacks * @since 4.2 */ @@ -30,11 +31,12 @@ public interface ReactiveAfterConvertCallback extends EntityCallback { /** - * Entity callback method invoked after a domain object is converted to be persisted. Can return - * either the same of a modified instance of the domain object. - * @param entity the domain object to save. - * @param collection name of the collection. - * @return a {@link Publisher} emitting the domain object to be persisted. + * Entity callback method invoked after a domain object is materialized from a document. + * + * @param entity the domain object (the result of the conversion). + * @param document must not be null. + * @param collection name of the document. + * @return the domain object that is the result of reading it from the document. */ Publisher onAfterConvert(T entity, CouchbaseDocument document, String collection); } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterSaveCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterSaveCallback.java new file mode 100644 index 000000000..68138170a --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterSaveCallback.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core.mapping.event; + +import org.reactivestreams.Publisher; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * Entity callback triggered after save of a CouchbaseDocument. + * + * @author Mico Piira + */ +@FunctionalInterface +public interface ReactiveAfterSaveCallback extends EntityCallback { + /** + * Entity callback method invoked after a domain object is saved. Can return either the same or a modified instance of the domain object. + * + * @param entity the domain object that was saved. + * @param document CouchbaseDocument representing the entity. + * @param collection name of the collection. + * @return the domain object that was persisted. + */ + Publisher onAfterSave(T entity, CouchbaseDocument document, String collection); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertCallback.java index ca6b61408..081ede271 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertCallback.java @@ -22,20 +22,18 @@ * Callback being invoked before a domain object is converted to be persisted. * * @author Jorge Rodríguez Martín + * @author Mico Piira * @see org.springframework.data.mapping.callback.ReactiveEntityCallbacks * @since 4.2 */ @FunctionalInterface public interface ReactiveBeforeConvertCallback extends EntityCallback { - /** - * Entity callback method invoked before a domain object is converted to be persisted. Can return - * either the same of a modified instance of the domain object. + * Entity callback method invoked before a domain object is converted to be persisted. * * @param entity the domain object to save. * @param collection name of the collection. - * @return a {@link Publisher} emitting the domain object to be persisted. + * @return the domain object to be persisted. */ Publisher onBeforeConvert(T entity, String collection); - } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeSaveCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeSaveCallback.java new file mode 100644 index 000000000..0ff1c73bc --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeSaveCallback.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core.mapping.event; + +import org.reactivestreams.Publisher; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * Entity callback triggered before save of a CouchbaseDocument. + * + * @author Mico Piira + */ +@FunctionalInterface +public interface ReactiveBeforeSaveCallback extends EntityCallback { + /** + * Entity callback method invoked before a domain object is saved. + * Can return either the same or a modified instance of the domain object and can modify CouchbaseDocument contents. + * This method is called after converting the entity to a CouchbaseDocument so effectively the document is used as + * outcome of invoking this callback. + * Changes to the domain object are not taken into account for saving, only changes to the document. + * Only transient fields of the entity should be changed in this callback. + * To change persistent the entity before being converted, use the {@link ReactiveBeforeConvertCallback}. + * + * @param entity the domain object to save. + * @param document CouchbaseDocument representing the entity. + * @param collection name of the collection. + * @return the domain object to be persisted. + */ + Publisher onBeforeSave(T entity, CouchbaseDocument document, String collection); +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java b/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java index 5e770f948..2a9877d8d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java +++ b/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java @@ -31,7 +31,6 @@ import org.springframework.data.config.ParsingUtils; import org.springframework.data.couchbase.config.BeanNames; import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; -import org.springframework.data.couchbase.core.mapping.event.AuditingEntityCallback; import org.springframework.data.couchbase.core.mapping.event.AuditingEventListener; import org.springframework.util.Assert; @@ -85,16 +84,6 @@ protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandle Assert.notNull(auditingHandlerDefinition, "BeanDefinition must not be null!"); Assert.notNull(registry, "BeanDefinitionRegistry must not be null!"); - // Register the AuditEntityCallback - - BeanDefinitionBuilder listenerBeanDefinitionBuilder = BeanDefinitionBuilder - .rootBeanDefinition(AuditingEntityCallback.class); - listenerBeanDefinitionBuilder - .addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); - - registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(), - AuditingEntityCallback.class.getName(), registry); - // Register the AuditingEventListener BeanDefinitionBuilder listenerBeanDefinitionBuilder2 = BeanDefinitionBuilder diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateCallbackIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateCallbackIntegrationTests.java new file mode 100644 index 000000000..4b42a240a --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateCallbackIntegrationTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2023 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.core.mapping.event.*; +import org.springframework.data.couchbase.core.mapping.id.GeneratedValue; +import org.springframework.data.couchbase.domain.Config; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import reactor.core.publisher.Mono; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Mico Piira + */ +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) +@SpringJUnitConfig({Config.class, CouchbaseTemplateCallbackIntegrationTests.Callbacks.class}) +@DirtiesContext +class CouchbaseTemplateCallbackIntegrationTests extends JavaIntegrationTests { + + @Autowired ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + @Autowired CouchbaseTemplate couchbaseTemplate; + + static class Callbacks { + + @Bean + ReactiveBeforeConvertCallback reactiveBeforeConvertCallback() { + return (entity, collection) -> Mono.just(new CallbacksTestEntity(entity.id(), entity.name() + "_beforeconvert")); + } + + @Bean + ReactiveAfterSaveCallback reactiveAfterSaveCallback() { + return (entity, document, collection) -> Mono.just(new CallbacksTestEntity(entity.id(), entity.name() + "_aftersave")); + } + + @Bean + ReactiveBeforeSaveCallback reactiveBeforeSaveCallback() { + return (entity, document, collection) -> + Mono.fromCallable(() -> document.put("name", document.get("name") + "_beforesave")) + .thenReturn(new CallbacksTestEntity(entity.id(), entity.name() + "_beforesave2")); + } + + @Bean + ReactiveAfterConvertCallback reactiveAfterConvertCallback() { + return (entity, document, collection) -> Mono.just(new CallbacksTestEntity(entity.id(), entity.name() + "_afterconvert")); + } + + @Bean + BeforeConvertCallback BeforeConvertCallback() { + return (entity, collection) -> new CouchbaseTemplateCallbackIntegrationTests.CallbacksTestEntity(entity.id(), entity.name() + "_beforeconvert"); + } + + @Bean + AfterSaveCallback AfterSaveCallback() { + return (entity, document, collection) -> new CouchbaseTemplateCallbackIntegrationTests.CallbacksTestEntity(entity.id(), entity.name() + "_aftersave"); + } + + @Bean + BeforeSaveCallback BeforeSaveCallback() { + return (entity, document, collection) -> { + document.put("name", document.get("name") + "_beforesave"); + return new CouchbaseTemplateCallbackIntegrationTests.CallbacksTestEntity(entity.id(), entity.name() + "_beforesave2"); + }; + } + + @Bean + AfterConvertCallback AfterConvertCallback() { + return (entity, document, collection) -> new CouchbaseTemplateCallbackIntegrationTests.CallbacksTestEntity(entity.id(), entity.name() + "_afterconvert"); + } + + } + + public record CallbacksTestEntity(@Id @GeneratedValue String id, String name) { } + + @Test + void testReactiveCallbacks() { + CallbacksTestEntity entity = new CallbacksTestEntity(UUID.randomUUID().toString(), "a"); + CallbacksTestEntity saved = reactiveCouchbaseTemplate.insertById(CallbacksTestEntity.class) + .one(entity) + .block(); + assertEquals("a_beforesave2_aftersave", saved.name()); + CallbacksTestEntity block = reactiveCouchbaseTemplate.findById(CallbacksTestEntity.class).one(entity.id()).block(); + assertEquals("a_beforeconvert_beforesave_afterconvert", block.name()); + } + + @Test + void testBlockingCallbacks() { + CouchbaseTemplateCallbackIntegrationTests.CallbacksTestEntity entity = new CouchbaseTemplateCallbackIntegrationTests.CallbacksTestEntity(UUID.randomUUID().toString(), "a"); + CouchbaseTemplateCallbackIntegrationTests.CallbacksTestEntity saved = couchbaseTemplate.insertById(CouchbaseTemplateCallbackIntegrationTests.CallbacksTestEntity.class) + .one(entity); + assertEquals("a_beforesave2_aftersave", saved.name()); + CouchbaseTemplateCallbackIntegrationTests.CallbacksTestEntity block = couchbaseTemplate.findById(CouchbaseTemplateCallbackIntegrationTests.CallbacksTestEntity.class).one(entity.id()); + assertEquals("a_beforeconvert_beforesave_afterconvert", block.name()); + } + +}