From a2baea7792990dfe4c09f1d41bf4dcbc6fddbd78 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 1 Feb 2021 11:49:06 +0100 Subject: [PATCH] Refactor MappingElasticsearchConverter. * Add support for SpEL expressions via @Value. * Simplify readCollectionOrArray to consider properly nested lists and maps * Simplify readMap to allow reading generic maps and entities in maps. * Report a fallback TypeInformation in DefaultElasticsearchTypeMapper to properly convert nested maps. We now no longer rely on isSimpleType when writing Maps. This is the preparation to consider Map as simple type. Resolves #1676. See #1675. (cherry picked from commit 877de9c51ce94e5efe294944cf752b20ac2f2cd6) --- .../DefaultElasticsearchTypeMapper.java | 19 +- .../MappingElasticsearchConverter.java | 828 ++++++++++++------ .../mapping/ElasticsearchSimpleTypes.java | 5 + 3 files changed, 565 insertions(+), 287 deletions(-) diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/DefaultElasticsearchTypeMapper.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/DefaultElasticsearchTypeMapper.java index f0596392a..2e0ec4bb2 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/DefaultElasticsearchTypeMapper.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/DefaultElasticsearchTypeMapper.java @@ -26,6 +26,8 @@ import org.springframework.data.convert.TypeInformationMapper; import org.springframework.data.mapping.Alias; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; /** @@ -38,6 +40,9 @@ import org.springframework.lang.Nullable; public class DefaultElasticsearchTypeMapper extends DefaultTypeMapper> implements ElasticsearchTypeMapper { + @SuppressWarnings("rawtypes") // + private static final TypeInformation MAP_TYPE_INFO = ClassTypeInformation.from(Map.class); + private final @Nullable String typeKey; public DefaultElasticsearchTypeMapper(@Nullable String typeKey) { @@ -62,11 +67,23 @@ public class DefaultElasticsearchTypeMapper extends DefaultTypeMapper getFallbackTypeFor(Map source) { + return MAP_TYPE_INFO; + } + /** * {@link TypeAliasAccessor} to store aliases in a {@link Map}. * 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 0cd59626b..a6145b98b 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 @@ -23,10 +23,12 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.expression.MapAccessor; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; @@ -35,7 +37,6 @@ import org.springframework.data.convert.CustomConversions; import org.springframework.data.elasticsearch.annotations.ScriptedField; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.document.SearchDocument; -import org.springframework.data.elasticsearch.core.join.JoinField; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentPropertyConverter; @@ -45,19 +46,25 @@ import org.springframework.data.elasticsearch.core.query.Field; import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator; import org.springframework.data.mapping.model.EntityInstantiator; import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; import org.springframework.data.mapping.model.PropertyValueProvider; +import org.springframework.data.mapping.model.SpELContext; +import org.springframework.data.mapping.model.SpELExpressionEvaluator; +import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider; import org.springframework.data.util.ClassTypeInformation; -import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation; import org.springframework.format.datetime.DateFormatterRegistrar; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; /** @@ -78,18 +85,22 @@ import org.springframework.util.ObjectUtils; public class MappingElasticsearchConverter implements ElasticsearchConverter, ApplicationContextAware, InitializingBean { + private static final String INCOMPATIBLE_TYPES = "Cannot convert %1$s of type %2$s into an instance of %3$s! Implement a custom Converter<%2$s, %3$s> and register it with the CustomConversions."; + private static final String INVALID_TYPE_TO_READ = "Expected to read Document %s into type %s but didn't find a PersistentEntity for the latter!"; + private static final Logger LOGGER = LoggerFactory.getLogger(MappingElasticsearchConverter.class); private final MappingContext, ElasticsearchPersistentProperty> mappingContext; private final GenericConversionService conversionService; // don't access directly, use getConversions(). to prevent null access - @Nullable private CustomConversions conversions = null; + 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) { @@ -105,6 +116,7 @@ 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 @@ -132,14 +144,13 @@ public class MappingElasticsearchConverter * @param conversions must not be {@literal null}. */ public void setConversions(CustomConversions conversions) { + + Assert.notNull(conversions, "CustomConversions must not be null"); + this.conversions = conversions; } private CustomConversions getConversions() { - - if (conversions == null) { - conversions = new ElasticsearchCustomConversions(Collections.emptyList()); - } return conversions; } @@ -159,53 +170,44 @@ public class MappingElasticsearchConverter @Override public R read(Class type, Document source) { TypeInformation typeHint = ClassTypeInformation.from((Class) ClassUtils.getUserClass(type)); - typeHint = (TypeInformation) typeMapper.readType(source, typeHint); - - if (getConversions().hasCustomReadTarget(Map.class, typeHint.getType())) { - R converted = conversionService.convert(source, typeHint.getType()); - if (converted == null) { - // EntityReader.read is defined as non nullable , so we cannot return null - throw new ConversionException("conversion service to type " + typeHint.getType().getName() + " returned null"); - } - return converted; - } - - if (typeHint.isMap() || ClassTypeInformation.OBJECT.equals(typeHint)) { - return (R) source; - } - - ElasticsearchPersistentEntity entity = mappingContext.getRequiredPersistentEntity(typeHint); - return readEntity(entity, source); + return read(typeHint, source); } protected R readEntity(ElasticsearchPersistentEntity entity, Map source) { ElasticsearchPersistentEntity targetEntity = computeClosestEntity(entity, source); - ElasticsearchPropertyValueProvider propertyValueProvider = new ElasticsearchPropertyValueProvider( - new MapValueAccessor(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", "ConstantConditions" }) - R instance = (R) instantiator.createInstance(targetEntity, - new PersistentEntityParameterValueProvider<>(targetEntity, propertyValueProvider, null)); + R instance = (R) instantiator.createInstance(targetEntity, propertyValueProvider); if (!targetEntity.requiresPropertyPopulation()) { return instance; } - R result = readProperties(targetEntity, instance, propertyValueProvider); + 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 accessor = new ConvertingPropertyAccessor<>( + 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)) { - accessor.setProperty(idProperty, document.getId()); + propertyAccessor.setProperty(idProperty, document.getId()); } } @@ -239,6 +241,18 @@ public class MappingElasticsearchConverter } + private ParameterValueProvider getParameterProvider( + ElasticsearchPersistentEntity entity, MapValueAccessor source, SpELExpressionEvaluator evaluator) { + + ElasticsearchPropertyValueProvider provider = new ElasticsearchPropertyValueProvider(source, evaluator); + + // TODO: Support for non-static inner classes via ObjectPath + PersistentEntityParameterValueProvider parameterProvider = new PersistentEntityParameterValueProvider<>( + entity, provider, null); + + return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider); + } + private boolean isAssignedSeqNo(long seqNo) { return seqNo >= 0; } @@ -270,19 +284,18 @@ public class MappingElasticsearchConverter @SuppressWarnings("unchecked") @Nullable - protected R readValue(@Nullable Object source, ElasticsearchPersistentProperty property, - TypeInformation targetType) { + protected R readValue(@Nullable Object value, ElasticsearchPersistentProperty property, TypeInformation type) { - if (source == null) { + if (value == null) { return null; } - Class rawType = targetType.getType(); + Class rawType = type.getType(); if (property.hasPropertyConverter()) { - source = propertyConverterRead(property, source); + value = propertyConverterRead(property, value); } else if (TemporalAccessor.class.isAssignableFrom(property.getType()) - && !getConversions().hasCustomReadTarget(source.getClass(), rawType)) { + && !getConversions().hasCustomReadTarget(value.getClass(), rawType)) { // log at most 5 times String propertyName = property.getOwner().getType().getSimpleName() + '.' + property.getName(); @@ -297,15 +310,61 @@ public class MappingElasticsearchConverter } } - if (getConversions().hasCustomReadTarget(source.getClass(), rawType)) { - return rawType.cast(conversionService.convert(source, rawType)); - } else if (source instanceof List) { - return readCollectionValue((List) source, property, targetType); - } else if (source instanceof Map) { - return readMapValue((Map) source, property, targetType); + 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); } - return (R) readSimpleValue(source, targetType); + if (Document.class.isAssignableFrom(rawType)) { + return (R) source; + } + + if (typeToUse.isMap()) { + return (R) 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) { @@ -334,96 +393,112 @@ public class MappingElasticsearchConverter 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 R readCollectionValue(@Nullable List source, ElasticsearchPersistentProperty property, - TypeInformation targetType) { + private Object readCollectionOrArray(TypeInformation targetType, Collection source) { - if (source == null) { - return null; + 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); } - Collection target = createCollectionForValue(targetType, source.size()); - TypeInformation componentType = targetType.getComponentType(); + for (Object element : source) { - for (Object value : source) { - - if (value == null) { - target.add(null); - } else if (componentType != null && !ClassTypeInformation.OBJECT.equals(componentType) - && isSimpleType(componentType.getType())) { - target.add(readSimpleValue(value, componentType)); - } else if (isSimpleType(value)) { - target.add(readSimpleValue(value, componentType != null ? componentType : targetType)); + if (element instanceof Map) { + items.add(read(componentType, (Map) element)); } else { - if (value instanceof List) { - target.add(readValue(value, property, property.getTypeInformation().getActualType())); - } else if (value instanceof Map) { - target - .add(readMapValue((Map) value, property, property.getTypeInformation().getActualType())); + 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 (R) target; + return getPotentiallyConvertedSimpleRead(items, targetType.getType()); } @SuppressWarnings("unchecked") - private R readMapValue(@Nullable Map source, ElasticsearchPersistentProperty property, - TypeInformation targetType) { + private R readMap(TypeInformation type, Map source) { - TypeInformation information = typeMapper.readType(source); - if (property.isEntity() && !property.isMap() || information != null) { + Assert.notNull(source, "Document must not be null!"); - ElasticsearchPersistentEntity targetEntity = information != null - ? mappingContext.getRequiredPersistentEntity(information) - : mappingContext.getRequiredPersistentEntity(property); - return readEntity(targetEntity, source); - } + 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()); - Map target = new LinkedHashMap<>(); for (Entry entry : source.entrySet()) { - String entryKey = entry.getKey(); - Object entryValue = entry.getValue(); + if (typeMapper.isTypeKey(entry.getKey())) { + continue; + } - if (entryValue == null) { - target.put(entryKey, null); - } else if (isSimpleType(entryValue)) { - target.put(entryKey, - readSimpleValue(entryValue, targetType.isMap() ? targetType.getMapValueType() : targetType)); + 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 { - - ElasticsearchPersistentEntity targetEntity = computeGenericValueTypeForRead(property, entryValue); - - if (targetEntity.getTypeInformation().isMap()) { - - Map valueMap = (Map) entryValue; - if (typeMapper.containsTypeInformation(valueMap)) { - target.put(entryKey, readEntity(targetEntity, valueMap)); - } else { - target.put(entryKey, readValue(valueMap, property, targetEntity.getTypeInformation())); - } - - } else if (targetEntity.getTypeInformation().isCollectionLike()) { - target.put(entryKey, readValue(entryValue, property, targetEntity.getTypeInformation().getActualType())); - } else { - target.put(entryKey, readEntity(targetEntity, (Map) entryValue)); - } + map.put(key, getPotentiallyConvertedSimpleRead(value, rawValueType)); } } - return (R) target; + return (R) map; + } + + @Nullable + private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, TypeInformation targetType) { + return getPotentiallyConvertedSimpleRead(value, targetType.getType()); } @SuppressWarnings({ "unchecked", "rawtypes" }) @Nullable - private Object readSimpleValue(@Nullable Object value, TypeInformation targetType) { + private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { - Class target = targetType.getType(); - - if (value == null || ClassUtils.isAssignableValue(target, value)) { + if (target == null || value == null || ClassUtils.isAssignableValue(target, value)) { return value; } @@ -475,35 +550,73 @@ public class MappingElasticsearchConverter } Class entityType = ClassUtils.getUserClass(source.getClass()); - TypeInformation type = ClassTypeInformation.from(entityType); + TypeInformation type = ClassTypeInformation.from(entityType); - if (requiresTypeHint(type, source.getClass(), null)) { - typeMapper.writeType(source.getClass(), sink); + if (requiresTypeHint(entityType)) { + typeMapper.writeType(type, sink); } - Optional> customTarget = getConversions().getCustomWriteTarget(entityType, Map.class); + writeInternal(source, sink, type); + } - if (customTarget.isPresent()) { - sink.putAll(conversionService.convert(source, Map.class)); + /** + * Internal write conversion method which should be used for nested invocations. + * + * @param source + * @param sink + * @param typeHint + */ + @SuppressWarnings("unchecked") + protected void writeInternal(@Nullable Object source, Map sink, + @Nullable TypeInformation typeHint) { + + if (null == source) { return; } - ElasticsearchPersistentEntity entity = type.getType().equals(entityType) - ? mappingContext.getRequiredPersistentEntity(type) - : mappingContext.getRequiredPersistentEntity(entityType); + Class entityType = source.getClass(); + Optional> customTarget = conversions.getCustomWriteTarget(entityType, Map.class); - writeEntity(entity, source, sink, null); - } - - protected void writeEntity(ElasticsearchPersistentEntity entity, Object source, Document sink, - @Nullable TypeInformation containingStructure) { - - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); - - if (requiresTypeHint(entity.getTypeInformation(), source.getClass(), containingStructure)) { - typeMapper.writeType(source.getClass(), sink); + if (customTarget.isPresent()) { + Map result = conversionService.convert(source, Map.class); + 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(typeHint, source, sink); + writeInternal(source, sink, entity); + } + + /** + * Internal write conversion method which should be used for nested invocations. + * + * @param source + * @param sink + * @param typeHint + */ + protected 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)); } @@ -529,6 +642,7 @@ public class MappingElasticsearchConverter if (property.hasPropertyConverter()) { value = propertyConverterWrite(property, value); + sink.set(property, value); } else if (TemporalAccessor.class.isAssignableFrom(property.getActualType()) && !getConversions().hasCustomWriteTarget(value.getClass())) { @@ -543,12 +657,10 @@ public class MappingElasticsearchConverter property.getType().getSimpleName(), propertyName); propertyWarnings.put(key, count + 1); } - } - - if (!isSimpleType(value)) { + } else if (!isSimpleType(value)) { writeProperty(property, value, sink); } else { - Object writeSimpleValue = getWriteSimpleValue(value); + Object writeSimpleValue = getPotentiallyConvertedSimpleWrite(value, Object.class); if (writeSimpleValue != null) { sink.set(property, writeSimpleValue); } @@ -570,6 +682,7 @@ public class MappingElasticsearchConverter return value; } + @SuppressWarnings("unchecked") protected void writeProperty(ElasticsearchPersistentProperty property, Object value, MapValueAccessor sink) { Optional> customWriteTarget = getConversions().getCustomWriteTarget(value.getClass()); @@ -580,24 +693,208 @@ public class MappingElasticsearchConverter return; } - TypeInformation typeHint = property.getTypeInformation(); - if (typeHint.equals(ClassTypeInformation.OBJECT)) { + TypeInformation valueType = ClassTypeInformation.from(value.getClass()); + TypeInformation type = property.getTypeInformation(); - if (value instanceof List) { - typeHint = ClassTypeInformation.LIST; - } else if (value instanceof Map) { - typeHint = ClassTypeInformation.MAP; - } else if (value instanceof Set) { - typeHint = ClassTypeInformation.SET; - } else if (value instanceof Collection) { - typeHint = ClassTypeInformation.COLLECTION; + 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(ClassTypeInformation.from(property.getRawType()), value, document); + writeInternal(value, document, entity); + sink.set(property, document); + } + + /** + * 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}. + * @return + */ + 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}. + * @return + */ + 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}. + * @return + */ + 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<>())); + } 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."); } } - sink.set(property, getWriteComplexValue(property, typeHint, 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. + * @return + */ + @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; + } + + /** + * Returns a {@link String} representation of the given {@link Map} key + * + * @param key + * @return + */ + private String potentiallyConvertMapKey(Object key) { + + if (key instanceof String) { + return (String) key; + } + + return conversions.hasCustomWriteTarget(key.getClass(), String.class) + ? (String) getPotentiallyConvertedSimpleWrite(key, Object.class) + : 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 + * @return + */ @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); + } + } + + 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; + } + + /** + * @deprecated since 4.2, use {@link #getPotentiallyConvertedSimpleWrite(Object, Class)} instead. + */ + @Nullable + @Deprecated protected Object getWriteSimpleValue(Object value) { Optional> customTarget = getConversions().getCustomWriteTarget(value.getClass()); @@ -608,163 +905,53 @@ public class MappingElasticsearchConverter return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; } - @SuppressWarnings("unchecked") + /** + * @deprecated since 4.2, use {@link #writeInternal(Object, Map, TypeInformation)} instead. + */ + @Deprecated protected Object getWriteComplexValue(ElasticsearchPersistentProperty property, TypeInformation typeHint, Object value) { - if (typeHint.isCollectionLike() || value instanceof Iterable) { - return writeCollectionValue(value, property, typeHint); - } - if (typeHint.isMap()) { - return writeMapValue((Map) value, property, typeHint); - } + Document document = Document.create(); + writeInternal(value, document, property.getTypeInformation()); - if (property.isEntity() || !isSimpleType(value)) { - return writeEntity(value, property); - } - - return value; + return document; } - private Object writeEntity(Object value, ElasticsearchPersistentProperty property) { - - Document target = Document.create(); - writeEntity(mappingContext.getRequiredPersistentEntity(value.getClass()), value, target, - property.getTypeInformation()); - return target; - } - - private Object writeMapValue(Map value, ElasticsearchPersistentProperty property, - TypeInformation typeHint) { - - Map target = new LinkedHashMap<>(); - Streamable> mapSource = Streamable.of(value.entrySet()); - - TypeInformation actualType = typeHint.getActualType(); - - if (actualType != null && !actualType.getType().equals(Object.class) - && isSimpleType(typeHint.getMapValueType().getType())) { - mapSource.forEach(it -> { - - if (it.getValue() == null) { - target.put(it.getKey(), null); - } else { - target.put(it.getKey(), getWriteSimpleValue(it.getValue())); - } - }); - } else { - - mapSource.forEach(it -> { - - Object converted = null; - if (it.getValue() != null) { - - if (isSimpleType(it.getValue())) { - converted = getWriteSimpleValue(it.getValue()); - } else { - converted = getWriteComplexValue(property, ClassTypeInformation.from(it.getValue().getClass()), - it.getValue()); - } - } - - target.put(it.getKey(), converted); - }); - } - - return target; - } - - private Object writeCollectionValue(Object value, ElasticsearchPersistentProperty property, - TypeInformation typeHint) { - - Streamable collectionSource = value instanceof Iterable ? Streamable.of((Iterable) value) - : Streamable.of(ObjectUtils.toObjectArray(value)); - - List target = new ArrayList<>(); - TypeInformation actualType = typeHint.getActualType(); - Class type = actualType != null ? actualType.getType() : null; - - if (type != null && !type.equals(Object.class) && isSimpleType(type)) { - // noinspection ReturnOfNull - collectionSource // - .map(element -> element != null ? getWriteSimpleValue(element) : null) // - .forEach(target::add); - } else { - - collectionSource.map(it -> { - - if (it == null) { - // noinspection ReturnOfNull - return null; - } - - if (isSimpleType(it)) { - return getWriteSimpleValue(it); - } - - return getWriteComplexValue(property, ClassTypeInformation.from(it.getClass()), it); - }).forEach(target::add); - - } - return target; - } // endregion // region helper methods - private Collection createCollectionForValue(TypeInformation collectionTypeInformation, int size) { - Class collectionType = collectionTypeInformation.isSubTypeOf(Collection.class) // - ? collectionTypeInformation.getType() // - : List.class; + /** + * Adds custom type 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 type of the property. + * + * @param type + * @param value must not be {@literal null}. + * @param sink must not be {@literal null}. + */ + protected void addCustomTypeKeyIfNecessary(@Nullable TypeInformation type, Object value, + Map sink) { - TypeInformation componentType = collectionTypeInformation.getComponentType() != null // - ? collectionTypeInformation.getComponentType() // - : ClassTypeInformation.OBJECT; + Class reference = type != null ? type.getActualType().getType() : Object.class; + Class valueType = ClassUtils.getUserClass(value.getClass()); - return collectionTypeInformation.getType().isArray() // - ? new ArrayList<>(size) // - : CollectionFactory.createCollection(collectionType, componentType.getType(), size); - } - - private ElasticsearchPersistentEntity computeGenericValueTypeForRead(ElasticsearchPersistentProperty property, - Object value) { - - return ClassTypeInformation.OBJECT.equals(property.getTypeInformation().getActualType()) - ? mappingContext.getRequiredPersistentEntity(value.getClass()) - : mappingContext.getRequiredPersistentEntity(property.getTypeInformation().getActualType()); - } - - private boolean requiresTypeHint(TypeInformation type, Class actualType, - @Nullable TypeInformation container) { - - if (container != null) { - - if (container.isCollectionLike()) { - if (type.equals(container.getActualType()) && type.getType().equals(actualType)) { - return false; - } - } - - if (container.isMap()) { - if (type.equals(container.getMapValueType()) && type.getType().equals(actualType)) { - return false; - } - } - - if (container.equals(type) && type.getType().equals(actualType)) { - return false; - } - - if (container.getRawTypeInformation().equals(type)) { - Class containerClass = container.getRawTypeInformation().getType(); - if (containerClass.equals(JoinField.class) && type.getType().equals(actualType)) { - return false; - } - } + boolean notTheSameClass = !valueType.equals(reference); + if (notTheSameClass) { + typeMapper.writeType(valueType, sink); } + } - return !getConversions().isSimpleType(type.getType()) && !type.isCollectionLike() - && !getConversions().hasCustomWriteTarget(type.getType()); + /** + * 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); } /** @@ -793,7 +980,24 @@ public class MappingElasticsearchConverter } private boolean isSimpleType(Class type) { - return getConversions().isSimpleType(type); + return !Map.class.isAssignableFrom(type) && getConversions().isSimpleType(type); + } + + /** + * 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 + * @return + */ + private static Collection asCollection(Object source) { + + if (source instanceof Collection) { + return (Collection) source; + } + + return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); } // endregion @@ -937,18 +1141,70 @@ public class MappingElasticsearchConverter class ElasticsearchPropertyValueProvider implements PropertyValueProvider { - final MapValueAccessor mapValueAccessor; + final MapValueAccessor accessor; + final SpELExpressionEvaluator evaluator; - ElasticsearchPropertyValueProvider(MapValueAccessor mapValueAccessor) { - this.mapValueAccessor = mapValueAccessor; + ElasticsearchPropertyValueProvider(MapValueAccessor accessor, SpELExpressionEvaluator evaluator) { + this.accessor = accessor; + this.evaluator = evaluator; } @SuppressWarnings("unchecked") @Override public T getPropertyValue(ElasticsearchPersistentProperty property) { - return (T) readValue(mapValueAccessor.get(property), property, property.getTypeInformation()); + + 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/mapping/ElasticsearchSimpleTypes.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchSimpleTypes.java index d2ea099b5..88b810209 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchSimpleTypes.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchSimpleTypes.java @@ -19,10 +19,14 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.mapping.model.SimpleTypeHolder; /** + * Utility to define simple types understood by Spring Data Elasticsearch and the Elasticsearch client. + * * @author Christoph Strobl + * @author Mark Paluch * @since 3.2 */ public class ElasticsearchSimpleTypes { @@ -35,6 +39,7 @@ public class ElasticsearchSimpleTypes { AUTOGENERATED_ID_TYPES = Collections.unmodifiableSet(classes); Set> simpleTypes = new HashSet<>(); + simpleTypes.add(Document.class); ELASTICSEARCH_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); }