diff --git a/src/main/asciidoc/reference/elasticsearch-object-mapping.adoc b/src/main/asciidoc/reference/elasticsearch-object-mapping.adoc index f0ed04b19..1653f0b61 100644 --- a/src/main/asciidoc/reference/elasticsearch-object-mapping.adoc +++ b/src/main/asciidoc/reference/elasticsearch-object-mapping.adoc @@ -34,8 +34,6 @@ The following annotations are available: The most important attributes are: ** `indexName`: the name of the index to store this entity in. This can contain a SpEL template expression like `"log-#{T(java.time.LocalDate).now().toString()}"` -** `type`: [line-through]#the mapping type. -If not set, the lowercased simple name of the class is used.# (deprecated since version 4.0) ** `createIndex`: flag whether to create an index on repository bootstrapping. Default value is _true_. See <> @@ -170,6 +168,22 @@ public class Person { NOTE: Type hints will not be written for nested Objects unless the properties type is `Object`, an interface or the actual value type does not match the properties declaration. +===== Disabling Type Hints + +It may be necessary to disable writing of type hints when the index that should be used already exists without having the type hints defined in its mapping and with the mapping mode set to strict. In this case, writing the type hint will produce an error, as the field cannot be added automatically. + +Type hints can be disabled for the whole application by overriding the method `writeTypeHints()` in a configuration class derived from `AbstractElasticsearchConfiguration` (see <>). + +As an alternativ they can be disabled for a single index with the `@Document` annotation: +==== +[source,java] +---- +@Document(indexName = "index", writeTypeHint = WriteTypeHint.FALSE) +---- +==== + +WARNING: We strongly advise against disabling Type Hints. Only do this if you are forced to. Disabling type hints can lead to documents not being retrieved correctly from Elasticsearch in case of polymorphic data or document retrieval may fail completely. + ==== Geospatial Types Geospatial types like `Point` & `GeoPoint` are converted into _lat/lon_ pairs. diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java b/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java index ae53f190d..c1a7b8ff1 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/Document.java @@ -105,4 +105,11 @@ * Configuration of version management. */ VersionType versionType() default VersionType.EXTERNAL; + + /** + * Defines if type hints should be written. {@see WriteTypeHint}. + * + * @since 4.3 + */ + WriteTypeHint writeTypeHint() default WriteTypeHint.DEFAULT; } diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/WriteTypeHint.java b/src/main/java/org/springframework/data/elasticsearch/annotations/WriteTypeHint.java new file mode 100644 index 000000000..0aae5f9f7 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/WriteTypeHint.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.annotations; + +import org.springframework.data.mapping.context.MappingContext; + +/** + * Defines if type hints should be written. Used by {@link Document} annotation. + * + * @author Peter-Josef Meisch + * @since 4.3 + */ +public enum WriteTypeHint { + + /** + * Use the global settings from the {@link MappingContext}. + */ + DEFAULT, + /** + * Always write type hints for the entity. + */ + TRUE, + /** + * Never write type hints for the entity. + */ + FALSE +} diff --git a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupport.java b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupport.java index bf7ca3dbf..d6a3e8152 100644 --- a/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupport.java +++ b/src/main/java/org/springframework/data/elasticsearch/config/ElasticsearchConfigurationSupport.java @@ -26,7 +26,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.core.type.filter.AnnotationTypeFilter; -import org.springframework.data.annotation.Persistent; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.core.RefreshPolicy; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; @@ -72,6 +71,7 @@ public SimpleElasticsearchMappingContext elasticsearchMappingContext( mappingContext.setInitialEntitySet(getInitialEntitySet()); mappingContext.setSimpleTypeHolder(elasticsearchCustomConversions.getSimpleTypeHolder()); mappingContext.setFieldNamingStrategy(fieldNamingStrategy()); + mappingContext.setWriteTypeHints(writeTypeHints()); return mappingContext; } @@ -171,4 +171,17 @@ protected RefreshPolicy refreshPolicy() { protected FieldNamingStrategy fieldNamingStrategy() { return PropertyNameFieldNamingStrategy.INSTANCE; } + + /** + * Flag specifiying if type hints (_class fields) should be written in the index. It is strongly advised to keep the + * default value of {@literal true}. If you need to write to an existing index that does not have a mapping defined + * for these fields and that has a strict mapping set, then it might be necessary to disable type hints. But notice + * that in this case reading polymorphic types may fail. + * + * @return flag if type hints should be written + * @since 4.3 + */ + protected boolean writeTypeHints() { + return true; + } } 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 c080d1e1f..86643e544 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 @@ -95,15 +95,8 @@ public class MappingElasticsearchConverter private final MappingContext, ElasticsearchPersistentProperty> mappingContext; private final GenericConversionService conversionService; - // don't access directly, use getConversions(). to prevent null access private CustomConversions conversions = new ElasticsearchCustomConversions(Collections.emptyList()); - private final EntityInstantiators instantiators = new EntityInstantiators(); - - private final ElasticsearchTypeMapper typeMapper; - - private final ConcurrentHashMap propertyWarnings = new ConcurrentHashMap<>(); - private final SpELContext spELContext; public MappingElasticsearchConverter( MappingContext, ElasticsearchPersistentProperty> mappingContext) { @@ -118,8 +111,6 @@ public MappingElasticsearchConverter( this.mappingContext = mappingContext; this.conversionService = conversionService != null ? conversionService : new DefaultConversionService(); - this.typeMapper = ElasticsearchTypeMapper.create(mappingContext); - this.spELContext = new SpELContext(new MapAccessor()); } @Override @@ -157,871 +148,1007 @@ private CustomConversions getConversions() { return conversions; } - /* - * (non-Javadoc) - * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() - */ @Override public void afterPropertiesSet() { DateFormatterRegistrar.addDateConverters(conversionService); getConversions().registerConvertersIn(conversionService); } - // region read + // region read/write - @SuppressWarnings("unchecked") @Override public R read(Class type, Document source) { - TypeInformation typeHint = ClassTypeInformation.from((Class) ClassUtils.getUserClass(type)); - R r = read(typeHint, source); + Reader reader = new Reader(mappingContext, conversionService, getConversions()); + return reader.read(type, source); + } - if (r == null) { - throw new ConversionException("could not convert into object of class " + type); - } + @Override + public void write(Object source, Document sink) { + + Assert.notNull(source, "source to map must not be null"); - return r; + Writer writer = new Writer(mappingContext, conversionService, getConversions()); + writer.write(source, sink); } - protected R readEntity(ElasticsearchPersistentEntity entity, Map source) { + /** + * base class for {@link Reader} and {@link Writer} keeping the common properties + */ + private static class Base { + + protected final MappingContext, ElasticsearchPersistentProperty> mappingContext; + protected final ElasticsearchTypeMapper typeMapper; + protected final GenericConversionService conversionService; + protected final CustomConversions conversions; + protected final ConcurrentHashMap propertyWarnings = new ConcurrentHashMap<>(); + + private Base( + MappingContext, ElasticsearchPersistentProperty> mappingContext, + GenericConversionService conversionService, CustomConversions conversions) { + this.mappingContext = mappingContext; + this.conversionService = conversionService; + this.conversions = conversions; + this.typeMapper = ElasticsearchTypeMapper.create(mappingContext); + } + } + + /** + * Class to do the actual writing. The methods originally were in the MappingElasticsearchConverter class, but are + * refactored to allow for keeping state during the conversion of an object. + */ + private static class Reader extends Base { - ElasticsearchPersistentEntity targetEntity = computeClosestEntity(entity, source); + private final SpELContext spELContext; + private final EntityInstantiators instantiators = new EntityInstantiators(); - SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(source, spELContext); - MapValueAccessor accessor = new MapValueAccessor(source); + public Reader( + MappingContext, ElasticsearchPersistentProperty> mappingContext, + GenericConversionService conversionService, CustomConversions conversions) { - PreferredConstructor persistenceConstructor = entity - .getPersistenceConstructor(); + super(mappingContext, conversionService, conversions); + this.spELContext = new SpELContext(new MapAccessor()); + } - ParameterValueProvider propertyValueProvider = persistenceConstructor != null - && persistenceConstructor.hasParameters() ? getParameterProvider(entity, accessor, evaluator) - : NoOpParameterValueProvider.INSTANCE; + @SuppressWarnings("unchecked") + R read(Class type, Document source) { - EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity); + TypeInformation typeHint = ClassTypeInformation.from((Class) ClassUtils.getUserClass(type)); + R r = read(typeHint, source); - @SuppressWarnings({ "unchecked" }) - R instance = (R) instantiator.createInstance(targetEntity, propertyValueProvider); + if (r == null) { + throw new ConversionException("could not convert into object of class " + type); + } - if (!targetEntity.requiresPropertyPopulation()) { - return instance; + return r; } - ElasticsearchPropertyValueProvider valueProvider = new ElasticsearchPropertyValueProvider(accessor, evaluator); - R result = readProperties(targetEntity, instance, valueProvider); - - if (source instanceof Document) { - Document document = (Document) source; - if (document.hasId()) { - ElasticsearchPersistentProperty idProperty = targetEntity.getIdProperty(); - PersistentPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>( - targetEntity.getPropertyAccessor(result), conversionService); - // Only deal with String because ES generated Ids are strings ! - if (idProperty != null && idProperty.getType().isAssignableFrom(String.class)) { - propertyAccessor.setProperty(idProperty, document.getId()); - } + @Nullable + @SuppressWarnings("unchecked") + private R read(TypeInformation type, Map source) { + + Assert.notNull(source, "Source must not be null!"); + + TypeInformation typeToUse = typeMapper.readType(source, type); + Class rawType = typeToUse.getType(); + + if (conversions.hasCustomReadTarget(source.getClass(), rawType)) { + return conversionService.convert(source, rawType); } - if (document.hasVersion()) { - long version = document.getVersion(); - ElasticsearchPersistentProperty versionProperty = targetEntity.getVersionProperty(); - // Only deal with Long because ES versions are longs ! - if (versionProperty != null && versionProperty.getType().isAssignableFrom(Long.class)) { - // check that a version was actually returned in the response, -1 would indicate that - // a search didn't request the version ids in the response, which would be an issue - Assert.isTrue(version != -1, "Version in response is -1"); - targetEntity.getPropertyAccessor(result).setProperty(versionProperty, version); - } + if (Document.class.isAssignableFrom(rawType)) { + return (R) source; } - if (targetEntity.hasSeqNoPrimaryTermProperty() && document.hasSeqNo() && document.hasPrimaryTerm()) { - if (isAssignedSeqNo(document.getSeqNo()) && isAssignedPrimaryTerm(document.getPrimaryTerm())) { - SeqNoPrimaryTerm seqNoPrimaryTerm = new SeqNoPrimaryTerm(document.getSeqNo(), document.getPrimaryTerm()); - ElasticsearchPersistentProperty property = targetEntity.getRequiredSeqNoPrimaryTermProperty(); - targetEntity.getPropertyAccessor(result).setProperty(property, seqNoPrimaryTerm); - } + if (typeToUse.isMap()) { + return readMap(typeToUse, source); + } + + if (typeToUse.equals(ClassTypeInformation.OBJECT)) { + return (R) source; + } + // Retrieve persistent entity info + + ElasticsearchPersistentEntity entity = mappingContext.getPersistentEntity(typeToUse); + + if (entity == null) { + throw new MappingException(String.format(INVALID_TYPE_TO_READ, source, typeToUse.getType())); } - } - if (source instanceof SearchDocument) { - SearchDocument searchDocument = (SearchDocument) source; - populateScriptFields(result, searchDocument); + return readEntity(entity, source); } - return result; + @SuppressWarnings("unchecked") + private R readMap(TypeInformation type, Map source) { - } + Assert.notNull(source, "Document must not be null!"); - private ParameterValueProvider getParameterProvider( - ElasticsearchPersistentEntity entity, MapValueAccessor source, SpELExpressionEvaluator evaluator) { + Class mapType = typeMapper.readType(source, type).getType(); - ElasticsearchPropertyValueProvider provider = new ElasticsearchPropertyValueProvider(source, evaluator); + TypeInformation keyType = type.getComponentType(); + TypeInformation valueType = type.getMapValueType(); - // TODO: Support for non-static inner classes via ObjectPath - // noinspection ConstantConditions - PersistentEntityParameterValueProvider parameterProvider = new PersistentEntityParameterValueProvider<>( - entity, provider, null); + Class rawKeyType = keyType != null ? keyType.getType() : null; + Class rawValueType = valueType != null ? valueType.getType() : null; - return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider); - } + Map map = CollectionFactory.createMap(mapType, rawKeyType, source.keySet().size()); - private boolean isAssignedSeqNo(long seqNo) { - return seqNo >= 0; - } + for (Entry entry : source.entrySet()) { - private boolean isAssignedPrimaryTerm(long primaryTerm) { - return primaryTerm > 0; - } + if (typeMapper.isTypeKey(entry.getKey())) { + continue; + } - protected R readProperties(ElasticsearchPersistentEntity entity, R instance, - ElasticsearchPropertyValueProvider valueProvider) { + Object key = entry.getKey(); - PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance), - conversionService); + if (rawKeyType != null && !rawKeyType.isAssignableFrom(key.getClass())) { + key = conversionService.convert(key, rawKeyType); + } - for (ElasticsearchPersistentProperty prop : entity) { + Object value = entry.getValue(); + TypeInformation defaultedValueType = valueType != null ? valueType : ClassTypeInformation.OBJECT; - if (entity.isConstructorArgument(prop) || !prop.isReadable()) { - continue; + if (value instanceof Map) { + map.put(key, read(defaultedValueType, (Map) value)); + } else if (value instanceof List) { + map.put(key, + readCollectionOrArray(valueType != null ? valueType : ClassTypeInformation.LIST, (List) value)); + } else { + map.put(key, getPotentiallyConvertedSimpleRead(value, rawValueType)); + } } - Object value = valueProvider.getPropertyValue(prop); - if (value != null) { - accessor.setProperty(prop, value); - } + return (R) map; } - return accessor.getBean(); - } + private R readEntity(ElasticsearchPersistentEntity entity, Map source) { - @Nullable - protected R readValue(@Nullable Object value, ElasticsearchPersistentProperty property, TypeInformation type) { + ElasticsearchPersistentEntity targetEntity = computeClosestEntity(entity, source); - if (value == null) { - return null; - } + SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(source, spELContext); + MapValueAccessor accessor = new MapValueAccessor(source); + + PreferredConstructor persistenceConstructor = entity + .getPersistenceConstructor(); + + ParameterValueProvider propertyValueProvider = persistenceConstructor != null + && persistenceConstructor.hasParameters() ? getParameterProvider(entity, accessor, evaluator) + : NoOpParameterValueProvider.INSTANCE; - Class rawType = type.getType(); + EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity); - if (property.hasPropertyConverter()) { - value = propertyConverterRead(property, value); - } else if (TemporalAccessor.class.isAssignableFrom(property.getType()) - && !getConversions().hasCustomReadTarget(value.getClass(), rawType)) { + @SuppressWarnings({ "unchecked" }) + R instance = (R) instantiator.createInstance(targetEntity, propertyValueProvider); - // log at most 5 times - String propertyName = property.getOwner().getType().getSimpleName() + '.' + property.getName(); - String key = propertyName + "-read"; - int count = propertyWarnings.computeIfAbsent(key, k -> 0); - if (count < 5) { - LOGGER.warn( - "Type {} of property {} is a TemporalAccessor class but has neither a @Field annotation defining the date type nor a registered converter for reading!" - + " It cannot be mapped from a complex object in Elasticsearch!", - property.getType().getSimpleName(), propertyName); - propertyWarnings.put(key, count + 1); + if (!targetEntity.requiresPropertyPopulation()) { + return instance; } - } - return readValue(value, type); - } + ElasticsearchPropertyValueProvider valueProvider = new ElasticsearchPropertyValueProvider(accessor, evaluator); + R result = readProperties(targetEntity, instance, valueProvider); + + if (source instanceof Document) { + Document document = (Document) source; + if (document.hasId()) { + ElasticsearchPersistentProperty idProperty = targetEntity.getIdProperty(); + PersistentPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>( + targetEntity.getPropertyAccessor(result), conversionService); + // Only deal with String because ES generated Ids are strings ! + if (idProperty != null && idProperty.getType().isAssignableFrom(String.class)) { + propertyAccessor.setProperty(idProperty, document.getId()); + } + } - @Nullable - @SuppressWarnings("unchecked") - private T readValue(Object value, TypeInformation type) { - - Class rawType = type.getType(); - - if (conversions.hasCustomReadTarget(value.getClass(), rawType)) { - return (T) conversionService.convert(value, rawType); - } else if (value instanceof List) { - return (T) readCollectionOrArray(type, (List) value); - } else if (value.getClass().isArray()) { - return (T) readCollectionOrArray(type, Arrays.asList((Object[]) value)); - } else if (value instanceof Map) { - return (T) read(type, (Map) value); - } else { - return (T) getPotentiallyConvertedSimpleRead(value, rawType); - } - } + if (document.hasVersion()) { + long version = document.getVersion(); + ElasticsearchPersistentProperty versionProperty = targetEntity.getVersionProperty(); + // Only deal with Long because ES versions are longs ! + if (versionProperty != null && versionProperty.getType().isAssignableFrom(Long.class)) { + // check that a version was actually returned in the response, -1 would indicate that + // a search didn't request the version ids in the response, which would be an issue + Assert.isTrue(version != -1, "Version in response is -1"); + targetEntity.getPropertyAccessor(result).setProperty(versionProperty, version); + } + } - @Nullable - @SuppressWarnings("unchecked") - private R read(TypeInformation type, Map source) { + if (targetEntity.hasSeqNoPrimaryTermProperty() && document.hasSeqNo() && document.hasPrimaryTerm()) { + if (isAssignedSeqNo(document.getSeqNo()) && isAssignedPrimaryTerm(document.getPrimaryTerm())) { + SeqNoPrimaryTerm seqNoPrimaryTerm = new SeqNoPrimaryTerm(document.getSeqNo(), document.getPrimaryTerm()); + ElasticsearchPersistentProperty property = targetEntity.getRequiredSeqNoPrimaryTermProperty(); + targetEntity.getPropertyAccessor(result).setProperty(property, seqNoPrimaryTerm); + } + } + } - Assert.notNull(source, "Source must not be null!"); + if (source instanceof SearchDocument) { + SearchDocument searchDocument = (SearchDocument) source; + populateScriptFields(result, searchDocument); + } - TypeInformation typeToUse = typeMapper.readType(source, type); - Class rawType = typeToUse.getType(); + return result; - if (conversions.hasCustomReadTarget(source.getClass(), rawType)) { - return conversionService.convert(source, rawType); } - if (Document.class.isAssignableFrom(rawType)) { - return (R) source; - } + private ParameterValueProvider getParameterProvider( + ElasticsearchPersistentEntity entity, MapValueAccessor source, SpELExpressionEvaluator evaluator) { - if (typeToUse.isMap()) { - return readMap(typeToUse, source); - } + ElasticsearchPropertyValueProvider provider = new ElasticsearchPropertyValueProvider(source, evaluator); - if (typeToUse.equals(ClassTypeInformation.OBJECT)) { - return (R) source; + // TODO: Support for non-static inner classes via ObjectPath + // noinspection ConstantConditions + PersistentEntityParameterValueProvider parameterProvider = new PersistentEntityParameterValueProvider<>( + entity, provider, null); + + return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider); } - // Retrieve persistent entity info - ElasticsearchPersistentEntity entity = mappingContext.getPersistentEntity(typeToUse); + private boolean isAssignedSeqNo(long seqNo) { + return seqNo >= 0; + } - if (entity == null) { - throw new MappingException(String.format(INVALID_TYPE_TO_READ, source, typeToUse.getType())); + private boolean isAssignedPrimaryTerm(long primaryTerm) { + return primaryTerm > 0; } - return readEntity(entity, source); - } + protected R readProperties(ElasticsearchPersistentEntity entity, R instance, + ElasticsearchPropertyValueProvider valueProvider) { - private Object propertyConverterRead(ElasticsearchPersistentProperty property, Object source) { - ElasticsearchPersistentPropertyConverter propertyConverter = Objects - .requireNonNull(property.getPropertyConverter()); + PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance), + conversionService); - if (source instanceof String[]) { - // convert to a List - source = Arrays.asList((String[]) source); - } + for (ElasticsearchPersistentProperty prop : entity) { - if (source instanceof List) { - source = ((List) source).stream().map(it -> convertOnRead(propertyConverter, it)).collect(Collectors.toList()); - } else if (source instanceof Set) { - source = ((Set) source).stream().map(it -> convertOnRead(propertyConverter, it)).collect(Collectors.toSet()); - } else { - source = convertOnRead(propertyConverter, source); - } - return source; - } + if (entity.isConstructorArgument(prop) || !prop.isReadable()) { + continue; + } + + Object value = valueProvider.getPropertyValue(prop); + if (value != null) { + accessor.setProperty(prop, value); + } + } - private Object convertOnRead(ElasticsearchPersistentPropertyConverter propertyConverter, Object source) { - if (String.class.isAssignableFrom(source.getClass())) { - source = propertyConverter.read((String) source); + return accessor.getBean(); } - return source; - } - /** - * Reads the given {@link Collection} into a collection of the given {@link TypeInformation}. - * - * @param targetType must not be {@literal null}. - * @param source must not be {@literal null}. - * @return the converted {@link Collection} or array, will never be {@literal null}. - */ - @SuppressWarnings("unchecked") - @Nullable - private Object readCollectionOrArray(TypeInformation targetType, Collection source) { + @Nullable + protected R readValue(@Nullable Object value, ElasticsearchPersistentProperty property, + TypeInformation type) { - Assert.notNull(targetType, "Target type must not be null!"); + if (value == null) { + return null; + } - Class collectionType = targetType.isSubTypeOf(Collection.class) // - ? targetType.getType() // - : List.class; + Class rawType = type.getType(); - TypeInformation componentType = targetType.getComponentType() != null // - ? targetType.getComponentType() // - : ClassTypeInformation.OBJECT; - Class rawComponentType = componentType.getType(); + if (property.hasPropertyConverter()) { + value = propertyConverterRead(property, value); + } else if (TemporalAccessor.class.isAssignableFrom(property.getType()) + && !conversions.hasCustomReadTarget(value.getClass(), rawType)) { - Collection items = targetType.getType().isArray() // - ? new ArrayList<>(source.size()) // - : CollectionFactory.createCollection(collectionType, rawComponentType, source.size()); + // log at most 5 times + String propertyName = property.getOwner().getType().getSimpleName() + '.' + property.getName(); + String key = propertyName + "-read"; + int count = propertyWarnings.computeIfAbsent(key, k -> 0); + if (count < 5) { + LOGGER.warn( + "Type {} of property {} is a TemporalAccessor class but has neither a @Field annotation defining the date type nor a registered converter for reading!" + + " It cannot be mapped from a complex object in Elasticsearch!", + property.getType().getSimpleName(), propertyName); + propertyWarnings.put(key, count + 1); + } + } - if (source.isEmpty()) { - return getPotentiallyConvertedSimpleRead(items, targetType); + return readValue(value, type); } - for (Object element : source) { + @Nullable + @SuppressWarnings("unchecked") + private T readValue(Object value, TypeInformation type) { - if (element instanceof Map) { - items.add(read(componentType, (Map) element)); - } else { + Class rawType = type.getType(); - if (!Object.class.equals(rawComponentType) && element instanceof Collection) { - if (!rawComponentType.isArray() && !ClassUtils.isAssignable(Iterable.class, rawComponentType)) { - throw new MappingException( - String.format(INCOMPATIBLE_TYPES, element, element.getClass(), rawComponentType)); - } - } - if (element instanceof List) { - items.add(readCollectionOrArray(componentType, (Collection) element)); - } else { - items.add(getPotentiallyConvertedSimpleRead(element, rawComponentType)); - } + if (conversions.hasCustomReadTarget(value.getClass(), rawType)) { + return (T) conversionService.convert(value, rawType); + } else if (value instanceof List) { + return (T) readCollectionOrArray(type, (List) value); + } else if (value.getClass().isArray()) { + return (T) readCollectionOrArray(type, Arrays.asList((Object[]) value)); + } else if (value instanceof Map) { + return (T) read(type, (Map) value); + } else { + return (T) getPotentiallyConvertedSimpleRead(value, rawType); } } - return getPotentiallyConvertedSimpleRead(items, targetType.getType()); - } + private Object propertyConverterRead(ElasticsearchPersistentProperty property, Object source) { + ElasticsearchPersistentPropertyConverter propertyConverter = Objects + .requireNonNull(property.getPropertyConverter()); - @SuppressWarnings("unchecked") - private R readMap(TypeInformation type, Map source) { + if (source instanceof String[]) { + // convert to a List + source = Arrays.asList((String[]) source); + } - Assert.notNull(source, "Document must not be null!"); + if (source instanceof List) { + source = ((List) source).stream().map(it -> convertOnRead(propertyConverter, it)) + .collect(Collectors.toList()); + } else if (source instanceof Set) { + source = ((Set) source).stream().map(it -> convertOnRead(propertyConverter, it)).collect(Collectors.toSet()); + } else { + source = convertOnRead(propertyConverter, source); + } + return source; + } - Class mapType = typeMapper.readType(source, type).getType(); + private Object convertOnRead(ElasticsearchPersistentPropertyConverter propertyConverter, Object source) { + if (String.class.isAssignableFrom(source.getClass())) { + source = propertyConverter.read((String) source); + } + return source; + } - TypeInformation keyType = type.getComponentType(); - TypeInformation valueType = type.getMapValueType(); + /** + * Reads the given {@link Collection} into a collection of the given {@link TypeInformation}. + * + * @param targetType must not be {@literal null}. + * @param source must not be {@literal null}. + * @return the converted {@link Collection} or array, will never be {@literal null}. + */ + @SuppressWarnings("unchecked") + @Nullable + private Object readCollectionOrArray(TypeInformation targetType, Collection source) { - Class rawKeyType = keyType != null ? keyType.getType() : null; - Class rawValueType = valueType != null ? valueType.getType() : null; + Assert.notNull(targetType, "Target type must not be null!"); - Map map = CollectionFactory.createMap(mapType, rawKeyType, source.keySet().size()); + Class collectionType = targetType.isSubTypeOf(Collection.class) // + ? targetType.getType() // + : List.class; - for (Entry entry : source.entrySet()) { + TypeInformation componentType = targetType.getComponentType() != null // + ? targetType.getComponentType() // + : ClassTypeInformation.OBJECT; + Class rawComponentType = componentType.getType(); - if (typeMapper.isTypeKey(entry.getKey())) { - continue; + Collection items = targetType.getType().isArray() // + ? new ArrayList<>(source.size()) // + : CollectionFactory.createCollection(collectionType, rawComponentType, source.size()); + + if (source.isEmpty()) { + return getPotentiallyConvertedSimpleRead(items, targetType); } - Object key = entry.getKey(); + for (Object element : source) { + + if (element instanceof Map) { + items.add(read(componentType, (Map) element)); + } else { - if (rawKeyType != null && !rawKeyType.isAssignableFrom(key.getClass())) { - key = conversionService.convert(key, rawKeyType); + if (!Object.class.equals(rawComponentType) && element instanceof Collection) { + if (!rawComponentType.isArray() && !ClassUtils.isAssignable(Iterable.class, rawComponentType)) { + throw new MappingException( + String.format(INCOMPATIBLE_TYPES, element, element.getClass(), rawComponentType)); + } + } + if (element instanceof List) { + items.add(readCollectionOrArray(componentType, (Collection) element)); + } else { + items.add(getPotentiallyConvertedSimpleRead(element, rawComponentType)); + } + } } - Object value = entry.getValue(); - TypeInformation defaultedValueType = valueType != null ? valueType : ClassTypeInformation.OBJECT; + return getPotentiallyConvertedSimpleRead(items, targetType.getType()); + } - if (value instanceof Map) { - map.put(key, read(defaultedValueType, (Map) value)); - } else if (value instanceof List) { - map.put(key, - readCollectionOrArray(valueType != null ? valueType : ClassTypeInformation.LIST, (List) value)); - } else { - map.put(key, getPotentiallyConvertedSimpleRead(value, rawValueType)); - } + @Nullable + private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, TypeInformation targetType) { + return getPotentiallyConvertedSimpleRead(value, targetType.getType()); } - return (R) map; - } + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Nullable + private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { - @Nullable - private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, TypeInformation targetType) { - return getPotentiallyConvertedSimpleRead(value, targetType.getType()); - } + if (target == null || value == null || ClassUtils.isAssignableValue(target, value)) { + return value; + } - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Nullable - private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { + if (conversions.hasCustomReadTarget(value.getClass(), target)) { + return conversionService.convert(value, target); + } - if (target == null || value == null || ClassUtils.isAssignableValue(target, value)) { - return value; - } + if (Enum.class.isAssignableFrom(target)) { + return Enum.valueOf((Class) target, value.toString()); + } - if (getConversions().hasCustomReadTarget(value.getClass(), target)) { return conversionService.convert(value, target); } - if (Enum.class.isAssignableFrom(target)) { - return Enum.valueOf((Class) target, value.toString()); - } - - return conversionService.convert(value, target); - } - - private void populateScriptFields(T result, SearchDocument searchDocument) { - Map> fields = searchDocument.getFields(); - if (!fields.isEmpty()) { - for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) { - ScriptedField scriptedField = field.getAnnotation(ScriptedField.class); - if (scriptedField != null) { - String name = scriptedField.name().isEmpty() ? field.getName() : scriptedField.name(); - Object value = searchDocument.getFieldValue(name); - if (value != null) { - field.setAccessible(true); - try { - field.set(result, value); - } catch (IllegalArgumentException e) { - throw new MappingException("failed to set scripted field: " + name + " with value: " + value, e); - } catch (IllegalAccessException e) { - throw new MappingException("failed to access scripted field: " + name, e); + private void populateScriptFields(T result, SearchDocument searchDocument) { + Map> fields = searchDocument.getFields(); + if (!fields.isEmpty()) { + for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) { + ScriptedField scriptedField = field.getAnnotation(ScriptedField.class); + if (scriptedField != null) { + String name = scriptedField.name().isEmpty() ? field.getName() : scriptedField.name(); + Object value = searchDocument.getFieldValue(name); + if (value != null) { + field.setAccessible(true); + try { + field.set(result, value); + } catch (IllegalArgumentException e) { + throw new MappingException("failed to set scripted field: " + name + " with value: " + value, e); + } catch (IllegalAccessException e) { + throw new MappingException("failed to access scripted field: " + name, e); + } } } } } } - } - // endregion - // region write - @Override - public void write(Object source, Document sink) { + /** + * Compute the type to use by checking the given entity against the store type; + */ + private ElasticsearchPersistentEntity computeClosestEntity(ElasticsearchPersistentEntity entity, + Map source) { - Assert.notNull(source, "source to map must not be null"); + TypeInformation typeToUse = typeMapper.readType(source); - if (source instanceof Map) { - // noinspection unchecked - sink.putAll((Map) source); - return; - } + if (typeToUse == null) { + return entity; + } - Class entityType = ClassUtils.getUserClass(source.getClass()); - TypeInformation typeInformation = ClassTypeInformation.from(entityType); + if (!entity.getTypeInformation().getType().isInterface() && !entity.getTypeInformation().isCollectionLike() + && !entity.getTypeInformation().isMap() + && !ClassUtils.isAssignableValue(entity.getType(), typeToUse.getType())) { + return entity; + } - if (requiresTypeHint(entityType)) { - typeMapper.writeType(typeInformation, sink); + return mappingContext.getRequiredPersistentEntity(typeToUse); } - writeInternal(source, sink, typeInformation); - } + class ElasticsearchPropertyValueProvider implements PropertyValueProvider { - /** - * Internal write conversion method which should be used for nested invocations. - * - * @param source the object to write - * @param sink the write destination - * @param typeInformation type information for the source - */ - @SuppressWarnings("unchecked") - protected void writeInternal(@Nullable Object source, Map sink, - @Nullable TypeInformation typeInformation) { + final MapValueAccessor accessor; + final SpELExpressionEvaluator evaluator; - if (null == source) { - return; - } + ElasticsearchPropertyValueProvider(MapValueAccessor accessor, SpELExpressionEvaluator evaluator) { + this.accessor = accessor; + this.evaluator = evaluator; + } - Class entityType = source.getClass(); - Optional> customTarget = conversions.getCustomWriteTarget(entityType, Map.class); + @Override + public T getPropertyValue(ElasticsearchPersistentProperty property) { - if (customTarget.isPresent()) { - Map result = conversionService.convert(source, Map.class); + String expression = property.getSpelExpression(); + Object value = expression != null ? evaluator.evaluate(expression) : accessor.get(property); - if (result != null) { - sink.putAll(result); + if (value == null) { + return null; + } + + return readValue(value, property, property.getTypeInformation()); } - return; } - if (Map.class.isAssignableFrom(entityType)) { - writeMapInternal((Map) source, sink, ClassTypeInformation.MAP); - return; - } + /** + * Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw + * resolved SpEL value. + * + * @author Mark Paluch + */ + private class ConverterAwareSpELExpressionParameterValueProvider + extends SpELExpressionParameterValueProvider { + + /** + * Creates a new {@link ConverterAwareSpELExpressionParameterValueProvider}. + * + * @param evaluator must not be {@literal null}. + * @param conversionService must not be {@literal null}. + * @param delegate must not be {@literal null}. + */ + public ConverterAwareSpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator, + ConversionService conversionService, ParameterValueProvider delegate) { + + super(evaluator, conversionService, delegate); + } - if (Collection.class.isAssignableFrom(entityType)) { - writeCollectionInternal((Collection) source, ClassTypeInformation.LIST, (Collection) sink); - return; + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.SpELExpressionParameterValueProvider#potentiallyConvertSpelValue(java.lang.Object, org.springframework.data.mapping.PreferredConstructor.Parameter) + */ + @Override + protected T potentiallyConvertSpelValue(Object object, + PreferredConstructor.Parameter parameter) { + return readValue(object, parameter.getType()); + } } - ElasticsearchPersistentEntity entity = mappingContext.getRequiredPersistentEntity(entityType); - addCustomTypeKeyIfNecessary(source, sink, typeInformation); - writeInternal(source, sink, entity); + enum NoOpParameterValueProvider implements ParameterValueProvider { + + INSTANCE; + + @Override + public T getParameterValue(PreferredConstructor.Parameter parameter) { + return null; + } + } } /** - * Internal write conversion method which should be used for nested invocations. - * - * @param source the object to write - * @param sink the write destination - * @param entity entity for the source + * Class to do the actual writing. The methods originally were in the MappingElasticsearchConverter class, but are + * refactored to allow for keeping state during the conversion of an object. */ - protected void writeInternal(@Nullable Object source, Map sink, - @Nullable ElasticsearchPersistentEntity entity) { + static private class Writer extends Base { - if (source == null) { - return; - } + private boolean writeTypeHints = true; - if (null == entity) { - throw new MappingException("No mapping metadata found for entity of type " + source.getClass().getName()); + public Writer( + MappingContext, ElasticsearchPersistentProperty> mappingContext, + GenericConversionService conversionService, CustomConversions conversions) { + super(mappingContext, conversionService, conversions); } - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); - writeProperties(entity, accessor, new MapValueAccessor(sink)); - } + void write(Object source, Document sink) { - protected void writeProperties(ElasticsearchPersistentEntity entity, PersistentPropertyAccessor accessor, - MapValueAccessor sink) { + if (source instanceof Map) { + // noinspection unchecked + sink.putAll((Map) source); + return; + } - for (ElasticsearchPersistentProperty property : entity) { + Class entityType = ClassUtils.getUserClass(source.getClass()); + ElasticsearchPersistentEntity entity = mappingContext.getPersistentEntity(entityType); - if (!property.isWritable()) { - continue; + if (entity != null) { + writeTypeHints = entity.writeTypeHints(); } - Object value = accessor.getProperty(property); + TypeInformation typeInformation = ClassTypeInformation.from(entityType); - if (value == null) { + if (writeTypeHints && requiresTypeHint(entityType)) { + typeMapper.writeType(typeInformation, sink); + } - if (property.storeNullValue()) { - sink.set(property, null); - } + writeInternal(source, sink, typeInformation); + } + + /** + * Internal write conversion method which should be used for nested invocations. + * + * @param source the object to write + * @param sink the write destination + * @param typeInformation type information for the source + */ + @SuppressWarnings("unchecked") + private void writeInternal(@Nullable Object source, Map sink, + @Nullable TypeInformation typeInformation) { - continue; + if (null == source) { + return; } - if (property.hasPropertyConverter()) { - value = propertyConverterWrite(property, value); - sink.set(property, value); - } else if (TemporalAccessor.class.isAssignableFrom(property.getActualType()) - && !getConversions().hasCustomWriteTarget(value.getClass())) { + Class entityType = source.getClass(); + Optional> customTarget = conversions.getCustomWriteTarget(entityType, Map.class); - // log at most 5 times - String propertyName = entity.getType().getSimpleName() + '.' + property.getName(); - String key = propertyName + "-write"; - int count = propertyWarnings.computeIfAbsent(key, k -> 0); - if (count < 5) { - LOGGER.warn( - "Type {} of property {} is a TemporalAccessor class but has neither a @Field annotation defining the date type nor a registered converter for writing!" - + " It will be mapped to a complex object in Elasticsearch!", - property.getType().getSimpleName(), propertyName); - propertyWarnings.put(key, count + 1); - } - } else if (!isSimpleType(value)) { - writeProperty(property, value, sink); - } else { - Object writeSimpleValue = getPotentiallyConvertedSimpleWrite(value, Object.class); - if (writeSimpleValue != null) { - sink.set(property, writeSimpleValue); + if (customTarget.isPresent()) { + Map result = conversionService.convert(source, Map.class); + + if (result != null) { + sink.putAll(result); } + return; } - } - } - private Object propertyConverterWrite(ElasticsearchPersistentProperty property, Object value) { - ElasticsearchPersistentPropertyConverter propertyConverter = Objects - .requireNonNull(property.getPropertyConverter()); + if (Map.class.isAssignableFrom(entityType)) { + writeMapInternal((Map) source, sink, ClassTypeInformation.MAP); + return; + } - if (value instanceof List) { - value = ((List) value).stream().map(propertyConverter::write).collect(Collectors.toList()); - } else if (value instanceof Set) { - value = ((Set) value).stream().map(propertyConverter::write).collect(Collectors.toSet()); - } else { - value = propertyConverter.write(value); + if (Collection.class.isAssignableFrom(entityType)) { + writeCollectionInternal((Collection) source, ClassTypeInformation.LIST, (Collection) sink); + return; + } + + ElasticsearchPersistentEntity entity = mappingContext.getRequiredPersistentEntity(entityType); + addCustomTypeKeyIfNecessary(source, sink, typeInformation); + writeInternal(source, sink, entity); } - return value; - } - @SuppressWarnings("unchecked") - protected void writeProperty(ElasticsearchPersistentProperty property, Object value, MapValueAccessor sink) { + /** + * Internal write conversion method which should be used for nested invocations. + * + * @param source the object to write + * @param sink the write destination + * @param entity entity for the source + */ + private void writeInternal(@Nullable Object source, Map sink, + @Nullable ElasticsearchPersistentEntity entity) { + + if (source == null) { + return; + } - Optional> customWriteTarget = getConversions().getCustomWriteTarget(value.getClass()); + if (null == entity) { + throw new MappingException("No mapping metadata found for entity of type " + source.getClass().getName()); + } - if (customWriteTarget.isPresent()) { - Class writeTarget = customWriteTarget.get(); - sink.set(property, conversionService.convert(value, writeTarget)); - return; + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); + writeProperties(entity, accessor, new MapValueAccessor(sink)); } - TypeInformation valueType = ClassTypeInformation.from(value.getClass()); - TypeInformation type = property.getTypeInformation(); + /** + * Check if a given type requires a type hint (aka {@literal _class} attribute) when writing to the document. + * + * @param type must not be {@literal null}. + * @return {@literal true} if not a simple type, {@link Collection} or type with custom write target. + */ + private boolean requiresTypeHint(Class type) { - if (valueType.isCollectionLike()) { - List collectionInternal = createCollection(asCollection(value), property); - sink.set(property, collectionInternal); - return; + return !isSimpleType(type) && !ClassUtils.isAssignable(Collection.class, type) + && !conversions.hasCustomWriteTarget(type, Document.class); } - if (valueType.isMap()) { - Map mapDbObj = createMap((Map) value, property); - sink.set(property, mapDbObj); - return; + private boolean isSimpleType(Object value) { + return isSimpleType(value.getClass()); } - // Lookup potential custom target type - Optional> basicTargetType = conversions.getCustomWriteTarget(value.getClass()); + private boolean isSimpleType(Class type) { + return !Map.class.isAssignableFrom(type) && conversions.isSimpleType(type); + } - if (basicTargetType.isPresent()) { + /** + * Writes the given {@link Map} to the given {@link Document} considering the given {@link TypeInformation}. + * + * @param source must not be {@literal null}. + * @param sink must not be {@literal null}. + * @param propertyType must not be {@literal null}. + */ + private Map writeMapInternal(Map source, Map sink, + TypeInformation propertyType) { - sink.set(property, conversionService.convert(value, basicTargetType.get())); - return; - } + for (Map.Entry entry : source.entrySet()) { - ElasticsearchPersistentEntity entity = valueType.isSubTypeOf(property.getType()) - ? mappingContext.getRequiredPersistentEntity(value.getClass()) - : mappingContext.getRequiredPersistentEntity(type); + Object key = entry.getKey(); + Object value = entry.getValue(); - Object existingValue = sink.get(property); - Map document = existingValue instanceof Map ? (Map) existingValue - : Document.create(); + if (isSimpleType(key.getClass())) { - addCustomTypeKeyIfNecessary(value, document, ClassTypeInformation.from(property.getRawType())); - writeInternal(value, document, entity); - sink.set(property, document); - } + String simpleKey = potentiallyConvertMapKey(key); + if (value == null || isSimpleType(value)) { + sink.put(simpleKey, getPotentiallyConvertedSimpleWrite(value, Object.class)); + } else if (value instanceof Collection || value.getClass().isArray()) { + sink.put(simpleKey, + writeCollectionInternal(asCollection(value), propertyType.getMapValueType(), new ArrayList<>())); + } else { + Map document = Document.create(); + TypeInformation valueTypeInfo = propertyType.isMap() ? propertyType.getMapValueType() + : ClassTypeInformation.OBJECT; + writeInternal(value, document, valueTypeInfo); - /** - * Writes the given {@link Collection} using the given {@link ElasticsearchPersistentProperty} information. - * - * @param collection must not be {@literal null}. - * @param property must not be {@literal null}. - */ - protected List createCollection(Collection collection, ElasticsearchPersistentProperty property) { - return writeCollectionInternal(collection, property.getTypeInformation(), new ArrayList<>(collection.size())); - } + sink.put(simpleKey, document); + } + } else { + throw new MappingException("Cannot use a complex object as a key value."); + } + } - /** - * Writes the given {@link Map} using the given {@link ElasticsearchPersistentProperty} information. - * - * @param map must not {@literal null}. - * @param property must not be {@literal null}. - */ - protected Map createMap(Map map, ElasticsearchPersistentProperty property) { + return sink; + } - Assert.notNull(map, "Given map must not be null!"); - Assert.notNull(property, "PersistentProperty must not be null!"); + /** + * Populates the given {@link Collection sink} with converted values from the given {@link Collection source}. + * + * @param source the collection to create a {@link Collection} for, must not be {@literal null}. + * @param type the {@link TypeInformation} to consider or {@literal null} if unknown. + * @param sink the {@link Collection} to write to. + */ + @SuppressWarnings("unchecked") + private List writeCollectionInternal(Collection source, @Nullable TypeInformation type, + Collection sink) { - return writeMapInternal(map, new LinkedHashMap<>(map.size()), property.getTypeInformation()); - } + TypeInformation componentType = null; - /** - * Writes the given {@link Map} to the given {@link Document} considering the given {@link TypeInformation}. - * - * @param source must not be {@literal null}. - * @param sink must not be {@literal null}. - * @param propertyType must not be {@literal null}. - */ - protected Map writeMapInternal(Map source, Map sink, - TypeInformation propertyType) { + List collection = sink instanceof List ? (List) sink : new ArrayList<>(sink); - for (Map.Entry entry : source.entrySet()) { + if (type != null) { + componentType = type.getComponentType(); + } - Object key = entry.getKey(); - Object value = entry.getValue(); + for (Object element : source) { - if (isSimpleType(key.getClass())) { + Class elementType = element == null ? null : element.getClass(); - String simpleKey = potentiallyConvertMapKey(key); - if (value == null || isSimpleType(value)) { - sink.put(simpleKey, getPotentiallyConvertedSimpleWrite(value, Object.class)); - } else if (value instanceof Collection || value.getClass().isArray()) { - sink.put(simpleKey, - writeCollectionInternal(asCollection(value), propertyType.getMapValueType(), new ArrayList<>())); + if (elementType == null || conversions.isSimpleType(elementType)) { + collection.add(getPotentiallyConvertedSimpleWrite(element, + componentType != null ? componentType.getType() : Object.class)); + } else if (element instanceof Collection || elementType.isArray()) { + collection.add(writeCollectionInternal(asCollection(element), componentType, new ArrayList<>())); } else { Map document = Document.create(); - TypeInformation valueTypeInfo = propertyType.isMap() ? propertyType.getMapValueType() - : ClassTypeInformation.OBJECT; - writeInternal(value, document, valueTypeInfo); - - sink.put(simpleKey, document); + writeInternal(element, document, componentType); + collection.add(document); } - } else { - throw new MappingException("Cannot use a complex object as a key value."); } + + return collection; } - return sink; - } + private void writeProperties(ElasticsearchPersistentEntity entity, PersistentPropertyAccessor accessor, + MapValueAccessor sink) { - /** - * Populates the given {@link Collection sink} with converted values from the given {@link Collection source}. - * - * @param source the collection to create a {@link Collection} for, must not be {@literal null}. - * @param type the {@link TypeInformation} to consider or {@literal null} if unknown. - * @param sink the {@link Collection} to write to. - */ - @SuppressWarnings("unchecked") - private List writeCollectionInternal(Collection source, @Nullable TypeInformation type, - Collection sink) { + for (ElasticsearchPersistentProperty property : entity) { - TypeInformation componentType = null; + if (!property.isWritable()) { + continue; + } - List collection = sink instanceof List ? (List) sink : new ArrayList<>(sink); + Object value = accessor.getProperty(property); - if (type != null) { - componentType = type.getComponentType(); - } + if (value == null) { - for (Object element : source) { + if (property.storeNullValue()) { + sink.set(property, null); + } - Class elementType = element == null ? null : element.getClass(); + continue; + } - if (elementType == null || conversions.isSimpleType(elementType)) { - collection.add(getPotentiallyConvertedSimpleWrite(element, - componentType != null ? componentType.getType() : Object.class)); - } else if (element instanceof Collection || elementType.isArray()) { - collection.add(writeCollectionInternal(asCollection(element), componentType, new ArrayList<>())); - } else { - Map document = Document.create(); - writeInternal(element, document, componentType); - collection.add(document); + if (property.hasPropertyConverter()) { + value = propertyConverterWrite(property, value); + sink.set(property, value); + } else if (TemporalAccessor.class.isAssignableFrom(property.getActualType()) + && !conversions.hasCustomWriteTarget(value.getClass())) { + + // log at most 5 times + String propertyName = entity.getType().getSimpleName() + '.' + property.getName(); + String key = propertyName + "-write"; + int count = propertyWarnings.computeIfAbsent(key, k -> 0); + if (count < 5) { + LOGGER.warn( + "Type {} of property {} is a TemporalAccessor class but has neither a @Field annotation defining the date type nor a registered converter for writing!" + + " It will be mapped to a complex object in Elasticsearch!", + property.getType().getSimpleName(), propertyName); + propertyWarnings.put(key, count + 1); + } + } else if (!isSimpleType(value)) { + writeProperty(property, value, sink); + } else { + Object writeSimpleValue = getPotentiallyConvertedSimpleWrite(value, Object.class); + if (writeSimpleValue != null) { + sink.set(property, writeSimpleValue); + } + } } } - return collection; - } + @SuppressWarnings("unchecked") + protected void writeProperty(ElasticsearchPersistentProperty property, Object value, MapValueAccessor sink) { - /** - * Returns a {@link String} representation of the given {@link Map} key - * - * @param key the key to convert - */ - private String potentiallyConvertMapKey(Object key) { + Optional> customWriteTarget = conversions.getCustomWriteTarget(value.getClass()); - if (key instanceof String) { - return (String) key; - } + if (customWriteTarget.isPresent()) { + Class writeTarget = customWriteTarget.get(); + sink.set(property, conversionService.convert(value, writeTarget)); + return; + } - if (conversions.hasCustomWriteTarget(key.getClass(), String.class)) { - Object potentiallyConvertedSimpleWrite = getPotentiallyConvertedSimpleWrite(key, Object.class); + TypeInformation valueType = ClassTypeInformation.from(value.getClass()); + TypeInformation type = property.getTypeInformation(); - if (potentiallyConvertedSimpleWrite == null) { - return key.toString(); + if (valueType.isCollectionLike()) { + List collectionInternal = createCollection(asCollection(value), property); + sink.set(property, collectionInternal); + return; } - return (String) potentiallyConvertedSimpleWrite; - } - return key.toString(); - } - /** - * Checks whether we have a custom conversion registered for the given value into an arbitrary simple Elasticsearch - * type. Returns the converted value if so. If not, we perform special enum handling or simply return the value as is. - * - * @param value value to convert - */ - @Nullable - private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nullable Class typeHint) { + if (valueType.isMap()) { + Map mapDbObj = createMap((Map) value, property); + sink.set(property, mapDbObj); + return; + } + + // Lookup potential custom target type + Optional> basicTargetType = conversions.getCustomWriteTarget(value.getClass()); + + if (basicTargetType.isPresent()) { + + sink.set(property, conversionService.convert(value, basicTargetType.get())); + return; + } - if (value == null) { - return null; + ElasticsearchPersistentEntity entity = valueType.isSubTypeOf(property.getType()) + ? mappingContext.getRequiredPersistentEntity(value.getClass()) + : mappingContext.getRequiredPersistentEntity(type); + + Object existingValue = sink.get(property); + Map document = existingValue instanceof Map ? (Map) existingValue + : Document.create(); + + addCustomTypeKeyIfNecessary(value, document, ClassTypeInformation.from(property.getRawType())); + writeInternal(value, document, entity); + sink.set(property, document); } - if (typeHint != null && Object.class != typeHint) { + /** + * Adds custom typeInformation information to the given {@link Map} if necessary. That is if the value is not the + * same as the one given. This is usually the case if you store a subtype of the actual declared typeInformation of + * the property. + * + * @param source must not be {@literal null}. + * @param sink must not be {@literal null}. + * @param type type to compare to + */ + private void addCustomTypeKeyIfNecessary(Object source, Map sink, + @Nullable TypeInformation type) { + + if (!writeTypeHints) { + return; + } - if (conversionService.canConvert(value.getClass(), typeHint)) { - value = conversionService.convert(value, typeHint); + Class reference; - if (value == null) { - return null; - } + if (type == null) { + reference = Object.class; + } else { + TypeInformation actualType = type.getActualType(); + reference = actualType == null ? Object.class : actualType.getType(); + } + Class valueType = ClassUtils.getUserClass(source.getClass()); + + boolean notTheSameClass = !valueType.equals(reference); + if (notTheSameClass) { + typeMapper.writeType(valueType, sink); } } - Optional> customTarget = conversions.getCustomWriteTarget(value.getClass()); + /** + * Returns a {@link String} representation of the given {@link Map} key + * + * @param key the key to convert + */ + private String potentiallyConvertMapKey(Object key) { - if (customTarget.isPresent()) { - return conversionService.convert(value, customTarget.get()); - } + if (key instanceof String) { + return (String) key; + } - if (ObjectUtils.isArray(value)) { + if (conversions.hasCustomWriteTarget(key.getClass(), String.class)) { + Object potentiallyConvertedSimpleWrite = getPotentiallyConvertedSimpleWrite(key, Object.class); - if (value instanceof byte[]) { - return value; + if (potentiallyConvertedSimpleWrite == null) { + return key.toString(); + } + return (String) potentiallyConvertedSimpleWrite; } - return asCollection(value); + return key.toString(); } - return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; - } + /** + * Checks whether we have a custom conversion registered for the given value into an arbitrary simple Elasticsearch + * type. Returns the converted value if so. If not, we perform special enum handling or simply return the value as + * is. + * + * @param value value to convert + */ + @Nullable + private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nullable Class typeHint) { - /** - * @deprecated since 4.2, use {@link #getPotentiallyConvertedSimpleWrite(Object, Class)} instead. - */ - @Nullable - @Deprecated - protected Object getWriteSimpleValue(Object value) { - Optional> customTarget = getConversions().getCustomWriteTarget(value.getClass()); + if (value == null) { + return null; + } - if (customTarget.isPresent()) { - return conversionService.convert(value, customTarget.get()); - } + if (typeHint != null && Object.class != typeHint) { - return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; - } + if (conversionService.canConvert(value.getClass(), typeHint)) { + value = conversionService.convert(value, typeHint); - /** - * @deprecated since 4.2, use {@link #writeInternal(Object, Map, TypeInformation)} instead. - */ - @Deprecated - protected Object getWriteComplexValue(ElasticsearchPersistentProperty property, - @SuppressWarnings("unused") TypeInformation typeHint, Object value) { + if (value == null) { + return null; + } + } + } - Document document = Document.create(); - writeInternal(value, document, property.getTypeInformation()); + Optional> customTarget = conversions.getCustomWriteTarget(value.getClass()); - return document; - } + if (customTarget.isPresent()) { + return conversionService.convert(value, customTarget.get()); + } - // endregion + if (ObjectUtils.isArray(value)) { - // region helper methods + if (value instanceof byte[]) { + return value; + } + return asCollection(value); + } - /** - * Adds custom typeInformation information to the given {@link Map} if necessary. That is if the value is not the same - * as the one given. This is usually the case if you store a subtype of the actual declared typeInformation of the - * property. - * - * @param source must not be {@literal null}. - * @param sink must not be {@literal null}. - * @param type type to compare to - */ - protected void addCustomTypeKeyIfNecessary(Object source, Map sink, - @Nullable TypeInformation type) { + return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; + } - Class reference; + private Object propertyConverterWrite(ElasticsearchPersistentProperty property, Object value) { + ElasticsearchPersistentPropertyConverter propertyConverter = Objects + .requireNonNull(property.getPropertyConverter()); - if (type == null) { - reference = Object.class; - } else { - TypeInformation actualType = type.getActualType(); - reference = actualType == null ? Object.class : actualType.getType(); + if (value instanceof List) { + value = ((List) value).stream().map(propertyConverter::write).collect(Collectors.toList()); + } else if (value instanceof Set) { + value = ((Set) value).stream().map(propertyConverter::write).collect(Collectors.toSet()); + } else { + value = propertyConverter.write(value); + } + return value; } - Class valueType = ClassUtils.getUserClass(source.getClass()); - boolean notTheSameClass = !valueType.equals(reference); - if (notTheSameClass) { - typeMapper.writeType(valueType, sink); + /** + * Writes the given {@link Collection} using the given {@link ElasticsearchPersistentProperty} information. + * + * @param collection must not be {@literal null}. + * @param property must not be {@literal null}. + */ + protected List createCollection(Collection collection, ElasticsearchPersistentProperty property) { + return writeCollectionInternal(collection, property.getTypeInformation(), new ArrayList<>(collection.size())); } - } - /** - * Check if a given type requires a type hint (aka {@literal _class} attribute) when writing to the document. - * - * @param type must not be {@literal null}. - * @return {@literal true} if not a simple type, {@link Collection} or type with custom write target. - */ - public boolean requiresTypeHint(Class type) { + /** + * Writes the given {@link Map} using the given {@link ElasticsearchPersistentProperty} information. + * + * @param map must not {@literal null}. + * @param property must not be {@literal null}. + */ + protected Map createMap(Map map, ElasticsearchPersistentProperty property) { - return !isSimpleType(type) && !ClassUtils.isAssignable(Collection.class, type) - && !conversions.hasCustomWriteTarget(type, Document.class); - } + Assert.notNull(map, "Given map must not be null!"); + Assert.notNull(property, "PersistentProperty must not be null!"); - /** - * Compute the type to use by checking the given entity against the store type; - */ - private ElasticsearchPersistentEntity computeClosestEntity(ElasticsearchPersistentEntity entity, - Map source) { + return writeMapInternal(map, new LinkedHashMap<>(map.size()), property.getTypeInformation()); + } - TypeInformation typeToUse = typeMapper.readType(source); + /** + * @deprecated since 4.2, use {@link #getPotentiallyConvertedSimpleWrite(Object, Class)} instead. + */ + @Nullable + @Deprecated + protected Object getWriteSimpleValue(Object value) { + Optional> customTarget = conversions.getCustomWriteTarget(value.getClass()); - if (typeToUse == null) { - return entity; - } + if (customTarget.isPresent()) { + return conversionService.convert(value, customTarget.get()); + } - if (!entity.getTypeInformation().getType().isInterface() && !entity.getTypeInformation().isCollectionLike() - && !entity.getTypeInformation().isMap() - && !ClassUtils.isAssignableValue(entity.getType(), typeToUse.getType())) { - return entity; + return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; } - return mappingContext.getRequiredPersistentEntity(typeToUse); - } + /** + * @deprecated since 4.2, use {@link #writeInternal(Object, Map, TypeInformation)} instead. + */ + @Deprecated + protected Object getWriteComplexValue(ElasticsearchPersistentProperty property, + @SuppressWarnings("unused") TypeInformation typeHint, Object value) { - private boolean isSimpleType(Object value) { - return isSimpleType(value.getClass()); - } + Document document = Document.create(); + writeInternal(value, document, property.getTypeInformation()); - private boolean isSimpleType(Class type) { - return !Map.class.isAssignableFrom(type) && getConversions().isSimpleType(type); - } + return document; + } - /** - * Returns given object as {@link Collection}. Will return the {@link Collection} as is if the source is a - * {@link Collection} already, will convert an array into a {@link Collection} or simply create a single element - * collection for everything else. - * - * @param source object to convert - */ - private static Collection asCollection(Object source) { + /** + * Returns given object as {@link Collection}. Will return the {@link Collection} as is if the source is a + * {@link Collection} already, will convert an array into a {@link Collection} or simply create a single element + * collection for everything else. + * + * @param source object to convert + */ + private static Collection asCollection(Object source) { - if (source instanceof Collection) { - return (Collection) source; - } + if (source instanceof Collection) { + return (Collection) source; + } - return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); + return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); + } } // endregion @@ -1260,71 +1387,4 @@ private Map getAsMap(Object result) { } } - class ElasticsearchPropertyValueProvider implements PropertyValueProvider { - - final MapValueAccessor accessor; - final SpELExpressionEvaluator evaluator; - - ElasticsearchPropertyValueProvider(MapValueAccessor accessor, SpELExpressionEvaluator evaluator) { - this.accessor = accessor; - this.evaluator = evaluator; - } - - @Override - public T getPropertyValue(ElasticsearchPersistentProperty property) { - - String expression = property.getSpelExpression(); - Object value = expression != null ? evaluator.evaluate(expression) : accessor.get(property); - - if (value == null) { - return null; - } - - return readValue(value, property, property.getTypeInformation()); - } - } - - /** - * Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw - * resolved SpEL value. - * - * @author Mark Paluch - */ - private class ConverterAwareSpELExpressionParameterValueProvider - extends SpELExpressionParameterValueProvider { - - /** - * Creates a new {@link ConverterAwareSpELExpressionParameterValueProvider}. - * - * @param evaluator must not be {@literal null}. - * @param conversionService must not be {@literal null}. - * @param delegate must not be {@literal null}. - */ - public ConverterAwareSpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator, - ConversionService conversionService, ParameterValueProvider delegate) { - - super(evaluator, conversionService, delegate); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.mapping.model.SpELExpressionParameterValueProvider#potentiallyConvertSpelValue(java.lang.Object, org.springframework.data.mapping.PreferredConstructor.Parameter) - */ - @Override - protected T potentiallyConvertSpelValue(Object object, - PreferredConstructor.Parameter parameter) { - return readValue(object, parameter.getType()); - } - } - - enum NoOpParameterValueProvider implements ParameterValueProvider { - - INSTANCE; - - @Override - public T getParameterValue(PreferredConstructor.Parameter parameter) { - return null; - } - } - } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java index 79e921a97..2cc1df4df 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java @@ -95,6 +95,8 @@ public class MappingBuilder { private final ElasticsearchConverter elasticsearchConverter; + private boolean writeTypeHints = true; + public MappingBuilder(ElasticsearchConverter elasticsearchConverter) { this.elasticsearchConverter = elasticsearchConverter; } @@ -111,6 +113,8 @@ public String buildPropertyMapping(Class clazz) throws MappingException { ElasticsearchPersistentEntity entity = elasticsearchConverter.getMappingContext() .getRequiredPersistentEntity(clazz); + writeTypeHints = entity.writeTypeHints(); + XContentBuilder builder = jsonBuilder().startObject(); // Dynamic templates @@ -128,11 +132,14 @@ public String buildPropertyMapping(Class clazz) throws MappingException { } private void writeTypeHintMapping(XContentBuilder builder) throws IOException { - builder.startObject(TYPEHINT_PROPERTY) // - .field(FIELD_PARAM_TYPE, TYPE_VALUE_KEYWORD) // - .field(FIELD_PARAM_INDEX, false) // - .field(FIELD_PARAM_DOC_VALUES, false) // - .endObject(); + + if (writeTypeHints) { + builder.startObject(TYPEHINT_PROPERTY) // + .field(FIELD_PARAM_TYPE, TYPE_VALUE_KEYWORD) // + .field(FIELD_PARAM_INDEX, false) // + .field(FIELD_PARAM_DOC_VALUES, false) // + .endObject(); + } } private void mapEntity(XContentBuilder builder, @Nullable ElasticsearchPersistentEntity entity, diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java index 650a25360..3a53b4b1c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentEntity.java @@ -17,11 +17,11 @@ import org.elasticsearch.index.VersionType; import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.index.Settings; import org.springframework.data.elasticsearch.core.join.JoinField; import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm; import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.lang.Nullable; /** @@ -148,4 +148,16 @@ default ElasticsearchPersistentProperty getRequiredSeqNoPrimaryTermProperty() { */ @Nullable String resolveRouting(T bean); + + /** + * @return the {@link FieldNamingStrategy} for the entity + * @since 4.3 + */ + FieldNamingStrategy getFieldNamingStrategy(); + + /** + * @return true if type hints on this entity should be written. + * @since 4.3 + */ + boolean writeTypeHints(); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java index 74b88a553..1195ad33a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java @@ -16,6 +16,7 @@ package org.springframework.data.elasticsearch.core.mapping; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm; import org.springframework.data.mapping.PersistentProperty; import org.springframework.lang.Nullable; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchMappingContext.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchMappingContext.java index 81f90c66a..9fd20c7e7 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchMappingContext.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchMappingContext.java @@ -37,6 +37,7 @@ public class SimpleElasticsearchMappingContext private static final FieldNamingStrategy DEFAULT_NAMING_STRATEGY = PropertyNameFieldNamingStrategy.INSTANCE; private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY; + private boolean writeTypeHints = true; /** * Configures the {@link FieldNamingStrategy} to be used to determine the field name if no manual mapping is applied. @@ -50,6 +51,15 @@ public void setFieldNamingStrategy(@Nullable FieldNamingStrategy fieldNamingStra this.fieldNamingStrategy = fieldNamingStrategy == null ? DEFAULT_NAMING_STRATEGY : fieldNamingStrategy; } + /** + * Sets the flag if type hints should be written in Entities created by this instance. + * + * @since 4.3 + */ + public void setWriteTypeHints(boolean writeTypeHints) { + this.writeTypeHints = writeTypeHints; + } + @Override protected boolean shouldCreatePersistentEntityFor(TypeInformation type) { return !ElasticsearchSimpleTypes.HOLDER.isSimpleType(type.getType()); @@ -57,12 +67,13 @@ protected boolean shouldCreatePersistentEntityFor(TypeInformation type) { @Override protected SimpleElasticsearchPersistentEntity createPersistentEntity(TypeInformation typeInformation) { - return new SimpleElasticsearchPersistentEntity<>(typeInformation); + return new SimpleElasticsearchPersistentEntity<>(typeInformation, + new SimpleElasticsearchPersistentEntity.ContextConfiguration(fieldNamingStrategy, writeTypeHints)); } @Override protected ElasticsearchPersistentProperty createPersistentProperty(Property property, SimpleElasticsearchPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { - return new SimpleElasticsearchPersistentProperty(property, owner, simpleTypeHolder, fieldNamingStrategy); + return new SimpleElasticsearchPersistentProperty(property, owner, simpleTypeHolder); } } 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 eaeebb38f..74bb804c2 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 @@ -34,6 +34,7 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory; import org.springframework.data.spel.ExpressionDependencies; import org.springframework.data.util.Lazy; @@ -66,10 +67,9 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit private static final Logger LOGGER = LoggerFactory.getLogger(SimpleElasticsearchPersistentEntity.class); private static final SpelExpressionParser PARSER = new SpelExpressionParser(); + private @Nullable final Document document; private @Nullable String indexName; private final Lazy settingsParameter; - @Deprecated private @Nullable String parentType; - @Deprecated private @Nullable ElasticsearchPersistentProperty parentIdProperty; private @Nullable ElasticsearchPersistentProperty seqNoPrimaryTermProperty; private @Nullable ElasticsearchPersistentProperty joinFieldProperty; private @Nullable VersionType versionType; @@ -77,18 +77,21 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit private final Map fieldNamePropertyCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap routingExpressions = new ConcurrentHashMap<>(); private @Nullable String routing; + private final ContextConfiguration contextConfiguration; private final ConcurrentHashMap indexNameExpressions = new ConcurrentHashMap<>(); private final Lazy indexNameEvaluationContext = Lazy.of(this::getIndexNameEvaluationContext); - public SimpleElasticsearchPersistentEntity(TypeInformation typeInformation) { + public SimpleElasticsearchPersistentEntity(TypeInformation typeInformation, + ContextConfiguration contextConfiguration) { super(typeInformation); + this.contextConfiguration = contextConfiguration; Class clazz = typeInformation.getType(); - org.springframework.data.elasticsearch.annotations.Document document = AnnotatedElementUtils - .findMergedAnnotation(clazz, org.springframework.data.elasticsearch.annotations.Document.class); + document = AnnotatedElementUtils.findMergedAnnotation(clazz, + org.springframework.data.elasticsearch.annotations.Document.class); // need a Lazy here, because we need the persistent properties available this.settingsParameter = Lazy.of(() -> buildSettingsParameter(clazz)); @@ -159,7 +162,31 @@ public boolean isCreateIndexAndMapping() { return createIndexAndMapping; } - // endregion + @Override + public FieldNamingStrategy getFieldNamingStrategy() { + return contextConfiguration.getFieldNamingStrategy(); + } + + @Override + public boolean writeTypeHints() { + + boolean writeTypeHints = contextConfiguration.writeTypeHints; + + if (document != null) { + switch (document.writeTypeHint()) { + case TRUE: + writeTypeHints = true; + break; + case FALSE: + writeTypeHints = false; + break; + case DEFAULT: + break; + } + } + + return writeTypeHints; + } @Override public void addPersistentProperty(ElasticsearchPersistentProperty property) { @@ -215,6 +242,7 @@ private void warnAboutBothSeqNoPrimaryTermAndVersionProperties() { * (non-Javadoc) * @see org.springframework.data.mapping.model.BasicPersistentEntity#setPersistentPropertyAccessorFactory(org.springframework.data.mapping.model.PersistentPropertyAccessorFactory) */ + @SuppressWarnings("SpellCheckingInspection") @Override public void setPersistentPropertyAccessorFactory(PersistentPropertyAccessorFactory factory) { @@ -327,6 +355,7 @@ private EvaluationContext getIndexNameEvaluationContext() { ExpressionDependencies expressionDependencies = expression != null ? ExpressionDependencies.discover(expression) : ExpressionDependencies.none(); + // noinspection ConstantConditions return getEvaluationContext(null, expressionDependencies); } @@ -350,6 +379,7 @@ public String resolveRouting(T bean) { Expression expression = routingExpressions.computeIfAbsent(routing, PARSER::parseExpression); ExpressionDependencies expressionDependencies = ExpressionDependencies.discover(expression); + // noinspection ConstantConditions EvaluationContext context = getEvaluationContext(null, expressionDependencies); context.setVariable("entity", bean); @@ -525,4 +555,22 @@ Settings toSettings() { } // endregion + + /** + * Configuration settings passed in from the creating {@link SimpleElasticsearchMappingContext}. + */ + static class ContextConfiguration { + + private final FieldNamingStrategy fieldNamingStrategy; + private final boolean writeTypeHints; + + ContextConfiguration(FieldNamingStrategy fieldNamingStrategy, boolean writeTypeHints) { + this.fieldNamingStrategy = fieldNamingStrategy; + this.writeTypeHints = writeTypeHints; + } + + public FieldNamingStrategy getFieldNamingStrategy() { + return fieldNamingStrategy; + } + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java index 11b7c2799..1d171715c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java @@ -64,23 +64,20 @@ public class SimpleElasticsearchPersistentProperty extends private static final Logger LOGGER = LoggerFactory.getLogger(SimpleElasticsearchPersistentProperty.class); private static final List SUPPORTED_ID_PROPERTY_NAMES = Arrays.asList("id", "document"); + private static final PropertyNameFieldNamingStrategy DEFAULT_FIELD_NAMING_STRATEGY = PropertyNameFieldNamingStrategy.INSTANCE; private final boolean isId; private final boolean isSeqNoPrimaryTerm; private final @Nullable String annotatedFieldName; @Nullable private ElasticsearchPersistentPropertyConverter propertyConverter; private final boolean storeNullValue; - private final FieldNamingStrategy fieldNamingStrategy; public SimpleElasticsearchPersistentProperty(Property property, - PersistentEntity owner, SimpleTypeHolder simpleTypeHolder, - @Nullable FieldNamingStrategy fieldNamingStrategy) { + PersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { super(property, owner, simpleTypeHolder); this.annotatedFieldName = getAnnotatedFieldName(); - this.fieldNamingStrategy = fieldNamingStrategy == null ? PropertyNameFieldNamingStrategy.INSTANCE - : fieldNamingStrategy; this.isId = super.isIdProperty() || (SUPPORTED_ID_PROPERTY_NAMES.contains(getFieldName()) && !hasExplicitFieldName()); this.isSeqNoPrimaryTerm = SeqNoPrimaryTerm.class.isAssignableFrom(getRawType()); @@ -248,6 +245,7 @@ private String getAnnotatedFieldName() { public String getFieldName() { if (annotatedFieldName == null) { + FieldNamingStrategy fieldNamingStrategy = getFieldNamingStrategy(); String fieldName = fieldNamingStrategy.getFieldName(this); if (!StringUtils.hasText(fieldName)) { @@ -261,6 +259,16 @@ public String getFieldName() { return annotatedFieldName; } + private FieldNamingStrategy getFieldNamingStrategy() { + PersistentEntity owner = getOwner(); + + if (owner instanceof ElasticsearchPersistentEntity) { + return ((ElasticsearchPersistentEntity) owner).getFieldNamingStrategy(); + } + + return DEFAULT_FIELD_NAMING_STRATEGY; + } + @Override public boolean isIdProperty() { return isId; diff --git a/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java index 413ade735..1d5e456eb 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java @@ -26,7 +26,6 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import org.json.JSONException; @@ -1176,8 +1175,74 @@ void shouldReadGeoJsonProperties() { } } - private String pointTemplate(String name, Point point) { - return String.format(Locale.ENGLISH, "\"%s\":{\"lat\":%.1f,\"lon\":%.1f}", name, point.getY(), point.getX()); + @Test // #1454 + @DisplayName("should write type hints if configured") + void shouldWriteTypeHintsIfConfigured() throws JSONException { + + ((SimpleElasticsearchMappingContext) mappingElasticsearchConverter.getMappingContext()).setWriteTypeHints(true); + PersonWithCars person = new PersonWithCars(); + person.setId("42"); + person.setName("Smith"); + Car car1 = new Car(); + car1.setModel("Ford Mustang"); + Car car2 = new ElectricCar(); + car2.setModel("Porsche Taycan"); + person.setCars(Arrays.asList(car1, car2)); + + String expected = "{\n" + // + " \"_class\": \"org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverterUnitTests$PersonWithCars\",\n" + + " \"id\": \"42\",\n" + // + " \"name\": \"Smith\",\n" + // + " \"cars\": [\n" + // + " {\n" + // + " \"model\": \"Ford Mustang\"\n" + // + " },\n" + // + " {\n" + // + " \"_class\": \"org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverterUnitTests$ElectricCar\",\n" + + " \"model\": \"Porsche Taycan\"\n" + // + " }\n" + // + " ]\n" + // + "}\n"; // + + Document document = Document.create(); + + mappingElasticsearchConverter.write(person, document); + + assertEquals(expected, document.toJson(), true); + } + + @Test // #1454 + @DisplayName("should not write type hints if configured") + void shouldNotWriteTypeHintsIfNotConfigured() throws JSONException { + + ((SimpleElasticsearchMappingContext) mappingElasticsearchConverter.getMappingContext()).setWriteTypeHints(false); + PersonWithCars person = new PersonWithCars(); + person.setId("42"); + person.setName("Smith"); + Car car1 = new Car(); + car1.setModel("Ford Mustang"); + Car car2 = new ElectricCar(); + car2.setModel("Porsche Taycan"); + person.setCars(Arrays.asList(car1, car2)); + + String expected = "{\n" + // + " \"id\": \"42\",\n" + // + " \"name\": \"Smith\",\n" + // + " \"cars\": [\n" + // + " {\n" + // + " \"model\": \"Ford Mustang\"\n" + // + " },\n" + // + " {\n" + // + " \"model\": \"Porsche Taycan\"\n" + // + " }\n" + // + " ]\n" + // + "}\n"; // + + Document document = Document.create(); + + mappingElasticsearchConverter.write(person, document); + + assertEquals(expected, document.toJson(), true); } private Map writeToMap(Object source) { @@ -1187,6 +1252,7 @@ private Map writeToMap(Object source) { return sink; } + // region entities public static class Sample { @Nullable public @ReadOnlyProperty String readOnly; @Nullable public @Transient String annotatedTransientProperty; @@ -2008,4 +2074,39 @@ public void setSaved(@Nullable String saved) { } } + private static class ElectricCar extends Car {} + + private static class PersonWithCars { + @Id @Nullable String id; + @Field(type = FieldType.Text) @Nullable private String name; + @Field(type = FieldType.Nested) @Nullable private List cars; + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + @Nullable + public String getName() { + return name; + } + + public void setName(@Nullable String name) { + this.name = name; + } + + @Nullable + public List getCars() { + return cars; + } + + public void setCars(@Nullable List cars) { + this.cars = cars; + } + } + // endregion } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java index ab60ed4f0..39b61e899 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java @@ -44,6 +44,7 @@ import org.springframework.data.elasticsearch.core.MappingContextBaseTests; import org.springframework.data.elasticsearch.core.completion.Completion; import org.springframework.data.elasticsearch.core.geo.GeoPoint; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm; import org.springframework.data.geo.Box; import org.springframework.data.geo.Circle; @@ -661,6 +662,119 @@ void shouldMapAccordingToTheAnnotatedProperties() throws JSONException { assertEquals(expected, mapping, false); } + @Test // #1454 + @DisplayName("should write type hints when context is configured to do so") + void shouldWriteTypeHintsWhenContextIsConfiguredToDoSo() throws JSONException { + + ((SimpleElasticsearchMappingContext) (elasticsearchConverter.get().getMappingContext())).setWriteTypeHints(true); + String expected = "{\n" + // + " \"properties\": {\n" + // + " \"_class\": {\n" + // + " \"type\": \"keyword\",\n" + // + " \"index\": false,\n" + // + " \"doc_values\": false\n" + // + " },\n" + // + " \"title\": {\n" + // + " \"type\": \"text\"\n" + // + " },\n" + // + " \"authors\": {\n" + // + " \"type\": \"nested\",\n" + // + " \"properties\": {\n" + // + " \"_class\": {\n" + // + " \"type\": \"keyword\",\n" + // + " \"index\": false,\n" + // + " \"doc_values\": false\n" + // + " }\n" + // + " }\n" + // + " }\n" + // + " }\n" + // + "}\n"; // + + String mapping = getMappingBuilder().buildPropertyMapping(Magazine.class); + + assertEquals(expected, mapping, true); + } + + @Test // #1454 + @DisplayName("should not write type hints when context is configured to not do so") + void shouldNotWriteTypeHintsWhenContextIsConfiguredToNotDoSo() throws JSONException { + + ((SimpleElasticsearchMappingContext) (elasticsearchConverter.get().getMappingContext())).setWriteTypeHints(false); + String expected = "{\n" + // + " \"properties\": {\n" + // + " \"title\": {\n" + // + " \"type\": \"text\"\n" + // + " },\n" + // + " \"authors\": {\n" + // + " \"type\": \"nested\",\n" + // + " \"properties\": {\n" + // + " }\n" + // + " }\n" + // + " }\n" + // + "}\n"; // + + String mapping = getMappingBuilder().buildPropertyMapping(Magazine.class); + + assertEquals(expected, mapping, true); + } + + @Test // #1454 + @DisplayName("should write type hints when context is configured to not do so but entity should") + void shouldWriteTypeHintsWhenContextIsConfiguredToNotDoSoButEntityShould() throws JSONException { + + ((SimpleElasticsearchMappingContext) (elasticsearchConverter.get().getMappingContext())).setWriteTypeHints(false); + String expected = "{\n" + // + " \"properties\": {\n" + // + " \"_class\": {\n" + // + " \"type\": \"keyword\",\n" + // + " \"index\": false,\n" + // + " \"doc_values\": false\n" + // + " },\n" + // + " \"title\": {\n" + // + " \"type\": \"text\"\n" + // + " },\n" + // + " \"authors\": {\n" + // + " \"type\": \"nested\",\n" + // + " \"properties\": {\n" + // + " \"_class\": {\n" + // + " \"type\": \"keyword\",\n" + // + " \"index\": false,\n" + // + " \"doc_values\": false\n" + // + " }\n" + // + " }\n" + // + " }\n" + // + " }\n" + // + "}\n"; // + + String mapping = getMappingBuilder().buildPropertyMapping(MagazineWithTypeHints.class); + + assertEquals(expected, mapping, true); + } + + @Test // #1454 + @DisplayName("should not write type hints when context is configured to do so but entity should not") + void shouldNotWriteTypeHintsWhenContextIsConfiguredToDoSoButEntityShouldNot() throws JSONException { + + ((SimpleElasticsearchMappingContext) (elasticsearchConverter.get().getMappingContext())).setWriteTypeHints(true); + String expected = "{\n" + // + " \"properties\": {\n" + // + " \"title\": {\n" + // + " \"type\": \"text\"\n" + // + " },\n" + // + " \"authors\": {\n" + // + " \"type\": \"nested\",\n" + // + " \"properties\": {\n" + // + " }\n" + // + " }\n" + // + " }\n" + // + "}\n"; // + + String mapping = getMappingBuilder().buildPropertyMapping(MagazineWithoutTypeHints.class); + + assertEquals(expected, mapping, true); + } + + // region entities @Document(indexName = "ignore-above-index") static class IgnoreAboveEntity { @Nullable @Id private String id; @@ -1555,4 +1669,26 @@ public void setField5(@Nullable LocalDateTime field5) { this.field5 = field5; } } + + @Document(indexName = "magazine") + private static class Magazine { + @Id @Nullable private String id; + @Field(type = Text) @Nullable private String title; + @Field(type = Nested) @Nullable private List authors; + } + + @Document(indexName = "magazine-without-type-hints", writeTypeHint = WriteTypeHint.FALSE) + private static class MagazineWithoutTypeHints { + @Id @Nullable private String id; + @Field(type = Text) @Nullable private String title; + @Field(type = Nested) @Nullable private List authors; + } + + @Document(indexName = "magazine-with-type-hints", writeTypeHint = WriteTypeHint.TRUE) + private static class MagazineWithTypeHints { + @Id @Nullable private String id; + @Field(type = Text) @Nullable private String title; + @Field(type = Nested) @Nullable private List authors; + } + // endregion } 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 bae5f44cc..596e24696 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 @@ -28,10 +28,14 @@ import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.Setting; +import org.springframework.data.elasticsearch.annotations.WriteTypeHint; import org.springframework.data.elasticsearch.core.MappingContextBaseTests; import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm; import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mapping.model.Property; +import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; @@ -52,13 +56,16 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase @DisplayName("properties setup") class PropertiesTests { + private final SimpleElasticsearchPersistentEntity.ContextConfiguration contextConfiguration = new SimpleElasticsearchPersistentEntity.ContextConfiguration( + PropertyNameFieldNamingStrategy.INSTANCE, true); + @Test public void shouldThrowExceptionGivenVersionPropertyIsNotLong() { TypeInformation typeInformation = ClassTypeInformation .from(EntityWithWrongVersionType.class); SimpleElasticsearchPersistentEntity entity = new SimpleElasticsearchPersistentEntity<>( - typeInformation); + typeInformation, contextConfiguration); assertThatThrownBy(() -> createProperty(entity, "version")).isInstanceOf(MappingException.class); } @@ -69,7 +76,7 @@ public void shouldThrowExceptionGivenMultipleVersionPropertiesArePresent() { TypeInformation typeInformation = ClassTypeInformation .from(EntityWithMultipleVersionField.class); SimpleElasticsearchPersistentEntity entity = new SimpleElasticsearchPersistentEntity<>( - typeInformation); + typeInformation, contextConfiguration); SimpleElasticsearchPersistentProperty persistentProperty1 = createProperty(entity, "version1"); SimpleElasticsearchPersistentProperty persistentProperty2 = createProperty(entity, "version2"); entity.addPersistentProperty(persistentProperty1); @@ -98,7 +105,7 @@ void shouldReportThatThereIsNoSeqNoPrimaryTermPropertyWhenThereIsNoSuchProperty( TypeInformation typeInformation = ClassTypeInformation .from(EntityWithoutSeqNoPrimaryTerm.class); SimpleElasticsearchPersistentEntity entity = new SimpleElasticsearchPersistentEntity<>( - typeInformation); + typeInformation, contextConfiguration); assertThat(entity.hasSeqNoPrimaryTermProperty()).isFalse(); } @@ -109,7 +116,7 @@ void shouldReportThatThereIsSeqNoPrimaryTermPropertyWhenThereIsSuchProperty() { TypeInformation typeInformation = ClassTypeInformation .from(EntityWithSeqNoPrimaryTerm.class); SimpleElasticsearchPersistentEntity entity = new SimpleElasticsearchPersistentEntity<>( - typeInformation); + typeInformation, contextConfiguration); entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm")); @@ -123,7 +130,7 @@ void shouldReturnSeqNoPrimaryTermPropertyWhenThereIsSuchProperty() { TypeInformation typeInformation = ClassTypeInformation .from(EntityWithSeqNoPrimaryTerm.class); SimpleElasticsearchPersistentEntity entity = new SimpleElasticsearchPersistentEntity<>( - typeInformation); + typeInformation, contextConfiguration); entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm")); EntityWithSeqNoPrimaryTerm instance = new EntityWithSeqNoPrimaryTerm(); SeqNoPrimaryTerm seqNoPrimaryTerm = new SeqNoPrimaryTerm(1, 2); @@ -142,7 +149,7 @@ void shouldNotAllowMoreThanOneSeqNoPrimaryTermProperties() { TypeInformation typeInformation = ClassTypeInformation .from(EntityWithSeqNoPrimaryTerm.class); SimpleElasticsearchPersistentEntity entity = new SimpleElasticsearchPersistentEntity<>( - typeInformation); + typeInformation, contextConfiguration); entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm")); assertThatThrownBy(() -> entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm2"))) @@ -165,10 +172,9 @@ class SettingsTests { @DisplayName("should error if index sorting parameters do not have the same number of arguments") void shouldErrorIfIndexSortingParametersDoNotHaveTheSameNumberOfArguments() { - assertThatThrownBy(() -> { - elasticsearchConverter.get().getMappingContext() - .getRequiredPersistentEntity(SettingsInvalidSortParameterSizes.class).getDefaultSettings(); - }).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> elasticsearchConverter.get().getMappingContext() + .getRequiredPersistentEntity(SettingsInvalidSortParameterSizes.class).getDefaultSettings()) + .isInstanceOf(IllegalArgumentException.class); } @Test // #1719 @@ -190,6 +196,75 @@ void shouldWriteSortParametersToSettingsObject() throws JSONException { } } + @Nested + @DisplayName("configuration") + class ConfigurationTests { + + @Test // #1454 + @DisplayName("should return FieldNamingStrategy from context configuration") + void shouldReturnFieldNamingStrategyFromContextConfiguration() { + + SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext(); + FieldNamingStrategy fieldNamingStrategy = new FieldNamingStrategy() { + @Override + public String getFieldName(PersistentProperty property) { + return property.getName() + "foo"; + } + }; + context.setFieldNamingStrategy(fieldNamingStrategy); + SimpleElasticsearchPersistentEntity persistentEntity = context + .getRequiredPersistentEntity(FieldNameEntity.class); + + assertThat(persistentEntity.getFieldNamingStrategy()).isSameAs(fieldNamingStrategy); + } + + @Test // #1454 + @DisplayName("should write type hints on default context settings") + void shouldWriteTypeHintsOnDefaultContextSettings() { + + SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext(); + SimpleElasticsearchPersistentEntity entity = context + .getRequiredPersistentEntity(DisableTypeHintNoSetting.class); + + assertThat(entity.writeTypeHints()).isTrue(); + } + + @Test // #1454 + @DisplayName("should not write type hints when configured in context settings") + void shouldNotWriteTypeHintsWhenConfiguredInContextSettings() { + + SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext(); + context.setWriteTypeHints(false); + SimpleElasticsearchPersistentEntity entity = context + .getRequiredPersistentEntity(DisableTypeHintNoSetting.class); + + assertThat(entity.writeTypeHints()).isFalse(); + } + + @Test // #1454 + @DisplayName("should not write type hints when configured explicitly on entity") + void shouldNotWriteTypeHintsWhenConfiguredExplicitlyOnEntity() { + + SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext(); + SimpleElasticsearchPersistentEntity entity = context + .getRequiredPersistentEntity(DisableTypeHintExplicitSetting.class); + + assertThat(entity.writeTypeHints()).isFalse(); + } + + @Test // #1454 + @DisplayName("should write type hints when configured explicitly on entity and global setting is false") + void shouldWriteTypeHintsWhenConfiguredExplicitlyOnEntityAndGlobalSettingIsFalse() { + + SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext(); + context.setWriteTypeHints(false); + SimpleElasticsearchPersistentEntity entity = context + .getRequiredPersistentEntity(EnableTypeHintExplicitSetting.class); + + assertThat(entity.writeTypeHints()).isTrue(); + } + } + // region helper functions private static SimpleElasticsearchPersistentProperty createProperty(SimpleElasticsearchPersistentEntity entity, String fieldName) { @@ -198,7 +273,7 @@ private static SimpleElasticsearchPersistentProperty createProperty(SimpleElasti java.lang.reflect.Field field = ReflectionUtils.findField(entity.getType(), fieldName); assertThat(field).isNotNull(); Property property = Property.of(type, field); - return new SimpleElasticsearchPersistentProperty(property, entity, SimpleTypeHolder.DEFAULT, null); + return new SimpleElasticsearchPersistentProperty(property, entity, SimpleTypeHolder.DEFAULT); } // endregion @@ -275,16 +350,29 @@ private static class SettingsInvalidSortParameterSizes { @Nullable @Field(name = "second-field", type = FieldType.Keyword) private String secondField; } -@Document(indexName = "dontcare") -// property names here, not field names -@Setting(sortFields = { "secondField", "firstField" }, sortModes = { Setting.SortMode.max, Setting.SortMode.min }, - sortOrders = { Setting.SortOrder.desc, Setting.SortOrder.asc }, - sortMissingValues = { Setting.SortMissing._last, Setting.SortMissing._first }) -private static class SettingsValidSortParameterSizes { - @Nullable @Id private String id; - @Nullable @Field(name = "first_field", type = FieldType.Keyword) private String firstField; - @Nullable @Field(name = "second_field", type = FieldType.Keyword) private String secondField; -} + @Document(indexName = "dontcare") + // property names here, not field names + @Setting(sortFields = { "secondField", "firstField" }, sortModes = { Setting.SortMode.max, Setting.SortMode.min }, + sortOrders = { Setting.SortOrder.desc, Setting.SortOrder.asc }, + sortMissingValues = { Setting.SortMissing._last, Setting.SortMissing._first }) + private static class SettingsValidSortParameterSizes { + @Nullable @Id private String id; + @Nullable @Field(name = "first_field", type = FieldType.Keyword) private String firstField; + @Nullable @Field(name = "second_field", type = FieldType.Keyword) private String secondField; + } + private static class DisableTypeHintNoSetting { + @Nullable @Id String id; + } + + @Document(indexName = "foo", writeTypeHint = WriteTypeHint.FALSE) + private static class DisableTypeHintExplicitSetting { + @Nullable @Id String id; + } + + @Document(indexName = "foo", writeTypeHint = WriteTypeHint.TRUE) + private static class EnableTypeHintExplicitSetting { + @Nullable @Id String id; + } // endregion } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentPropertyUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentPropertyUnitTests.java index fd9a76092..8ac934880 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentPropertyUnitTests.java @@ -36,6 +36,7 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mapping.model.Property; +import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.mapping.model.SnakeCaseFieldNamingStrategy; import org.springframework.data.util.ClassTypeInformation; @@ -200,20 +201,22 @@ void shouldRequirePatternForCustomDateFormat() { @DisplayName("should use default FieldNamingStrategy") void shouldUseDefaultFieldNamingStrategy() { + SimpleElasticsearchPersistentEntity.ContextConfiguration contextConfiguration = new SimpleElasticsearchPersistentEntity.ContextConfiguration( + PropertyNameFieldNamingStrategy.INSTANCE, true); + ElasticsearchPersistentEntity entity = new SimpleElasticsearchPersistentEntity<>( - ClassTypeInformation.from(FieldNamingStrategyEntity.class)); + ClassTypeInformation.from(FieldNamingStrategyEntity.class), contextConfiguration); ClassTypeInformation type = ClassTypeInformation.from(FieldNamingStrategyEntity.class); java.lang.reflect.Field field = ReflectionUtils.findField(FieldNamingStrategyEntity.class, "withoutCustomFieldName"); SimpleElasticsearchPersistentProperty property = new SimpleElasticsearchPersistentProperty(Property.of(type, field), - entity, SimpleTypeHolder.DEFAULT, null); + entity, SimpleTypeHolder.DEFAULT); assertThat(property.getFieldName()).isEqualTo("withoutCustomFieldName"); field = ReflectionUtils.findField(FieldNamingStrategyEntity.class, "withCustomFieldName"); - property = new SimpleElasticsearchPersistentProperty(Property.of(type, field), entity, SimpleTypeHolder.DEFAULT, - null); + property = new SimpleElasticsearchPersistentProperty(Property.of(type, field), entity, SimpleTypeHolder.DEFAULT); assertThat(property.getFieldName()).isEqualTo("CUStomFIEldnAME"); } @@ -223,25 +226,27 @@ void shouldUseDefaultFieldNamingStrategy() { void shouldUseCustomFieldNamingStrategy() { FieldNamingStrategy fieldNamingStrategy = new SnakeCaseFieldNamingStrategy(); + SimpleElasticsearchPersistentEntity.ContextConfiguration contextConfiguration = new SimpleElasticsearchPersistentEntity.ContextConfiguration( + fieldNamingStrategy, true); ElasticsearchPersistentEntity entity = new SimpleElasticsearchPersistentEntity<>( - ClassTypeInformation.from(FieldNamingStrategyEntity.class)); + ClassTypeInformation.from(FieldNamingStrategyEntity.class), contextConfiguration); ClassTypeInformation type = ClassTypeInformation.from(FieldNamingStrategyEntity.class); java.lang.reflect.Field field = ReflectionUtils.findField(FieldNamingStrategyEntity.class, "withoutCustomFieldName"); SimpleElasticsearchPersistentProperty property = new SimpleElasticsearchPersistentProperty(Property.of(type, field), - entity, SimpleTypeHolder.DEFAULT, fieldNamingStrategy); + entity, SimpleTypeHolder.DEFAULT); assertThat(property.getFieldName()).isEqualTo("without_custom_field_name"); field = ReflectionUtils.findField(FieldNamingStrategyEntity.class, "withCustomFieldName"); - property = new SimpleElasticsearchPersistentProperty(Property.of(type, field), entity, SimpleTypeHolder.DEFAULT, - fieldNamingStrategy); + property = new SimpleElasticsearchPersistentProperty(Property.of(type, field), entity, SimpleTypeHolder.DEFAULT); assertThat(property.getFieldName()).isEqualTo("CUStomFIEldnAME"); } + // region entities static class FieldNameProperty { @Nullable @Field(name = "by-name") String fieldProperty; } @@ -319,4 +324,5 @@ public void setWithCustomFieldName(String withCustomFieldName) { this.withCustomFieldName = withCustomFieldName; } } + // endregion }