From 91742b111435e54ea0cb4ff54725d2830013a869 Mon Sep 17 00:00:00 2001 From: Peter-Josef Meisch Date: Sun, 25 Apr 2021 21:57:13 +0200 Subject: [PATCH] Allow disabling TypeHints. Original Pull Request #1788 Closes #1788 --- .../elasticsearch-object-mapping.adoc | 18 +- .../elasticsearch/annotations/Document.java | 7 + .../annotations/WriteTypeHint.java | 40 + .../ElasticsearchConfigurationSupport.java | 15 +- .../MappingElasticsearchConverter.java | 1718 +++++++++-------- .../core/index/MappingBuilder.java | 17 +- .../ElasticsearchPersistentEntity.java | 14 +- .../ElasticsearchPersistentProperty.java | 1 + .../SimpleElasticsearchMappingContext.java | 15 +- .../SimpleElasticsearchPersistentEntity.java | 60 +- ...SimpleElasticsearchPersistentProperty.java | 18 +- ...appingElasticsearchConverterUnitTests.java | 107 +- .../core/index/MappingBuilderUnitTests.java | 136 ++ ...pleElasticsearchPersistentEntityTests.java | 130 +- ...sticsearchPersistentPropertyUnitTests.java | 22 +- 15 files changed, 1435 insertions(+), 883 deletions(-) create mode 100644 src/main/java/org/springframework/data/elasticsearch/annotations/WriteTypeHint.java 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 @@ public @interface Document { * 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.ClassPathScanningCandidateComponen 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 class ElasticsearchConfigurationSupport { mappingContext.setInitialEntitySet(getInitialEntitySet()); mappingContext.setSimpleTypeHolder(elasticsearchCustomConversions.getSimpleTypeHolder()); mappingContext.setFieldNamingStrategy(fieldNamingStrategy()); + mappingContext.setWriteTypeHints(writeTypeHints()); return mappingContext; } @@ -171,4 +171,17 @@ public class ElasticsearchConfigurationSupport { 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 class 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 @@ public class MappingElasticsearchConverter 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); - - if (r == null) { - throw new ConversionException("could not convert into object of class " + type); - } - - return r; + Reader reader = new Reader(mappingContext, conversionService, getConversions()); + return reader.read(type, source); } - protected R readEntity(ElasticsearchPersistentEntity entity, Map source) { - - ElasticsearchPersistentEntity targetEntity = computeClosestEntity(entity, source); - - 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; - - EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity); - - @SuppressWarnings({ "unchecked" }) - R instance = (R) instantiator.createInstance(targetEntity, propertyValueProvider); - - if (!targetEntity.requiresPropertyPopulation()) { - return instance; - } - - 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()); - } - } - - 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 (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 (source instanceof SearchDocument) { - SearchDocument searchDocument = (SearchDocument) source; - populateScriptFields(result, searchDocument); - } - - return result; - - } - - private ParameterValueProvider getParameterProvider( - ElasticsearchPersistentEntity entity, MapValueAccessor source, SpELExpressionEvaluator evaluator) { - - ElasticsearchPropertyValueProvider provider = new ElasticsearchPropertyValueProvider(source, evaluator); - - // TODO: Support for non-static inner classes via ObjectPath - // noinspection ConstantConditions - PersistentEntityParameterValueProvider parameterProvider = new PersistentEntityParameterValueProvider<>( - entity, provider, null); - - return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider); - } - - private boolean isAssignedSeqNo(long seqNo) { - return seqNo >= 0; - } - - private boolean isAssignedPrimaryTerm(long primaryTerm) { - return primaryTerm > 0; - } - - protected R readProperties(ElasticsearchPersistentEntity entity, R instance, - ElasticsearchPropertyValueProvider valueProvider) { - - PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance), - conversionService); - - for (ElasticsearchPersistentProperty prop : entity) { - - if (entity.isConstructorArgument(prop) || !prop.isReadable()) { - continue; - } - - Object value = valueProvider.getPropertyValue(prop); - if (value != null) { - accessor.setProperty(prop, value); - } - } - - return accessor.getBean(); - } - - @Nullable - protected R readValue(@Nullable Object value, ElasticsearchPersistentProperty property, TypeInformation type) { - - if (value == null) { - return null; - } - - Class rawType = type.getType(); - - if (property.hasPropertyConverter()) { - value = propertyConverterRead(property, value); - } else if (TemporalAccessor.class.isAssignableFrom(property.getType()) - && !getConversions().hasCustomReadTarget(value.getClass(), rawType)) { - - // 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); - } - } - - return readValue(value, type); - } - - @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); - } - } - - @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.class.isAssignableFrom(rawType)) { - return (R) source; - } - - 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())); - } - - return readEntity(entity, source); - } - - private Object propertyConverterRead(ElasticsearchPersistentProperty property, Object source) { - ElasticsearchPersistentPropertyConverter propertyConverter = Objects - .requireNonNull(property.getPropertyConverter()); - - if (source instanceof String[]) { - // convert to a List - source = Arrays.asList((String[]) source); - } - - 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; - } - - private Object convertOnRead(ElasticsearchPersistentPropertyConverter propertyConverter, Object source) { - if (String.class.isAssignableFrom(source.getClass())) { - source = propertyConverter.read((String) source); - } - 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) { - - Assert.notNull(targetType, "Target type must not be null!"); - - Class collectionType = targetType.isSubTypeOf(Collection.class) // - ? targetType.getType() // - : List.class; - - TypeInformation componentType = targetType.getComponentType() != null // - ? targetType.getComponentType() // - : ClassTypeInformation.OBJECT; - Class rawComponentType = componentType.getType(); - - Collection items = targetType.getType().isArray() // - ? new ArrayList<>(source.size()) // - : CollectionFactory.createCollection(collectionType, rawComponentType, source.size()); - - if (source.isEmpty()) { - return getPotentiallyConvertedSimpleRead(items, targetType); - } - - for (Object element : source) { - - if (element instanceof Map) { - items.add(read(componentType, (Map) element)); - } else { - - 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)); - } - } - } - - return getPotentiallyConvertedSimpleRead(items, targetType.getType()); - } - - @SuppressWarnings("unchecked") - private R readMap(TypeInformation type, Map source) { - - Assert.notNull(source, "Document must not be null!"); - - Class mapType = typeMapper.readType(source, type).getType(); - - TypeInformation keyType = type.getComponentType(); - TypeInformation valueType = type.getMapValueType(); - - Class rawKeyType = keyType != null ? keyType.getType() : null; - Class rawValueType = valueType != null ? valueType.getType() : null; - - Map map = CollectionFactory.createMap(mapType, rawKeyType, source.keySet().size()); - - for (Entry entry : source.entrySet()) { - - if (typeMapper.isTypeKey(entry.getKey())) { - continue; - } - - Object key = entry.getKey(); - - if (rawKeyType != null && !rawKeyType.isAssignableFrom(key.getClass())) { - key = conversionService.convert(key, rawKeyType); - } - - Object value = entry.getValue(); - TypeInformation defaultedValueType = valueType != null ? valueType : ClassTypeInformation.OBJECT; - - 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)); - } - } - - return (R) map; - } - - @Nullable - private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, TypeInformation targetType) { - return getPotentiallyConvertedSimpleRead(value, targetType.getType()); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Nullable - private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { - - if (target == null || value == null || ClassUtils.isAssignableValue(target, value)) { - return value; - } - - 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); - } - } - } - } - } - } - // endregion - - // region write @Override public void write(Object source, Document sink) { Assert.notNull(source, "source to map must not be null"); - if (source instanceof Map) { - // noinspection unchecked - sink.putAll((Map) source); - return; - } - - Class entityType = ClassUtils.getUserClass(source.getClass()); - TypeInformation typeInformation = ClassTypeInformation.from(entityType); - - if (requiresTypeHint(entityType)) { - typeMapper.writeType(typeInformation, sink); - } - - writeInternal(source, sink, typeInformation); + Writer writer = new Writer(mappingContext, conversionService, getConversions()); + writer.write(source, sink); } /** - * 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 + * base class for {@link Reader} and {@link Writer} keeping the common properties */ - @SuppressWarnings("unchecked") - protected void writeInternal(@Nullable Object source, Map sink, - @Nullable TypeInformation typeInformation) { + private static class Base { - if (null == source) { - return; + 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 entityType = source.getClass(); - Optional> customTarget = conversions.getCustomWriteTarget(entityType, Map.class); - - if (customTarget.isPresent()) { - Map result = conversionService.convert(source, Map.class); - - if (result != null) { - sink.putAll(result); - } - return; - } - - if (Map.class.isAssignableFrom(entityType)) { - writeMapInternal((Map) source, sink, ClassTypeInformation.MAP); - return; - } - - 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); } /** - * 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) { + private static class Reader extends Base { - if (source == null) { - return; + private final SpELContext spELContext; + private final EntityInstantiators instantiators = new EntityInstantiators(); + + public Reader( + MappingContext, ElasticsearchPersistentProperty> mappingContext, + GenericConversionService conversionService, CustomConversions conversions) { + + super(mappingContext, conversionService, conversions); + this.spELContext = new SpELContext(new MapAccessor()); } - if (null == entity) { - throw new MappingException("No mapping metadata found for entity of type " + source.getClass().getName()); - } + @SuppressWarnings("unchecked") + R read(Class type, Document source) { - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); - writeProperties(entity, accessor, new MapValueAccessor(sink)); - } + TypeInformation typeHint = ClassTypeInformation.from((Class) ClassUtils.getUserClass(type)); + R r = read(typeHint, source); - protected void writeProperties(ElasticsearchPersistentEntity entity, PersistentPropertyAccessor accessor, - MapValueAccessor sink) { - - for (ElasticsearchPersistentProperty property : entity) { - - if (!property.isWritable()) { - continue; + if (r == null) { + throw new ConversionException("could not convert into object of class " + type); } - Object value = accessor.getProperty(property); + return r; + } - if (value == null) { + @Nullable + @SuppressWarnings("unchecked") + private R read(TypeInformation type, Map source) { - if (property.storeNullValue()) { - sink.set(property, null); + 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.class.isAssignableFrom(rawType)) { + return (R) source; + } + + 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())); + } + + return readEntity(entity, source); + } + + @SuppressWarnings("unchecked") + private R readMap(TypeInformation type, Map source) { + + Assert.notNull(source, "Document must not be null!"); + + Class mapType = typeMapper.readType(source, type).getType(); + + TypeInformation keyType = type.getComponentType(); + TypeInformation valueType = type.getMapValueType(); + + Class rawKeyType = keyType != null ? keyType.getType() : null; + Class rawValueType = valueType != null ? valueType.getType() : null; + + Map map = CollectionFactory.createMap(mapType, rawKeyType, source.keySet().size()); + + for (Entry entry : source.entrySet()) { + + if (typeMapper.isTypeKey(entry.getKey())) { + continue; } - continue; + Object key = entry.getKey(); + + if (rawKeyType != null && !rawKeyType.isAssignableFrom(key.getClass())) { + key = conversionService.convert(key, rawKeyType); + } + + Object value = entry.getValue(); + TypeInformation defaultedValueType = valueType != null ? valueType : ClassTypeInformation.OBJECT; + + 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)); + } } + return (R) map; + } + + private R readEntity(ElasticsearchPersistentEntity entity, Map source) { + + ElasticsearchPersistentEntity targetEntity = computeClosestEntity(entity, source); + + 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; + + EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity); + + @SuppressWarnings({ "unchecked" }) + R instance = (R) instantiator.createInstance(targetEntity, propertyValueProvider); + + if (!targetEntity.requiresPropertyPopulation()) { + return instance; + } + + 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()); + } + } + + 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 (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 (source instanceof SearchDocument) { + SearchDocument searchDocument = (SearchDocument) source; + populateScriptFields(result, searchDocument); + } + + return result; + + } + + private ParameterValueProvider getParameterProvider( + ElasticsearchPersistentEntity entity, MapValueAccessor source, SpELExpressionEvaluator evaluator) { + + ElasticsearchPropertyValueProvider provider = new ElasticsearchPropertyValueProvider(source, evaluator); + + // TODO: Support for non-static inner classes via ObjectPath + // noinspection ConstantConditions + PersistentEntityParameterValueProvider parameterProvider = new PersistentEntityParameterValueProvider<>( + entity, provider, null); + + return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider); + } + + private boolean isAssignedSeqNo(long seqNo) { + return seqNo >= 0; + } + + private boolean isAssignedPrimaryTerm(long primaryTerm) { + return primaryTerm > 0; + } + + protected R readProperties(ElasticsearchPersistentEntity entity, R instance, + ElasticsearchPropertyValueProvider valueProvider) { + + PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance), + conversionService); + + for (ElasticsearchPersistentProperty prop : entity) { + + if (entity.isConstructorArgument(prop) || !prop.isReadable()) { + continue; + } + + Object value = valueProvider.getPropertyValue(prop); + if (value != null) { + accessor.setProperty(prop, value); + } + } + + return accessor.getBean(); + } + + @Nullable + protected R readValue(@Nullable Object value, ElasticsearchPersistentProperty property, + TypeInformation type) { + + if (value == null) { + return null; + } + + Class rawType = type.getType(); + if (property.hasPropertyConverter()) { - value = propertyConverterWrite(property, value); - sink.set(property, value); - } else if (TemporalAccessor.class.isAssignableFrom(property.getActualType()) - && !getConversions().hasCustomWriteTarget(value.getClass())) { + value = propertyConverterRead(property, value); + } else if (TemporalAccessor.class.isAssignableFrom(property.getType()) + && !conversions.hasCustomReadTarget(value.getClass(), rawType)) { // log at most 5 times - String propertyName = entity.getType().getSimpleName() + '.' + property.getName(); - String key = propertyName + "-write"; + 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 writing!" - + " It will be mapped to a complex object in Elasticsearch!", + "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); } - } else if (!isSimpleType(value)) { - writeProperty(property, value, sink); + } + + return readValue(value, type); + } + + @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 { - Object writeSimpleValue = getPotentiallyConvertedSimpleWrite(value, Object.class); - if (writeSimpleValue != null) { - sink.set(property, writeSimpleValue); - } + return (T) getPotentiallyConvertedSimpleRead(value, rawType); } } - } - private Object propertyConverterWrite(ElasticsearchPersistentProperty property, Object value) { - ElasticsearchPersistentPropertyConverter propertyConverter = Objects - .requireNonNull(property.getPropertyConverter()); + private Object propertyConverterRead(ElasticsearchPersistentProperty property, Object source) { + ElasticsearchPersistentPropertyConverter propertyConverter = Objects + .requireNonNull(property.getPropertyConverter()); - 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; - } + if (source instanceof String[]) { + // convert to a List + source = Arrays.asList((String[]) source); + } - @SuppressWarnings("unchecked") - protected void writeProperty(ElasticsearchPersistentProperty property, Object value, MapValueAccessor sink) { - - Optional> customWriteTarget = getConversions().getCustomWriteTarget(value.getClass()); - - if (customWriteTarget.isPresent()) { - Class writeTarget = customWriteTarget.get(); - sink.set(property, conversionService.convert(value, writeTarget)); - return; + 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; } - TypeInformation valueType = ClassTypeInformation.from(value.getClass()); - TypeInformation type = property.getTypeInformation(); - - if (valueType.isCollectionLike()) { - List collectionInternal = createCollection(asCollection(value), property); - sink.set(property, collectionInternal); - return; + private Object convertOnRead(ElasticsearchPersistentPropertyConverter propertyConverter, Object source) { + if (String.class.isAssignableFrom(source.getClass())) { + source = propertyConverter.read((String) source); + } + return source; } - if (valueType.isMap()) { - Map mapDbObj = createMap((Map) value, property); - sink.set(property, mapDbObj); - return; - } + /** + * 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) { - // Lookup potential custom target type - Optional> basicTargetType = conversions.getCustomWriteTarget(value.getClass()); + Assert.notNull(targetType, "Target type must not be null!"); - if (basicTargetType.isPresent()) { + Class collectionType = targetType.isSubTypeOf(Collection.class) // + ? targetType.getType() // + : List.class; - sink.set(property, conversionService.convert(value, basicTargetType.get())); - return; - } + TypeInformation componentType = targetType.getComponentType() != null // + ? targetType.getComponentType() // + : ClassTypeInformation.OBJECT; + Class rawComponentType = componentType.getType(); - ElasticsearchPersistentEntity entity = valueType.isSubTypeOf(property.getType()) - ? mappingContext.getRequiredPersistentEntity(value.getClass()) - : mappingContext.getRequiredPersistentEntity(type); + Collection items = targetType.getType().isArray() // + ? new ArrayList<>(source.size()) // + : CollectionFactory.createCollection(collectionType, rawComponentType, source.size()); - Object existingValue = sink.get(property); - Map document = existingValue instanceof Map ? (Map) existingValue - : Document.create(); + if (source.isEmpty()) { + return getPotentiallyConvertedSimpleRead(items, targetType); + } - addCustomTypeKeyIfNecessary(value, document, ClassTypeInformation.from(property.getRawType())); - writeInternal(value, document, entity); - sink.set(property, document); - } + for (Object element : source) { - /** - * 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())); - } - - /** - * 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) { - - Assert.notNull(map, "Given map must not be null!"); - Assert.notNull(property, "PersistentProperty must not be null!"); - - return writeMapInternal(map, new LinkedHashMap<>(map.size()), property.getTypeInformation()); - } - - /** - * 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) { - - for (Map.Entry entry : source.entrySet()) { - - Object key = entry.getKey(); - Object value = entry.getValue(); - - if (isSimpleType(key.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 (element instanceof Map) { + items.add(read(componentType, (Map) element)); } else { - Map document = Document.create(); - TypeInformation valueTypeInfo = propertyType.isMap() ? propertyType.getMapValueType() - : ClassTypeInformation.OBJECT; - writeInternal(value, document, valueTypeInfo); - sink.put(simpleKey, document); + 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)); + } + } + } + + return getPotentiallyConvertedSimpleRead(items, targetType.getType()); + } + + @Nullable + private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, TypeInformation targetType) { + return getPotentiallyConvertedSimpleRead(value, targetType.getType()); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Nullable + private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { + + if (target == null || value == null || ClassUtils.isAssignableValue(target, value)) { + return value; + } + + if (conversions.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); + } + } + } } - } else { - throw new MappingException("Cannot use a complex object as a key value."); } } - return sink; - } + /** + * Compute the type to use by checking the given entity against the store type; + */ + private ElasticsearchPersistentEntity computeClosestEntity(ElasticsearchPersistentEntity entity, + Map source) { - /** - * 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) { + TypeInformation typeToUse = typeMapper.readType(source); - TypeInformation componentType = null; - - List collection = sink instanceof List ? (List) sink : new ArrayList<>(sink); - - if (type != null) { - componentType = type.getComponentType(); - } - - for (Object element : source) { - - Class elementType = element == null ? null : element.getClass(); - - 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 (typeToUse == null) { + return entity; } - } - return collection; - } - - /** - * Returns a {@link String} representation of the given {@link Map} key - * - * @param key the key to convert - */ - private String potentiallyConvertMapKey(Object key) { - - if (key instanceof String) { - return (String) key; - } - - if (conversions.hasCustomWriteTarget(key.getClass(), String.class)) { - Object potentiallyConvertedSimpleWrite = getPotentiallyConvertedSimpleWrite(key, Object.class); - - if (potentiallyConvertedSimpleWrite == null) { - return key.toString(); + if (!entity.getTypeInformation().getType().isInterface() && !entity.getTypeInformation().isCollectionLike() + && !entity.getTypeInformation().isMap() + && !ClassUtils.isAssignableValue(entity.getType(), typeToUse.getType())) { + return entity; } - 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 (value == null) { - return null; + return mappingContext.getRequiredPersistentEntity(typeToUse); } - if (typeHint != null && Object.class != typeHint) { + class ElasticsearchPropertyValueProvider implements PropertyValueProvider { - if (conversionService.canConvert(value.getClass(), typeHint)) { - value = conversionService.convert(value, typeHint); + 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()); } } - Optional> customTarget = conversions.getCustomWriteTarget(value.getClass()); + /** + * Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw + * resolved SpEL value. + * + * @author Mark Paluch + */ + private class ConverterAwareSpELExpressionParameterValueProvider + extends SpELExpressionParameterValueProvider { - if (customTarget.isPresent()) { - return conversionService.convert(value, customTarget.get()); - } + /** + * 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) { - if (ObjectUtils.isArray(value)) { - - if (value instanceof byte[]) { - return value; + 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()); } - return asCollection(value); } - return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; - } + enum NoOpParameterValueProvider implements ParameterValueProvider { - /** - * @deprecated since 4.2, use {@link #getPotentiallyConvertedSimpleWrite(Object, Class)} instead. - */ - @Nullable - @Deprecated - protected Object getWriteSimpleValue(Object value) { - Optional> customTarget = getConversions().getCustomWriteTarget(value.getClass()); + INSTANCE; - if (customTarget.isPresent()) { - return conversionService.convert(value, customTarget.get()); - } - - return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; - } - - /** - * @deprecated since 4.2, use {@link #writeInternal(Object, Map, TypeInformation)} instead. - */ - @Deprecated - protected Object getWriteComplexValue(ElasticsearchPersistentProperty property, - @SuppressWarnings("unused") TypeInformation typeHint, Object value) { - - Document document = Document.create(); - writeInternal(value, document, property.getTypeInformation()); - - return document; - } - - // endregion - - // region helper methods - - /** - * 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) { - - Class reference; - - 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); + @Override + public T getParameterValue(PreferredConstructor.Parameter parameter) { + return null; + } } } /** - * 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. + * 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. */ - public boolean requiresTypeHint(Class type) { + static private class Writer extends Base { - return !isSimpleType(type) && !ClassUtils.isAssignable(Collection.class, type) - && !conversions.hasCustomWriteTarget(type, Document.class); - } + private boolean writeTypeHints = true; - /** - * Compute the type to use by checking the given entity against the store type; - */ - private ElasticsearchPersistentEntity computeClosestEntity(ElasticsearchPersistentEntity entity, - Map source) { - - TypeInformation typeToUse = typeMapper.readType(source); - - if (typeToUse == null) { - return entity; + public Writer( + MappingContext, ElasticsearchPersistentProperty> mappingContext, + GenericConversionService conversionService, CustomConversions conversions) { + super(mappingContext, conversionService, conversions); } - if (!entity.getTypeInformation().getType().isInterface() && !entity.getTypeInformation().isCollectionLike() - && !entity.getTypeInformation().isMap() - && !ClassUtils.isAssignableValue(entity.getType(), typeToUse.getType())) { - return entity; + void write(Object source, Document sink) { + + if (source instanceof Map) { + // noinspection unchecked + sink.putAll((Map) source); + return; + } + + Class entityType = ClassUtils.getUserClass(source.getClass()); + ElasticsearchPersistentEntity entity = mappingContext.getPersistentEntity(entityType); + + if (entity != null) { + writeTypeHints = entity.writeTypeHints(); + } + + TypeInformation typeInformation = ClassTypeInformation.from(entityType); + + if (writeTypeHints && requiresTypeHint(entityType)) { + typeMapper.writeType(typeInformation, sink); + } + + writeInternal(source, sink, typeInformation); } - return mappingContext.getRequiredPersistentEntity(typeToUse); - } + /** + * 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) { - private boolean isSimpleType(Object value) { - return isSimpleType(value.getClass()); - } + if (null == source) { + return; + } - private boolean isSimpleType(Class type) { - return !Map.class.isAssignableFrom(type) && getConversions().isSimpleType(type); - } + Class entityType = source.getClass(); + Optional> customTarget = conversions.getCustomWriteTarget(entityType, Map.class); - /** - * 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 (customTarget.isPresent()) { + Map result = conversionService.convert(source, Map.class); - if (source instanceof Collection) { - return (Collection) source; + if (result != null) { + sink.putAll(result); + } + return; + } + + if (Map.class.isAssignableFrom(entityType)) { + writeMapInternal((Map) source, sink, ClassTypeInformation.MAP); + return; + } + + 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 source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); + /** + * 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; + } + + if (null == entity) { + throw new MappingException("No mapping metadata found for entity of type " + source.getClass().getName()); + } + + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); + writeProperties(entity, accessor, new MapValueAccessor(sink)); + } + + /** + * 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) { + + return !isSimpleType(type) && !ClassUtils.isAssignable(Collection.class, type) + && !conversions.hasCustomWriteTarget(type, Document.class); + } + + private boolean isSimpleType(Object value) { + return isSimpleType(value.getClass()); + } + + private boolean isSimpleType(Class type) { + return !Map.class.isAssignableFrom(type) && conversions.isSimpleType(type); + } + + /** + * 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) { + + for (Map.Entry entry : source.entrySet()) { + + Object key = entry.getKey(); + Object value = entry.getValue(); + + if (isSimpleType(key.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<>())); + } else { + Map document = Document.create(); + TypeInformation valueTypeInfo = propertyType.isMap() ? propertyType.getMapValueType() + : ClassTypeInformation.OBJECT; + writeInternal(value, document, valueTypeInfo); + + sink.put(simpleKey, document); + } + } else { + throw new MappingException("Cannot use a complex object as a key value."); + } + } + + return 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) { + + TypeInformation componentType = null; + + List collection = sink instanceof List ? (List) sink : new ArrayList<>(sink); + + if (type != null) { + componentType = type.getComponentType(); + } + + for (Object element : source) { + + Class elementType = element == null ? null : element.getClass(); + + 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); + } + } + + return collection; + } + + private void writeProperties(ElasticsearchPersistentEntity entity, PersistentPropertyAccessor accessor, + MapValueAccessor sink) { + + for (ElasticsearchPersistentProperty property : entity) { + + if (!property.isWritable()) { + continue; + } + + Object value = accessor.getProperty(property); + + if (value == null) { + + if (property.storeNullValue()) { + sink.set(property, null); + } + + continue; + } + + 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); + } + } + } + } + + @SuppressWarnings("unchecked") + protected void writeProperty(ElasticsearchPersistentProperty property, Object value, MapValueAccessor sink) { + + Optional> customWriteTarget = conversions.getCustomWriteTarget(value.getClass()); + + if (customWriteTarget.isPresent()) { + Class writeTarget = customWriteTarget.get(); + sink.set(property, conversionService.convert(value, writeTarget)); + return; + } + + TypeInformation valueType = ClassTypeInformation.from(value.getClass()); + TypeInformation type = property.getTypeInformation(); + + if (valueType.isCollectionLike()) { + List collectionInternal = createCollection(asCollection(value), property); + sink.set(property, collectionInternal); + return; + } + + 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; + } + + 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); + } + + /** + * 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; + } + + Class reference; + + 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); + } + } + + /** + * Returns a {@link String} representation of the given {@link Map} key + * + * @param key the key to convert + */ + private String potentiallyConvertMapKey(Object key) { + + if (key instanceof String) { + return (String) key; + } + + if (conversions.hasCustomWriteTarget(key.getClass(), String.class)) { + Object potentiallyConvertedSimpleWrite = getPotentiallyConvertedSimpleWrite(key, Object.class); + + if (potentiallyConvertedSimpleWrite == null) { + return key.toString(); + } + 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 (value == null) { + return null; + } + + if (typeHint != null && Object.class != typeHint) { + + if (conversionService.canConvert(value.getClass(), typeHint)) { + value = conversionService.convert(value, typeHint); + + if (value == null) { + return null; + } + } + } + + Optional> customTarget = conversions.getCustomWriteTarget(value.getClass()); + + if (customTarget.isPresent()) { + return conversionService.convert(value, customTarget.get()); + } + + if (ObjectUtils.isArray(value)) { + + if (value instanceof byte[]) { + return value; + } + return asCollection(value); + } + + return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; + } + + private Object propertyConverterWrite(ElasticsearchPersistentProperty property, Object value) { + ElasticsearchPersistentPropertyConverter propertyConverter = Objects + .requireNonNull(property.getPropertyConverter()); + + 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; + } + + /** + * 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())); + } + + /** + * 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) { + + Assert.notNull(map, "Given map must not be null!"); + Assert.notNull(property, "PersistentProperty must not be null!"); + + return writeMapInternal(map, new LinkedHashMap<>(map.size()), property.getTypeInformation()); + } + + /** + * @deprecated since 4.2, use {@link #getPotentiallyConvertedSimpleWrite(Object, Class)} instead. + */ + @Nullable + @Deprecated + protected Object getWriteSimpleValue(Object value) { + Optional> customTarget = conversions.getCustomWriteTarget(value.getClass()); + + if (customTarget.isPresent()) { + return conversionService.convert(value, customTarget.get()); + } + + return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; + } + + /** + * @deprecated since 4.2, use {@link #writeInternal(Object, Map, TypeInformation)} instead. + */ + @Deprecated + protected Object getWriteComplexValue(ElasticsearchPersistentProperty property, + @SuppressWarnings("unused") TypeInformation typeHint, Object value) { + + Document document = Document.create(); + writeInternal(value, document, property.getTypeInformation()); + + 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) { + + if (source instanceof Collection) { + return (Collection) source; + } + + return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); + } } // endregion @@ -1260,71 +1387,4 @@ public class MappingElasticsearchConverter } } - 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 class MappingBuilder { ElasticsearchPersistentEntity entity = elasticsearchConverter.getMappingContext() .getRequiredPersistentEntity(clazz); + writeTypeHints = entity.writeTypeHints(); + XContentBuilder builder = jsonBuilder().startObject(); // Dynamic templates @@ -128,11 +132,14 @@ public class MappingBuilder { } 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 @@ package org.springframework.data.elasticsearch.core.mapping; 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 @@ public interface ElasticsearchPersistentEntity extends PersistentEntity type) { return !ElasticsearchSimpleTypes.HOLDER.isSimpleType(type.getType()); @@ -57,12 +67,13 @@ public class SimpleElasticsearchMappingContext @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.elasticsearch.core.join.JoinField; 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 class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit 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 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit * (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 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit ExpressionDependencies expressionDependencies = expression != null ? ExpressionDependencies.discover(expression) : ExpressionDependencies.none(); + // noinspection ConstantConditions return getEvaluationContext(null, expressionDependencies); } @@ -350,6 +379,7 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit 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 @@ public class SimpleElasticsearchPersistentEntity extends BasicPersistentEntit } // 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 @@ public class SimpleElasticsearchPersistentProperty extends public String getFieldName() { if (annotatedFieldName == null) { + FieldNamingStrategy fieldNamingStrategy = getFieldNamingStrategy(); String fieldName = fieldNamingStrategy.getFieldName(this); if (!StringUtils.hasText(fieldName)) { @@ -261,6 +259,16 @@ public class SimpleElasticsearchPersistentProperty extends 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.Collections; 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 @@ public class MappingElasticsearchConverterUnitTests { } } - 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 @@ public class MappingElasticsearchConverterUnitTests { return sink; } + // region entities public static class Sample { @Nullable public @ReadOnlyProperty String readOnly; @Nullable public @Transient String annotatedTransientProperty; @@ -2008,4 +2074,39 @@ public class MappingElasticsearchConverterUnitTests { } } + 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.annotations.*; 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 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests { 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 class MappingBuilderUnitTests extends MappingContextBaseTests { 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.Document; 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 class SimpleElasticsearchPersistentEntityTests extends MappingContextBase 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 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase TypeInformation typeInformation = ClassTypeInformation .from(EntityWithoutSeqNoPrimaryTerm.class); SimpleElasticsearchPersistentEntity entity = new SimpleElasticsearchPersistentEntity<>( - typeInformation); + typeInformation, contextConfiguration); assertThat(entity.hasSeqNoPrimaryTermProperty()).isFalse(); } @@ -109,7 +116,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase TypeInformation typeInformation = ClassTypeInformation .from(EntityWithSeqNoPrimaryTerm.class); SimpleElasticsearchPersistentEntity entity = new SimpleElasticsearchPersistentEntity<>( - typeInformation); + typeInformation, contextConfiguration); entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm")); @@ -123,7 +130,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase 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 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase 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 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase @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 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase } } + @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 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase 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 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase @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.elasticsearch.core.query.SeqNoPrimaryTerm; 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 @@ public class SimpleElasticsearchPersistentPropertyUnitTests { @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 @@ public class SimpleElasticsearchPersistentPropertyUnitTests { 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 class SimpleElasticsearchPersistentPropertyUnitTests { this.withCustomFieldName = withCustomFieldName; } } + // endregion }