diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 8d179a2ab..8ca2a67b6 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -24,6 +24,8 @@ include::reference/elasticsearch-clients.adoc[] include::reference/elasticsearch-object-mapping.adoc[] include::reference/elasticsearch-operations.adoc[] include::reference/elasticsearch-repositories.adoc[] +include::{spring-data-commons-docs}/auditing.adoc[] +include::reference/elasticsearch-auditing.adoc[] include::reference/elasticsearch-misc.adoc[] :leveloffset: -1 diff --git a/src/main/asciidoc/reference/elasticsearch-auditing.adoc b/src/main/asciidoc/reference/elasticsearch-auditing.adoc new file mode 100644 index 000000000..4b805cbdf --- /dev/null +++ b/src/main/asciidoc/reference/elasticsearch-auditing.adoc @@ -0,0 +1,68 @@ +[[elasticsearch.auditing]] +== Elasticsearch Auditing + +=== Preparing entities + +In order for the auditing code to be able to decide wether an entity instance is new, the entity must implement the `Persistable` interface which is defined as follows: + +[source,java] +---- +package org.springframework.data.domain; + +import org.springframework.lang.Nullable; + +public interface Persistable { + @Nullable + ID getId(); + + boolean isNew(); +} +---- + +As the existence of an Id is not a sufficient criterion to determine if an enitity is new in Elasticsearch, additional information is necessary. One way is to use the creation-relevant auditing fields for this decision: + +A `Person` entity might look as follows - omitting getter and setter methods for brevity: + +[source,java] +---- +@Document(indexName = "person") +public class Person implements Persistable { + @Id private Long id; + private String lastName; + private String firstName; + @Field(type = FieldType.Date, format = DateFormat.basic_date_time) + private Instant createdDate; + private String createdBy + @Field(type = FieldType.Date, format = DateFormat.basic_date_time) + private Instant lastModifiedDate; + private String lastModifiedBy; + + public Long getId() { <1> + return id; + } + + @Override + public boolean isNew() { + return id == null || (createdDate == null && createdBy == null); <2> + } +} +---- +<1> the getter also is the required implementation from the interface +<2> an object is new if it either has no `id` or none of fields containing creation attributes are set. + +=== Activating auditing + +After the entities have been set up and providing the `AuditorAware` the Auditing must be activated by setting the `@EnableElasticsearchAuditing` on a configuration class: + +[source,java] +---- +@Configuration +@EnableElasticsearchRepositories +@EnableElasticsearchAuditing +class MyConfiguration { + // configuration code +} +---- + +If your code contains more than one `AuditorAware` bean for different types, you must provide the name of the bean to use as an argument to the `auditorAwareRef` parameter of the + `@EnableElasticsearchAuditing` annotation. diff --git a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingBeanDefinitionParser.java b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingBeanDefinitionParser.java new file mode 100644 index 000000000..92ba52477 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingBeanDefinitionParser.java @@ -0,0 +1,109 @@ +/* + * 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.elasticsearch.config; + +import static org.springframework.data.config.ParsingUtils.*; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.data.auditing.config.IsNewAwareAuditingHandlerBeanDefinitionParser; +import org.springframework.data.elasticsearch.core.event.AuditingEntityCallback; +import org.springframework.data.elasticsearch.core.event.ReactiveAuditingEntityCallback; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +/** + * {@link BeanDefinitionParser} to register a {@link AuditingEntityCallback} to transparently set auditing information + * on an entity. + * + * @author Peter-Josef Meisch + */ +public class ElasticsearchAuditingBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + private static String MAPPING_CONTEXT_BEAN_NAME = "simpleElasticsearchMappingContext"; + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element) + */ + @Override + protected Class getBeanClass(Element element) { + return AuditingEntityCallback.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#shouldGenerateId() + */ + @Override + protected boolean shouldGenerateId() { + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder) + */ + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + + String mappingContextRef = element.getAttribute("mapping-context-ref"); + + if (!StringUtils.hasText(mappingContextRef)) { + + BeanDefinitionRegistry registry = parserContext.getRegistry(); + + if (!registry.containsBeanDefinition(MAPPING_CONTEXT_BEAN_NAME)) { + registry.registerBeanDefinition(MAPPING_CONTEXT_BEAN_NAME, + new RootBeanDefinition(SimpleElasticsearchMappingContext.class)); + } + + mappingContextRef = MAPPING_CONTEXT_BEAN_NAME; + } + + IsNewAwareAuditingHandlerBeanDefinitionParser parser = new IsNewAwareAuditingHandlerBeanDefinitionParser( + mappingContextRef); + parser.parse(element, parserContext); + + AbstractBeanDefinition isNewAwareAuditingHandler = getObjectFactoryBeanDefinition(parser.getResolvedBeanName(), + parserContext.extractSource(element)); + builder.addConstructorArgValue(isNewAwareAuditingHandler); + + if (ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)) { + registerReactiveAuditingEntityCallback(parserContext.getRegistry(), isNewAwareAuditingHandler, + parserContext.extractSource(element)); + } + } + + private void registerReactiveAuditingEntityCallback(BeanDefinitionRegistry registry, + AbstractBeanDefinition isNewAwareAuditingHandler, @Nullable Object source) { + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEntityCallback.class); + + builder.addConstructorArgValue(isNewAwareAuditingHandler); + builder.getRawBeanDefinition().setSource(source); + + registry.registerBeanDefinition(ReactiveAuditingEntityCallback.class.getName(), builder.getBeanDefinition()); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrar.java b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrar.java new file mode 100644 index 000000000..1b9b765f2 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrar.java @@ -0,0 +1,181 @@ +/* + * 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.elasticsearch.config; + +import java.lang.annotation.Annotation; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; +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.elasticsearch.core.convert.MappingElasticsearchConverter; +import org.springframework.data.elasticsearch.core.event.AuditingEntityCallback; +import org.springframework.data.elasticsearch.core.event.ReactiveAuditingEntityCallback; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.util.Assert; + +/** + * {@link ImportBeanDefinitionRegistrar} to enable {@link EnableElasticsearchAuditing} annotation. + * + * @author Peter-Josef Meisch + * @since 4.0 + */ +class ElasticsearchAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAnnotation() + */ + @Override + protected Class getAnnotation() { + return EnableElasticsearchAuditing.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditingHandlerBeanName() + */ + @Override + protected String getAuditingHandlerBeanName() { + return "elasticsearchAuditingHandler"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry) + */ + @Override + public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) { + + Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null!"); + Assert.notNull(registry, "BeanDefinitionRegistry must not be null!"); + + super.registerBeanDefinitions(annotationMetadata, registry); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditHandlerBeanDefinitionBuilder(org.springframework.data.auditing.config.AuditingConfiguration) + */ + @Override + protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) { + + Assert.notNull(configuration, "AuditingConfiguration must not be null!"); + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(IsNewAwareAuditingHandler.class); + + BeanDefinitionBuilder definition = BeanDefinitionBuilder + .genericBeanDefinition(ElasticsearchMappingContextLookup.class); + definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + + builder.addConstructorArgValue(definition.getBeanDefinition()); + return configureDefaultAuditHandlerAttributes(configuration, builder); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#registerAuditListener(org.springframework.beans.factory.config.BeanDefinition, org.springframework.beans.factory.support.BeanDefinitionRegistry) + */ + @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(AuditingEntityCallback.class); + listenerBeanDefinitionBuilder + .addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); + + registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(), + AuditingEntityCallback.class.getName(), registry); + + if (ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)) { + registerReactiveAuditingEntityCallback(registry, auditingHandlerDefinition.getSource()); + } + } + + private void registerReactiveAuditingEntityCallback(BeanDefinitionRegistry registry, Object source) { + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEntityCallback.class); + + builder.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry)); + builder.getRawBeanDefinition().setSource(source); + + registerInfrastructureBeanWithId(builder.getBeanDefinition(), ReactiveAuditingEntityCallback.class.getName(), + registry); + } + + /** + * Simple helper to be able to wire the {@link MappingContext} from a {@link MappingElasticsearchConverter} bean + * available in the application context. + * + * @author Oliver Gierke + */ + static class ElasticsearchMappingContextLookup implements + FactoryBean, ElasticsearchPersistentProperty>> { + + private final MappingElasticsearchConverter converter; + + /** + * Creates a new {@link ElasticsearchMappingContextLookup} for the given {@link MappingElasticsearchConverter}. + * + * @param converter must not be {@literal null}. + */ + public ElasticsearchMappingContextLookup(MappingElasticsearchConverter converter) { + this.converter = converter; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObject() + */ + @Override + public MappingContext, ElasticsearchPersistentProperty> getObject() + throws Exception { + return converter.getMappingContext(); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ + @Override + public Class getObjectType() { + return MappingContext.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#isSingleton() + */ + @Override + public boolean isSingleton() { + return true; + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/config/EnableElasticsearchAuditing.java b/src/main/java/org/springframework/data/elasticsearch/config/EnableElasticsearchAuditing.java new file mode 100644 index 000000000..a2e6e9f7e --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/config/EnableElasticsearchAuditing.java @@ -0,0 +1,70 @@ +/* + * 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.elasticsearch.config; + +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 Elasticsearch via annotation configuration. + * + * @author Peter-Josef Meisch + * @since 4.0 + */ +@Inherited +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(ElasticsearchAuditingRegistrar.class) +public @interface EnableElasticsearchAuditing { + + /** + * Configures the {@link AuditorAware} bean to be used to lookup the current principal. + * + * @return + */ + String auditorAwareRef() default ""; + + /** + * Configures whether the creation and modification dates are set. Defaults to {@literal true}. + * + * @return + */ + boolean setDates() default true; + + /** + * Configures whether the entity shall be marked as modified on creation. Defaults to {@literal true}. + * + * @return + */ + 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. + * + * @return + */ + String dateTimeProviderRef() default ""; +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java index c2bbb4173..553a68781 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/AbstractElasticsearchTemplate.java @@ -6,7 +6,6 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -23,6 +22,7 @@ import org.springframework.data.elasticsearch.ElasticsearchException; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse; +import org.springframework.data.elasticsearch.core.event.BeforeConvertCallback; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; @@ -33,6 +33,7 @@ import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder; import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.data.elasticsearch.core.query.Query; +import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.util.CloseableIterator; import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; @@ -49,6 +50,8 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper protected @Nullable ElasticsearchConverter elasticsearchConverter; protected @Nullable RequestFactory requestFactory; + private @Nullable EntityCallbacks entityCallbacks; + // region Initialization protected void initialize(ElasticsearchConverter elasticsearchConverter) { @@ -66,12 +69,33 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper } @Override - public void setApplicationContext(ApplicationContext context) throws BeansException { + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + + if (entityCallbacks == null) { + setEntityCallbacks(EntityCallbacks.create(applicationContext)); + } if (elasticsearchConverter instanceof ApplicationContextAware) { - ((ApplicationContextAware) elasticsearchConverter).setApplicationContext(context); + ((ApplicationContextAware) elasticsearchConverter).setApplicationContext(applicationContext); } } + + /** + * Set the {@link EntityCallbacks} instance to use when invoking {@link EntityCallbacks callbacks} like the + * {@link org.springframework.data.elasticsearch.core.event.BeforeConvertCallback}. + *

+ * Overrides potentially existing {@link EntityCallbacks}. + * + * @param entityCallbacks must not be {@literal null}. + * @throws IllegalArgumentException if the given instance is {@literal null}. + * @since 4.0 + */ + public void setEntityCallbacks(EntityCallbacks entityCallbacks) { + + Assert.notNull(entityCallbacks, "entityCallbacks must not be null"); + + this.entityCallbacks = entityCallbacks; + } // endregion // region DocumentOperations @@ -388,6 +412,37 @@ public abstract class AbstractElasticsearchTemplate implements ElasticsearchOper .withObject(entity) // .build(); } + // endregion + + // region callbacks + protected T maybeCallbackBeforeConvert(T entity) { + + if (entityCallbacks != null) { + return entityCallbacks.callback(BeforeConvertCallback.class, entity); + } + + return entity; + } + + protected void maybeCallbackBeforeConvertWithQuery(Object query) { + + if (query instanceof IndexQuery) { + IndexQuery indexQuery = (IndexQuery) query; + Object queryObject = indexQuery.getObject(); + + if (queryObject != null) { + queryObject = maybeCallbackBeforeConvert(queryObject); + indexQuery.setObject(queryObject); + } + } + } + + // this can be called with either a List or a List; these query classes + // don't have a common bas class, therefore the List argument + protected void maybeCallbackBeforeConvertWithQueries(List queries) { + queries.forEach(this::maybeCallbackBeforeConvertWithQuery); + } // endregion + } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchRestTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchRestTemplate.java index b7b014420..147d66b7e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchRestTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchRestTemplate.java @@ -37,6 +37,7 @@ import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.reindex.DeleteByQueryRequest; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.search.suggest.SuggestBuilder; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; @@ -134,12 +135,16 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate { // region DocumentOperations @Override public String index(IndexQuery query, IndexCoordinates index) { + + maybeCallbackBeforeConvertWithQuery(query); + IndexRequest request = requestFactory.indexRequest(query, index); String documentId = execute(client -> client.index(request, RequestOptions.DEFAULT).getId()); // We should call this because we are not going through a mapper. - if (query.getObject() != null) { - setPersistentEntityId(query.getObject(), documentId); + Object queryObject = query.getObject(); + if (queryObject != null) { + setPersistentEntityId(queryObject, documentId); } return documentId; } @@ -166,6 +171,7 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate { @Override protected boolean doExists(String id, IndexCoordinates index) { GetRequest request = requestFactory.getRequest(id, index); + request.fetchSourceContext(FetchSourceContext.DO_NOT_FETCH_SOURCE); return execute(client -> client.get(request, RequestOptions.DEFAULT).isExists()); } @@ -219,6 +225,7 @@ public class ElasticsearchRestTemplate extends AbstractElasticsearchTemplate { } private List doBulkOperation(List queries, BulkOptions bulkOptions, IndexCoordinates index) { + maybeCallbackBeforeConvertWithQueries(queries); BulkRequest bulkRequest = requestFactory.bulkRequest(queries, bulkOptions, index); return checkForBulkOperationFailure(execute(client -> client.bulk(bulkRequest, RequestOptions.DEFAULT))); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java index 4042c32e6..d8ef07650 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplate.java @@ -136,12 +136,18 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate { // region DocumentOperations @Override public String index(IndexQuery query, IndexCoordinates index) { + + maybeCallbackBeforeConvertWithQuery(query); + IndexRequestBuilder indexRequestBuilder = requestFactory.indexRequestBuilder(client, query, index); String documentId = indexRequestBuilder.execute().actionGet().getId(); + // We should call this because we are not going through a mapper. - if (query.getObject() != null) { - setPersistentEntityId(query.getObject(), documentId); + Object queryObject = query.getObject(); + if (queryObject != null) { + setPersistentEntityId(queryObject, documentId); } + return documentId; } @@ -167,6 +173,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate { @Override protected boolean doExists(String id, IndexCoordinates index) { GetRequestBuilder getRequestBuilder = requestFactory.getRequestBuilder(client, id, index); + getRequestBuilder.setFetchSource(false); return getRequestBuilder.execute().actionGet().isExists(); } @@ -223,6 +230,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate { } private List doBulkOperation(List queries, BulkOptions bulkOptions, IndexCoordinates index) { + maybeCallbackBeforeConvertWithQueries(queries); BulkRequestBuilder bulkRequest = requestFactory.bulkRequestBuilder(client, queries, bulkOptions, index); return checkForBulkOperationFailure(bulkRequest.execute().actionGet()); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java index b0ed7c877..519c06f0e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java @@ -56,6 +56,9 @@ import org.elasticsearch.search.sort.SortOrder; import org.reactivestreams.Publisher; 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.domain.Sort; import org.springframework.data.elasticsearch.ElasticsearchException; import org.springframework.data.elasticsearch.NoSuchIndexException; @@ -67,6 +70,7 @@ import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchC import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.document.DocumentAdapters; import org.springframework.data.elasticsearch.core.document.SearchDocument; +import org.springframework.data.elasticsearch.core.event.ReactiveBeforeConvertCallback; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; @@ -78,6 +82,7 @@ import org.springframework.data.elasticsearch.core.query.NativeSearchQuery; import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.core.query.UpdateQuery; +import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; import org.springframework.data.mapping.context.MappingContext; import org.springframework.http.HttpStatus; import org.springframework.lang.Nullable; @@ -93,7 +98,7 @@ import org.springframework.util.Assert; * @author Aleksei Arsenev * @since 3.2 */ -public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOperations { +public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOperations, ApplicationContextAware { private static final Logger QUERY_LOGGER = LoggerFactory .getLogger("org.springframework.data.elasticsearch.core.QUERY"); @@ -108,6 +113,8 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera private @Nullable RefreshPolicy refreshPolicy = RefreshPolicy.IMMEDIATE; private @Nullable IndicesOptions indicesOptions = IndicesOptions.strictExpandOpenAndForbidClosedIgnoreThrottled(); + private @Nullable ReactiveEntityCallbacks entityCallbacks; + // region Initialization public ReactiveElasticsearchTemplate(ReactiveElasticsearchClient client) { this(client, new MappingElasticsearchConverter(new SimpleElasticsearchMappingContext())); @@ -125,6 +132,31 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera this.operations = new EntityOperations(this.mappingContext); this.requestFactory = new RequestFactory(converter); } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + + if (entityCallbacks == null) { + setEntityCallbacks(ReactiveEntityCallbacks.create(applicationContext)); + } + } + + /** + * Set the {@link ReactiveEntityCallbacks} instance to use when invoking {@link ReactiveEntityCallbacks callbacks} + * like the {@link ReactiveBeforeConvertCallback}. + *

+ * Overrides potentially existing {@link ReactiveEntityCallbacks}. + * + * @param entityCallbacks must not be {@literal null}. + * @throws IllegalArgumentException if the given instance is {@literal null}. + * @since 4.0 + */ + public void setEntityCallbacks(ReactiveEntityCallbacks entityCallbacks) { + + Assert.notNull(entityCallbacks, "EntityCallbacks must not be null!"); + + this.entityCallbacks = entityCallbacks; + } // endregion // region DocumentOperations @@ -289,7 +321,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera private Mono doIndex(Object value, AdaptibleEntity entity, IndexCoordinates index) { - return Mono.defer(() -> { + return maybeCallBeforeConvert(value).flatMap(it -> { IndexRequest request = getIndexRequest(value, entity, index); request = prepareIndexRequest(value, request); return doIndex(request); @@ -821,4 +853,14 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera return potentiallyTranslatedException != null ? potentiallyTranslatedException : runtimeException; } + // region callbacks + protected Mono maybeCallBeforeConvert(T entity) { + + if (null != entityCallbacks) { + return entityCallbacks.callback(ReactiveBeforeConvertCallback.class, entity); + } + + return Mono.just(entity); + } + // endregion } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitSupport.java b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitSupport.java index 842f9d39f..4947e62d1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/SearchHitSupport.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/SearchHitSupport.java @@ -25,7 +25,7 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage; import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl; -import org.springframework.data.elasticsearch.support.ReactiveSupport; +import org.springframework.data.repository.util.ReactiveWrappers; import org.springframework.lang.Nullable; /** @@ -78,7 +78,7 @@ public final class SearchHitSupport { return unwrapSearchHits(searchHits.getSearchHits()); } - if (ReactiveSupport.isReactorAvailable()) { + if (ReactiveWrappers.isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR)) { if (result instanceof Flux) { Flux flux = (Flux) result; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java index ab7cb01e6..28d74c2f6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java @@ -15,17 +15,8 @@ */ package org.springframework.data.elasticsearch.core.convert; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; import org.elasticsearch.search.aggregations.Aggregations; @@ -38,8 +29,6 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.convert.CustomConversions; -import org.springframework.data.convert.EntityInstantiator; -import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.ElasticsearchException; import org.springframework.data.elasticsearch.annotations.ScriptedField; @@ -57,6 +46,8 @@ import org.springframework.data.elasticsearch.core.query.CriteriaQuery; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.util.ClassTypeInformation; @@ -108,6 +99,7 @@ public class MappingElasticsearchConverter @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + if (mappingContext instanceof ApplicationContextAware) { ((ApplicationContextAware) mappingContext).setApplicationContext(applicationContext); } @@ -653,7 +645,7 @@ public class MappingElasticsearchConverter collectionSource.map(it -> { if (it == null) { - //noinspection ReturnOfNull + // noinspection ReturnOfNull return null; } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallback.java new file mode 100644 index 000000000..779b32ba5 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallback.java @@ -0,0 +1,56 @@ +/* + * 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.elasticsearch.core.event; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.core.Ordered; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.util.Assert; + +/** + * {@link EntityCallback} to populate auditing related fields on an entity about to be saved. + * + * @author Peter-Josef Meisch + * @since 4.0 + */ +public class AuditingEntityCallback implements BeforeConvertCallback, Ordered { + + private final ObjectFactory auditingHandlerFactory; + + /** + * Creates a new {@link AuditingEntityCallback} using the given {@link IsNewAwareAuditingHandler} provided by the + * given {@link ObjectFactory}. + * + * @param auditingHandlerFactory must not be {@literal null}. + */ + public AuditingEntityCallback(ObjectFactory auditingHandlerFactory) { + + Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); + + this.auditingHandlerFactory = auditingHandlerFactory; + } + + @Override + public Object onBeforeConvert(Object entity) { + return auditingHandlerFactory.getObject().markAudited(entity); + } + + @Override + public int getOrder() { + return 100; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/support/ReactiveSupport.java b/src/main/java/org/springframework/data/elasticsearch/core/event/BeforeConvertCallback.java similarity index 54% rename from src/main/java/org/springframework/data/elasticsearch/support/ReactiveSupport.java rename to src/main/java/org/springframework/data/elasticsearch/core/event/BeforeConvertCallback.java index 205ec8e9f..85d50706f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/support/ReactiveSupport.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/BeforeConvertCallback.java @@ -13,27 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.elasticsearch.support; +package org.springframework.data.elasticsearch.core.event; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.springframework.data.repository.util.ClassUtils; +import org.springframework.data.mapping.callback.EntityCallback; /** + * Callback being invoked before a domain object is converted to be persisted. + * * @author Peter-Josef Meisch * @since 4.0 */ -public final class ReactiveSupport { - private ReactiveSupport() {} +@FunctionalInterface +public interface BeforeConvertCallback extends EntityCallback { /** - * @return true if project reactor is on the classpath + * Callback method that will be invoked before an entity is persisted. Can return the same or a different instance of + * the domain entity class. + * + * @param entity the entity being converted + * @return the entity to be converted */ - public static boolean isReactorAvailable() { - AtomicBoolean available = new AtomicBoolean(false); - ClassUtils.ifPresent("reactor.core.publisher.Flux", null, aClass -> { - available.set(true); - }); - return available.get(); - } + T onBeforeConvert(T entity); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallback.java new file mode 100644 index 000000000..4051d5f71 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallback.java @@ -0,0 +1,58 @@ +/* + * 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.elasticsearch.core.event; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.core.Ordered; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.mapping.callback.EntityCallback; +import org.springframework.util.Assert; + +/** + * {@link EntityCallback} to populate auditing related fields on an entity about to be saved. + * + * @author Peter-Josef Meisch + * @since 4.0 + */ +public class ReactiveAuditingEntityCallback implements ReactiveBeforeConvertCallback, Ordered { + + private final ObjectFactory auditingHandlerFactory; + + /** + * Creates a new {@link ReactiveAuditingEntityCallback} using the given {@link IsNewAwareAuditingHandler} provided by + * the given {@link ObjectFactory}. + * + * @param auditingHandlerFactory must not be {@literal null}. + */ + public ReactiveAuditingEntityCallback(ObjectFactory auditingHandlerFactory) { + + Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!"); + + this.auditingHandlerFactory = auditingHandlerFactory; + } + + @Override + public Mono onBeforeConvert(Object entity) { + return Mono.just(auditingHandlerFactory.getObject().markAudited(entity)); + } + + @Override + public int getOrder() { + return 100; + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveBeforeConvertCallback.java b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveBeforeConvertCallback.java new file mode 100644 index 000000000..e09b3ed25 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/ReactiveBeforeConvertCallback.java @@ -0,0 +1,38 @@ +/* + * 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.elasticsearch.core.event; + +import org.reactivestreams.Publisher; +import org.springframework.data.mapping.callback.EntityCallback; + +/** + * Callback being invoked before a domain object is converted to be persisted. + * + * @author Peter-Josef Meisch + * @since 4.0 + */ +@FunctionalInterface +public interface ReactiveBeforeConvertCallback extends EntityCallback { + + /** + * Callback method that will be invoked before an entity is persisted. Can return the same or a different instance of + * the domain entity class. + * + * @param entity the entity being converted + * @return the entity to be converted + */ + Publisher onBeforeConvert(T entity); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/event/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/event/package-info.java new file mode 100644 index 000000000..a48d01eb0 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/event/package-info.java @@ -0,0 +1,6 @@ +/** + * classes and interfaces related to Spring Data Elasticsearch events and callbacks. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.data.elasticsearch.core.event; diff --git a/src/test/java/org/springframework/data/elasticsearch/JUnit5SampleReactiveRestClientBasedTests.java b/src/test/java/org/springframework/data/elasticsearch/JUnit5SampleReactiveRestClientBasedTests.java new file mode 100644 index 000000000..b7c3856f9 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/JUnit5SampleReactiveRestClientBasedTests.java @@ -0,0 +1,47 @@ +/* + * 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.elasticsearch; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.test.context.ContextConfiguration; + +/** + * class demonstrating the setup of a JUnit 5 test in Spring Data Elasticsearch that uses the reactive rest client. The + * ContextConfiguration must include the {@link ElasticsearchRestTemplateConfiguration} class. + * + * @author Peter-Josef Meisch + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { ReactiveElasticsearchRestTemplateConfiguration.class }) +@DisplayName("a sample JUnit 5 test with reactive rest client") +public class JUnit5SampleReactiveRestClientBasedTests { + + @Autowired private ReactiveElasticsearchOperations elasticsearchOperations; + + @Test + @DisplayName("should have a ReactiveElasticsearchOperations") + void shouldHaveARestTemplate() { + assertThat(elasticsearchOperations).isNotNull().isInstanceOf(ReactiveElasticsearchOperations.class); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/config/AuditingIntegrationTest.java b/src/test/java/org/springframework/data/elasticsearch/config/AuditingIntegrationTest.java new file mode 100644 index 000000000..4c5f80d21 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/config/AuditingIntegrationTest.java @@ -0,0 +1,142 @@ +/* + * 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.elasticsearch.config; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.domain.Persistable; +import org.springframework.data.elasticsearch.core.event.BeforeConvertCallback; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.lang.Nullable; + +/** + * @author Peter-Josef Meisch + */ +public abstract class AuditingIntegrationTest { + + public static AuditorAware auditorProvider() { + return new AuditorAware() { + int count = 0; + + @Override + public Optional getCurrentAuditor() { + return Optional.of("Auditor " + (++count)); + } + }; + } + + @Autowired ApplicationContext applicationContext; + + @Test // DATAES-68 + void shouldEnableAuditingAndSetAuditingDates() throws InterruptedException { + SimpleElasticsearchMappingContext mappingContext = applicationContext + .getBean(SimpleElasticsearchMappingContext.class); + + mappingContext.getPersistentEntity(Entity.class); + + EntityCallbacks callbacks = EntityCallbacks.create(applicationContext); + + Entity entity = new Entity(); + entity.setId("1"); + entity = callbacks.callback(BeforeConvertCallback.class, entity); + + assertThat(entity.getCreated()).isNotNull(); + assertThat(entity.getModified()).isEqualTo(entity.created); + assertThat(entity.getCreatedBy()).isEqualTo("Auditor 1"); + assertThat(entity.getModifiedBy()).isEqualTo("Auditor 1"); + + Thread.sleep(10); + + entity = callbacks.callback(BeforeConvertCallback.class, entity); + + assertThat(entity.getCreated()).isNotNull(); + assertThat(entity.getModified()).isNotEqualTo(entity.created); + assertThat(entity.getCreatedBy()).isEqualTo("Auditor 1"); + assertThat(entity.getModifiedBy()).isEqualTo("Auditor 2"); + } + + static class Entity implements Persistable { + private @Nullable @Id String id; + private @Nullable @CreatedDate LocalDateTime created; + private @Nullable LocalDateTime modified; + private @Nullable @CreatedBy String createdBy; + private @Nullable @LastModifiedBy String modifiedBy; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public LocalDateTime getCreated() { + return created; + } + + public void setCreated(@Nullable LocalDateTime created) { + this.created = created; + } + + public void setModified(@Nullable LocalDateTime modified) { + this.modified = modified; + } + + @Nullable + @LastModifiedDate + public LocalDateTime getModified() { + return modified; + } + + @Nullable + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(@Nullable String createdBy) { + this.createdBy = createdBy; + } + + @Nullable + public String getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(@Nullable String modifiedBy) { + this.modifiedBy = modifiedBy; + } + + @Override + public boolean isNew() { + return id == null || (created == null && createdBy == null); + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrarUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrarUnitTests.java new file mode 100644 index 000000000..0b21105f6 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchAuditingRegistrarUnitTests.java @@ -0,0 +1,50 @@ +/* + * 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.elasticsearch.config; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Unit tests for {@link ElasticsearchAuditingRegistrar}. + * + * @author Oliver Gierke + * @author Peter-Josef Meisch + */ +@ExtendWith(MockitoExtension.class) +public class ElasticsearchAuditingRegistrarUnitTests { + + ElasticsearchAuditingRegistrar registrar = new ElasticsearchAuditingRegistrar(); + + @Mock AnnotationMetadata metadata; + @Mock BeanDefinitionRegistry registry; + + @Test // DATAES-68 + public void rejectsNullAnnotationMetadata() { + assertThatIllegalArgumentException().isThrownBy(() -> registrar.registerBeanDefinitions(null, registry)); + } + + @Test // DATAES-68 + public void rejectsNullBeanDefinitionRegistry() { + assertThatIllegalArgumentException().isThrownBy(() -> registrar.registerBeanDefinitions(metadata, null)); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchRestAuditingIntegrationTest.java b/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchRestAuditingIntegrationTest.java new file mode 100644 index 000000000..99f0134e3 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchRestAuditingIntegrationTest.java @@ -0,0 +1,41 @@ +/* + * 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.elasticsearch.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Peter-Josef Meisch + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { ElasticsearchRestAuditingIntegrationTest.Config.class }) +public class ElasticsearchRestAuditingIntegrationTest extends AuditingIntegrationTest { + + @Import({ ElasticsearchRestTemplateConfiguration.class }) + @EnableElasticsearchAuditing(auditorAwareRef = "auditorAware") + static class Config { + + @Bean + public AuditorAware auditorAware() { + return auditorProvider(); + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchTransportAuditingIntegrationTest.java b/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchTransportAuditingIntegrationTest.java new file mode 100644 index 000000000..2526bbf03 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/config/ElasticsearchTransportAuditingIntegrationTest.java @@ -0,0 +1,41 @@ +/* + * 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.elasticsearch.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Peter-Josef Meisch + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { ElasticsearchTransportAuditingIntegrationTest.Config.class }) +public class ElasticsearchTransportAuditingIntegrationTest extends AuditingIntegrationTest { + + @Import({ ElasticsearchTemplateConfiguration.class }) + @EnableElasticsearchAuditing(auditorAwareRef = "auditorAware") + static class Config { + + @Bean + public AuditorAware auditorAware() { + return auditorProvider(); + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/config/ReactiveAuditingIntegrationTest.java b/src/test/java/org/springframework/data/elasticsearch/config/ReactiveAuditingIntegrationTest.java new file mode 100644 index 000000000..a9bf3541c --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/config/ReactiveAuditingIntegrationTest.java @@ -0,0 +1,160 @@ +/* + * 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.elasticsearch.config; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.domain.Persistable; +import org.springframework.data.elasticsearch.core.event.ReactiveBeforeConvertCallback; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; +import org.springframework.lang.Nullable; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Peter-Josef Meisch + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { ReactiveAuditingIntegrationTest.Config.class }) +public class ReactiveAuditingIntegrationTest { + + public static AuditorAware auditorProvider() { + return new AuditorAware() { + int count = 0; + + @Override + public Optional getCurrentAuditor() { + return Optional.of("Auditor " + (++count)); + } + }; + } + + @Import({ ReactiveElasticsearchRestTemplateConfiguration.class }) + @EnableElasticsearchAuditing(auditorAwareRef = "auditorAware") + static class Config { + + @Bean + public AuditorAware auditorAware() { + return auditorProvider(); + } + } + + @Autowired ApplicationContext applicationContext; + + @Test // DATAES-68 + void shouldEnableAuditingAndSetAuditingDates() throws InterruptedException { + SimpleElasticsearchMappingContext mappingContext = applicationContext + .getBean(SimpleElasticsearchMappingContext.class); + + mappingContext.getPersistentEntity(Entity.class); + + ReactiveEntityCallbacks callbacks = ReactiveEntityCallbacks.create(applicationContext); + + Entity entity = new Entity(); + entity.setId("1"); + entity = callbacks.callback(ReactiveBeforeConvertCallback.class, entity).block(); + + assertThat(entity.getCreated()).isNotNull(); + assertThat(entity.getModified()).isEqualTo(entity.created); + assertThat(entity.getCreatedBy()).isEqualTo("Auditor 1"); + assertThat(entity.getModifiedBy()).isEqualTo("Auditor 1"); + + Thread.sleep(10); + + entity = callbacks.callback(ReactiveBeforeConvertCallback.class, entity).block(); + + assertThat(entity.getCreated()).isNotNull(); + assertThat(entity.getModified()).isNotEqualTo(entity.created); + assertThat(entity.getCreatedBy()).isEqualTo("Auditor 1"); + assertThat(entity.getModifiedBy()).isEqualTo("Auditor 2"); + } + + static class Entity implements Persistable { + private @Nullable @Id String id; + private @Nullable @CreatedDate LocalDateTime created; + private @Nullable LocalDateTime modified; + private @Nullable @CreatedBy String createdBy; + private @Nullable @LastModifiedBy String modifiedBy; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public LocalDateTime getCreated() { + return created; + } + + public void setCreated(@Nullable LocalDateTime created) { + this.created = created; + } + + public void setModified(@Nullable LocalDateTime modified) { + this.modified = modified; + } + + @Nullable + @LastModifiedDate + public LocalDateTime getModified() { + return modified; + } + + @Nullable + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(@Nullable String createdBy) { + this.createdBy = createdBy; + } + + @Nullable + public String getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(@Nullable String modifiedBy) { + this.modifiedBy = modifiedBy; + } + + @Override + public boolean isNew() { + return id == null || (created == null && createdBy == null); + } + + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallbackTests.java b/src/test/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallbackTests.java new file mode 100644 index 000000000..52dad6ea9 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/event/AuditingEntityCallbackTests.java @@ -0,0 +1,163 @@ +/* + * 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.elasticsearch.core.event; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.Ordered; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.lang.Nullable; + +/** + * @author Peter-Josef Meisch + */ +@ExtendWith(MockitoExtension.class) +class AuditingEntityCallbackTests { + + IsNewAwareAuditingHandler handler; + AuditingEntityCallback callback; + + @BeforeEach + void setUp() { + SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext(); + context.getPersistentEntity(Sample.class); + handler = spy(new IsNewAwareAuditingHandler(PersistentEntities.of(context))); + callback = new AuditingEntityCallback(() -> handler); + } + + @Test // DATAES-68 + void shouldThrowExceptionOnNullFactory() { + assertThatIllegalArgumentException().isThrownBy(() -> new AuditingEntityCallback(null)); + } + + @Test // DATAES-68 + void shouldHaveOrder100() { + assertThat(callback).isInstanceOf(Ordered.class); + assertThat(callback.getOrder()).isEqualTo(100); + } + + @Test // DATAES-68 + void shouldCallHandler() { + Sample entity = new Sample(); + entity.setId("42"); + callback.onBeforeConvert(entity); + + verify(handler).markAudited(eq(entity)); + } + + @Test // DATAES-68 + void shouldReturnObjectFromHandler() { + Sample sample1 = new Sample(); + sample1.setId("1"); + Sample sample2 = new Sample(); + sample2.setId("2"); + doReturn(sample2).when(handler).markAudited(any()); + + Sample result = (Sample) callback.onBeforeConvert(sample1); + + assertThat(result).isSameAs(sample2); + } + + static class Sample { + + @Nullable @Id String id; + @Nullable @CreatedDate LocalDateTime createdDate; + @Nullable @CreatedBy String createdBy; + @Nullable @LastModifiedDate LocalDateTime modified; + + @Nullable + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Nullable + public LocalDateTime getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(LocalDateTime createdDate) { + this.createdDate = createdDate; + } + + @Nullable + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(@Nullable String createdBy) { + this.createdBy = createdBy; + } + + @Nullable + public LocalDateTime getModified() { + return modified; + } + + public void setModified(LocalDateTime modified) { + this.modified = modified; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Sample sample = (Sample) o; + + if (id != null ? !id.equals(sample.id) : sample.id != null) + return false; + if (createdDate != null ? !createdDate.equals(sample.createdDate) : sample.createdDate != null) + return false; + if (createdBy != null ? !createdBy.equals(sample.createdBy) : sample.createdBy != null) + return false; + return modified != null ? modified.equals(sample.modified) : sample.modified == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0); + result = 31 * result + (createdBy != null ? createdBy.hashCode() : 0); + result = 31 * result + (modified != null ? modified.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Sample{" + "id='" + id + '\'' + ", createdDate=" + createdDate + ", createdBy='" + createdBy + '\'' + + ", modified=" + modified + '}'; + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/event/ElasticsearchOperationsCallbackTest.java b/src/test/java/org/springframework/data/elasticsearch/core/event/ElasticsearchOperationsCallbackTest.java new file mode 100644 index 000000000..0637711c0 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/event/ElasticsearchOperationsCallbackTest.java @@ -0,0 +1,100 @@ +/* + * 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.elasticsearch.core.event; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.IndexOperations; +import org.springframework.stereotype.Component; + +/** + * @author Peter-Josef Meisch + */ +abstract class ElasticsearchOperationsCallbackTest { + + @Autowired private ElasticsearchOperations operations; + + @Configuration + static class Config { + + @Component + static class SampleEntityBeforeConvertCallback implements BeforeConvertCallback { + @Override + public SampleEntity onBeforeConvert(SampleEntity entity) { + entity.setText("converted"); + return entity; + } + } + } + + @BeforeEach + void setUp() { + IndexOperations indexOps = operations.indexOps(SampleEntity.class); + indexOps.delete(); + indexOps.create(); + indexOps.putMapping(indexOps.createMapping(SampleEntity.class)); + } + + @AfterEach + void tearDown() { + IndexOperations indexOps = operations.indexOps(SampleEntity.class); + indexOps.delete(); + } + + @Test + void shouldCallBeforeConvertCallback() { + SampleEntity entity = new SampleEntity("1", "test"); + + SampleEntity saved = operations.save(entity); + + assertThat(saved.getText()).isEqualTo("converted"); + } + + @Document(indexName = "test-operations-callback") + static class SampleEntity { + @Id private String id; + private String text; + + public SampleEntity(String id, String text) { + this.id = id; + this.text = text; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/event/ElasticsearchRestOperationsCallbackTest.java b/src/test/java/org/springframework/data/elasticsearch/core/event/ElasticsearchRestOperationsCallbackTest.java new file mode 100644 index 000000000..f6c1170f9 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/event/ElasticsearchRestOperationsCallbackTest.java @@ -0,0 +1,27 @@ +/* + * 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.elasticsearch.core.event; + +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Peter-Josef Meisch + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { ElasticsearchRestTemplateConfiguration.class, ElasticsearchOperationsCallbackTest.Config.class }) +class ElasticsearchRestOperationsCallbackTest extends ElasticsearchOperationsCallbackTest {} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/event/ElasticsearchTransportOperationsCallbackTest.java b/src/test/java/org/springframework/data/elasticsearch/core/event/ElasticsearchTransportOperationsCallbackTest.java new file mode 100644 index 000000000..d05848e23 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/event/ElasticsearchTransportOperationsCallbackTest.java @@ -0,0 +1,27 @@ +/* + * 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.elasticsearch.core.event; + +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Peter-Josef Meisch + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { ElasticsearchTemplateConfiguration.class, ElasticsearchOperationsCallbackTest.Config.class }) +class ElasticsearchTransportOperationsCallbackTest extends ElasticsearchOperationsCallbackTest {} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallbackTests.java b/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallbackTests.java new file mode 100644 index 000000000..4d15016c8 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveAuditingEntityCallbackTests.java @@ -0,0 +1,167 @@ +/* + * 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.elasticsearch.core.event; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import reactor.test.StepVerifier; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.Ordered; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.lang.Nullable; + +/** + * @author Peter-Josef Meisch + */ +@ExtendWith(MockitoExtension.class) +class ReactiveAuditingEntityCallbackTests { + + IsNewAwareAuditingHandler handler; + ReactiveAuditingEntityCallback callback; + + @BeforeEach + void setUp() { + SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext(); + context.getPersistentEntity(Sample.class); + handler = spy(new IsNewAwareAuditingHandler(PersistentEntities.of(context))); + callback = new ReactiveAuditingEntityCallback(() -> handler); + } + + @Test // DATAES-68 + void shouldThrowExceptionOnNullFactory() { + assertThatIllegalArgumentException().isThrownBy(() -> new AuditingEntityCallback(null)); + } + + @Test // DATAES-68 + void shouldHaveOrder100() { + assertThat(callback).isInstanceOf(Ordered.class); + assertThat(callback.getOrder()).isEqualTo(100); + } + + @Test // DATAES-68 + void shouldCallHandler() { + Sample entity = new Sample(); + entity.setId("42"); + callback.onBeforeConvert(entity); + + verify(handler).markAudited(eq(entity)); + } + + @Test // DATAES-68 + void shouldReturnObjectFromHandler() { + Sample sample1 = new Sample(); + sample1.setId("1"); + Sample sample2 = new Sample(); + sample2.setId("2"); + doReturn(sample2).when(handler).markAudited(any()); + + callback.onBeforeConvert(sample1) // + .as(StepVerifier::create) // + .consumeNextWith(it -> { // + assertThat(it).isSameAs(sample2); // + }).verifyComplete(); + } + + static class Sample { + + @Nullable @Id String id; + @Nullable @CreatedDate LocalDateTime createdDate; + @Nullable @CreatedBy String createdBy; + @Nullable @LastModifiedDate LocalDateTime modified; + + @Nullable + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Nullable + public LocalDateTime getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(LocalDateTime createdDate) { + this.createdDate = createdDate; + } + + @Nullable + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(@Nullable String createdBy) { + this.createdBy = createdBy; + } + + @Nullable + public LocalDateTime getModified() { + return modified; + } + + public void setModified(LocalDateTime modified) { + this.modified = modified; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Sample sample = (Sample) o; + + if (id != null ? !id.equals(sample.id) : sample.id != null) + return false; + if (createdDate != null ? !createdDate.equals(sample.createdDate) : sample.createdDate != null) + return false; + if (createdBy != null ? !createdBy.equals(sample.createdBy) : sample.createdBy != null) + return false; + return modified != null ? modified.equals(sample.modified) : sample.modified == null; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0); + result = 31 * result + (createdBy != null ? createdBy.hashCode() : 0); + result = 31 * result + (modified != null ? modified.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Sample{" + "id='" + id + '\'' + ", createdDate=" + createdDate + ", createdBy='" + createdBy + '\'' + + ", modified=" + modified + '}'; + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveElasticsearchOperationsCallbackTest.java b/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveElasticsearchOperationsCallbackTest.java new file mode 100644 index 000000000..4ca299b7a --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/event/ReactiveElasticsearchOperationsCallbackTest.java @@ -0,0 +1,115 @@ +/* + * 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.elasticsearch.core.event; + +import static org.assertj.core.api.Assertions.*; + +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.IndexOperations; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchRestTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.stereotype.Component; +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Peter-Josef Meisch + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { ReactiveElasticsearchOperationsCallbackTest.Config.class }) +public class ReactiveElasticsearchOperationsCallbackTest { + + @Configuration + @Import({ ReactiveElasticsearchRestTemplateConfiguration.class, ElasticsearchRestTemplateConfiguration.class }) + static class Config { + @Component + static class SampleEntityBeforeConvertCallback implements ReactiveBeforeConvertCallback { + @Override + public Mono onBeforeConvert(SampleEntity entity) { + entity.setText("reactive-converted"); + return Mono.just(entity); + } + } + + } + + @Autowired private ReactiveElasticsearchOperations operations; + @Autowired private ElasticsearchOperations nonreactiveOperations; + + @BeforeEach + void setUp() { + IndexOperations indexOps = nonreactiveOperations.indexOps(SampleEntity.class); + indexOps.create(); + indexOps.putMapping(indexOps.createMapping(SampleEntity.class)); + } + + @AfterEach + void tearDown() { + IndexOperations indexOps = nonreactiveOperations.indexOps(SampleEntity.class); + indexOps.delete(); + } + + @Test // DATES-68 + void shouldCallCallbackOnSave() { + SampleEntity sample = new SampleEntity("42", "initial"); + + operations.save(sample) // + .as(StepVerifier::create) // + .consumeNextWith(it -> { // + assertThat(it.text).isEqualTo("reactive-converted"); // + }) // + .verifyComplete(); + } + + @Document(indexName = "test-operations-reactive-callback") + static class SampleEntity { + @Id private String id; + private String text; + + public SampleEntity(String id, String text) { + this.id = id; + this.text = text; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java index ec103ba56..8ca227615 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java @@ -104,7 +104,7 @@ public class SimpleElasticsearchPersistentEntityTests { } - private class EntityWithWrongVersionType { + private static class EntityWithWrongVersionType { @Nullable @Version private String version; @@ -118,7 +118,7 @@ public class SimpleElasticsearchPersistentEntityTests { } } - private class EntityWithMultipleVersionField { + private static class EntityWithMultipleVersionField { @Nullable @Version private Long version1; @Nullable @Version private Long version2; @@ -143,7 +143,6 @@ public class SimpleElasticsearchPersistentEntityTests { } // DATAES-462 - static class TwoScoreProperties { @Score float first; diff --git a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ElasticsearchRestTemplateConfiguration.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ElasticsearchRestTemplateConfiguration.java index e06f60bfd..d7085b24a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ElasticsearchRestTemplateConfiguration.java +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ElasticsearchRestTemplateConfiguration.java @@ -27,16 +27,14 @@ import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfig /** * Configuration for Spring Data Elasticsearch using - * {@link org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate}. The required - * {@link ClusterConnectionInfo} bean must be provided by the testclass. + * {@link org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate}. * * @author Peter-Josef Meisch */ @Configuration public class ElasticsearchRestTemplateConfiguration extends AbstractElasticsearchConfiguration { - @Autowired - private ClusterConnectionInfo clusterConnectionInfo; + @Autowired private ClusterConnectionInfo clusterConnectionInfo; @Override @Bean diff --git a/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ReactiveElasticsearchRestTemplateConfiguration.java b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ReactiveElasticsearchRestTemplateConfiguration.java new file mode 100644 index 000000000..df8533c44 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/junit/jupiter/ReactiveElasticsearchRestTemplateConfiguration.java @@ -0,0 +1,51 @@ +/* + * 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.elasticsearch.junit.jupiter; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; +import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients; +import org.springframework.data.elasticsearch.config.AbstractReactiveElasticsearchConfiguration; + +/** + * Configuration for Spring Data Elasticsearch Integration Tests using + * {@link org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations} + * + * @author Peter-Josef Meisch + */ +@Configuration +public class ReactiveElasticsearchRestTemplateConfiguration extends AbstractReactiveElasticsearchConfiguration { + + @Autowired private ClusterConnectionInfo clusterConnectionInfo; + + @Override + public ReactiveElasticsearchClient reactiveElasticsearchClient() { + String elasticsearchHostPort = clusterConnectionInfo.getHost() + ':' + clusterConnectionInfo.getHttpPort(); + + ClientConfiguration.TerminalClientConfigurationBuilder configurationBuilder = ClientConfiguration.builder() // + .connectedTo(elasticsearchHostPort); + + if (clusterConnectionInfo.isUseSsl()) { + configurationBuilder = ((ClientConfiguration.MaybeSecureClientConfigurationBuilder) configurationBuilder) + .usingSsl(); + } + + return ReactiveRestClients.create(configurationBuilder.build()); + + } +}