From b311c6f5e106783ec0faf80bf2ce7603227f467a Mon Sep 17 00:00:00 2001 From: mikereiche Date: Fri, 28 May 2021 16:04:50 -0400 Subject: [PATCH] Add missing hooks for callbacks. 1) the previous change (#955) to CouchbaseAuditingRegister intended to register AuditingEntityCallback instead of AuditingEventListener but only changed the name that AuditingEventListener was registered with. Since AuditingEventListener does exactly the same thing as AuditingEntityCallback, it behaved as those AuditingEntityCallback was being used. 2) It seems that the two mechanisms - EntityCallback and EventListener both do more-or-less the same thing. I'm not sure why there are two mechanisms to do the same thing. 3) Although the ReactiveAuditingEntityCallback mechanism is triggered only for reactive operations and the non-reactive AuditingEntityCallback is triggered only for non-reactive operations, the events are triggered for both. Since there was a desire to have distinct callbacks for reactive/non-reactive operations, I also made distinct events for reactive/non-reactive operations. However both the reactive and non-reactive event mechanisms are triggered for either reactive and non-reactive operations, so it is necessary for the mechanism to determine if it should act on the event. 4) Although there is an AbstractCouchbaseEventListener that is extended by LoggingEventListener and ValidatingEventListener, AuditingEventListener does not extend it (maybe it should). Closes #1074. --- .../core/CouchbaseTemplateSupport.java | 14 +- .../core/NonReactiveSupportWrapper.java | 10 +- .../ReactiveCouchbaseTemplateSupport.java | 47 +-- .../ReactiveInsertByIdOperationSupport.java | 16 +- .../ReactiveRemoveByIdOperationSupport.java | 31 +- .../ReactiveReplaceByIdOperationSupport.java | 16 +- .../core/ReactiveTemplateSupport.java | 7 +- .../ReactiveUpsertByIdOperationSupport.java | 12 +- .../data/couchbase/core/TemplateSupport.java | 7 +- .../mapping/event/AfterConvertCallback.java | 18 +- .../mapping/event/AuditingEntityCallback.java | 23 +- .../mapping/event/AuditingEventListener.java | 37 ++- .../event/ReactiveAfterDeleteEvent.java | 28 ++ .../mapping/event/ReactiveAfterSaveEvent.java | 30 ++ .../event/ReactiveAuditingEntityCallback.java | 29 +- .../event/ReactiveAuditingEventListener.java | 87 +++++ .../event/ReactiveBeforeConvertEvent.java | 28 ++ .../event/ReactiveBeforeDeleteEvent.java | 28 ++ .../event/ReactiveBeforeSaveEvent.java | 30 ++ .../auditing/CouchbaseAuditingRegistrar.java | 17 +- .../ReactiveCouchbaseAuditingRegistrar.java | 29 +- ...hbaseTemplateKeyValueIntegrationTests.java | 3 - ...hbaseTemplateKeyValueIntegrationTests.java | 299 ++++++++++++++++++ .../data/couchbase/domain/Config.java | 12 +- .../couchbase/domain/NaiveAuditorAware.java | 3 +- .../domain/ReactiveNaiveAuditorAware.java | 3 +- ...chbaseRepositoryQueryIntegrationTests.java | 2 +- ...aseRepositoryKeyValueIntegrationTests.java | 2 +- 28 files changed, 759 insertions(+), 109 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterDeleteEvent.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterSaveEvent.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEventListener.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertEvent.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeDeleteEvent.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeSaveEvent.java create mode 100644 src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java 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 711377770..140f4d9c4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -26,6 +26,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.mapping.event.AfterConvertCallback; +import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; 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; @@ -95,16 +96,21 @@ public T decodeEntity(String id, String source, long cas, Class entityCla } @Override - public Object applyUpdatedCas(final Object entity, final long cas) { + public Object applyUpdatedCas(final Object entity, CouchbaseDocument converted, final long cas) { + Object returnValue; final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); if (versionProperty != null) { accessor.setProperty(versionProperty, cas); - return accessor.getBean(); + returnValue = accessor.getBean(); + } else { + returnValue = entity; } - return entity; + maybeEmitEvent(new AfterSaveEvent(returnValue, converted)); + + return returnValue; } @Override @@ -172,7 +178,7 @@ public void setEntityCallbacks(EntityCallbacks entityCallbacks) { this.entityCallbacks = entityCallbacks; } - void maybeEmitEvent(CouchbaseMappingEvent event) { + public void maybeEmitEvent(CouchbaseMappingEvent event) { if (canPublishEvent()) { try { this.applicationContext.publishEvent(event); 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 adf0d5709..6f890a7e7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -17,6 +17,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import reactor.core.publisher.Mono; /** @@ -44,8 +45,8 @@ public Mono decodeEntity(String id, String source, long cas, Class ent } @Override - public Mono applyUpdatedCas(Object entity, long cas) { - return Mono.fromSupplier(() -> support.applyUpdatedCas(entity, cas)); + public Mono applyUpdatedCas(Object entity, CouchbaseDocument converted, long cas) { + return Mono.fromSupplier(() -> support.applyUpdatedCas(entity, converted, cas)); } @Override @@ -62,4 +63,9 @@ public Long getCas(Object entity) { public String getJavaNameForEntity(Class clazz) { return support.getJavaNameForEntity(clazz); } + + @Override + public void maybeEmitEvent(CouchbaseMappingEvent event) { + support.maybeEmitEvent(event); + } } 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 d83b09ef2..d12393707 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -16,6 +16,10 @@ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; +import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterSaveEvent; +import reactor.core.publisher.Mono; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; @@ -26,11 +30,11 @@ 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.BeforeConvertEvent; -import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterConvertCallback; import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeConvertCallback; +import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeConvertEvent; +import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeSaveEvent; import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.callback.EntityCallbacks; @@ -38,7 +42,6 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.util.Assert; -import reactor.core.publisher.Mono; /** * Internal encode/decode support for {@link ReactiveCouchbaseTemplate}. @@ -65,16 +68,13 @@ public ReactiveCouchbaseTemplateSupport(final CouchbaseConverter converter, @Override public Mono encodeEntity(final Object entityToEncode) { - return Mono.just(entityToEncode) - .doOnNext(entity -> maybeEmitEvent(new BeforeConvertEvent<>(entity))) - .flatMap(entity -> maybeCallBeforeConvert(entity, "")) - .map(maybeNewEntity -> { + return Mono.just(entityToEncode).doOnNext(entity -> maybeEmitEvent(new ReactiveBeforeConvertEvent<>(entity))) + .flatMap(entity -> maybeCallBeforeConvert(entity, "")).map(maybeNewEntity -> { final CouchbaseDocument converted = new CouchbaseDocument(); converter.write(maybeNewEntity, converted); return converted; - }) - .flatMap(converted -> maybeCallAfterConvert(entityToEncode, converted, "").thenReturn(converted)) - .doOnNext(converted -> maybeEmitEvent(new BeforeSaveEvent<>(entityToEncode, converted))); + }).flatMap(converted -> maybeCallAfterConvert(entityToEncode, converted, "").thenReturn(converted)) + .doOnNext(converted -> maybeEmitEvent(new ReactiveBeforeSaveEvent<>(entityToEncode, converted))); } @Override @@ -98,17 +98,22 @@ public Mono decodeEntity(String id, String source, long cas, Class ent } @Override - public Mono applyUpdatedCas(final Object entity, final long cas) { + public Mono applyUpdatedCas(final Object entity, CouchbaseDocument converted, final long cas) { return Mono.fromSupplier(() -> { + Object returnValue; final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); + final CouchbasePersistentEntity persistentEntity = mappingContext + .getRequiredPersistentEntity(entity.getClass()); final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); if (versionProperty != null) { accessor.setProperty(versionProperty, cas); - return accessor.getBean(); + returnValue = accessor.getBean(); + } else { + returnValue = entity; } - return entity; + maybeEmitEvent(new ReactiveAfterSaveEvent(returnValue, converted)); + return returnValue; }); } @@ -116,7 +121,8 @@ public Mono applyUpdatedCas(final Object entity, final long cas) { public Mono applyUpdatedId(final Object entity, Object id) { return Mono.fromSupplier(() -> { final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); + final CouchbasePersistentEntity persistentEntity = mappingContext + .getRequiredPersistentEntity(entity.getClass()); final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty(); if (idProperty != null) { @@ -130,8 +136,7 @@ public Mono applyUpdatedId(final Object entity, Object id) { @Override public Long getCas(final Object entity) { final ConvertingPropertyAccessor accessor = getPropertyAccessor(entity); - final CouchbasePersistentEntity persistentEntity = mappingContext - .getRequiredPersistentEntity(entity.getClass()); + final CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass()); final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); long cas = 0; @@ -166,9 +171,9 @@ public void setApplicationContext(ApplicationContext applicationContext) throws } /** - * Set the {@link ReactiveEntityCallbacks} instance to use when invoking {@link - * org.springframework.data.mapping.callback.ReactiveEntityCallbacks callbacks} like the {@link - * ReactiveBeforeConvertCallback}. + * Set the {@link ReactiveEntityCallbacks} instance to use when invoking + * {@link org.springframework.data.mapping.callback.ReactiveEntityCallbacks callbacks} like the + * {@link ReactiveBeforeConvertCallback}. *

* Overrides potentially existing {@link EntityCallbacks}. * @@ -180,7 +185,7 @@ public void setReactiveEntityCallbacks(ReactiveEntityCallbacks reactiveEntityCal this.reactiveEntityCallbacks = reactiveEntityCallbacks; } - void maybeEmitEvent(CouchbaseMappingEvent event) { + public void maybeEmitEvent(CouchbaseMappingEvent event) { if (canPublishEvent()) { try { this.applicationContext.publishEvent(event); 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..53a047943 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -78,16 +78,16 @@ static class ReactiveInsertByIdSupport implements ReactiveInsertById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, - options != null ? options : InsertOptions.insertOptions()); - LOG.trace("statement: {} scope: {} collection: {} options: {}", "insertById", pArgs.getScope(), - pArgs.getCollection(), pArgs.getOptions()); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, + options != null ? options : InsertOptions.insertOptions()); + LOG.trace("statement: {} scope: {} collection: {} options: {}", "insertById", pArgs.getScope(), + pArgs.getCollection(), pArgs.getOptions()); return Mono.just(object).flatMap(support::encodeEntity) - .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive() - .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) + .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive() + .insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted)) .flatMap(result -> support.applyUpdatedId(object, converted.getId()) - .flatMap(insertedObject -> (Mono) support.applyUpdatedCas(insertedObject, result.cas())))) + .flatMap(updatedObject -> support.applyUpdatedCas(updatedObject, converted, result.cas())))) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); 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..a337c39e6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -15,6 +15,8 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterDeleteEvent; +import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeDeleteEvent; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -68,19 +70,22 @@ static class ReactiveRemoveByIdSupport implements ReactiveRemoveById { @Override public Mono one(final String id) { - PseudoArgs pArgs = new PseudoArgs(template, scope, collection, - options != null ? options : RemoveOptions.removeOptions()); - return Mono.just(id) - .flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) - .getCollection(pArgs.getCollection()).reactive().remove(id, buildRemoveOptions(pArgs.getOptions())) - .map(r -> RemoveResult.from(docId, r))) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }); + PseudoArgs pArgs = new PseudoArgs(template, scope, collection, + options != null ? options : RemoveOptions.removeOptions()); + return Mono.just(id).map(r -> { + template.support().maybeEmitEvent(new ReactiveBeforeDeleteEvent<>(r)); + return r; + }).flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive().remove(id, buildRemoveOptions(pArgs.getOptions())).map(r -> { + template.support().maybeEmitEvent(new ReactiveAfterDeleteEvent<>(r)); + return RemoveResult.from(docId, r); + })).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }); } @Override 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..bca6ecd09 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -78,14 +78,14 @@ static class ReactiveReplaceByIdSupport implements ReactiveReplaceById { @Override public Mono one(T object) { - PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, - options != null ? options : ReplaceOptions.replaceOptions()); - LOG.trace("statement: {} pArgs: {}", "replaceById", pArgs); - return Mono.just(object).flatMap(support::encodeEntity).flatMap(converted -> template.getCouchbaseClientFactory() - .withScope(pArgs.getScope()).getCollection(pArgs.getCollection()).reactive() - .replace(converted.getId(), converted.export(), buildReplaceOptions(pArgs.getOptions(), object, converted)) - .flatMap(result -> support.applyUpdatedId(object, converted.getId()) - .flatMap(replacedObject -> (Mono) support.applyUpdatedCas(replacedObject, result.cas())))) + PseudoArgs pArgs = new PseudoArgs<>(template, scope, collection, + options != null ? options : ReplaceOptions.replaceOptions()); + LOG.trace("statement: {} pArgs: {}", "replaceById", pArgs); + return Mono.just(object).flatMap(support::encodeEntity) + .flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope()) + .getCollection(pArgs.getCollection()).reactive() + .replace(converted.getId(), converted.export(), buildReplaceOptions(pArgs.getOptions(), object, converted)) + .flatMap(result -> support.applyUpdatedCas(object, converted, result.cas()))) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) throwable); 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 d5585d1a7..387045954 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -17,6 +17,7 @@ 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 { @@ -25,11 +26,13 @@ public interface ReactiveTemplateSupport { Mono decodeEntity(String id, String source, long cas, Class entityClass); - Mono applyUpdatedCas(Object entity, long cas); + Mono applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); - Mono applyUpdatedId(Object entity, Object id); + Mono applyUpdatedId(T entity, Object id); Long getCas(Object entity); String getJavaNameForEntity(Class clazz); + + 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 4ae126860..2c923b5da 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -75,14 +75,14 @@ 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 != null ? options : UpsertOptions.upsertOptions()); 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 -> (Mono) support.applyUpdatedCas(updatedObject, result.cas())))) + .flatMap(updatedObject -> support.applyUpdatedCas(updatedObject, converted, result.cas())))) .onErrorMap(throwable -> { if (throwable instanceof RuntimeException) { return template.potentiallyConvertRuntimeException((RuntimeException) 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 f9029a08b..372543c3f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.core; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; public interface TemplateSupport { @@ -23,11 +24,13 @@ public interface TemplateSupport { T decodeEntity(String id, String source, long cas, Class entityClass); - Object applyUpdatedCas(Object entity, long cas); + T applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); - Object applyUpdatedId(Object entity, Object id); + T applyUpdatedId(T entity, Object id); long getCas(Object entity); String getJavaNameForEntity(Class clazz); + + void maybeEmitEvent(CouchbaseMappingEvent event); } 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 8c8976ae9..ec84d93f2 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 @@ -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,29 +15,27 @@ */ package org.springframework.data.couchbase.core.mapping.event; -import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.mapping.callback.EntityCallback; /** - * Callback being invoked after a domain object is materialized from a {@link Document} when reading results. + * Callback being invoked after a domain object is materialized from a {@link CouchbaseDocument} when reading results. * - * @author Roman Puchkovskiy - * @author Mark Paluch * @author Michael Reiche * @see org.springframework.data.mapping.callback.EntityCallbacks - * @since 3.0 + * @since 4.2 */ @FunctionalInterface public interface AfterConvertCallback extends EntityCallback { /** - * Entity callback method invoked after a domain object is materialized from a {@link Document}. 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 {@link CouchbaseDocument}. Can return + * either the same or a modified instance of the domain object. * * @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 Document}. + * @return the domain object that is the result of reading it from the {@link CouchbaseDocument}. */ - T onAfterConvert(T entity, Document document, String collection); + T onAfterConvert(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 index 08c0489fa..acde35146 100644 --- 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 @@ -15,24 +15,27 @@ */ 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, Ordered { +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} @@ -41,7 +44,6 @@ public class AuditingEntityCallback implements BeforeConvertCallback, Or * @param auditingHandlerFactory must not be {@literal null}. */ public AuditingEntityCallback(ObjectFactory auditingHandlerFactory) { - Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); this.auditingHandlerFactory = auditingHandlerFactory; } @@ -52,7 +54,19 @@ public AuditingEntityCallback(ObjectFactory auditingH */ @Override public Object onBeforeConvert(Object entity, String collection) { - return auditingHandlerFactory.getObject().markAudited(entity); + //LOG.debug("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.debug("onAfterConvert " + document); + return entity; } /* @@ -64,5 +78,4 @@ public int getOrder() { return 100; } - } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java index d5ecffa7f..0ee5aaabf 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AuditingEventListener.java @@ -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. @@ -18,6 +18,8 @@ import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.ObjectFactory; import org.springframework.context.ApplicationListener; import org.springframework.data.auditing.AuditingHandler; @@ -33,7 +35,7 @@ * @author Mark Paluch * @author Michael Reiche */ -public class AuditingEventListener implements ApplicationListener> { +public class AuditingEventListener implements ApplicationListener> { private final ObjectFactory auditingHandlerFactory; @@ -41,9 +43,11 @@ public AuditingEventListener() { this.auditingHandlerFactory = null; } + private static final Logger LOG = LoggerFactory.getLogger(AuditingEventListener.class); + /** * Creates a new {@link AuditingEventListener} using the given {@link MappingContext} and {@link AuditingHandler} - * provided by the given {@link ObjectFactory}. + * provided by the given {@link ObjectFactory}. Registered in CouchbaseAuditingRegistrar * * @param auditingHandlerFactory must not be {@literal null}. */ @@ -57,8 +61,29 @@ public AuditingEventListener(ObjectFactory auditingHa * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) */ @Override - public void onApplicationEvent(BeforeConvertEvent event) { - Optional.ofNullable(event.getSource())// - .ifPresent(it -> auditingHandlerFactory.getObject().markAudited(it)); + public void onApplicationEvent(CouchbaseMappingEvent event) { + if (event instanceof BeforeConvertEvent) { + Optional.ofNullable(event.getSource())// + .ifPresent(it -> auditingHandlerFactory.getObject().markAudited(it)); + // LOG.info(event.getClass().getSimpleName() + " " + event); + } + if (event instanceof BeforeSaveEvent) { + // LOG.info(event.getClass().getSimpleName() + " " + event); + } + if (event instanceof AfterSaveEvent) { + // LOG.info(event.getClass().getSimpleName() + " " + event); + } + if (event instanceof BeforeDeleteEvent) { + // LOG.info(event.getClass().getSimpleName() + " " + event); + } + if (event instanceof AfterDeleteEvent) { + // LOG.info(event.getClass().getSimpleName() + " " + event); + } + if (!event.getClass().getSimpleName().startsWith("Reactive")) { + if (LOG.isDebugEnabled()) { + LOG.debug(event.getClass().getSimpleName() + " " + event.getSource()); + } + } } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterDeleteEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterDeleteEvent.java new file mode 100644 index 000000000..52c798940 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterDeleteEvent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.mapping.event; + +/** + * @author Michael Reiche + */ +public class ReactiveAfterDeleteEvent extends CouchbaseMappingEvent { + + public ReactiveAfterDeleteEvent(E source) { + super(source, null); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterSaveEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterSaveEvent.java new file mode 100644 index 000000000..3c0a97a32 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAfterSaveEvent.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2020 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.mapping.event; + +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; + +/** + * @author Michael Reiche + */ +public class ReactiveAfterSaveEvent extends CouchbaseMappingEvent { + + public ReactiveAfterSaveEvent(E source, CouchbaseDocument document) { + super(source, document); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEntityCallback.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEntityCallback.java index 1939a5d09..5f2348daf 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEntityCallback.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEntityCallback.java @@ -15,27 +15,33 @@ */ package org.springframework.data.couchbase.core.mapping.event; +import reactor.core.publisher.Mono; + import org.reactivestreams.Publisher; +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.ReactiveIsNewAwareAuditingHandler; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.mapping.callback.EntityCallback; import org.springframework.data.mapping.context.MappingContext; import org.springframework.util.Assert; /** - * Reactive {@link EntityCallback} to populate auditing related fields on an entity about to be - * saved. Based on ReactiveAfterConvertCallback + * Reactive {@link EntityCallback} to populate auditing related fields on an entity about to be saved. Based on ReactiveAfterConvertCallback * * @author Jorge Rodríguez Martín + * @authoer Michael Reiche * @since 4.2 */ -public class ReactiveAuditingEntityCallback implements ReactiveBeforeConvertCallback, Ordered { +public class ReactiveAuditingEntityCallback + implements ReactiveBeforeConvertCallback, ReactiveAfterConvertCallback, Ordered { private final ObjectFactory auditingHandlerFactory; + private static final Logger LOG = LoggerFactory.getLogger(ReactiveAuditingEntityCallback.class); /** * Creates a new {@link ReactiveAuditingEntityCallback} using the given {@link MappingContext} and @@ -44,7 +50,7 @@ public class ReactiveAuditingEntityCallback implements ReactiveBeforeConvertCall * @param auditingHandlerFactory must not be {@literal null}. */ public ReactiveAuditingEntityCallback(final ObjectFactory auditingHandlerFactory) { - Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); + Assert.notNull(auditingHandlerFactory, "ReactiveIsNewAwareAuditingHandler must not be null!"); this.auditingHandlerFactory = auditingHandlerFactory; } @@ -57,9 +63,22 @@ public ReactiveAuditingEntityCallback(final ObjectFactory onBeforeConvert(final Object entity, final String collection) { + LOG.debug("onBeforeConvert " + entity.toString()); return this.auditingHandlerFactory.getObject().markAudited(entity); } + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.couchbase.core.mapping.event.ReactiveAfterConvertCallback#onAfterConvert + * (java.lang.Object, CouchbaseDocument, java.lang.String) + */ + @Override + public Publisher onAfterConvert(Object entity, CouchbaseDocument document, String collection) { + LOG.debug("onAfterConvert " + document.toString()); + return Mono.just(entity); + } /* * (non-Javadoc) * diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEventListener.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEventListener.java new file mode 100644 index 000000000..7d516dd5e --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveAuditingEventListener.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.mapping.event; + +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.util.Assert; + +/** + * Reactive Event listener to populate auditing related fields on an entity about to be saved. + * + * @author Michael Reiche + */ +public class ReactiveAuditingEventListener implements ApplicationListener> { + + private final ObjectFactory auditingHandlerFactory; + + public ReactiveAuditingEventListener() { + this.auditingHandlerFactory = null; + } + + private static final Logger LOG = LoggerFactory.getLogger(ReactiveAuditingEventListener.class); + + /** + * Creates a new {@link ReactiveAuditingEventListener} using the given {@link MappingContext} and + * {@link AuditingHandler} provided by the given {@link ObjectFactory}. Registered in CouchbaseAuditingRegistrar + * + * @param auditingHandlerFactory must not be {@literal null}. + */ + public ReactiveAuditingEventListener(ObjectFactory auditingHandlerFactory) { + Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); + this.auditingHandlerFactory = auditingHandlerFactory; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) + */ + @Override + public void onApplicationEvent(CouchbaseMappingEvent event) { + if (event instanceof ReactiveBeforeConvertEvent) { + Optional.ofNullable(event.getSource())// + .ifPresent(it -> auditingHandlerFactory.getObject().markAudited(it)); + // LOG.info(event.getClass().getSimpleName() + " " + event); + } + if (event instanceof ReactiveBeforeSaveEvent) { + // LOG.info(event.getClass().getSimpleName() + " " + event); + } + if (event instanceof ReactiveAfterSaveEvent) { + // LOG.info(event.getClass().getSimpleName() + " " + event); + } + if (event instanceof ReactiveBeforeDeleteEvent) { + // LOG.info(event.getClass().getSimpleName() + " " + event); + } + if (event instanceof ReactiveAfterDeleteEvent) { + // LOG.info(event.getClass().getSimpleName() + " " + event); + } + if (event.getClass().getSimpleName().startsWith("Reactive")) { + if (LOG.isDebugEnabled()) { + LOG.debug(event.getClass().getSimpleName() + " " + event.getSource()); + } + } + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertEvent.java new file mode 100644 index 000000000..8f3ff6c30 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeConvertEvent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.mapping.event; + +/** + * @author Michael Reiche + */ +public class ReactiveBeforeConvertEvent extends CouchbaseMappingEvent { + + public ReactiveBeforeConvertEvent(E source) { + super(source, null); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeDeleteEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeDeleteEvent.java new file mode 100644 index 000000000..161c2a45d --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeDeleteEvent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.mapping.event; + +/** + * @author Michael Reiche + */ +public class ReactiveBeforeDeleteEvent extends CouchbaseMappingEvent { + + public ReactiveBeforeDeleteEvent(E source) { + super(source, null); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeSaveEvent.java b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeSaveEvent.java new file mode 100644 index 000000000..b96e32b65 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/ReactiveBeforeSaveEvent.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.mapping.event; + +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; + +/** + * @author Michael Reiche + */ +public class ReactiveBeforeSaveEvent extends CouchbaseMappingEvent { + + public ReactiveBeforeSaveEvent(E source, CouchbaseDocument document) { + super(source, document); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java b/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java index 902150d3e..7120205ce 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 @@ -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. @@ -80,13 +80,26 @@ 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(AuditingEventListener.class); + .rootBeanDefinition(AuditingEntityCallback.class); listenerBeanDefinitionBuilder .addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(), AuditingEntityCallback.class.getName(), registry); + + // Register the AuditingEventListener + + BeanDefinitionBuilder listenerBeanDefinitionBuilder2 = BeanDefinitionBuilder + .rootBeanDefinition(AuditingEventListener.class); + listenerBeanDefinitionBuilder2 + .addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); + + registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder2.getBeanDefinition(), + AuditingEventListener.class.getName(), registry); + } private void ensureMappingContext(BeanDefinitionRegistry registry, Object source) { 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 5f71197d7..a38cbcbfa 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,8 +1,9 @@ package org.springframework.data.couchbase.repository.auditing; -import static org.springframework.data.couchbase.config.BeanNames.*; +import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_AUDITING_HANDLER; import java.lang.annotation.Annotation; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -12,14 +13,16 @@ import org.springframework.data.auditing.config.AuditingConfiguration; import org.springframework.data.config.ParsingUtils; import org.springframework.data.couchbase.core.mapping.event.ReactiveAuditingEntityCallback; +import org.springframework.data.couchbase.core.mapping.event.ReactiveAuditingEventListener; import org.springframework.util.Assert; - /** - * A support registrar that allows to set up reactive auditing for Couchbase (including {@link org.springframework.data.auditing.ReactiveAuditingHandler} and { - * IsNewStrategyFactory} set up). See {@link EnableReactiveCouchbaseAuditing} for the associated annotation. + * A support registrar that allows to set up reactive auditing for Couchbase (including + * {@link org.springframework.data.auditing.ReactiveAuditingHandler} and { IsNewStrategyFactory} set up). See + * {@link EnableReactiveCouchbaseAuditing} for the associated annotation. * * @author Jorge Rodríguez Martín + * @author Michael Reiche * @since 4.2 */ class ReactiveCouchbaseAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { @@ -69,13 +72,27 @@ protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandle Assert.notNull(auditingHandlerDefinition, "BeanDefinition must not be null!"); Assert.notNull(registry, "BeanDefinitionRegistry must not be null!"); + // Register the AuditingEntityCallback + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEntityCallback.class); builder.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); builder.getRawBeanDefinition().setSource(auditingHandlerDefinition.getSource()); - registerInfrastructureBeanWithId(builder.getBeanDefinition(), - ReactiveAuditingEntityCallback.class.getName(), registry); + registerInfrastructureBeanWithId(builder.getBeanDefinition(), ReactiveAuditingEntityCallback.class.getName(), + registry); + + // Register the AuditingEventListener + + BeanDefinitionBuilder builder2 = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEventListener.class); + + builder2 + .addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); + builder.getRawBeanDefinition().setSource(auditingHandlerDefinition.getSource()); + + registerInfrastructureBeanWithId(builder2.getBeanDefinition(), ReactiveAuditingEventListener.class.getName(), + registry); + } } diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java index f2d3fb819..c09801059 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -89,7 +89,6 @@ void upsertAndFindById() { assertEquals(user, found); couchbaseTemplate.removeById().one(user.getId()); - reactiveCouchbaseTemplate.replaceById(User.class).withDurability(PersistTo.ACTIVE, ReplicateTo.THREE).one(user); } @Test @@ -280,9 +279,7 @@ void existsById() { User user = new User(id, "firstname", "lastname"); User inserted = couchbaseTemplate.insertById(User.class).one(user); assertEquals(user, inserted); - assertTrue(couchbaseTemplate.existsById().one(id)); - } @Test diff --git a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java new file mode 100644 index 000000000..d4a479719 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java @@ -0,0 +1,299 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +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.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.time.Duration; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.couchbase.core.ExecutableRemoveByIdOperation.ExecutableRemoveById; +import org.springframework.data.couchbase.core.support.OneAndAllEntityReactive; +import org.springframework.data.couchbase.domain.PersonValue; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserAnnotated; +import org.springframework.data.couchbase.domain.UserAnnotated2; +import org.springframework.data.couchbase.domain.UserAnnotated3; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.couchbase.util.JavaIntegrationTests; + +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.ReplicateTo; + +/** + * KV tests Theses tests rely on a cb server running. + * + * @author Michael Nitschinger + * @author Michael Reiche + */ +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) +class ReactiveCouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { + + @BeforeEach + @Override + public void beforeEach() { + super.beforeEach(); + reactiveCouchbaseTemplate.removeByQuery(User.class).all(); + reactiveCouchbaseTemplate.removeByQuery(UserAnnotated.class).all(); + reactiveCouchbaseTemplate.removeByQuery(UserAnnotated2.class).all(); + } + + @Test + void upsertAndFindById() { + User user = new User(UUID.randomUUID().toString(), "firstname", "lastname"); + User modified = reactiveCouchbaseTemplate.upsertById(User.class).one(user).block(); + assertEquals(user, modified); + + modified = reactiveCouchbaseTemplate.replaceById(User.class).one(user).block(); + assertEquals(user, modified); + + user.setVersion(12345678); + assertThrows(DataIntegrityViolationException.class, + () -> reactiveCouchbaseTemplate.replaceById(User.class).one(user).block()); + + User found = reactiveCouchbaseTemplate.findById(User.class).one(user.getId()).block(); + user.setVersion(found.getVersion()); + assertEquals(user, found); + + reactiveCouchbaseTemplate.removeById().one(user.getId()).block(); + } + + @Test + void withDurability() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + Class clazz = User.class; // for now, just User.class. There is no Durability annotation. + // insert, replace, upsert + for (OneAndAllEntityReactive operator : new OneAndAllEntityReactive[] { + reactiveCouchbaseTemplate.insertById(clazz), reactiveCouchbaseTemplate.replaceById(clazz), + reactiveCouchbaseTemplate.upsertById(clazz) }) { + // create an entity of type clazz + Constructor cons = clazz.getConstructor(String.class, String.class, String.class); + User user = (User) cons.newInstance("" + operator.getClass().getSimpleName() + "_" + clazz.getSimpleName(), + "firstname", "lastname"); + + if (clazz.equals(User.class)) { // User.java doesn't have an durability annotation + operator = (OneAndAllEntityReactive) ((WithDurability) operator).withDurability(PersistTo.ACTIVE, + ReplicateTo.NONE); + } + + // if replace, we need to insert a document to replace + if (operator instanceof ReactiveReplaceByIdOperation.ReactiveReplaceById) { + reactiveCouchbaseTemplate.insertById(User.class).one(user).block(); + } + // call to insert/replace/update + User returned = operator.one(user).block(); + assertEquals(user, returned); + User found = reactiveCouchbaseTemplate.findById(User.class).one(user.getId()).block(); + assertEquals(user, found); + + if (operator instanceof ReactiveReplaceByIdOperation.ReactiveReplaceById) { + reactiveCouchbaseTemplate.removeById().withDurability(PersistTo.ACTIVE, ReplicateTo.NONE).one(user.getId()) + .block(); + User removed = (User) reactiveCouchbaseTemplate.findById(user.getClass()).one(user.getId()).block(); + assertNull(removed, "found should have been null as document should be removed"); + } + } + + } + + @Test + void withExpiryAndExpiryAnnotation() + throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { + // ( withExpiry(), expiry=1, expiryExpression=${myExpiry} ) X ( insert, + // replace, upsert ) + Set users = new HashSet<>(); // set of all documents we will insert + // Entity classes + for (Class clazz : new Class[] { User.class, UserAnnotated.class, UserAnnotated2.class, UserAnnotated3.class }) { + // insert, replace, upsert + for (OneAndAllEntityReactive operator : new OneAndAllEntityReactive[] { + reactiveCouchbaseTemplate.insertById(clazz), reactiveCouchbaseTemplate.replaceById(clazz), + reactiveCouchbaseTemplate.upsertById(clazz) }) { + + // create an entity of type clazz + Constructor cons = clazz.getConstructor(String.class, String.class, String.class); + User user = (User) cons.newInstance("" + operator.getClass().getSimpleName() + "_" + clazz.getSimpleName(), + "firstname", "lastname"); + + if (clazz.equals(User.class)) { // User.java doesn't have an expiry annotation + operator = (OneAndAllEntityReactive) ((WithExpiry) operator).withExpiry(Duration.ofSeconds(1)); + } else if (clazz.equals(UserAnnotated3.class)) { // override the expiry from the annotation with no expiry + operator = (OneAndAllEntityReactive) ((WithExpiry) operator).withExpiry(Duration.ofSeconds(0)); + } + + // if replace or remove, we need to insert a document to replace + if (operator instanceof ReactiveReplaceByIdOperation.ReactiveReplaceById || operator instanceof ExecutableRemoveById) { + reactiveCouchbaseTemplate.insertById(User.class).one(user).block(); + } + // call to insert/replace/update + User returned = operator.one(user).block(); + assertEquals(user, returned); + users.add(user); + } + } + // check that they are gone after a few seconds. + sleepSecs(4); + for (User user : users) { + User found = reactiveCouchbaseTemplate.findById(user.getClass()).one(user.getId()).block(); + if (found instanceof UserAnnotated3) { + assertNotNull(found, "found should be non null as it was set to have no expirty"); + } else { + assertNull(found, "found should have been null as document should be expired"); + } + } + + } + + @Test + void findDocWhichDoesNotExist() { + assertNull(reactiveCouchbaseTemplate.findById(User.class).one(UUID.randomUUID().toString()).block()); + } + + @Test + void upsertAndReplaceById() { + User user = new User(UUID.randomUUID().toString(), "firstname", "lastname"); + User modified = reactiveCouchbaseTemplate.upsertById(User.class).one(user).block(); + assertEquals(user, modified); + + User toReplace = new User(modified.getId(), "some other", "lastname"); + reactiveCouchbaseTemplate.replaceById(User.class).one(toReplace).block(); + + User loaded = reactiveCouchbaseTemplate.findById(User.class).one(toReplace.getId()).block(); + assertEquals("some other", loaded.getFirstname()); + + reactiveCouchbaseTemplate.removeById().one(toReplace.getId()).block(); + + } + + @Test + void upsertAndRemoveById() { + { + User user = new User(UUID.randomUUID().toString(), "firstname", "lastname"); + User modified = reactiveCouchbaseTemplate.upsertById(User.class).one(user).block(); + assertEquals(user, modified); + + RemoveResult removeResult = reactiveCouchbaseTemplate.removeById().one(user.getId()).block(); + assertEquals(user.getId(), removeResult.getId()); + assertTrue(removeResult.getCas() != 0); + assertTrue(removeResult.getMutationToken().isPresent()); + + assertNull(reactiveCouchbaseTemplate.findById(User.class).one(user.getId()).block()); + } + { + User user = new User(UUID.randomUUID().toString(), "firstname", "lastname"); + User modified = reactiveCouchbaseTemplate.upsertById(User.class).one(user).block(); + assertEquals(user, modified); + + // careful now - user and modified are the same object. The object has the new cas (@Version version) + Long savedCas = modified.getVersion(); + modified.setVersion(123); + assertThrows(DataIntegrityViolationException.class, () -> reactiveCouchbaseTemplate.removeById() + .withCas(reactiveCouchbaseTemplate.support().getCas(modified)).one(modified.getId()).block()); + modified.setVersion(savedCas); + reactiveCouchbaseTemplate.removeById().withCas(reactiveCouchbaseTemplate.support().getCas(modified)) + .one(modified.getId()).block(); + } + } + + @Test + void insertById() { + User user = new User(UUID.randomUUID().toString(), "firstname", "lastname"); + User inserted = reactiveCouchbaseTemplate.insertById(User.class).one(user).block(); + assertEquals(user, inserted); + assertThrows(DuplicateKeyException.class, () -> reactiveCouchbaseTemplate.insertById(User.class).one(user).block()); + } + + @Test + void insertByIdwithDurability() { + User user = new User(UUID.randomUUID().toString(), "firstname", "lastname"); + User inserted = reactiveCouchbaseTemplate.insertById(User.class).withDurability(PersistTo.ACTIVE, ReplicateTo.NONE) + .one(user).block(); + assertEquals(user, inserted); + assertThrows(DuplicateKeyException.class, () -> reactiveCouchbaseTemplate.insertById(User.class).one(user).block()); + } + + @Test + void existsById() { + String id = UUID.randomUUID().toString(); + assertFalse(reactiveCouchbaseTemplate.existsById().one(id).block()); + + User user = new User(id, "firstname", "lastname"); + User inserted = reactiveCouchbaseTemplate.insertById(User.class).one(user).block(); + assertEquals(user, inserted); + + assertTrue(reactiveCouchbaseTemplate.existsById().one(id).block()); + + } + + @Test + @IgnoreWhen(clusterTypes = ClusterType.MOCKED) + void saveAndFindImmutableById() { + PersonValue personValue = new PersonValue(UUID.randomUUID().toString(), 123, "f", "l"); + PersonValue inserted; + PersonValue upserted; + PersonValue replaced; + + inserted = reactiveCouchbaseTemplate.insertById(PersonValue.class).one(personValue).block(); + assertNotEquals(0, inserted.getVersion()); + PersonValue foundInserted = reactiveCouchbaseTemplate.findById(PersonValue.class).one(inserted.getId()).block(); + assertNotNull(foundInserted, "inserted personValue not found"); + assertEquals(inserted, foundInserted); + + // upsert will insert + reactiveCouchbaseTemplate.removeById().one(inserted.getId()); + upserted = reactiveCouchbaseTemplate.upsertById(PersonValue.class).one(inserted).block(); + assertNotEquals(0, upserted.getVersion()); + PersonValue foundUpserted = reactiveCouchbaseTemplate.findById(PersonValue.class).one(upserted.getId()).block(); + assertNotNull(foundUpserted, "upserted personValue not found"); + assertEquals(upserted, foundUpserted); + + // upsert will replace + upserted = reactiveCouchbaseTemplate.upsertById(PersonValue.class).one(inserted).block(); + assertNotEquals(0, upserted.getVersion()); + PersonValue foundUpserted2 = reactiveCouchbaseTemplate.findById(PersonValue.class).one(upserted.getId()).block(); + assertNotNull(foundUpserted2, "upserted personValue not found"); + assertEquals(upserted, foundUpserted2); + + replaced = reactiveCouchbaseTemplate.replaceById(PersonValue.class).one(upserted).block(); + assertNotEquals(0, replaced.getVersion()); + PersonValue foundReplaced = reactiveCouchbaseTemplate.findById(PersonValue.class).one(replaced.getId()).block(); + assertNotNull(foundReplaced, "replaced personValue not found"); + assertEquals(replaced, foundReplaced); + + } + + private void sleepSecs(int i) { + try { + Thread.sleep(i * 1000); + } catch (InterruptedException ie) {} + } + +} 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..12bee0662 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -33,6 +33,7 @@ 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.ReactiveRepositoryOperationsMapping; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; @@ -48,7 +49,9 @@ */ @Configuration @EnableCouchbaseRepositories -@EnableCouchbaseAuditing(auditorAwareRef = "auditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") +@EnableCouchbaseAuditing(auditorAwareRef="auditorAwareRef", dateTimeProviderRef="dateTimeProviderRef") // this activates auditing +@EnableReactiveCouchbaseAuditing(auditorAwareRef="reactiveAuditorAwareRef", dateTimeProviderRef="dateTimeProviderRef") // this activates auditing + public class Config extends AbstractCouchbaseConfiguration { String bucketname = "travel-sample"; String username = "Administrator"; @@ -104,6 +107,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 +185,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/NaiveAuditorAware.java b/src/test/java/org/springframework/data/couchbase/domain/NaiveAuditorAware.java index b1c54f27b..797a2dabb 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/NaiveAuditorAware.java +++ b/src/test/java/org/springframework/data/couchbase/domain/NaiveAuditorAware.java @@ -32,7 +32,8 @@ */ public class NaiveAuditorAware implements AuditorAware { - private Optional auditor = Optional.of("auditor"); + static public final String AUDITOR = "auditor"; + private Optional auditor = Optional.of(AUDITOR); @Override public Optional getCurrentAuditor() { 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 447ac1276..0f3b08145 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/ReactiveNaiveAuditorAware.java +++ b/src/test/java/org/springframework/data/couchbase/domain/ReactiveNaiveAuditorAware.java @@ -27,9 +27,10 @@ */ public class ReactiveNaiveAuditorAware implements ReactiveAuditorAware { + public static final String AUDITOR = "reactive_auditor"; @Override public Mono getCurrentAuditor() { - return Mono.just("auditor"); + return Mono.just(AUDITOR); } } 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..49ddbcee5 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -421,7 +421,7 @@ void findBySimplePropertyAudited() { Airport saved = airportRepository.save(vie); List airports1 = airportRepository.findAllByIata("vie"); assertEquals(saved, airports1.get(0)); - assertEquals(saved.getCreatedBy(), "auditor"); // NaiveAuditorAware will provide this + assertEquals(saved.getCreatedBy(), NaiveAuditorAware.AUDITOR); // NaiveAuditorAware will provide this } finally { airportRepository.delete(vie); } 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 6888dd581..77e6fe2a4 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,7 @@ void findByIdAudited() { Airport saved = airportRepository.save(vie).block(); Airport airport1 = airportRepository.findById(saved.getId()).block(); assertEquals(airport1, saved); - assertEquals(saved.getCreatedBy(), "auditor"); // NaiveAuditorAware will provide this + assertEquals(saved.getCreatedBy(), ReactiveNaiveAuditorAware.AUDITOR); // ReactiveNaiveAuditorAware will provide this } finally { airportRepository.delete(vie).block(); }