diff --git a/pom.xml b/pom.xml index 2c268cf1a..5ea448368 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,13 @@ - + 4.0.0 org.springframework.data spring-data-elasticsearch - 4.2.0-SNAPSHOT + 4.2.0-GH-1676-SNAPSHOT org.springframework.data.build diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java index c1a55c217..6734c54b0 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchTemplate.java @@ -277,28 +277,26 @@ private T updateIndexedObject(T entity, IndexedObjectInformation indexedObje routingResolver); adaptibleEntity.populateIdIfNecessary(indexedObjectInformation.getId()); - ElasticsearchPersistentEntity persistentEntity = getRequiredPersistentEntity(entity.getClass()); - PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(entity); - - if (indexedObjectInformation.getSeqNo() != null && indexedObjectInformation.getPrimaryTerm() != null - && persistentEntity.hasSeqNoPrimaryTermProperty()) { - ElasticsearchPersistentProperty seqNoPrimaryTermProperty = persistentEntity.getSeqNoPrimaryTermProperty(); - propertyAccessor.setProperty(seqNoPrimaryTermProperty, - new SeqNoPrimaryTerm(indexedObjectInformation.getSeqNo(), indexedObjectInformation.getPrimaryTerm())); - } + ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(entity.getClass()); + if (persistentEntity != null) { + PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(entity); + + if (indexedObjectInformation.getSeqNo() != null && indexedObjectInformation.getPrimaryTerm() != null + && persistentEntity.hasSeqNoPrimaryTermProperty()) { + ElasticsearchPersistentProperty seqNoPrimaryTermProperty = persistentEntity.getSeqNoPrimaryTermProperty(); + propertyAccessor.setProperty(seqNoPrimaryTermProperty, + new SeqNoPrimaryTerm(indexedObjectInformation.getSeqNo(), indexedObjectInformation.getPrimaryTerm())); + } - if (indexedObjectInformation.getVersion() != null && persistentEntity.hasVersionProperty()) { - ElasticsearchPersistentProperty versionProperty = persistentEntity.getVersionProperty(); - propertyAccessor.setProperty(versionProperty, indexedObjectInformation.getVersion()); + if (indexedObjectInformation.getVersion() != null && persistentEntity.hasVersionProperty()) { + ElasticsearchPersistentProperty versionProperty = persistentEntity.getVersionProperty(); + propertyAccessor.setProperty(versionProperty, indexedObjectInformation.getVersion()); + } } return entity; } - private ElasticsearchPersistentEntity getRequiredPersistentEntity(Class clazz) { - return converter.getMappingContext().getRequiredPersistentEntity(clazz); - } - @Override public Flux multiGet(Query query, Class clazz) { return multiGet(query, clazz, getIndexCoordinatesFor(clazz)); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java b/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java index eeee31524..2adfe70eb 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/RequestFactory.java @@ -107,6 +107,7 @@ import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.*; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -1730,12 +1731,18 @@ private ElasticsearchPersistentEntity getPersistentEntity(@Nullable Class @Nullable private String getPersistentEntityId(Object entity) { - Object identifier = elasticsearchConverter.getMappingContext() // - .getRequiredPersistentEntity(entity.getClass()) // - .getIdentifierAccessor(entity).getIdentifier(); + MappingContext, ElasticsearchPersistentProperty> mappingContext = elasticsearchConverter + .getMappingContext(); - if (identifier != null) { - return identifier.toString(); + ElasticsearchPersistentEntity persistentEntity = mappingContext.getPersistentEntity(entity.getClass()); + + if (persistentEntity != null) { + Object identifier = persistentEntity // + .getIdentifierAccessor(entity).getIdentifier(); + + if (identifier != null) { + return identifier.toString(); + } } return null; @@ -1743,8 +1750,16 @@ private String getPersistentEntityId(Object entity) { private VersionType retrieveVersionTypeFromPersistentEntity(Class clazz) { - VersionType versionType = elasticsearchConverter.getMappingContext().getRequiredPersistentEntity(clazz) - .getVersionType(); + MappingContext, ElasticsearchPersistentProperty> mappingContext = elasticsearchConverter + .getMappingContext(); + + ElasticsearchPersistentEntity persistentEntity = mappingContext.getPersistentEntity(clazz); + + VersionType versionType = null; + + if (persistentEntity != null) { + versionType = persistentEntity.getVersionType(); + } return versionType != null ? versionType : VersionType.EXTERNAL; } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/completion/Completion.java b/src/main/java/org/springframework/data/elasticsearch/core/completion/Completion.java index ef8673316..57cbec193 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/completion/Completion.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/completion/Completion.java @@ -20,11 +20,11 @@ public class Completion { @Nullable private Map> contexts; @Nullable private Integer weight; + @PersistenceConstructor public Completion(String[] input) { this.input = input; } - @PersistenceConstructor public Completion(List input) { this.input = input.toArray(new String[0]); } 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.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 @@ 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 DefaultElasticsearchTypeMapper(@Nullable String typeKey, TypeAliasAccesso this.typeKey = typeKey; } - @Override + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.MongoTypeMapper#isTypeKey(java.lang.String) + */ public boolean isTypeKey(String key) { return typeKey != null && typeKey.equals(key); } + /* + * (non-Javadoc) + * @see org.springframework.data.convert.DefaultTypeMapper#getFallbackTypeFor(java.lang.Object) + */ + @Override + protected TypeInformation 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 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.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.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 @@ 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 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 ConversionService getConversionService() { * @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 void afterPropertiesSet() { @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 @@ protected R readEntity(ElasticsearchPersistentEntity entity, Map 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 @@ protected R readProperties(ElasticsearchPersistentEntity entity, R instan @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 @@ protected R readValue(@Nullable Object source, ElasticsearchPersistentProper } } - 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!"); - return (R) readSimpleValue(source, targetType); + 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 (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 @@ private Object convertOnRead(ElasticsearchPersistentPropertyConverter propertyCo 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!"); - Collection target = createCollectionForValue(targetType, source.size()); - TypeInformation componentType = targetType.getComponentType(); + Class collectionType = targetType.isSubTypeOf(Collection.class) // + ? targetType.getType() // + : List.class; - for (Object value : source) { + TypeInformation componentType = targetType.getComponentType() != null // + ? targetType.getComponentType() // + : ClassTypeInformation.OBJECT; + Class rawComponentType = componentType.getType(); - 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)); + 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 (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(); - Map target = new LinkedHashMap<>(); - for (Entry entry : source.entrySet()) { + TypeInformation keyType = type.getComponentType(); + TypeInformation valueType = type.getMapValueType(); - String entryKey = entry.getKey(); - Object entryValue = entry.getValue(); + Class rawKeyType = keyType != null ? keyType.getType() : null; + Class rawValueType = valueType != null ? valueType.getType() : null; - if (entryValue == null) { - target.put(entryKey, null); - } else if (isSimpleType(entryValue)) { - target.put(entryKey, - readSimpleValue(entryValue, targetType.isMap() ? targetType.getMapValueType() : targetType)); - } else { + Map map = CollectionFactory.createMap(mapType, rawKeyType, source.keySet().size()); - ElasticsearchPersistentEntity targetEntity = computeGenericValueTypeForRead(property, entryValue); + for (Entry entry : source.entrySet()) { - if (targetEntity.getTypeInformation().isMap()) { + if (typeMapper.isTypeKey(entry.getKey())) { + continue; + } - Map valueMap = (Map) entryValue; - if (typeMapper.containsTypeInformation(valueMap)) { - target.put(entryKey, readEntity(targetEntity, valueMap)); - } else { - target.put(entryKey, readValue(valueMap, property, targetEntity.getTypeInformation())); - } + Object key = entry.getKey(); - } else if (targetEntity.getTypeInformation().isCollectionLike()) { - target.put(entryKey, readValue(entryValue, property, targetEntity.getTypeInformation().getActualType())); - } else { - target.put(entryKey, readEntity(targetEntity, (Map) entryValue)); - } + 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) target; + return (R) map; } - @SuppressWarnings({ "unchecked", "rawtypes" }) @Nullable - private Object readSimpleValue(@Nullable Object value, TypeInformation targetType) { + private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, TypeInformation targetType) { + return getPotentiallyConvertedSimpleRead(value, targetType.getType()); + } - Class target = targetType.getType(); + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Nullable + private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { - if (value == null || ClassUtils.isAssignableValue(target, value)) { + if (target == null || value == null || ClassUtils.isAssignableValue(target, value)) { return value; } @@ -475,35 +550,73 @@ public void write(Object source, Document sink) { } 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); + } + + /** + * 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; + } + + Class entityType = source.getClass(); + Optional> customTarget = conversions.getCustomWriteTarget(entityType, Map.class); if (customTarget.isPresent()) { - sink.putAll(conversionService.convert(source, Map.class)); + Map result = conversionService.convert(source, Map.class); + sink.putAll(result); + return; + } + + if (Map.class.isAssignableFrom(entityType)) { + writeMapInternal((Map) source, sink, ClassTypeInformation.MAP); return; } - ElasticsearchPersistentEntity entity = type.getType().equals(entityType) - ? mappingContext.getRequiredPersistentEntity(type) - : mappingContext.getRequiredPersistentEntity(entityType); + if (Collection.class.isAssignableFrom(entityType)) { + writeCollectionInternal((Collection) source, ClassTypeInformation.LIST, (Collection) sink); + return; + } - writeEntity(entity, source, sink, null); + ElasticsearchPersistentEntity entity = mappingContext.getRequiredPersistentEntity(entityType); + addCustomTypeKeyIfNecessary(typeHint, source, sink); + writeInternal(source, sink, entity); } - protected void writeEntity(ElasticsearchPersistentEntity entity, Object source, Document sink, - @Nullable TypeInformation containingStructure) { + /** + * 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) { - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); + if (source == null) { + return; + } - if (requiresTypeHint(entity.getTypeInformation(), source.getClass(), containingStructure)) { - typeMapper.writeType(source.getClass(), sink); + 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 @@ protected void writeProperties(ElasticsearchPersistentEntity entity, Persiste 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 @@ protected void writeProperties(ElasticsearchPersistentEntity entity, Persiste 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 @@ private Object propertyConverterWrite(ElasticsearchPersistentProperty property, return value; } + @SuppressWarnings("unchecked") protected void writeProperty(ElasticsearchPersistentProperty property, Object value, MapValueAccessor sink) { Optional> customWriteTarget = getConversions().getCustomWriteTarget(value.getClass()); @@ -580,191 +693,265 @@ protected void writeProperty(ElasticsearchPersistentProperty property, Object va 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; } - sink.set(property, getWriteComplexValue(property, typeHint, value)); - } - - @Nullable - protected Object getWriteSimpleValue(Object value) { - Optional> customTarget = getConversions().getCustomWriteTarget(value.getClass()); - - if (customTarget.isPresent()) { - return conversionService.convert(value, customTarget.get()); + if (valueType.isMap()) { + Map mapDbObj = createMap((Map) value, property); + sink.set(property, mapDbObj); + return; } - return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; - } + // Lookup potential custom target type + Optional> basicTargetType = conversions.getCustomWriteTarget(value.getClass()); - @SuppressWarnings("unchecked") - protected Object getWriteComplexValue(ElasticsearchPersistentProperty property, TypeInformation typeHint, - Object value) { + if (basicTargetType.isPresent()) { - if (typeHint.isCollectionLike() || value instanceof Iterable) { - return writeCollectionValue(value, property, typeHint); - } - if (typeHint.isMap()) { - return writeMapValue((Map) value, property, typeHint); + sink.set(property, conversionService.convert(value, basicTargetType.get())); + return; } - if (property.isEntity() || !isSimpleType(value)) { - return writeEntity(value, property); - } + ElasticsearchPersistentEntity entity = valueType.isSubTypeOf(property.getType()) + ? mappingContext.getRequiredPersistentEntity(value.getClass()) + : mappingContext.getRequiredPersistentEntity(type); - return value; + 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())); } - private Object writeEntity(Object value, ElasticsearchPersistentProperty property) { + /** + * 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!"); - Document target = Document.create(); - writeEntity(mappingContext.getRequiredPersistentEntity(value.getClass()), value, target, - property.getTypeInformation()); - return target; + return writeMapInternal(map, new LinkedHashMap<>(map.size()), property.getTypeInformation()); } - private Object writeMapValue(Map value, ElasticsearchPersistentProperty property, - TypeInformation typeHint) { + /** + * 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) { - Map target = new LinkedHashMap<>(); - Streamable> mapSource = Streamable.of(value.entrySet()); + for (Map.Entry entry : source.entrySet()) { - TypeInformation actualType = typeHint.getActualType(); + Object key = entry.getKey(); + Object value = entry.getValue(); - if (actualType != null && !actualType.getType().equals(Object.class) - && isSimpleType(typeHint.getMapValueType().getType())) { - mapSource.forEach(it -> { + if (isSimpleType(key.getClass())) { - if (it.getValue() == null) { - target.put(it.getKey(), null); + 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 { - target.put(it.getKey(), getWriteSimpleValue(it.getValue())); + Map document = Document.create(); + TypeInformation valueTypeInfo = propertyType.isMap() ? propertyType.getMapValueType() + : ClassTypeInformation.OBJECT; + writeInternal(value, document, valueTypeInfo); + + sink.put(simpleKey, document); } - }); - } else { + } else { + throw new MappingException("Cannot use a complex object as a key value."); + } + } + + return sink; + } - mapSource.forEach(it -> { + /** + * 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) { - Object converted = null; - if (it.getValue() != null) { + TypeInformation componentType = null; - if (isSimpleType(it.getValue())) { - converted = getWriteSimpleValue(it.getValue()); - } else { - converted = getWriteComplexValue(property, ClassTypeInformation.from(it.getValue().getClass()), - it.getValue()); - } - } + List collection = sink instanceof List ? (List) sink : new ArrayList<>(sink); - target.put(it.getKey(), converted); - }); + if (type != null) { + componentType = type.getComponentType(); } - return target; + 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 Object writeCollectionValue(Object value, ElasticsearchPersistentProperty property, - TypeInformation typeHint) { + /** + * Returns a {@link String} representation of the given {@link Map} key + * + * @param key + * @return + */ + private String potentiallyConvertMapKey(Object key) { - Streamable collectionSource = value instanceof Iterable ? Streamable.of((Iterable) value) - : Streamable.of(ObjectUtils.toObjectArray(value)); + if (key instanceof String) { + return (String) key; + } - List target = new ArrayList<>(); - TypeInformation actualType = typeHint.getActualType(); - Class type = actualType != null ? actualType.getType() : null; + return conversions.hasCustomWriteTarget(key.getClass(), String.class) + ? (String) getPotentiallyConvertedSimpleWrite(key, Object.class) + : key.toString(); + } - if (type != null && !type.equals(Object.class) && isSimpleType(type)) { - // noinspection ReturnOfNull - collectionSource // - .map(element -> element != null ? getWriteSimpleValue(element) : null) // - .forEach(target::add); - } else { + /** + * 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) { - collectionSource.map(it -> { + if (value == null) { + return null; + } - if (it == null) { - // noinspection ReturnOfNull - return null; - } + if (typeHint != null && Object.class != typeHint) { - if (isSimpleType(it)) { - return getWriteSimpleValue(it); - } + if (conversionService.canConvert(value.getClass(), typeHint)) { + value = conversionService.convert(value, typeHint); + } + } - return getWriteComplexValue(property, ClassTypeInformation.from(it.getClass()), it); - }).forEach(target::add); + Optional> customTarget = conversions.getCustomWriteTarget(value.getClass()); + if (customTarget.isPresent()) { + return conversionService.convert(value, customTarget.get()); } - return target; - } - // endregion - // region helper methods - private Collection createCollectionForValue(TypeInformation collectionTypeInformation, int size) { + if (ObjectUtils.isArray(value)) { - Class collectionType = collectionTypeInformation.isSubTypeOf(Collection.class) // - ? collectionTypeInformation.getType() // - : List.class; + if (value instanceof byte[]) { + return value; + } + return asCollection(value); + } - TypeInformation componentType = collectionTypeInformation.getComponentType() != null // - ? collectionTypeInformation.getComponentType() // - : ClassTypeInformation.OBJECT; + return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; + } - return collectionTypeInformation.getType().isArray() // - ? new ArrayList<>(size) // - : CollectionFactory.createCollection(collectionType, componentType.getType(), size); + /** + * @deprecated since 4.2, use {@link #getPotentiallyConvertedSimpleWrite(Object, Class)} instead. + */ + @Nullable + @Deprecated + protected Object getWriteSimpleValue(Object value) { + Optional> customTarget = getConversions().getCustomWriteTarget(value.getClass()); + + if (customTarget.isPresent()) { + return conversionService.convert(value, customTarget.get()); + } + + return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; } - private ElasticsearchPersistentEntity computeGenericValueTypeForRead(ElasticsearchPersistentProperty property, + /** + * @deprecated since 4.2, use {@link #writeInternal(Object, Map, TypeInformation)} instead. + */ + @Deprecated + protected Object getWriteComplexValue(ElasticsearchPersistentProperty property, TypeInformation typeHint, Object value) { - return ClassTypeInformation.OBJECT.equals(property.getTypeInformation().getActualType()) - ? mappingContext.getRequiredPersistentEntity(value.getClass()) - : mappingContext.getRequiredPersistentEntity(property.getTypeInformation().getActualType()); - } + Document document = Document.create(); + writeInternal(value, document, property.getTypeInformation()); - private boolean requiresTypeHint(TypeInformation type, Class actualType, - @Nullable TypeInformation container) { + return document; + } - if (container != null) { + // endregion - if (container.isCollectionLike()) { - if (type.equals(container.getActualType()) && type.getType().equals(actualType)) { - return false; - } - } + // region helper methods - if (container.isMap()) { - if (type.equals(container.getMapValueType()) && type.getType().equals(actualType)) { - return false; - } - } + /** + * 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) { - if (container.equals(type) && type.getType().equals(actualType)) { - return false; - } + Class reference = type != null ? type.getActualType().getType() : Object.class; + Class valueType = ClassUtils.getUserClass(value.getClass()); - 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); } + } + + /** + * 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 !getConversions().isSimpleType(type.getType()) && !type.isCollectionLike() - && !getConversions().hasCustomWriteTarget(type.getType()); + return !isSimpleType(type) && !ClassUtils.isAssignable(Collection.class, type) + && !conversions.hasCustomWriteTarget(type, Document.class); } /** @@ -793,7 +980,24 @@ private boolean isSimpleType(Object value) { } 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 @@ private Map getAsMap(Object result) { 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..6e8981d93 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 @@ -17,12 +17,17 @@ import java.util.Collections; import java.util.HashSet; +import java.util.Map; 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 +40,9 @@ public class ElasticsearchSimpleTypes { AUTOGENERATED_ID_TYPES = Collections.unmodifiableSet(classes); Set> simpleTypes = new HashSet<>(); + simpleTypes.add(Document.class); + simpleTypes.add(Map.class); + ELASTICSEARCH_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); }