diff --git a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java index 696afab7..2d985fe1 100644 --- a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java +++ b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java @@ -23,6 +23,7 @@ package org.springframework.data.couchbase.config; * * @author Michael Nitschinger * @author Simon Baslé + * @author Michael Reiche */ public class BeanNames { @@ -48,4 +49,8 @@ public class BeanNames { */ public static final String COUCHBASE_MAPPING_CONTEXT = "couchbaseMappingContext"; + /** + * The name for the bean that will handle audit trail marking of entities. + */ + public static final String COUCHBASE_AUDITING_HANDLER = "couchbaseAuditingHandler"; } diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java index 7819c02e..d94739c9 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -17,9 +17,7 @@ package org.springframework.data.couchbase.core; import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.*; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.CouchbaseClientFactory; @@ -33,6 +31,14 @@ import org.springframework.lang.Nullable; import com.couchbase.client.java.Collection; +/** + * Implements Couchbase operations + * findBy, insertBy, upsertBy, replaceBy, removeBy, existsBy + * + * @author Michael Nitschinger + * @author Michael Reiche + * @since 3.0 + */ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContextAware { private final CouchbaseClientFactory clientFactory; @@ -161,6 +167,8 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { prepareIndexCreator(applicationContext); + templateSupport.setApplicationContext(applicationContext); + reactiveCouchbaseTemplate.setApplicationContext(applicationContext); } private void prepareIndexCreator(ApplicationContext context) { 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 77a8328c..1de8508f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -16,23 +16,41 @@ package org.springframework.data.couchbase.core; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.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.*; import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.util.Assert; -public class CouchbaseTemplateSupport { +/** + * encode/decode support for CouchbaseTemplate + * + * @author Michael Nitschinger + * @author Michael Reiche + * @since 3.0 + */ +public class CouchbaseTemplateSupport implements ApplicationContextAware { + private static final Logger LOG = LoggerFactory.getLogger(CouchbaseTemplateSupport.class); private final CouchbaseConverter converter; private final MappingContext, CouchbasePersistentProperty> mappingContext; // TODO: this should be replaced I think private final TranslationService translationService; + private ApplicationContext applicationContext; + EntityCallbacks entityCallbacks; public CouchbaseTemplateSupport(final CouchbaseConverter converter) { this.converter = converter; @@ -41,8 +59,12 @@ public class CouchbaseTemplateSupport { } public CouchbaseDocument encodeEntity(final Object entityToEncode) { + maybeEmitEvent(new BeforeConvertEvent<>(entityToEncode)); + Object maybeNewEntity = maybeCallBeforeConvert(entityToEncode, ""); final CouchbaseDocument converted = new CouchbaseDocument(); - converter.write(entityToEncode, converted); + converter.write(maybeNewEntity, converted); + maybeCallAfterConvert(entityToEncode, converted, ""); + maybeEmitEvent(new BeforeSaveEvent<>(entityToEncode, converted)); return converted; } @@ -52,7 +74,8 @@ public class CouchbaseTemplateSupport { T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted)); final ConvertingPropertyAccessor accessor = getPropertyAccessor(readEntity); - CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity(readEntity.getClass()); + CouchbasePersistentEntity persistentEntity = mappingContext.getRequiredPersistentEntity( + readEntity.getClass()); if (persistentEntity.getVersionProperty() != null) { accessor.setProperty(persistentEntity.getVersionProperty(), cas); @@ -62,7 +85,8 @@ public class CouchbaseTemplateSupport { public void applyUpdatedCas(final Object entity, final long cas) { 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) { @@ -82,4 +106,66 @@ public class CouchbaseTemplateSupport { return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService()); } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + if (entityCallbacks == null) { + setEntityCallbacks(EntityCallbacks.create(applicationContext)); + } + } + + /** + * Set the {@link EntityCallbacks} instance to use when invoking + * {@link org.springframework.data.mapping.callback.EntityCallback callbacks} like the {@link BeforeConvertCallback}. + *

+ * Overrides potentially existing {@link EntityCallbacks}. + * + * @param entityCallbacks must not be {@literal null}. + * @throws IllegalArgumentException if the given instance is {@literal null}. + * @since 2.2 + */ + public void setEntityCallbacks(EntityCallbacks entityCallbacks) { + Assert.notNull(entityCallbacks, "EntityCallbacks must not be null!"); + this.entityCallbacks = entityCallbacks; + } + + void maybeEmitEvent(CouchbaseMappingEvent event) { + if (canPublishEvent()) { + try { + this.applicationContext.publishEvent(event); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + LOG.info("maybeEmitEvent called, but CouchbaseTemplate not initialized with applicationContext"); + } + + } + + private boolean canPublishEvent() { + return this.applicationContext != null; + } + + protected T maybeCallBeforeConvert(T object, String collection) { + if (entityCallbacks != null) { + try { + return entityCallbacks.callback(BeforeConvertCallback.class, object, collection); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + LOG.info("maybeCallBeforeConvert called, but CouchbaseTemplate not initialized with applicationContext"); + } + return object; + } + + protected T maybeCallAfterConvert(T object, CouchbaseDocument document, String collection) { + if (null != entityCallbacks) { + return entityCallbacks.callback(AfterConvertCallback.class, object, document, collection); + } else { + LOG.info("maybeCallAfterConvert called, but CouchbaseTemplate not initialized with applicationContext"); + } + return object; + } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java index 1fffbd1b..cd8d275b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -16,6 +16,10 @@ package org.springframework.data.couchbase.core; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.CouchbaseClientFactory; @@ -23,7 +27,13 @@ import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import com.couchbase.client.java.Collection; -public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations { +/** + * template class for Reactive Couchbase operations + * + * @author Michael Nitschinger + * @author Michael Reiche + */ +public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, ApplicationContextAware { private final CouchbaseClientFactory clientFactory; private final CouchbaseConverter converter; @@ -132,4 +142,9 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations { return resolved == null ? ex : resolved; } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + templateSupport.setApplicationContext(applicationContext); + } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index 2206d4b8..2b36c344 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -33,6 +33,8 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.convert.EntityInstantiator; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbaseList; @@ -50,6 +52,7 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PreferredConstructor.Parameter; import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; @@ -63,6 +66,7 @@ import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.lang.Nullable; /** * A mapping converter for Couchbase. The converter is responsible for reading from and writing to entities and @@ -72,6 +76,7 @@ import org.springframework.util.CollectionUtils; * @author Oliver Gierke * @author Geoffrey Mina * @author Mark Paluch + * @author Michael Reiche */ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implements ApplicationContextAware { @@ -104,6 +109,12 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem */ protected CouchbaseTypeMapper typeMapper; + /** + * Callbacks for Audit Mechanism + */ + private @Nullable + EntityCallbacks entityCallbacks; + public MappingCouchbaseConverter() { super(new DefaultConversionService()); @@ -127,7 +138,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem * attribute. * * @param mappingContext the mapping context to use. - * @param typeKey the attribute name to use to store complex types class name. + * @param typeKey the attribute name to use to store complex types class name. */ public MappingCouchbaseConverter( final MappingContext, CouchbasePersistentProperty> mappingContext, @@ -156,7 +167,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem /** * Check if one class is a subtype of the other. * - * @param left the first class. + * @param left the first class. * @param right the second class. * @return true if it is a subtype, false otherwise. */ @@ -182,9 +193,9 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem /** * Read an incoming {@link CouchbaseDocument} into the target entity. * - * @param type the type information of the target entity. + * @param type the type information of the target entity. * @param source the document to convert. - * @param the entity type. + * @param the entity type. * @return the converted entity. */ protected R read(final TypeInformation type, final CouchbaseDocument source) { @@ -194,10 +205,10 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem /** * Read an incoming {@link CouchbaseDocument} into the target entity. * - * @param type the type information of the target entity. + * @param type the type information of the target entity. * @param source the document to convert. * @param parent an optional parent object. - * @param the entity type. + * @param the entity type. * @return the converted entity. */ @SuppressWarnings("unchecked") @@ -217,8 +228,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem return (R) readMap(typeToUse, source, parent); } - CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext - .getRequiredPersistentEntity(typeToUse); + CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext.getRequiredPersistentEntity( + typeToUse); return read(entity, source, parent); } @@ -232,10 +243,11 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem * @param entity the target entity. * @param source the document to convert. * @param parent an optional parent object. - * @param the entity type. + * @param the entity type. * @return the converted entity. */ - protected R read(final CouchbasePersistentEntity entity, final CouchbaseDocument source, final Object parent) { + protected R read(final CouchbasePersistentEntity entity, final CouchbaseDocument source, + final Object parent) { final DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(source, spELContext); ParameterValueProvider provider = getParameterProvider(entity, source, evaluator, parent); @@ -247,8 +259,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem entity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { - if (!doesPropertyExistInSource(prop) || entity.isConstructorArgument(prop) || isIdConstructionProperty(prop) - || prop.isAnnotationPresent(N1qlJoin.class)) { + if (!doesPropertyExistInSource(prop) || entity.isConstructorArgument(prop) || isIdConstructionProperty( + prop) || prop.isAnnotationPresent(N1qlJoin.class)) { return; } Object obj = prop.isIdProperty() ? source.getId() : getValueInternal(prop, source, instance); @@ -277,8 +289,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem * Loads the property value through the value provider. * * @param property the source property. - * @param source the source document. - * @param parent the optional parent. + * @param source the source document. + * @param parent the optional parent. * @return the actual property value. */ protected Object getValueInternal(final CouchbasePersistentProperty property, final CouchbaseDocument source, @@ -289,10 +301,10 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem /** * Creates a new parameter provider. * - * @param entity the persistent entity. - * @param source the source document. + * @param entity the persistent entity. + * @param source the source document. * @param evaluator the SPEL expression evaluator. - * @param parent the optional parent. + * @param parent the optional parent. * @return a new parameter value provider. */ private ParameterValueProvider getParameterProvider( @@ -309,7 +321,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem /** * Recursively parses the a map from the source document. * - * @param type the type information for the document. + * @param type the type information for the document. * @param source the source document. * @param parent the optional parent. * @return the recursively parsed map. @@ -350,7 +362,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem /** * Potentially convert simple values like ENUMs. * - * @param value the value to convert. + * @param value the value to convert. * @param target the target object. * @return the potentially converted object. */ @@ -394,15 +406,16 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem writeInternal(source, target, type); if (target.getId() == null) { - throw new MappingException("An ID property is needed, but not found/could not be generated on this entity."); + throw new MappingException( + "An ID property is needed, but not found/could not be generated on this entity."); } } /** * Convert a source object into a {@link CouchbaseDocument} target. * - * @param source the source object. - * @param target the target document. + * @param source the source object. + * @param target the target document. * @param typeHint the type information for the source. */ @SuppressWarnings("unchecked") @@ -552,7 +565,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem * * @param source the source object. * @param target the target document. - * @param prop the property information. + * @param prop the property information. */ @SuppressWarnings("unchecked") private void writePropertyInternal(final Object source, final CouchbaseDocument target, @@ -577,6 +590,18 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem return; } + if (valueType.getType().equals(java.util.Optional.class)) { + if (source == null) + return; + Optional o = (Optional) source; + if (o.isPresent()) { + writeSimpleInternal(o.get(), target, prop.getFieldName()); + } else { + writeSimpleInternal(null, target, prop.getFieldName()); + } + return; + } + Optional> basicTargetType = conversions.getCustomWriteTarget(source.getClass()); if (basicTargetType.isPresent()) { @@ -590,9 +615,9 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem CouchbaseDocument propertyDoc = new CouchbaseDocument(); addCustomTypeKeyIfNecessary(type, source, propertyDoc); - CouchbasePersistentEntity entity = isSubtype(prop.getType(), source.getClass()) - ? mappingContext.getRequiredPersistentEntity(source.getClass()) - : mappingContext.getRequiredPersistentEntity(type); + CouchbasePersistentEntity entity = isSubtype(prop.getType(), source.getClass()) ? + mappingContext.getRequiredPersistentEntity(source.getClass()) : + mappingContext.getRequiredPersistentEntity(type); writeInternal(source, propertyDoc, entity); target.put(name, propertyDoc); } @@ -600,7 +625,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem /** * Wrapper method to create the underlying map. * - * @param map the source map. + * @param map the source map. * @param prop the persistent property. * @return the written couchbase document. */ @@ -616,7 +641,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem * * @param source the source object. * @param target the target document. - * @param type the type information for the document. + * @param type the type information for the document. * @return the written couchbase document. */ private CouchbaseDocument writeMapInternal(final Map source, final CouchbaseDocument target, @@ -635,7 +660,9 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem new CouchbaseList(conversions.getSimpleTypeHolder()), type.getMapValueType())); } else { CouchbaseDocument embeddedDoc = new CouchbaseDocument(); - TypeInformation valueTypeInfo = type.isMap() ? type.getMapValueType() : ClassTypeInformation.OBJECT; + TypeInformation valueTypeInfo = type.isMap() ? + type.getMapValueType() : + ClassTypeInformation.OBJECT; writeInternal(val, embeddedDoc, valueTypeInfo); target.put(simpleKey, embeddedDoc); } @@ -651,7 +678,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem * Helper method to create the underlying collection/list. * * @param collection the collection to write. - * @param prop the property information. + * @param prop the property information. * @return the created couchbase list. */ private CouchbaseList createCollection(final Collection collection, final CouchbasePersistentProperty prop) { @@ -664,7 +691,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem * * @param source the source object. * @param target the target document. - * @param type the type information for the document. + * @param type the type information for the document. * @return the created couchbase list. */ private CouchbaseList writeCollectionInternal(final Collection source, final CouchbaseList target, @@ -677,8 +704,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem if (elementType == null || conversions.isSimpleType(elementType)) { target.put(getPotentiallyConvertedSimpleWrite(element)); } else if (element instanceof Collection || elementType.isArray()) { - target.put(writeCollectionInternal(asCollection(element), new CouchbaseList(conversions.getSimpleTypeHolder()), - componentType)); + target.put(writeCollectionInternal(asCollection(element), + new CouchbaseList(conversions.getSimpleTypeHolder()), componentType)); } else { CouchbaseDocument embeddedDoc = new CouchbaseDocument(); @@ -695,12 +722,13 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem * Read a collection from the source object. * * @param targetType the target type. - * @param source the list as source. - * @param parent the optional parent. + * @param source the list as source. + * @param parent the optional parent. * @return the instantiated collection. */ @SuppressWarnings("unchecked") - private Object readCollection(final TypeInformation targetType, final CouchbaseList source, final Object parent) { + private Object readCollection(final TypeInformation targetType, final CouchbaseList source, + final Object parent) { Assert.notNull(targetType, "Target type must not be null!"); Class collectionType = targetType.getType(); @@ -709,8 +737,9 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem } collectionType = Collection.class.isAssignableFrom(collectionType) ? collectionType : List.class; - Collection items = targetType.getType().isArray() ? new ArrayList() - : CollectionFactory.createCollection(collectionType, source.size(false)); + Collection items = targetType.getType().isArray() ? + new ArrayList() : + CollectionFactory.createCollection(collectionType, source.size(false)); TypeInformation componentType = targetType.getComponentType(); Class rawComponentType = componentType == null ? null : componentType.getType(); @@ -735,7 +764,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem * * @param source the source object. * @param target the target document. - * @param key the key of the object. + * @param key the key of the object. */ private void writeSimpleInternal(final Object source, final CouchbaseDocument target, final String key) { target.put(key, getPotentiallyConvertedSimpleWrite(source)); @@ -748,14 +777,14 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem Optional> customTarget = conversions.getCustomWriteTarget(value.getClass()); - return customTarget.map(it -> (Object) conversionService.convert(value, it)) - .orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value); + return customTarget.map(it -> (Object) conversionService.convert(value, it)).orElseGet( + () -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value); } /** * Add a custom type key if needed. * - * @param type the type information. + * @param type the type information. * @param source th the source object. * @param target the target document. */ @@ -772,15 +801,34 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; + if (entityCallbacks == null) { + setEntityCallbacks(EntityCallbacks.create(applicationContext)); + } + } + + /** + * COPIED + * Set the {@link EntityCallbacks} instance to use when invoking + * {@link org.springframework.data.mapping.callback.EntityCallback callbacks} like the {@link AfterConvertCallback}. + *

+ * Overrides potentially existing {@link EntityCallbacks}. + * + * @param entityCallbacks must not be {@literal null}. + * @throws IllegalArgumentException if the given instance is {@literal null}. + * @since 3.0 + */ + public void setEntityCallbacks(EntityCallbacks entityCallbacks) { + Assert.notNull(entityCallbacks, "EntityCallbacks must not be null!"); + this.entityCallbacks = entityCallbacks; } /** * Helper method to read the value based on the value type. * - * @param value the value to convert. - * @param type the type information. + * @param value the value to convert. + * @param type the type information. * @param parent the optional parent. - * @param the target type. + * @param the target type. * @return the converted object. */ @SuppressWarnings("unchecked") @@ -913,8 +961,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem private final Object parent; public ConverterAwareSpELExpressionParameterValueProvider(final SpELExpressionEvaluator evaluator, - final ConversionService conversionService, final ParameterValueProvider delegate, - final Object parent) { + final ConversionService conversionService, + final ParameterValueProvider delegate, final Object parent) { super(evaluator, conversionService, delegate); this.parent = parent; } 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 new file mode 100644 index 00000000..588d89b4 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/AfterConvertCallback.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.couchbase.core.mapping.event; + +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * Callback being invoked after a domain object is materialized from a {@link Document} when reading results. + * + * @author Roman Puchkovskiy + * @author Mark Paluch + * @author Michael Reiche + * @see org.springframework.data.mapping.callback.EntityCallbacks + * @since 3.0 + */ +@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. + * + * @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}. + */ + T onAfterConvert(T entity, Document document, String collection); +} 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 33025720..d5ecffa7 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 @@ -31,11 +31,16 @@ import org.springframework.util.Assert; * @author Oliver Gierke * @author Simon Baslé * @author Mark Paluch + * @author Michael Reiche */ public class AuditingEventListener implements ApplicationListener> { private final ObjectFactory auditingHandlerFactory; + public AuditingEventListener() { + this.auditingHandlerFactory = null; + } + /** * Creates a new {@link AuditingEventListener} using the given {@link MappingContext} and {@link AuditingHandler} * provided by the given {@link ObjectFactory}. @@ -51,8 +56,8 @@ public class AuditingEventListener implements ApplicationListener event) { - Optional.ofNullable(event.getSource())// .ifPresent(it -> auditingHandlerFactory.getObject().markAudited(it)); } 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 new file mode 100644 index 00000000..307692f2 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/event/BeforeConvertCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019-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.mapping.callback.EntityCallback; + +/** + * Callback being invoked before a domain object is converted to be persisted. + * + * @author Mark Paluch + * @author Michael Reiche + * @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. + * + * @param entity the domain object to save. + * @param collection name of the collection. + * @return the domain object to be persisted. + */ + T onBeforeConvert(T entity, 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 new file mode 100644 index 00000000..127a53c0 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/auditing/CouchbaseAuditingRegistrar.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.repository.auditing; + +import java.lang.annotation.Annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport; +import org.springframework.data.auditing.config.AuditingConfiguration; +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.AuditingEventListener; +import org.springframework.util.Assert; + +/** + * A support registrar that allows to set up auditing for Couchbase (including {@link AuditingHandler} + * and { IsNewStrategyFactory} set up). See {@link EnableCouchbaseAuditing} for the associated annotation. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @author Simon Baslé + * @author Michael Reiche + */ +public class CouchbaseAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { + + @Override + protected Class getAnnotation() { + return EnableCouchbaseAuditing.class; + } + + @Override + protected String getAuditingHandlerBeanName() { + return BeanNames.COUCHBASE_AUDITING_HANDLER; + } + + @Override + public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) { + Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null!"); + Assert.notNull(registry, "BeanDefinitionRegistry must not be null!"); + + ensureMappingContext(registry, annotationMetadata); + super.registerBeanDefinitions(annotationMetadata, registry); + } + + @Override + protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) { + Assert.notNull(configuration, "AuditingConfiguration must not be null!"); + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(IsNewAwareAuditingHandler.class); + builder.addConstructorArgReference(BeanNames.COUCHBASE_MAPPING_CONTEXT); + return configureDefaultAuditHandlerAttributes(configuration, builder); + } + + @Override + protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition, + BeanDefinitionRegistry registry) { + Assert.notNull(auditingHandlerDefinition, "BeanDefinition must not be null!"); + Assert.notNull(registry, "BeanDefinitionRegistry must not be null!"); + + BeanDefinitionBuilder listenerBeanDefinitionBuilder = BeanDefinitionBuilder + .rootBeanDefinition(AuditingEventListener.class); + listenerBeanDefinitionBuilder.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition( + getAuditingHandlerBeanName(), registry)); + + registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(), + AuditingEventListener.class.getName(), registry); + } + + private void ensureMappingContext(BeanDefinitionRegistry registry, Object source) { + if (!registry.containsBeanDefinition(BeanNames.COUCHBASE_MAPPING_CONTEXT)) { + RootBeanDefinition definition = new RootBeanDefinition(CouchbaseMappingContext.class); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + definition.setSource(source); + + registry.registerBeanDefinition(BeanNames.COUCHBASE_MAPPING_CONTEXT, definition); + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableCouchbaseAuditing.java b/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableCouchbaseAuditing.java new file mode 100644 index 00000000..f522b0d8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/auditing/EnableCouchbaseAuditing.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2016 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.repository.auditing; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; + +/** + * Annotation to enable auditing in Couchbase via annotation configuration. + * + * @author Thomas Darimont + * @author Oliver Gierke + * @author Simon Baslé + * @author Michael Reiche + */ +@Inherited +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(CouchbaseAuditingRegistrar.class) +public @interface EnableCouchbaseAuditing { + /** + * Configures the {@link AuditorAware} bean to be used to lookup the current principal. + */ + String auditorAwareRef() default "auditorAwareRef"; + + /** + * Configures whether the creation and modification dates are set. Defaults to {@literal true}. + */ + boolean setDates() default true; + + /** + * Configures whether the entity shall be marked as modified on creation. Defaults to {@literal true}. + */ + boolean modifyOnCreate() default true; + + /** + * Configures a {@link DateTimeProvider} bean name that allows customizing the {@link org.joda.time.DateTime} to be + * used for setting creation and modification dates. + */ + String dateTimeProviderRef() default "dateTimeProviderRef"; +} 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 9d4116cd..77659b2c 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.data.couchbase.core; import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; import java.io.IOException; import java.time.Duration; @@ -26,17 +27,30 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.DuplicateKeyException; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; +/** + * KV tests + * + * Theses tests rely on a cb server running. + * + * @author Michael Nitschinger + * @author Michael Reiche + */ +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationTests { private static CouchbaseClientFactory couchbaseClientFactory; @@ -55,8 +69,8 @@ class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationT @BeforeEach void beforeEach() { - CouchbaseConverter couchbaseConverter = new MappingCouchbaseConverter(); - couchbaseTemplate = new CouchbaseTemplate(couchbaseClientFactory, couchbaseConverter); + ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + couchbaseTemplate = (CouchbaseTemplate)ac.getBean(COUCHBASE_TEMPLATE); } @Test @@ -67,6 +81,8 @@ class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationT User found = couchbaseTemplate.findById(User.class).one(user.getId()); assertEquals(user, found); + + couchbaseTemplate.removeById().one(user.getId()); } @Test @@ -86,6 +102,9 @@ class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationT User loaded = couchbaseTemplate.findById(User.class).one(toReplace.getId()); assertEquals("some other", loaded.getFirstname()); + + couchbaseTemplate.removeById().one(toReplace.getId()); + } @Test @@ -99,7 +118,8 @@ class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationT assertTrue(removeResult.getCas() != 0); assertTrue(removeResult.getMutationToken().isPresent()); - assertThrows(DataRetrievalFailureException.class, () -> couchbaseTemplate.findById(User.class).one(user.getId())); + assertThrows(DataRetrievalFailureException.class, + () -> couchbaseTemplate.findById(User.class).one(user.getId())); } @Test @@ -109,10 +129,11 @@ class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationT assertEquals(user, inserted); assertThrows(DuplicateKeyException.class, () -> couchbaseTemplate.insertById(User.class).one(user)); + couchbaseTemplate.removeById().one(user.getId()); + } @Test - @IgnoreWhen(clusterTypes = ClusterType.MOCKED) void existsById() { String id = UUID.randomUUID().toString(); assertFalse(couchbaseTemplate.existsById().one(id)); @@ -122,6 +143,8 @@ class CouchbaseTemplateKeyValueIntegrationTests extends ClusterAwareIntegrationT assertEquals(user, inserted); assertTrue(couchbaseTemplate.existsById().one(id)); + couchbaseTemplate.removeById().one(user.getId()); + } } diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java index 4ae8e834..475f3559 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java @@ -17,30 +17,46 @@ package org.springframework.data.couchbase.core; import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; import java.io.IOException; +import java.time.Instant; +import java.time.temporal.TemporalAccessor; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.UUID; +import org.springframework.data.couchbase.domain.Config; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; -import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.domain.NaiveAuditorAware; import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.TestContext; import com.couchbase.client.core.error.IndexExistsException; import com.couchbase.client.java.query.QueryScanConsistency; +/** + * Query tests + * + * Theses tests rely on a cb server running + * + * @author Michael Nitschinger + * @author Michael Reiche + */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) class CouchbaseTemplateQueryIntegrationTests extends ClusterAwareIntegrationTests { @@ -65,26 +81,44 @@ class CouchbaseTemplateQueryIntegrationTests extends ClusterAwareIntegrationTest @BeforeEach void beforeEach() { - CouchbaseConverter couchbaseConverter = new MappingCouchbaseConverter(); - couchbaseTemplate = new CouchbaseTemplate(couchbaseClientFactory, couchbaseConverter); + ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); } @Test void findByQuery() { - User user1 = new User(UUID.randomUUID().toString(), "user1", "user1"); - User user2 = new User(UUID.randomUUID().toString(), "user2", "user2"); + try { + User user1 = new User(UUID.randomUUID().toString(), "user1", "user1"); + User user2 = new User(UUID.randomUUID().toString(), "user2", "user2"); - couchbaseTemplate.upsertById(User.class).all(Arrays.asList(user1, user2)); + couchbaseTemplate.upsertById(User.class).all(Arrays.asList(user1, user2)); - final List foundUsers = couchbaseTemplate.findByQuery(User.class) - .consistentWith(QueryScanConsistency.REQUEST_PLUS).all(); + final List foundUsers = couchbaseTemplate.findByQuery(User.class).consistentWith( + QueryScanConsistency.REQUEST_PLUS).all(); - assertEquals(2, foundUsers.size()); - for (User u : foundUsers) { - assertTrue(u.equals(user1) || u.equals(user2)); + for (User u : foundUsers) { + System.out.println(u); + if (!(u.equals(user1) || u.equals(user2))) { + // somebody didn't clean up after themselves. + couchbaseTemplate.removeById().one(u.getId()); + } + } + assertEquals(2, foundUsers.size()); + TemporalAccessor auditTime = new AuditingDateTimeProvider().getNow().get(); + long auditMillis = Instant.from(auditTime).toEpochMilli(); + String auditUser = new NaiveAuditorAware().getCurrentAuditor().get(); + + for (User u : foundUsers) { + assertTrue(u.equals(user1) || u.equals(user2)); + assertEquals(auditUser, u.getCreator()); + assertEquals(auditMillis, u.getCreatedDate()); + assertEquals(auditUser, u.getLastModifiedBy()); + assertEquals(auditMillis, u.getLastModifiedDate()); + } + } finally { + couchbaseTemplate.removeByQuery(User.class).all(); } } - @Test void removeByQuery() { User user1 = new User(UUID.randomUUID().toString(), "user1", "user1"); @@ -97,8 +131,10 @@ class CouchbaseTemplateQueryIntegrationTests extends ClusterAwareIntegrationTest couchbaseTemplate.removeByQuery(User.class).consistentWith(QueryScanConsistency.REQUEST_PLUS).all(); - assertThrows(DataRetrievalFailureException.class, () -> couchbaseTemplate.findById(User.class).one(user1.getId())); - assertThrows(DataRetrievalFailureException.class, () -> couchbaseTemplate.findById(User.class).one(user2.getId())); + assertThrows(DataRetrievalFailureException.class, + () -> couchbaseTemplate.findById(User.class).one(user1.getId())); + assertThrows(DataRetrievalFailureException.class, + () -> couchbaseTemplate.findById(User.class).one(user2.getId())); } } diff --git a/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java index 0f4037b0..65af9594 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CustomTypeKeyIntegrationTests.java @@ -30,11 +30,14 @@ import org.springframework.data.couchbase.core.convert.DefaultCouchbaseTypeMappe import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.java.kv.GetResult; @SpringJUnitConfig(CustomTypeKeyIntegrationTests.Config.class) +@IgnoreWhen(clusterTypes = ClusterType.MOCKED) public class CustomTypeKeyIntegrationTests extends ClusterAwareIntegrationTests { private static final String CUSTOM_TYPE_KEY = "javaClass"; @@ -48,6 +51,9 @@ public class CustomTypeKeyIntegrationTests extends ClusterAwareIntegrationTests clientFactory.getBucket().waitUntilReady(Duration.ofSeconds(5)); User user = new User(UUID.randomUUID().toString(), "firstname", "lastname"); + // When using 'mocked', this call runs fine when the test class is ran by itself, + // but it times-out when ran together with all the tests under + // org.springframework.data.couchbase User modified = operations.upsertById(User.class).one(user); assertEquals(user, modified); diff --git a/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java b/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java new file mode 100644 index 00000000..e604563e --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AbstractEntity.java @@ -0,0 +1,64 @@ +package org.springframework.data.couchbase.domain; + +import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.core.mapping.id.GeneratedValue; +import org.springframework.data.couchbase.core.mapping.id.GenerationStrategy; + +import java.util.UUID; + +/** + * @author Oliver Gierke + */ +@Document +public class AbstractEntity { + + @Id + @GeneratedValue(strategy = GenerationStrategy.UNIQUE) + private UUID id; + + public AbstractEntity(){ } + + /** + * set the id + */ + public void setId(UUID id) { + this.id=id; + } + /** + * @return the id + */ + public UUID getId() { + return id; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (this.id == null || obj == null || !(this.getClass().equals(obj.getClass()))) { + return false; + } + + AbstractEntity that = (AbstractEntity) obj; + + return this.id.equals(that.getId()); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return id == null ? 0 : id.hashCode(); + } +} + diff --git a/src/test/java/org/springframework/data/couchbase/domain/Config.java b/src/test/java/org/springframework/data/couchbase/domain/Config.java new file mode 100644 index 00000000..32ad566d --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -0,0 +1,49 @@ +package org.springframework.data.couchbase.domain; + +import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; + +@Configuration +@EnableCouchbaseRepositories +@EnableCouchbaseAuditing //this activates auditing +public class Config extends AbstractCouchbaseConfiguration { + String bucketname = "travel-sample"; + String username = "Administrator"; + String password = "password"; + String connectionString = "127.0.0.1"; + + @Override + public String getConnectionString() { + return connectionString; + } + + @Override + public String getUserName() { + return username; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getBucketName() { + return bucketname; + } + + @Bean(name = "auditorAwareRef") + public NaiveAuditorAware testAuditorAware() { + return new NaiveAuditorAware(); + } + + @Bean(name = "dateTimeProviderRef") + public DateTimeProvider testDateTimeProvider() { + return new AuditingDateTimeProvider(); + } +} \ No newline at end of file diff --git a/src/test/java/org/springframework/data/couchbase/domain/NaiveAuditorAware.java b/src/test/java/org/springframework/data/couchbase/domain/NaiveAuditorAware.java new file mode 100644 index 00000000..f28932cb --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/NaiveAuditorAware.java @@ -0,0 +1,30 @@ +package org.springframework.data.couchbase.domain; + +import org.springframework.data.domain.AuditorAware; + +import java.util.Optional; + +// These are the classes that would be used for a real getCurrentAuditor() implementation +//import org.springframework.security.core.Authentication; +//import org.springframework.security.core.context.SecurityContextHolder; +//import org.springframework.security.core.userdetails.User; + +/** + * This class returns a string that represents the current user + * + * @author Michael Reiche + * @since 3.0 + */ +public class NaiveAuditorAware implements AuditorAware { + + private Optional auditor = Optional.of("auditor"); + + @Override + public Optional getCurrentAuditor() { + return auditor; + } + + public void setAuditor(String auditor) { + this.auditor = Optional.of(auditor); + } +} \ No newline at end of file diff --git a/src/test/java/org/springframework/data/couchbase/domain/Person.java b/src/test/java/org/springframework/data/couchbase/domain/Person.java new file mode 100644 index 00000000..a9c05330 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/Person.java @@ -0,0 +1,90 @@ +package org.springframework.data.couchbase.domain; + +import org.springframework.data.annotation.*; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.core.mapping.event.AuditingEventListener; +import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; + +import java.time.ZonedDateTime; +import java.util.Date; +import java.util.Optional; +import java.util.UUID; + +@Document +public class Person extends AbstractEntity { + Optional firstname; + Optional lastname; + + @CreatedBy + private String creator; + + @LastModifiedBy + private String lastModifiedBy; + + @LastModifiedDate + private long lastModification; + + @CreatedDate + private long creationDate; // =System.currentTimeMillis(); + + @Version + private long version; + + public Person(){ + } + + public Person(String firstname, String lastname){ + this(); + setFirstname(firstname); + setLastname(lastname); + } + + public Person(int id,String firstname, String lastname){ + this(firstname,lastname); + setId(new UUID(id, id)); + } + + public Optional getFirstname(){ + return firstname; + } + public void setFirstname(String firstname) { + this.firstname = firstname == null ? null : ( Optional.ofNullable(firstname.equals("")?null:firstname) ); + } + public void setFirstname(Optional firstname) { + this.firstname = firstname; + } + + public Optional getLastname(){ + return lastname; + } + public void setLastname(String lastname) { + this.lastname = lastname == null ? null : ( Optional.ofNullable(lastname.equals("")?null:lastname) ); + } + public void setLastname(Optional lastname) { + this.lastname = lastname; + } + + public String toString(){ + StringBuilder sb=new StringBuilder(); + sb.append("Person : {\n"); + sb.append(" id : "+getId()); + sb.append(optional(", firstname",firstname)); + sb.append(optional( ", lastname", lastname)); + sb.append(", version : "+version); + if(creator != null) sb.append(", creator : "+creator); + if(creationDate != 0) sb.append(", creationDate : "+creationDate); + if(lastModifiedBy != null) sb.append(", lastModifiedBy : "+lastModifiedBy); + if(lastModification != 0) sb.append(", lastModification : "+lastModification); + sb.append("}"); + return sb.toString(); + } + + static String optional(String name, Optional obj){ + if(obj != null) + if(obj.isPresent()) + return(" "+name+ ": '"+obj.get()+"'\n"); + else + return" "+name+": null\n" ; + return ""; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/User.java b/src/test/java/org/springframework/data/couchbase/domain/User.java index 595b06df..73449c68 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/User.java +++ b/src/test/java/org/springframework/data/couchbase/domain/User.java @@ -18,17 +18,33 @@ package org.springframework.data.couchbase.domain; import java.util.Objects; -import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.*; import org.springframework.data.couchbase.core.mapping.Document; +/** + * User entity for tests + * + * @author Michael Nitschinger + * @author Michael Reiche + */ + @Document public class User { - @Id private String id; - + @Id + private String id; private String firstname; private String lastname; + @CreatedBy + private String createdBy; + @CreatedDate + private long createdDate; + @LastModifiedBy + private String lastModifiedBy; + @LastModifiedDate + private long lastModifiedDate; + @Version + long version; @PersistenceConstructor public User(final String id, final String firstname, final String lastname) { @@ -49,6 +65,26 @@ public class User { return lastname; } + public long getCreatedDate() { + return createdDate; + } + + public String getCreator() { + return createdBy; + } + + public long getLastModifiedDate() { + return lastModifiedDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public long getVersion() { + return version; + } + @Override public boolean equals(Object o) { if (this == o) @@ -56,8 +92,8 @@ public class User { if (o == null || getClass() != o.getClass()) return false; User user = (User) o; - return Objects.equals(id, user.id) && Objects.equals(firstname, user.firstname) - && Objects.equals(lastname, user.lastname); + return Objects.equals(id, user.id) && Objects.equals(firstname, user.firstname) && Objects.equals(lastname, + user.lastname); } @Override @@ -67,6 +103,8 @@ public class User { @Override public String toString() { - return "User{" + "id='" + id + '\'' + ", firstname='" + firstname + '\'' + ", lastname='" + lastname + '\'' + '}'; + return "User{" + "id='" + id + '\'' + ", firstname='" + firstname + '\'' + ", lastname='" + lastname + '\'' + + ", createdBy='" + createdBy + '\'' + ", createdDate='" + createdDate + '\'' + ", lastModifiedBy='" + + lastModifiedBy + '\'' + ", lastModifiedDate='" + lastModifiedDate + '\'' + '}'; } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java index 923e678b..01a41099 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java @@ -20,7 +20,13 @@ import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.UUID; +/** + * User Repository for tests + * @author Michael Nitschinger + * @author Michael Reiche + */ @Repository public interface UserRepository extends PagingAndSortingRepository { diff --git a/src/test/java/org/springframework/data/couchbase/domain/time/AuditingDateTimeProvider.java b/src/test/java/org/springframework/data/couchbase/domain/time/AuditingDateTimeProvider.java new file mode 100644 index 00000000..4782e457 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/time/AuditingDateTimeProvider.java @@ -0,0 +1,24 @@ +package org.springframework.data.couchbase.domain.time; + +import org.springframework.data.auditing.DateTimeProvider; + +import java.time.Instant; +import java.time.temporal.TemporalAccessor; +import java.util.Optional; + +public class AuditingDateTimeProvider implements DateTimeProvider { + + private DateTimeService dateTimeService = new FixedDateTimeService(); + + public AuditingDateTimeProvider() { + } + + public AuditingDateTimeProvider(DateTimeService dateTimeService) { + this.dateTimeService = dateTimeService; + } + + @Override + public Optional getNow() { + return Optional.of(Instant.ofEpochSecond(dateTimeService.getCurrentDateAndTime().toEpochSecond())); + } +} \ No newline at end of file diff --git a/src/test/java/org/springframework/data/couchbase/domain/time/CurrentDateTimeService.java b/src/test/java/org/springframework/data/couchbase/domain/time/CurrentDateTimeService.java new file mode 100644 index 00000000..1d195305 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/time/CurrentDateTimeService.java @@ -0,0 +1,10 @@ +package org.springframework.data.couchbase.domain.time; + +import java.time.ZonedDateTime; + +public class CurrentDateTimeService implements DateTimeService { + @Override + public ZonedDateTime getCurrentDateAndTime() { + return ZonedDateTime.now(); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/time/DateTimeService.java b/src/test/java/org/springframework/data/couchbase/domain/time/DateTimeService.java new file mode 100644 index 00000000..49674167 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/time/DateTimeService.java @@ -0,0 +1,7 @@ +package org.springframework.data.couchbase.domain.time; + +import java.time.ZonedDateTime; + +public interface DateTimeService { + ZonedDateTime getCurrentDateAndTime(); +} \ No newline at end of file diff --git a/src/test/java/org/springframework/data/couchbase/domain/time/FixedDateTimeService.java b/src/test/java/org/springframework/data/couchbase/domain/time/FixedDateTimeService.java new file mode 100644 index 00000000..0d234a99 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/time/FixedDateTimeService.java @@ -0,0 +1,15 @@ +package org.springframework.data.couchbase.domain.time; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public class FixedDateTimeService implements DateTimeService { + @Override + public ZonedDateTime getCurrentDateAndTime() { + return ZonedDateTime.of(2020, 1, 1, 0, 0, 0, 0, ZoneId.of("GMT-8")); + } + + public static void main(String[] args) { + System.out.println((new FixedDateTimeService()).getCurrentDateAndTime()); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java index 003cc0ca..9ccd2433 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java @@ -28,21 +28,30 @@ import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +/** + * Repository KV tests + * + * @author Michael Nitschinger + * @author Michael Reiche + */ @SpringJUnitConfig(CouchbaseRepositoryKeyValueIntegrationTests.Config.class) @IgnoreWhen(clusterTypes = ClusterType.MOCKED) public class CouchbaseRepositoryKeyValueIntegrationTests extends ClusterAwareIntegrationTests { - @Autowired UserRepository userRepository; + @Autowired + UserRepository userRepository; @Test + @IgnoreWhen(clusterTypes = ClusterType.MOCKED) void saveAndFindById() { User user = new User(UUID.randomUUID().toString(), "f", "l"); - + // this currently fails when using mocked in integration.properties with status "UNKNOWN" assertFalse(userRepository.existsById(user.getId())); userRepository.save(user); @@ -52,6 +61,7 @@ public class CouchbaseRepositoryKeyValueIntegrationTests extends ClusterAwareInt found.ifPresent(u -> assertEquals(user, u)); assertTrue(userRepository.existsById(user.getId())); + userRepository.delete(user); } @Configuration