Dataes 716 - Add value mapping to the ElasticsearchMappingConverter.

Original PR: #366
This commit is contained in:
Peter-Josef Meisch 2019-12-28 19:25:25 +01:00 committed by GitHub
parent d2b7df87f4
commit a68c6ba5d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 820 additions and 245 deletions

View File

@ -0,0 +1,42 @@
/*
* Copyright 2019 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.core.convert;
/**
* @author Peter-Josef Meisch
*/
public class ConversionException extends RuntimeException {
public ConversionException() {
super();
}
public ConversionException(String message) {
super(message);
}
public ConversionException(String message, Throwable cause) {
super(message, cause);
}
public ConversionException(Throwable cause) {
super(cause);
}
protected ConversionException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.data.elasticsearch.core.convert;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.convert.EntityConverter;
import org.springframework.data.domain.Pageable;
@ -27,6 +28,7 @@ import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.lang.Nullable;
@ -41,28 +43,6 @@ import org.springframework.util.Assert;
public interface ElasticsearchConverter
extends EntityConverter<ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty, Object, Document> {
/**
* Convert a given {@literal idValue} to its {@link String} representation taking potentially registered
* {@link org.springframework.core.convert.converter.Converter Converters} into account.
*
* @param idValue must not be {@literal null}.
* @return never {@literal null}.
* @since 3.2
*/
default String convertId(Object idValue) {
Assert.notNull(idValue, "idValue must not be null!");
if (!getConversionService().canConvert(idValue.getClass(), String.class)) {
return idValue.toString();
}
return getConversionService().convert(idValue, String.class);
}
<T> AggregatedPage<SearchHit<T>> mapResults(SearchDocumentResponse response, Class<T> clazz,
@Nullable Pageable pageable);
/**
* Get the configured {@link ProjectionFactory}. <br />
* <strong>NOTE</strong> Should be overwritten in implementation to make use of the type cache.
@ -73,20 +53,35 @@ public interface ElasticsearchConverter
return new SpelAwareProxyProjectionFactory();
}
// region read
/**
* Map a single {@link Document} to an instance of the given type.
*
* @param document the document to map
* @param type must not be {@literal null}.
* @param <T>
* @param <T> the class of type
* @return can be {@literal null} if the document is null or {@link Document#isEmpty()} is true.
* @since 4.0
*/
@Nullable
<T> T mapDocument(@Nullable Document document, Class<T> type);
/**
* Map a list of {@link Document}s to a list of instance of the given type.
*
* @param documents must not be {@literal null}.
* @param type must not be {@literal null}.
* @param <T> the class of type
* @return a list obtained by calling {@link #mapDocument(Document, Class)} on the elements of the list.
* @since 4.0
*/
default <T> List<T> mapDocuments(List<Document> documents, Class<T> type) {
return documents.stream().map(document -> mapDocument(document, type)).collect(Collectors.toList());
}
/**
* builds a {@link SearchHits} from a {@link SearchDocumentResponse}.
*
* @param <T> the clazz of the type, must not be {@literal null}.
* @param type the type of the returned data, must not be {@literal null}.
* @param searchDocumentResponse the response to read from, must not be {@literal null}.
@ -106,22 +101,53 @@ public interface ElasticsearchConverter
*/
<T> SearchHit<T> read(Class<T> type, SearchDocument searchDocument);
<T> AggregatedPage<SearchHit<T>> mapResults(SearchDocumentResponse response, Class<T> clazz,
@Nullable Pageable pageable);
// endregion
// region write
/**
* Map a list of {@link Document}s to alist of instance of the given type.
* Convert a given {@literal idValue} to its {@link String} representation taking potentially registered
* {@link org.springframework.core.convert.converter.Converter Converters} into account.
*
* @param documents must not be {@literal null}.
* @param type must not be {@literal null}.
* @param <T>
* @return a list obtained by calling {@link #mapDocument(Document, Class)} on the elements of the list.
* @since 4.0
* @param idValue must not be {@literal null}.
* @return never {@literal null}.
* @since 3.2
*/
<T> List<T> mapDocuments(List<Document> documents, Class<T> type);
default String convertId(Object idValue) {
Assert.notNull(idValue, "idValue must not be null!");
if (!getConversionService().canConvert(idValue.getClass(), String.class)) {
return idValue.toString();
}
return getConversionService().convert(idValue, String.class);
}
/**
* Map an object to a {@link Document}.
*
* @param source
* @param source the object to map
* @return will not be {@literal null}.
*/
Document mapObject(Object source);
default Document mapObject(@Nullable Object source) {
Document target = Document.create();
write(source, target);
return target;
}
// endregion
/**
* Updates a query by renaming the property names in the query to the correct mapped field names and the values to the
* converted values if the {@link ElasticsearchPersistentProperty} for a property has a
* {@link org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentPropertyConverter}.
*
* @param criteriaQuery the query that is internally updated
* @param domainClass the class of the object that is searched with the query
*/
// region query
void updateQuery(CriteriaQuery criteriaQuery, Class<?> domainClass);
// endregion
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2019 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.core.convert;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.temporal.TemporalAccessor;
import java.util.concurrent.ConcurrentHashMap;
import org.elasticsearch.common.time.DateFormatter;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.util.Assert;
/**
* Provides Converter instances to convert to and from Dates in the different date and time formats that elasticsearch
* understands.
*
* @author Peter-Josef Meisch
* @since 4.0
*/
final public class ElasticsearchDateConverter {
private static final ConcurrentHashMap<String, ElasticsearchDateConverter> converters = new ConcurrentHashMap<>();
private final DateFormatter dateFormatter;
/**
* Creates an ElasticsearchDateConverter for the given {@link DateFormat}.
*
* @param dateFormat must not be @{literal null}
* @return converter
*/
public static ElasticsearchDateConverter of(DateFormat dateFormat) {
Assert.notNull(dateFormat, "dateFormat must not be null");
return of(dateFormat.name());
}
/**
* Creates an ElasticsearchDateConverter for the given pattern.
*
* @param pattern must not be {@literal null}
* @return converter
*/
public static ElasticsearchDateConverter of(String pattern) {
Assert.notNull(pattern, "pattern must not be null");
return converters.computeIfAbsent(pattern, p -> new ElasticsearchDateConverter(DateFormatter.forPattern(p)));
}
private ElasticsearchDateConverter(DateFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}
/**
* Formats the given {@link TemporalAccessor} int a String
*
* @param accessor must not be {@literal null}
* @return the formatted object
*/
public String format(TemporalAccessor accessor) {
return dateFormatter.format(accessor);
}
/**
* Parses a String into an object
*
* @param input the String to parse, must not be {@literal null}.
* @param type the class to return
* @param <T> the class of type
* @return the new created object
*/
public <T extends TemporalAccessor> T parse(String input, Class<T> type) {
TemporalAccessor accessor = dateFormatter.parse(input);
try {
Method method = type.getMethod("from", TemporalAccessor.class);
Object o = method.invoke(null, accessor);
return type.cast(o);
} catch (NoSuchMethodException e) {
throw new ConversionException("no 'from' factory method found in class " + type.getName());
} catch (IllegalAccessException | InvocationTargetException e) {
throw new ConversionException("could not create object of class " + type.getName(), e);
}
}
}

View File

@ -15,8 +15,6 @@
*/
package org.springframework.data.elasticsearch.core.convert;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -54,6 +52,8 @@ import org.springframework.data.elasticsearch.core.document.SearchDocument;
import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentPropertyConverter;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
@ -133,40 +133,86 @@ public class MappingElasticsearchConverter
this.conversions = conversions;
}
/**
* Set the {@link ElasticsearchTypeMapper} to use for reading / writing type hints.
*
* @param typeMapper must not be {@literal null}.
*/
public void setTypeMapper(ElasticsearchTypeMapper typeMapper) {
this.typeMapper = typeMapper;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
DateFormatterRegistrar.addDateConverters(conversionService);
conversions.registerConvertersIn(conversionService);
}
@SuppressWarnings("unchecked")
// region read
@Override
public <T> AggregatedPage<SearchHit<T>> mapResults(SearchDocumentResponse response, Class<T> type,
Pageable pageable) {
List<SearchHit<T>> results = response.getSearchDocuments().stream() //
.map(searchDocument -> read(type, searchDocument)) //
.collect(Collectors.toList());
return new AggregatedPageImpl<>(results, pageable, response);
}
@Override
public <T> SearchHits<T> read(Class<T> type, SearchDocumentResponse searchDocumentResponse) {
Assert.notNull(type, "type must not be null");
Assert.notNull(searchDocumentResponse, "searchDocumentResponse must not be null");
long totalHits = searchDocumentResponse.getTotalHits();
float maxScore = searchDocumentResponse.getMaxScore();
String scrollId = searchDocumentResponse.getScrollId();
List<SearchHit<T>> searchHits = searchDocumentResponse.getSearchDocuments().stream() //
.map(searchDocument -> read(type, searchDocument)) //
.collect(Collectors.toList());
Aggregations aggregations = searchDocumentResponse.getAggregations();
return new SearchHits<T>(totalHits, maxScore, scrollId, searchHits, aggregations);
}
public <T> SearchHit<T> read(Class<T> type, SearchDocument searchDocument) {
Assert.notNull(type, "type must not be null");
Assert.notNull(searchDocument, "searchDocument must not be null");
String id = searchDocument.hasId() ? searchDocument.getId() : null;
float score = searchDocument.getScore();
Object[] sortValues = searchDocument.getSortValues();
T content = mapDocument(searchDocument, type);
return new SearchHit<T>(id, score, sortValues, content);
}
@Override
@Nullable
public <R> R read(Class<R> type, Document source) {
return doRead(source, ClassTypeInformation.from((Class<R>) ClassUtils.getUserClass(type)));
public <T> T mapDocument(@Nullable Document document, Class<T> type) {
if (document == null) {
return null;
}
T mappedResult = read(type, document);
return type.isInterface() || !ClassUtils.isAssignableValue(type, mappedResult)
? getProjectionFactory().createProjection(type, mappedResult)
: type.cast(mappedResult);
}
@SuppressWarnings("unchecked")
@Nullable
protected <R> R doRead(Document source, TypeInformation<R> typeHint) {
@Override
public <R> R read(Class<R> type, Document source) {
TypeInformation<R> typeHint = ClassTypeInformation.from((Class<R>) ClassUtils.getUserClass(type));
typeHint = (TypeInformation<R>) typeMapper.readType(source, typeHint);
if (conversions.hasCustomReadTarget(Map.class, typeHint.getType())) {
return conversionService.convert(source, 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)) {
@ -177,7 +223,6 @@ public class MappingElasticsearchConverter
return readEntity(entity, source);
}
@SuppressWarnings("unchecked")
protected <R> R readEntity(ElasticsearchPersistentEntity<?> entity, Map<String, Object> source) {
ElasticsearchPersistentEntity<?> targetEntity = computeClosestEntity(entity, source);
@ -187,6 +232,7 @@ public class MappingElasticsearchConverter
EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity);
@SuppressWarnings("unchecked")
R instance = (R) instantiator.createInstance(targetEntity,
new PersistentEntityParameterValueProvider<>(targetEntity, propertyValueProvider, null));
@ -248,7 +294,7 @@ public class MappingElasticsearchConverter
Object value = valueProvider.getPropertyValue(prop);
if (value != null) {
accessor.setProperty(prop, valueProvider.getPropertyValue(prop));
accessor.setProperty(prop, value);
}
}
@ -256,6 +302,7 @@ public class MappingElasticsearchConverter
}
@SuppressWarnings("unchecked")
@Nullable
protected <R> R readValue(@Nullable Object source, ElasticsearchPersistentProperty property,
TypeInformation<R> targetType) {
@ -263,11 +310,15 @@ public class MappingElasticsearchConverter
return null;
}
if (property.hasPropertyConverter() && String.class.isAssignableFrom(source.getClass())) {
source = property.getPropertyConverter().read((String) source);
}
Class<R> rawType = targetType.getType();
if (conversions.hasCustomReadTarget(source.getClass(), rawType)) {
return rawType.cast(conversionService.convert(source, rawType));
} else if (source instanceof List) {
return readCollectionValue((List) source, property, targetType);
return readCollectionValue((List<?>) source, property, targetType);
} else if (source instanceof Map) {
return readMapValue((Map<String, Object>) source, property, targetType);
}
@ -275,11 +326,40 @@ public class MappingElasticsearchConverter
return (R) readSimpleValue(source, targetType);
}
@SuppressWarnings("unchecked")
@Nullable
private <R> R readCollectionValue(@Nullable List<?> source, ElasticsearchPersistentProperty property,
TypeInformation<R> targetType) {
if (source == null) {
return null;
}
Collection<Object> target = createCollectionForValue(targetType, source.size());
for (Object value : source) {
if (isSimpleType(value)) {
target.add(
readSimpleValue(value, targetType.getComponentType() != null ? targetType.getComponentType() : targetType));
} else {
if (value instanceof List) {
target.add(readValue(value, property, property.getTypeInformation().getActualType()));
} else {
target.add(readEntity(computeGenericValueTypeForRead(property, value), (Map<String, Object>) value));
}
}
}
return (R) target;
}
@SuppressWarnings("unchecked")
private <R> R readMapValue(@Nullable Map<String, Object> source, ElasticsearchPersistentProperty property,
TypeInformation<R> targetType) {
TypeInformation information = typeMapper.readType(source);
TypeInformation<?> information = typeMapper.readType(source);
if (property.isEntity() && !property.isMap() || information != null) {
ElasticsearchPersistentEntity<?> targetEntity = information != null
@ -300,9 +380,9 @@ public class MappingElasticsearchConverter
if (targetEntity.getTypeInformation().isMap()) {
Map<String, Object> valueMap = (Map) entry.getValue();
Map<String, Object> valueMap = (Map<String, Object>) entry.getValue();
if (typeMapper.containsTypeInformation(valueMap)) {
target.put(entry.getKey(), readEntity(targetEntity, (Map) entry.getValue()));
target.put(entry.getKey(), readEntity(targetEntity, valueMap));
} else {
target.put(entry.getKey(), readValue(valueMap, property, targetEntity.getTypeInformation()));
}
@ -311,7 +391,7 @@ public class MappingElasticsearchConverter
target.put(entry.getKey(),
readValue(entry.getValue(), property, targetEntity.getTypeInformation().getActualType()));
} else {
target.put(entry.getKey(), readEntity(targetEntity, (Map) entry.getValue()));
target.put(entry.getKey(), readEntity(targetEntity, (Map<String, Object>) entry.getValue()));
}
}
}
@ -319,40 +399,13 @@ public class MappingElasticsearchConverter
return (R) target;
}
@SuppressWarnings("unchecked")
private <R> R readCollectionValue(@Nullable List<?> source, ElasticsearchPersistentProperty property,
TypeInformation<R> targetType) {
if (source == null) {
return null;
}
Collection<Object> target = createCollectionForValue(targetType, source.size());
for (Object value : source) {
if (isSimpleType(value)) {
target.add(
readSimpleValue(value, targetType.getComponentType() != null ? targetType.getComponentType() : targetType));
} else {
if (value instanceof List) {
target.add(readValue(value, property, property.getTypeInformation().getActualType()));
} else {
target.add(readEntity(computeGenericValueTypeForRead(property, value), (Map) value));
}
}
}
return (R) target;
}
@SuppressWarnings("unchecked")
@SuppressWarnings({ "unchecked", "rawtypes" })
@Nullable
private Object readSimpleValue(@Nullable Object value, TypeInformation<?> targetType) {
Class<?> target = targetType.getType();
if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) {
if (value == null || ClassUtils.isAssignableValue(target, value)) {
return value;
}
@ -367,16 +420,38 @@ public class MappingElasticsearchConverter
return conversionService.convert(value, target);
}
@SuppressWarnings("unchecked")
@Override
public void write(@Nullable Object source, Document sink) {
if (source == null) {
return;
private <T> void populateScriptFields(T result, SearchDocument searchDocument) {
Map<String, List<Object>> 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 ElasticsearchException("failed to set scripted field: " + name + " with value: " + value, e);
} catch (IllegalAccessException e) {
throw new ElasticsearchException("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<String, Object>) source);
return;
}
@ -388,41 +463,22 @@ public class MappingElasticsearchConverter
typeMapper.writeType(source.getClass(), sink);
}
doWrite(source, sink, type);
}
protected void doWrite(@Nullable Object source, Document sink, @Nullable TypeInformation<? extends Object> typeHint) {
if (source == null) {
return;
}
Class<?> entityType = source.getClass();
Optional<Class<?>> customTarget = conversions.getCustomWriteTarget(entityType, Map.class);
if (customTarget.isPresent()) {
sink.putAll(conversionService.convert(source, Map.class));
return;
}
if (typeHint != null) {
ElasticsearchPersistentEntity<?> entity = type.getType().equals(entityType)
? mappingContext.getRequiredPersistentEntity(type)
: mappingContext.getRequiredPersistentEntity(entityType);
ElasticsearchPersistentEntity<?> entity = typeHint.getType().equals(entityType)
? mappingContext.getRequiredPersistentEntity(typeHint)
: mappingContext.getRequiredPersistentEntity(entityType);
writeEntity(entity, source, sink, null);
return;
}
// write Entity
ElasticsearchPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(entityType);
writeEntity(entity, source, sink, null);
}
protected void writeEntity(ElasticsearchPersistentEntity<?> entity, Object source, Document sink,
@Nullable TypeInformation containingStructure) {
@Nullable TypeInformation<?> containingStructure) {
PersistentPropertyAccessor<?> accessor = entity.getPropertyAccessor(source);
@ -448,10 +504,18 @@ public class MappingElasticsearchConverter
continue;
}
if (property.hasPropertyConverter()) {
ElasticsearchPersistentPropertyConverter propertyConverter = property.getPropertyConverter();
value = propertyConverter.write(value);
}
if (!isSimpleType(value)) {
writeProperty(property, value, sink);
} else {
sink.set(property, getWriteSimpleValue(value));
Object writeSimpleValue = getWriteSimpleValue(value);
if (writeSimpleValue != null) {
sink.set(property, writeSimpleValue);
}
}
}
}
@ -461,7 +525,6 @@ public class MappingElasticsearchConverter
Optional<Class<?>> customWriteTarget = conversions.getCustomWriteTarget(value.getClass());
if (customWriteTarget.isPresent()) {
Class<?> writeTarget = customWriteTarget.get();
sink.set(property, conversionService.convert(value, writeTarget));
return;
@ -484,12 +547,8 @@ public class MappingElasticsearchConverter
sink.set(property, getWriteComplexValue(property, typeHint, value));
}
@Nullable
protected Object getWriteSimpleValue(Object value) {
if (value == null) {
return null;
}
Optional<Class<?>> customTarget = conversions.getCustomWriteTarget(value.getClass());
if (customTarget.isPresent()) {
@ -583,67 +642,30 @@ public class MappingElasticsearchConverter
}
return target;
}
// endregion
@Override
@Nullable
public <T> T mapDocument(@Nullable Document document, Class<T> type) {
// region helper methods
private Collection<Object> createCollectionForValue(TypeInformation<?> collectionTypeInformation, int size) {
if (document == null) {
return null;
}
Class<?> collectionType = collectionTypeInformation.isSubTypeOf(Collection.class) //
? collectionTypeInformation.getType() //
: List.class;
Object mappedResult = read(type, document);
TypeInformation<?> componentType = collectionTypeInformation.getComponentType() != null //
? collectionTypeInformation.getComponentType() //
: ClassTypeInformation.OBJECT;
if (mappedResult == null) {
return (T) null;
}
return type.isInterface() || !ClassUtils.isAssignableValue(type, mappedResult)
? getProjectionFactory().createProjection(type, mappedResult)
: type.cast(mappedResult);
return collectionTypeInformation.getType().isArray() //
? new ArrayList<>(size) //
: CollectionFactory.createCollection(collectionType, componentType.getType(), size);
}
@Override
public <T> SearchHits<T> read(Class<T> type, SearchDocumentResponse searchDocumentResponse) {
private ElasticsearchPersistentEntity<?> computeGenericValueTypeForRead(ElasticsearchPersistentProperty property,
Object value) {
Assert.notNull(type, "type must not be null");
Assert.notNull(searchDocumentResponse, "searchDocumentResponse must not be null");
long totalHits = searchDocumentResponse.getTotalHits();
float maxScore = searchDocumentResponse.getMaxScore();
String scrollId = searchDocumentResponse.getScrollId();
List<SearchHit<T>> searchHits = searchDocumentResponse.getSearchDocuments().stream() //
.map(searchDocument -> read(type, searchDocument)) //
.collect(Collectors.toList());
Aggregations aggregations = searchDocumentResponse.getAggregations();
return new SearchHits<T>(totalHits, maxScore, scrollId, searchHits, aggregations);
}
@Override
public <T> SearchHit<T> read(Class<T> type, SearchDocument searchDocument) {
Assert.notNull(type, "type must not be null");
Assert.notNull(searchDocument, "searchDocument must not be null");
String id = searchDocument.hasId() ? searchDocument.getId() : null;
float score = searchDocument.getScore();
Object[] sortValues = searchDocument.getSortValues();
T content = mapDocument(searchDocument, type);
return new SearchHit<T>(id, score, sortValues, content);
}
@Override
public <T> List<T> mapDocuments(List<Document> documents, Class<T> type) {
return documents.stream().map(it -> mapDocument(it, type)).collect(Collectors.toList());
}
@Override
public Document mapObject(Object source) {
Document target = Document.create();
write(source, target);
return target;
return ClassTypeInformation.OBJECT.equals(property.getTypeInformation().getActualType())
? mappingContext.getRequiredPersistentEntity(value.getClass())
: mappingContext.getRequiredPersistentEntity(property.getTypeInformation().getActualType());
}
private boolean requiresTypeHint(TypeInformation<?> type, Class<?> actualType,
@ -697,29 +719,6 @@ public class MappingElasticsearchConverter
return mappingContext.getRequiredPersistentEntity(typeToUse);
}
private ElasticsearchPersistentEntity<?> computeGenericValueTypeForRead(ElasticsearchPersistentProperty property,
Object value) {
return ClassTypeInformation.OBJECT.equals(property.getTypeInformation().getActualType())
? mappingContext.getRequiredPersistentEntity(value.getClass())
: mappingContext.getRequiredPersistentEntity(property.getTypeInformation().getActualType());
}
private Collection<Object> createCollectionForValue(TypeInformation<?> collectionTypeInformation, int size) {
Class<?> collectionType = collectionTypeInformation.isSubTypeOf(Collection.class) //
? collectionTypeInformation.getType() //
: List.class;
TypeInformation<?> componentType = collectionTypeInformation.getComponentType() != null //
? collectionTypeInformation.getComponentType() //
: ClassTypeInformation.OBJECT;
return collectionTypeInformation.getType().isArray() //
? new ArrayList<>(size) //
: CollectionFactory.createCollection(collectionType, componentType.getType(), size);
}
private boolean isSimpleType(Object value) {
return isSimpleType(value.getClass());
}
@ -727,40 +726,40 @@ public class MappingElasticsearchConverter
private boolean isSimpleType(Class<?> type) {
return conversions.isSimpleType(type);
}
// endregion
// region queries
@Override
public <T> AggregatedPage<SearchHit<T>> mapResults(SearchDocumentResponse response, Class<T> type,
Pageable pageable) {
public void updateQuery(CriteriaQuery criteriaQuery, Class<?> domainClass) {
ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(domainClass);
List<SearchHit<T>> results = response.getSearchDocuments().stream() //
.map(searchDocument -> read(type, searchDocument)) //
.collect(Collectors.toList());
if (persistentEntity != null) {
criteriaQuery.getCriteria().getCriteriaChain().forEach(criteria -> {
String name = criteria.getField().getName();
ElasticsearchPersistentProperty property = persistentEntity.getPersistentProperty(name);
return new AggregatedPageImpl<>(results, pageable, response);
}
if (property != null && property.getName().equals(name)) {
criteria.getField().setName(property.getFieldName());
private <T> void populateScriptFields(T result, SearchDocument searchDocument) {
Map<String, List<Object>> 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 ElasticsearchException("failed to set scripted field: " + name + " with value: " + value, e);
} catch (IllegalAccessException e) {
throw new ElasticsearchException("failed to access scripted field: " + name, e);
}
if (property.hasPropertyConverter()) {
ElasticsearchPersistentPropertyConverter propertyConverter = property.getPropertyConverter();
criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> {
Object value = criteriaEntry.getValue();
if (value.getClass().isArray()) {
Object[] objects = (Object[]) value;
for (int i = 0; i < objects.length; i++) {
objects[i] = propertyConverter.write(objects[i]);
}
} else {
criteriaEntry.setValue(propertyConverter.write(value));
}
});
}
}
}
});
}
}
// endregion
static class MapValueAccessor {
@ -770,6 +769,7 @@ public class MappingElasticsearchConverter
this.target = target;
}
@Nullable
public Object get(ElasticsearchPersistentProperty property) {
if (target instanceof Document) {
@ -801,7 +801,7 @@ public class MappingElasticsearchConverter
Map<String, Object> source = target;
Object result = null;
while (source != null && parts.hasNext()) {
while (parts.hasNext()) {
result = source.get(parts.next());
@ -826,22 +826,25 @@ public class MappingElasticsearchConverter
target.put(property.getFieldName(), value);
}
@SuppressWarnings("unchecked")
private Map<String, Object> getAsMap(Object result) {
if (result instanceof Map) {
return (Map) result;
// noinspection unchecked
return (Map<String, Object>) result;
}
throw new IllegalArgumentException(String.format("%s is not a Map.", result));
}
}
@RequiredArgsConstructor
class ElasticsearchPropertyValueProvider implements PropertyValueProvider<ElasticsearchPersistentProperty> {
final MapValueAccessor mapValueAccessor;
ElasticsearchPropertyValueProvider(MapValueAccessor mapValueAccessor) {
this.mapValueAccessor = mapValueAccessor;
}
@SuppressWarnings("unchecked")
@Override
public <T> T getPropertyValue(ElasticsearchPersistentProperty property) {

View File

@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.core.mapping;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.lang.Nullable;
/**
* ElasticsearchPersistentProperty
@ -60,7 +61,20 @@ public interface ElasticsearchPersistentProperty extends PersistentProperty<Elas
*/
boolean isParentProperty();
public enum PropertyToFieldNameConverter implements Converter<ElasticsearchPersistentProperty, String> {
/**
* @return true if an {@link ElasticsearchPersistentPropertyConverter} is available for this instance.
* @since 4.0
*/
boolean hasPropertyConverter();
/**
* @return the {@link ElasticsearchPersistentPropertyConverter} for this instance.
* @since 4.0
*/
@Nullable
ElasticsearchPersistentPropertyConverter getPropertyConverter();
enum PropertyToFieldNameConverter implements Converter<ElasticsearchPersistentProperty, String> {
INSTANCE;
@ -68,4 +82,17 @@ public interface ElasticsearchPersistentProperty extends PersistentProperty<Elas
return source.getFieldName();
}
}
/**
* when building CriteriaQueries use the name; the fieldname is set later with
* {@link org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter}.
*/
enum QueryPropertyToFieldNameConverter implements Converter<ElasticsearchPersistentProperty, String> {
INSTANCE;
public String convert(ElasticsearchPersistentProperty source) {
return source.getName();
}
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2019 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.core.mapping;
/**
* Interface defining methods to convert a property value to a String and back.
*
* @author Peter-Josef Meisch
*/
public interface ElasticsearchPersistentPropertyConverter {
/**
* converts the property value to a String.
*
* @param property the property value to convert, must not be {@literal null}
* @return String representation.
*/
String write(Object property);
/**
* converts a property value from a String.
*
* @param s the property to convert, must not be {@literal null}
* @return property value
*/
Object read(String s);
}

View File

@ -15,12 +15,16 @@
*/
package org.springframework.data.elasticsearch.core.mapping;
import java.time.temporal.TemporalAccessor;
import java.util.Arrays;
import java.util.List;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.Parent;
import org.springframework.data.elasticsearch.annotations.Score;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchDateConverter;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
@ -49,6 +53,7 @@ public class SimpleElasticsearchPersistentProperty extends
private final boolean isParent;
private final boolean isId;
private final @Nullable String annotatedFieldName;
private ElasticsearchPersistentPropertyConverter propertyConverter;
public SimpleElasticsearchPersistentProperty(Property property,
PersistentEntity<?, ElasticsearchPersistentProperty> owner, SimpleTypeHolder simpleTypeHolder) {
@ -72,6 +77,58 @@ public class SimpleElasticsearchPersistentProperty extends
if (isParent && !getType().equals(String.class)) {
throw new MappingException(String.format("Parent property %s must be of type String!", property.getName()));
}
initDateConverter();
}
@Override
public boolean hasPropertyConverter() {
return propertyConverter != null;
}
@Override
public ElasticsearchPersistentPropertyConverter getPropertyConverter() {
return propertyConverter;
}
/**
* Initializes an {@link ElasticsearchPersistentPropertyConverter} if this property is annotated as a Field with type
* {@link FieldType#Date}, has a {@link DateFormat} set and if the type of the property is one of the Java8 temporal
* classes.
*/
private void initDateConverter() {
Field field = findAnnotation(Field.class);
if (field != null && field.type() == FieldType.Date && TemporalAccessor.class.isAssignableFrom(getType())) {
DateFormat dateFormat = field.format();
ElasticsearchDateConverter converter = null;
if (dateFormat == DateFormat.custom) {
String pattern = field.pattern();
if (StringUtils.hasLength(pattern)) {
converter = ElasticsearchDateConverter.of(pattern);
}
} else if (dateFormat != DateFormat.none) {
converter = ElasticsearchDateConverter.of(dateFormat);
}
if (converter != null) {
ElasticsearchDateConverter dateConverter = converter;
propertyConverter = new ElasticsearchPersistentPropertyConverter() {
@Override
public String write(Object property) {
return dateConverter.format((TemporalAccessor) property);
}
@SuppressWarnings("unchecked")
@Override
public Object read(String s) {
return dateConverter.parse(s, (Class<? extends TemporalAccessor>) getType());
}
};
}
}
}
@Nullable

View File

@ -624,6 +624,7 @@ public class Criteria {
public static class CriteriaEntry {
private OperationKey key;
private Object value;
CriteriaEntry(OperationKey key, Object value) {
@ -635,6 +636,10 @@ public class Criteria {
return key;
}
public void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}

View File

@ -20,13 +20,11 @@ package org.springframework.data.elasticsearch.core.query;
*
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Peter-Josef Meisch
*/
public interface Field {
/**
* Get the name of the field used in schema.xml of elasticsearch server
*
* @return
*/
void setName(String name);
String getName();
}

View File

@ -15,17 +15,26 @@
*/
package org.springframework.data.elasticsearch.core.query;
import org.springframework.util.Assert;
/**
* The most trivial implementation of a Field
*
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Peter-Josef Meisch
*/
public class SimpleField implements Field {
private final String name;
private String name;
public SimpleField(String name) {
setName(name);
}
@Override
public void setName(String name) {
Assert.notNull(name, "name must not be null");
this.name = name;
}

View File

@ -19,6 +19,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHitSupport;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
@ -45,12 +46,14 @@ public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery
private static final int DEFAULT_STREAM_BATCH_SIZE = 500;
private final PartTree tree;
private final ElasticsearchConverter elasticsearchConverter;
private final MappingContext<?, ElasticsearchPersistentProperty> mappingContext;
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations) {
super(method, elasticsearchOperations);
this.tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType());
this.mappingContext = elasticsearchOperations.getElasticsearchConverter().getMappingContext();
this.elasticsearchConverter = elasticsearchOperations.getElasticsearchConverter();
this.mappingContext = elasticsearchConverter.getMappingContext();
}
@Override
@ -58,7 +61,10 @@ public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery
ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters);
CriteriaQuery query = createQuery(accessor);
Assert.notNull(query, "unsupported query");
Class<?> clazz = queryMethod.getEntityInformation().getJavaType();
elasticsearchConverter.updateQuery(query, clazz);
IndexCoordinates index = elasticsearchOperations.getIndexCoordinatesFor(clazz);
Object result = null;

View File

@ -65,7 +65,8 @@ public class ElasticsearchQueryCreator extends AbstractQueryCreator<CriteriaQuer
PersistentPropertyPath<ElasticsearchPersistentProperty> path = context
.getPersistentPropertyPath(part.getProperty());
return new CriteriaQuery(from(part,
new Criteria(path.toDotPath(ElasticsearchPersistentProperty.PropertyToFieldNameConverter.INSTANCE)), iterator));
new Criteria(path.toDotPath(ElasticsearchPersistentProperty.QueryPropertyToFieldNameConverter.INSTANCE)),
iterator));
}
@Override
@ -76,7 +77,8 @@ public class ElasticsearchQueryCreator extends AbstractQueryCreator<CriteriaQuer
PersistentPropertyPath<ElasticsearchPersistentProperty> path = context
.getPersistentPropertyPath(part.getProperty());
return base.addCriteria(from(part,
new Criteria(path.toDotPath(ElasticsearchPersistentProperty.PropertyToFieldNameConverter.INSTANCE)), iterator));
new Criteria(path.toDotPath(ElasticsearchPersistentProperty.QueryPropertyToFieldNameConverter.INSTANCE)),
iterator));
}
@Override

View File

@ -0,0 +1,5 @@
/**
* Infrastructure for the Elasticsearch document-to-object mapping subsystem.
*/
@org.springframework.lang.NonNullApi
package org.springframework.data.elasticsearch.repository.query.parser;

View File

@ -0,0 +1,104 @@
/*
* Copyright 2019 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.core;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import java.time.LocalDate;
import java.util.Collections;
import org.json.JSONException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
/**
* Tests for the mapping of {@link CriteriaQuery} by a
* {@link org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter}. In the same package as
* {@link CriteriaQueryProcessor} as this is needed to get the String represenation to assert.
*
* @author Peter-Josef Meisch
*/
public class CriteriaQueryMappingTests {
MappingElasticsearchConverter mappingElasticsearchConverter;
@BeforeEach
void setUp() {
SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
mappingContext.setInitialEntitySet(Collections.singleton(Person.class));
mappingContext.afterPropertiesSet();
mappingElasticsearchConverter = new MappingElasticsearchConverter(mappingContext, new GenericConversionService());
mappingElasticsearchConverter.afterPropertiesSet();
}
@Test
void shouldMapNamesAndConvertValuesInCriteriaQuery() throws JSONException {
// use POJO properties and types in the query building
CriteriaQuery criteriaQuery = new CriteriaQuery(
new Criteria("birthDate").between(LocalDate.of(1989, 11, 9), LocalDate.of(1990, 11, 9)).or("birthDate").is(LocalDate.of(2019, 12, 28)));
// mapped field name and converted parameter
String expected = '{' + //
" \"bool\" : {" + //
" \"should\" : [" + //
" {" + //
" \"range\" : {" + //
" \"birth-date\" : {" + //
" \"from\" : \"09.11.1989\"," + //
" \"to\" : \"09.11.1990\"," + //
" \"include_lower\" : true," + //
" \"include_upper\" : true" + //
" }" + //
" }" + //
" }," + //
" {" + //
" \"query_string\" : {" + //
" \"query\" : \"28.12.2019\"," + //
" \"fields\" : [" + //
" \"birth-date^1.0\"" + //
" ]" + //
" }" + //
" }" + //
" ]" + //
" }" + //
'}'; //
mappingElasticsearchConverter.updateQuery(criteriaQuery, Person.class);
String queryString = new CriteriaQueryProcessor().createQueryFromCriteria(criteriaQuery.getCriteria()).toString();
assertEquals(expected, queryString, false);
}
static class Person {
@Id String id;
@Field(name = "first-name") String firstName;
@Field(name = "last-name") String lastName;
@Field(name = "birth-date", type = FieldType.Date, format = DateFormat.custom,
pattern = "dd.MM.yyyy") LocalDate birthDate;
}
}

View File

@ -0,0 +1,50 @@
package org.springframework.data.elasticsearch.core.convert;
import static org.assertj.core.api.Assertions.*;
import java.time.LocalDate;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.data.elasticsearch.annotations.DateFormat;
/**
* @author Peter-Josef Meisch
*/
class ElasticsearchDateConverterTests {
@ParameterizedTest
@EnumSource(DateFormat.class)
void shouldCreateConvertersForAllKnownFormats(DateFormat dateFormat) {
if (dateFormat == DateFormat.none) {
return;
}
String pattern = (dateFormat != DateFormat.custom) ? dateFormat.name() : "dd.MM.yyyy";
ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(pattern);
assertThat(converter).isNotNull();
}
@Test
void shouldConvertToString() {
LocalDate localDate = LocalDate.of(2019, 12, 27);
ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.basic_date);
String formatted = converter.format(localDate);
assertThat(formatted).isEqualTo("20191227");
}
@Test
void shouldParseFromString() {
LocalDate localDate = LocalDate.of(2019, 12, 27);
ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.basic_date);
LocalDate parsed = converter.parse("20191227", LocalDate.class);
assertThat(parsed).isEqualTo(localDate);
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.data.elasticsearch.core.convert;
import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
@ -26,16 +27,17 @@ import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.json.JSONException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.ConversionService;
@ -47,10 +49,15 @@ import org.springframework.data.annotation.Transient;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.GeoPointField;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Point;
@ -294,7 +301,7 @@ public class MappingElasticsearchConverterUnitTests {
public void writesNestedEntity() {
Person person = new Person();
person.birthdate = new Date();
person.birthDate = LocalDate.now();
person.gender = Gender.MAN;
person.address = observatoryRoad;
@ -574,6 +581,45 @@ public class MappingElasticsearchConverterUnitTests {
assertThat(target.address).isEqualTo(bigBunsCafe);
}
@Test // DATAES-716
void shouldWriteLocalDate() throws JSONException {
Person person = new Person();
person.id = "4711";
person.firstName = "John";
person.lastName = "Doe";
person.birthDate = LocalDate.of(2000, 8, 22);
person.gender = Gender.MAN;
String expected = '{' + //
" \"id\": \"4711\"," + //
" \"first-name\": \"John\"," + //
" \"last-name\": \"Doe\"," + //
" \"birth-date\": \"22.08.2000\"," + //
" \"gender\": \"MAN\"" + //
'}';
Document document = Document.create();
mappingElasticsearchConverter.write(person, document);
String json = document.toJson();
assertEquals(expected, json, false);
}
@Test
void shouldReadLocalDate() {
Document document = Document.create();
document.put("id", "4711");
document.put("first-name", "John");
document.put("last-name", "Doe");
document.put("birth-date", "22.08.2000");
document.put("gender", "MAN");
Person person = mappingElasticsearchConverter.read(Person.class, document);
assertThat(person.getId()).isEqualTo("4711");
assertThat(person.getBirthDate()).isEqualTo(LocalDate.of(2000, 8, 22));
assertThat(person.getGender()).isEqualTo(Gender.MAN);
}
private String pointTemplate(String name, Point point) {
return String.format(Locale.ENGLISH, "\"%s\":{\"lat\":%.1f,\"lon\":%.1f}", name, point.getX(), point.getY());
}
@ -598,7 +644,10 @@ public class MappingElasticsearchConverterUnitTests {
@Id String id;
String name;
Date birthdate;
@Field(name = "first-name") String firstName;
@Field(name = "last-name") String lastName;
@Field(name = "birth-date", type = FieldType.Date, format = DateFormat.custom,
pattern = "dd.MM.yyyy") LocalDate birthDate;
Gender gender;
Address address;
@ -759,5 +808,4 @@ public class MappingElasticsearchConverterUnitTests {
@GeoPointField private double[] pointD;
}
}

View File

@ -33,6 +33,7 @@ import java.lang.Boolean;
import java.lang.Double;
import java.lang.Integer;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@ -957,7 +958,7 @@ public class MappingBuilderTests extends MappingContextBaseTests {
@Field(copyTo = { "foo", "bar" }) private String copyTo;
@Field(ignoreAbove = 42) private String ignoreAbove;
@Field(type = FieldType.Integer) private String type;
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "YYYYMMDD") private String date;
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "YYYYMMDD") private LocalDate date;
@Field(analyzer = "ana", searchAnalyzer = "sana", normalizer = "norma") private String analyzers;
@Field(type = Keyword, docValues = true) private String docValuesTrue;
@Field(type = Keyword, docValues = false) private String docValuesFalse;

View File

@ -17,8 +17,14 @@ package org.springframework.data.elasticsearch.core.mapping;
import static org.assertj.core.api.Assertions.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import org.junit.jupiter.api.Test;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.Score;
import org.springframework.data.mapping.MappingException;
@ -62,6 +68,47 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
assertThat(persistentProperty.getFieldName()).isEqualTo("by-value");
}
@Test
// DATAES-716
void shouldSetPropertyConverters() {
SimpleElasticsearchPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(DatesProperty.class);
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getRequiredPersistentProperty("date");
assertThat(persistentProperty.hasPropertyConverter()).isFalse();
persistentProperty = persistentEntity.getRequiredPersistentProperty("localDate");
assertThat(persistentProperty.hasPropertyConverter()).isTrue();
assertThat(persistentProperty.getPropertyConverter()).isNotNull();
persistentProperty = persistentEntity.getRequiredPersistentProperty("localDateTime");
assertThat(persistentProperty.hasPropertyConverter()).isTrue();
assertThat(persistentProperty.getPropertyConverter()).isNotNull();
}
@Test
// DATAES-716
void shouldConvertFromLocalDate() {
SimpleElasticsearchPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(DatesProperty.class);
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getRequiredPersistentProperty("localDate");
LocalDate localDate = LocalDate.of(2019, 12, 27);
String converted = persistentProperty.getPropertyConverter().write(localDate);
assertThat(converted).isEqualTo("27.12.2019");
}
@Test
// DATAES-716
void shouldConvertToLocalDate() {
SimpleElasticsearchPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(DatesProperty.class);
ElasticsearchPersistentProperty persistentProperty = persistentEntity.getRequiredPersistentProperty("localDate");
Object converted = persistentProperty.getPropertyConverter().read("27.12.2019");
assertThat(converted).isInstanceOf(LocalDate.class);
assertThat(converted).isEqualTo(LocalDate.of(2019, 12, 27));
}
static class InvalidScoreProperty {
@Score String scoreProperty;
}
@ -73,4 +120,10 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
static class FieldValueProperty {
@Field(value = "by-value") String fieldProperty;
}
static class DatesProperty {
@Field(type = FieldType.Date, format = DateFormat.basic_date) Date date;
@Field(type = FieldType.Date, format = DateFormat.custom, pattern = "dd.MM.yyyy") LocalDate localDate;
@Field(type = FieldType.Date, format = DateFormat.basic_date_time) LocalDateTime localDateTime;
}
}