From a42de9b51ba6633daab1c00857446e218c835fe3 Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Sat, 5 Dec 2020 22:53:18 +0100 Subject: [PATCH] DATAES-362 - Add support for composable meta annotations. Original PR: #566 --- .../data/elasticsearch/annotations/Field.java | 2 +- .../elasticsearch/annotations/MultiField.java | 3 +- .../core/AbstractDefaultIndexOperations.java | 36 ++-- .../core/DefaultReactiveIndexOperations.java | 21 ++- .../core/mapping/IndexCoordinates.java | 19 +++ .../SimpleElasticsearchPersistentEntity.java | 16 +- .../ComposableAnnotationsUnitTest.java | 157 ++++++++++++++++++ 7 files changed, 229 insertions(+), 25 deletions(-) create mode 100644 src/test/java/org/springframework/data/elasticsearch/annotations/ComposableAnnotationsUnitTest.java diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java index 9686f0209..cda5ffd62 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Field.java @@ -36,7 +36,7 @@ import org.springframework.core.annotation.AliasFor; * @author Aleksei Arsenev */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Documented @Inherited public @interface Field { diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/MultiField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/MultiField.java index 876bd8aa0..c5dea703f 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/MultiField.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/MultiField.java @@ -27,9 +27,10 @@ import java.lang.annotation.Target; * @author Artur Konczak * @author Jonathan Yan * @author Xiao Yu + * @author Peter-Josef Meisch */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Documented public @interface MultiField { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/AbstractDefaultIndexOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/AbstractDefaultIndexOperations.java index 3f9ac3b35..2ab488f4c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/AbstractDefaultIndexOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/AbstractDefaultIndexOperations.java @@ -25,6 +25,8 @@ import java.util.Set; import org.elasticsearch.cluster.metadata.AliasMetadata; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.elasticsearch.UncategorizedElasticsearchException; import org.springframework.data.elasticsearch.annotations.Mapping; @@ -105,9 +107,13 @@ abstract class AbstractDefaultIndexOperations implements IndexOperations { Document settings = null; - if (clazz.isAnnotationPresent(Setting.class)) { - String settingPath = clazz.getAnnotation(Setting.class).settingPath(); - settings = loadSettings(settingPath); + if (AnnotatedElementUtils.hasAnnotation(clazz, Setting.class)) { + AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(clazz, Setting.class); + + if (attributes != null) { + String settingPath = attributes.getString("settingPath"); + settings = loadSettings(settingPath); + } } if (settings == null) { @@ -224,22 +230,28 @@ abstract class AbstractDefaultIndexOperations implements IndexOperations { protected Document buildMapping(Class clazz) { // load mapping specified in Mapping annotation if present - if (clazz.isAnnotationPresent(Mapping.class)) { - String mappingPath = clazz.getAnnotation(Mapping.class).mappingPath(); + if (AnnotatedElementUtils.hasAnnotation(clazz, Mapping.class)) { + AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(clazz, Mapping.class); - if (!StringUtils.isEmpty(mappingPath)) { - String mappings = ResourceUtil.readFileFromClasspath(mappingPath); + if (attributes != null) { + String mappingPath = attributes.getString("mappingPath"); - if (!StringUtils.isEmpty(mappings)) { - return Document.parse(mappings); + if (StringUtils.hasText(mappingPath)) { + String mappings = ResourceUtil.readFileFromClasspath(mappingPath); + + if (StringUtils.hasText(mappings)) { + return Document.parse(mappings); + } + } else { + LOGGER.info("mappingPath in @Mapping has to be defined. Building mappings using @Field"); } - } else { - LOGGER.info("mappingPath in @Mapping has to be defined. Building mappings using @Field"); } } // build mapping from field annotations - try { + try + + { String mapping = new MappingBuilder(elasticsearchConverter).buildPropertyMapping(clazz); return Document.parse(mapping); } catch (Exception e) { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/DefaultReactiveIndexOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/DefaultReactiveIndexOperations.java index 9c7379eb6..3a06ff8fc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/DefaultReactiveIndexOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/DefaultReactiveIndexOperations.java @@ -37,6 +37,8 @@ import org.elasticsearch.client.indices.IndexTemplatesExistRequest; import org.elasticsearch.client.indices.PutIndexTemplateRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.elasticsearch.NoSuchIndexException; import org.springframework.data.elasticsearch.annotations.Mapping; @@ -157,9 +159,13 @@ class DefaultReactiveIndexOperations implements ReactiveIndexOperations { @Override public Mono createMapping(Class clazz) { - if (clazz.isAnnotationPresent(Mapping.class)) { - String mappingPath = clazz.getAnnotation(Mapping.class).mappingPath(); - return loadDocument(mappingPath, "@Mapping"); + if (AnnotatedElementUtils.hasAnnotation(clazz, Mapping.class)) { + AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(clazz, Mapping.class); + + if (attributes != null) { + String mappingPath = clazz.getAnnotation(Mapping.class).mappingPath(); + return loadDocument(mappingPath, "@Mapping"); + } } String mapping = new MappingBuilder(converter).buildPropertyMapping(clazz); @@ -198,10 +204,13 @@ class DefaultReactiveIndexOperations implements ReactiveIndexOperations { @Override public Mono createSettings(Class clazz) { - if (clazz.isAnnotationPresent(Setting.class)) { - String settingPath = clazz.getAnnotation(Setting.class).settingPath(); + if (AnnotatedElementUtils.hasAnnotation(clazz, Setting.class)) { + AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(clazz, Setting.class); - return loadDocument(settingPath, "@Setting"); + if (attributes != null) { + String settingPath = attributes.getString("settingPath"); + return loadDocument(settingPath, "@Setting"); + } } return Mono.just(getRequiredPersistentEntity(clazz).getDefaultSettings()); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/IndexCoordinates.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/IndexCoordinates.java index d2061a7d2..8a3a55cbb 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/IndexCoordinates.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/IndexCoordinates.java @@ -52,6 +52,25 @@ public class IndexCoordinates { return Arrays.copyOf(indexNames, indexNames.length); } + /** + * @since 4.2 + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IndexCoordinates that = (IndexCoordinates) o; + return Arrays.equals(indexNames, that.indexNames); + } + + /** + * @since 4.2 + */ + @Override + public int hashCode() { + return Arrays.hashCode(indexNames); + } + @Override public String toString() { return "IndexCoordinates{" + "indexNames=" + Arrays.toString(indexNames) + '}'; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java index 274269c0a..0f24a6ac0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.index.VersionType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.elasticsearch.annotations.Parent; import org.springframework.data.elasticsearch.annotations.Setting; import org.springframework.data.elasticsearch.core.document.Document; @@ -87,9 +88,11 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit super(typeInformation); Class clazz = typeInformation.getType(); - if (clazz.isAnnotationPresent(org.springframework.data.elasticsearch.annotations.Document.class)) { - org.springframework.data.elasticsearch.annotations.Document document = clazz - .getAnnotation(org.springframework.data.elasticsearch.annotations.Document.class); + org.springframework.data.elasticsearch.annotations.Document document = AnnotatedElementUtils + .findMergedAnnotation(clazz, org.springframework.data.elasticsearch.annotations.Document.class); + + if (document != null) { + Assert.hasText(document.indexName(), " Unknown indexName. Make sure the indexName is defined. e.g @Document(indexName=\"foo\")"); this.indexName = document.indexName(); @@ -101,8 +104,11 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit this.versionType = document.versionType(); this.createIndexAndMapping = document.createIndex(); } - if (clazz.isAnnotationPresent(Setting.class)) { - this.settingPath = typeInformation.getType().getAnnotation(Setting.class).settingPath(); + + Setting setting = AnnotatedElementUtils.getMergedAnnotation(clazz, Setting.class); + + if (setting != null) { + this.settingPath = setting.settingPath(); } } diff --git a/src/test/java/org/springframework/data/elasticsearch/annotations/ComposableAnnotationsUnitTest.java b/src/test/java/org/springframework/data/elasticsearch/annotations/ComposableAnnotationsUnitTest.java new file mode 100644 index 000000000..f2e2539dc --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/annotations/ComposableAnnotationsUnitTest.java @@ -0,0 +1,157 @@ +/* + * 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.annotations; + +import static org.assertj.core.api.Assertions.*; +import static org.skyscreamer.jsonassert.JSONAssert.*; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +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 java.time.LocalDate; + +import org.json.JSONException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; +import org.springframework.data.elasticsearch.core.index.MappingBuilder; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchPersistentEntity; + +/** + * @author Peter-Josef Meisch + */ +public class ComposableAnnotationsUnitTest { + + private static SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext(); + private static MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext); + private static MappingBuilder mappingBuilder = new MappingBuilder(converter); + + @Test // DATAES-362 + @DisplayName("Document annotation should be composable") + void documentAnnotationShouldBeComposable() { + + SimpleElasticsearchPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(ComposedAnnotationEntity.class); + + assertThat(entity.getIndexCoordinates()).isEqualTo(IndexCoordinates.of("test-no-create")); + assertThat(entity.isCreateIndexAndMapping()).isFalse(); + assertThat(entity.getShards()).isEqualTo((short) 42); + } + + @Test // DATAES-362 + @DisplayName("Field annotation should be composable") + void fieldAnnotationShouldBeComposable() { + SimpleElasticsearchPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(ComposedAnnotationEntity.class); + + ElasticsearchPersistentProperty property = entity.getRequiredPersistentProperty("nullValue"); + + assertThat(property.getFieldName()).isEqualTo("null-value"); + assertThat(property.storeNullValue()).isTrue(); + } + + @Test // DATAES-362 + @DisplayName("should use composed Field annotations in MappingBuilder") + void shouldUseComposedFieldAnnotationsInMappingBuilder() throws JSONException { + + String expected = "{\n" + // + " \"properties\":{\n" + // + " \"null-value\": {\n" + // + " \"null_value\": \"NULL\"\n" + // + " },\n" + // + " \"theDate\": {\n" + // + " \"type\": \"date\",\n" + // + " \"format\": \"date\"\n" + // + " },\n" + // + " \"multiField\": {\n" + // + " \"type\": \"text\",\n" + // + " \"fields\": {\n" + // + " \"keyword\": {\n" + // + " \"type\": \"keyword\"\n" + // + " }\n" + // + " }\n" + // + " }\n" + // + " }\n" + // + "}\n"; // + + String mapping = mappingBuilder.buildPropertyMapping(ComposedAnnotationEntity.class); + + assertEquals(expected, mapping, false); + } + + @Inherited + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE }) + @Document(indexName = "", createIndex = false, shards = 42) + public @interface DocumentNoCreate { + + @AliasFor(value = "indexName", annotation = Document.class) + String indexName(); + } + + @Inherited + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @Field(storeNullValue = true, nullValue = "NULL") + public @interface NullValueField { + @AliasFor(value = "name", annotation = Field.class) + String name(); + } + + @Inherited + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @Field(type = FieldType.Date, format = DateFormat.date) + public @interface LocalDateField { + @AliasFor(value = "name", annotation = Field.class) + String name() default ""; + } + + @Inherited + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @MultiField(mainField = @Field(type = FieldType.Text), + otherFields = { @InnerField(suffix = "keyword", type = FieldType.Keyword) }) + public @interface TextKeywordField { + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @DocumentNoCreate(indexName = "test-no-create") + static class ComposedAnnotationEntity { + @Id private String id; + @NullValueField(name = "null-value") private String nullValue; + @LocalDateField private LocalDate theDate; + @TextKeywordField private String multiField; + } +}