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; package org.springframework.data.elasticsearch.core.convert;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.convert.EntityConverter; import org.springframework.data.convert.EntityConverter;
import org.springframework.data.domain.Pageable; 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.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; 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.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -41,28 +43,6 @@ import org.springframework.util.Assert;
public interface ElasticsearchConverter public interface ElasticsearchConverter
extends EntityConverter<ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty, Object, Document> { 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 /> * Get the configured {@link ProjectionFactory}. <br />
* <strong>NOTE</strong> Should be overwritten in implementation to make use of the type cache. * <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(); return new SpelAwareProxyProjectionFactory();
} }
// region read
/** /**
* Map a single {@link Document} to an instance of the given type. * Map a single {@link Document} to an instance of the given type.
* *
* @param document the document to map * @param document the document to map
* @param type must not be {@literal null}. * @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. * @return can be {@literal null} if the document is null or {@link Document#isEmpty()} is true.
* @since 4.0 * @since 4.0
*/ */
@Nullable @Nullable
<T> T mapDocument(@Nullable Document document, Class<T> type); <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}. * builds a {@link SearchHits} from a {@link SearchDocumentResponse}.
*
* @param <T> the clazz of the type, must not be {@literal null}. * @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 type the type of the returned data, must not be {@literal null}.
* @param searchDocumentResponse the response to read from, 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> 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 idValue must not be {@literal null}.
* @param type must not be {@literal null}. * @return never {@literal null}.
* @param <T> * @since 3.2
* @return a list obtained by calling {@link #mapDocument(Document, Class)} on the elements of the list.
* @since 4.0
*/ */
<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}. * Map an object to a {@link Document}.
* *
* @param source * @param source the object to map
* @return will not be {@literal null}. * @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; package org.springframework.data.elasticsearch.core.convert;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; 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.document.SearchDocumentResponse;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; 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.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
@ -133,40 +133,86 @@ public class MappingElasticsearchConverter
this.conversions = conversions; 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) * (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/ */
@Override @Override
public void afterPropertiesSet() { public void afterPropertiesSet() {
DateFormatterRegistrar.addDateConverters(conversionService); DateFormatterRegistrar.addDateConverters(conversionService);
conversions.registerConvertersIn(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 @Override
@Nullable @Nullable
public <R> R read(Class<R> type, Document source) { public <T> T mapDocument(@Nullable Document document, Class<T> type) {
return doRead(source, ClassTypeInformation.from((Class<R>) ClassUtils.getUserClass(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") @SuppressWarnings("unchecked")
@Nullable @Override
protected <R> R doRead(Document source, TypeInformation<R> typeHint) { 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); typeHint = (TypeInformation<R>) typeMapper.readType(source, typeHint);
if (conversions.hasCustomReadTarget(Map.class, typeHint.getType())) { 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)) { if (typeHint.isMap() || ClassTypeInformation.OBJECT.equals(typeHint)) {
@ -177,7 +223,6 @@ public class MappingElasticsearchConverter
return readEntity(entity, source); return readEntity(entity, source);
} }
@SuppressWarnings("unchecked")
protected <R> R readEntity(ElasticsearchPersistentEntity<?> entity, Map<String, Object> source) { protected <R> R readEntity(ElasticsearchPersistentEntity<?> entity, Map<String, Object> source) {
ElasticsearchPersistentEntity<?> targetEntity = computeClosestEntity(entity, source); ElasticsearchPersistentEntity<?> targetEntity = computeClosestEntity(entity, source);
@ -187,6 +232,7 @@ public class MappingElasticsearchConverter
EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity); EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity);
@SuppressWarnings("unchecked")
R instance = (R) instantiator.createInstance(targetEntity, R instance = (R) instantiator.createInstance(targetEntity,
new PersistentEntityParameterValueProvider<>(targetEntity, propertyValueProvider, null)); new PersistentEntityParameterValueProvider<>(targetEntity, propertyValueProvider, null));
@ -248,7 +294,7 @@ public class MappingElasticsearchConverter
Object value = valueProvider.getPropertyValue(prop); Object value = valueProvider.getPropertyValue(prop);
if (value != null) { if (value != null) {
accessor.setProperty(prop, valueProvider.getPropertyValue(prop)); accessor.setProperty(prop, value);
} }
} }
@ -256,6 +302,7 @@ public class MappingElasticsearchConverter
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Nullable
protected <R> R readValue(@Nullable Object source, ElasticsearchPersistentProperty property, protected <R> R readValue(@Nullable Object source, ElasticsearchPersistentProperty property,
TypeInformation<R> targetType) { TypeInformation<R> targetType) {
@ -263,11 +310,15 @@ public class MappingElasticsearchConverter
return null; return null;
} }
if (property.hasPropertyConverter() && String.class.isAssignableFrom(source.getClass())) {
source = property.getPropertyConverter().read((String) source);
}
Class<R> rawType = targetType.getType(); Class<R> rawType = targetType.getType();
if (conversions.hasCustomReadTarget(source.getClass(), rawType)) { if (conversions.hasCustomReadTarget(source.getClass(), rawType)) {
return rawType.cast(conversionService.convert(source, rawType)); return rawType.cast(conversionService.convert(source, rawType));
} else if (source instanceof List) { } else if (source instanceof List) {
return readCollectionValue((List) source, property, targetType); return readCollectionValue((List<?>) source, property, targetType);
} else if (source instanceof Map) { } else if (source instanceof Map) {
return readMapValue((Map<String, Object>) source, property, targetType); return readMapValue((Map<String, Object>) source, property, targetType);
} }
@ -275,11 +326,40 @@ public class MappingElasticsearchConverter
return (R) readSimpleValue(source, targetType); 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") @SuppressWarnings("unchecked")
private <R> R readMapValue(@Nullable Map<String, Object> source, ElasticsearchPersistentProperty property, private <R> R readMapValue(@Nullable Map<String, Object> source, ElasticsearchPersistentProperty property,
TypeInformation<R> targetType) { TypeInformation<R> targetType) {
TypeInformation information = typeMapper.readType(source); TypeInformation<?> information = typeMapper.readType(source);
if (property.isEntity() && !property.isMap() || information != null) { if (property.isEntity() && !property.isMap() || information != null) {
ElasticsearchPersistentEntity<?> targetEntity = information != null ElasticsearchPersistentEntity<?> targetEntity = information != null
@ -300,9 +380,9 @@ public class MappingElasticsearchConverter
if (targetEntity.getTypeInformation().isMap()) { if (targetEntity.getTypeInformation().isMap()) {
Map<String, Object> valueMap = (Map) entry.getValue(); Map<String, Object> valueMap = (Map<String, Object>) entry.getValue();
if (typeMapper.containsTypeInformation(valueMap)) { if (typeMapper.containsTypeInformation(valueMap)) {
target.put(entry.getKey(), readEntity(targetEntity, (Map) entry.getValue())); target.put(entry.getKey(), readEntity(targetEntity, valueMap));
} else { } else {
target.put(entry.getKey(), readValue(valueMap, property, targetEntity.getTypeInformation())); target.put(entry.getKey(), readValue(valueMap, property, targetEntity.getTypeInformation()));
} }
@ -311,7 +391,7 @@ public class MappingElasticsearchConverter
target.put(entry.getKey(), target.put(entry.getKey(),
readValue(entry.getValue(), property, targetEntity.getTypeInformation().getActualType())); readValue(entry.getValue(), property, targetEntity.getTypeInformation().getActualType()));
} else { } 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; return (R) target;
} }
@SuppressWarnings("unchecked") @SuppressWarnings({ "unchecked", "rawtypes" })
private <R> R readCollectionValue(@Nullable List<?> source, ElasticsearchPersistentProperty property, @Nullable
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")
private Object readSimpleValue(@Nullable Object value, TypeInformation<?> targetType) { private Object readSimpleValue(@Nullable Object value, TypeInformation<?> targetType) {
Class<?> target = targetType.getType(); Class<?> target = targetType.getType();
if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) { if (value == null || ClassUtils.isAssignableValue(target, value)) {
return value; return value;
} }
@ -367,16 +420,38 @@ public class MappingElasticsearchConverter
return conversionService.convert(value, target); return conversionService.convert(value, target);
} }
@SuppressWarnings("unchecked") private <T> void populateScriptFields(T result, SearchDocument searchDocument) {
@Override Map<String, List<Object>> fields = searchDocument.getFields();
public void write(@Nullable Object source, Document sink) { if (!fields.isEmpty()) {
for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) {
if (source == null) { ScriptedField scriptedField = field.getAnnotation(ScriptedField.class);
return; 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) { if (source instanceof Map) {
// noinspection unchecked
sink.putAll((Map<String, Object>) source); sink.putAll((Map<String, Object>) source);
return; return;
} }
@ -388,41 +463,22 @@ public class MappingElasticsearchConverter
typeMapper.writeType(source.getClass(), sink); 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); Optional<Class<?>> customTarget = conversions.getCustomWriteTarget(entityType, Map.class);
if (customTarget.isPresent()) { if (customTarget.isPresent()) {
sink.putAll(conversionService.convert(source, Map.class)); sink.putAll(conversionService.convert(source, Map.class));
return; 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); writeEntity(entity, source, sink, null);
} }
protected void writeEntity(ElasticsearchPersistentEntity<?> entity, Object source, Document sink, protected void writeEntity(ElasticsearchPersistentEntity<?> entity, Object source, Document sink,
@Nullable TypeInformation containingStructure) { @Nullable TypeInformation<?> containingStructure) {
PersistentPropertyAccessor<?> accessor = entity.getPropertyAccessor(source); PersistentPropertyAccessor<?> accessor = entity.getPropertyAccessor(source);
@ -448,10 +504,18 @@ public class MappingElasticsearchConverter
continue; continue;
} }
if (property.hasPropertyConverter()) {
ElasticsearchPersistentPropertyConverter propertyConverter = property.getPropertyConverter();
value = propertyConverter.write(value);
}
if (!isSimpleType(value)) { if (!isSimpleType(value)) {
writeProperty(property, value, sink); writeProperty(property, value, sink);
} else { } 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()); Optional<Class<?>> customWriteTarget = conversions.getCustomWriteTarget(value.getClass());
if (customWriteTarget.isPresent()) { if (customWriteTarget.isPresent()) {
Class<?> writeTarget = customWriteTarget.get(); Class<?> writeTarget = customWriteTarget.get();
sink.set(property, conversionService.convert(value, writeTarget)); sink.set(property, conversionService.convert(value, writeTarget));
return; return;
@ -484,12 +547,8 @@ public class MappingElasticsearchConverter
sink.set(property, getWriteComplexValue(property, typeHint, value)); sink.set(property, getWriteComplexValue(property, typeHint, value));
} }
@Nullable
protected Object getWriteSimpleValue(Object value) { protected Object getWriteSimpleValue(Object value) {
if (value == null) {
return null;
}
Optional<Class<?>> customTarget = conversions.getCustomWriteTarget(value.getClass()); Optional<Class<?>> customTarget = conversions.getCustomWriteTarget(value.getClass());
if (customTarget.isPresent()) { if (customTarget.isPresent()) {
@ -583,67 +642,30 @@ public class MappingElasticsearchConverter
} }
return target; return target;
} }
// endregion
@Override // region helper methods
@Nullable private Collection<Object> createCollectionForValue(TypeInformation<?> collectionTypeInformation, int size) {
public <T> T mapDocument(@Nullable Document document, Class<T> type) {
if (document == null) { Class<?> collectionType = collectionTypeInformation.isSubTypeOf(Collection.class) //
return null; ? collectionTypeInformation.getType() //
} : List.class;
Object mappedResult = read(type, document); TypeInformation<?> componentType = collectionTypeInformation.getComponentType() != null //
? collectionTypeInformation.getComponentType() //
: ClassTypeInformation.OBJECT;
if (mappedResult == null) { return collectionTypeInformation.getType().isArray() //
return (T) null; ? new ArrayList<>(size) //
} : CollectionFactory.createCollection(collectionType, componentType.getType(), size);
return type.isInterface() || !ClassUtils.isAssignableValue(type, mappedResult)
? getProjectionFactory().createProjection(type, mappedResult)
: type.cast(mappedResult);
} }
@Override private ElasticsearchPersistentEntity<?> computeGenericValueTypeForRead(ElasticsearchPersistentProperty property,
public <T> SearchHits<T> read(Class<T> type, SearchDocumentResponse searchDocumentResponse) { Object value) {
Assert.notNull(type, "type must not be null"); return ClassTypeInformation.OBJECT.equals(property.getTypeInformation().getActualType())
Assert.notNull(searchDocumentResponse, "searchDocumentResponse must not be null"); ? mappingContext.getRequiredPersistentEntity(value.getClass())
: mappingContext.getRequiredPersistentEntity(property.getTypeInformation().getActualType());
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;
} }
private boolean requiresTypeHint(TypeInformation<?> type, Class<?> actualType, private boolean requiresTypeHint(TypeInformation<?> type, Class<?> actualType,
@ -697,29 +719,6 @@ public class MappingElasticsearchConverter
return mappingContext.getRequiredPersistentEntity(typeToUse); 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) { private boolean isSimpleType(Object value) {
return isSimpleType(value.getClass()); return isSimpleType(value.getClass());
} }
@ -727,40 +726,40 @@ public class MappingElasticsearchConverter
private boolean isSimpleType(Class<?> type) { private boolean isSimpleType(Class<?> type) {
return conversions.isSimpleType(type); return conversions.isSimpleType(type);
} }
// endregion
// region queries
@Override @Override
public <T> AggregatedPage<SearchHit<T>> mapResults(SearchDocumentResponse response, Class<T> type, public void updateQuery(CriteriaQuery criteriaQuery, Class<?> domainClass) {
Pageable pageable) { ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(domainClass);
List<SearchHit<T>> results = response.getSearchDocuments().stream() // if (persistentEntity != null) {
.map(searchDocument -> read(type, searchDocument)) // criteriaQuery.getCriteria().getCriteriaChain().forEach(criteria -> {
.collect(Collectors.toList()); 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) { if (property.hasPropertyConverter()) {
Map<String, List<Object>> fields = searchDocument.getFields(); ElasticsearchPersistentPropertyConverter propertyConverter = property.getPropertyConverter();
if (!fields.isEmpty()) { criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> {
for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) { Object value = criteriaEntry.getValue();
ScriptedField scriptedField = field.getAnnotation(ScriptedField.class); if (value.getClass().isArray()) {
if (scriptedField != null) { Object[] objects = (Object[]) value;
String name = scriptedField.name().isEmpty() ? field.getName() : scriptedField.name(); for (int i = 0; i < objects.length; i++) {
Object value = searchDocument.getFieldValue(name); objects[i] = propertyConverter.write(objects[i]);
if (value != null) { }
field.setAccessible(true); } else {
try { criteriaEntry.setValue(propertyConverter.write(value));
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
static class MapValueAccessor { static class MapValueAccessor {
@ -770,6 +769,7 @@ public class MappingElasticsearchConverter
this.target = target; this.target = target;
} }
@Nullable
public Object get(ElasticsearchPersistentProperty property) { public Object get(ElasticsearchPersistentProperty property) {
if (target instanceof Document) { if (target instanceof Document) {
@ -801,7 +801,7 @@ public class MappingElasticsearchConverter
Map<String, Object> source = target; Map<String, Object> source = target;
Object result = null; Object result = null;
while (source != null && parts.hasNext()) { while (parts.hasNext()) {
result = source.get(parts.next()); result = source.get(parts.next());
@ -826,22 +826,25 @@ public class MappingElasticsearchConverter
target.put(property.getFieldName(), value); target.put(property.getFieldName(), value);
} }
@SuppressWarnings("unchecked")
private Map<String, Object> getAsMap(Object result) { private Map<String, Object> getAsMap(Object result) {
if (result instanceof Map) { 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)); throw new IllegalArgumentException(String.format("%s is not a Map.", result));
} }
} }
@RequiredArgsConstructor
class ElasticsearchPropertyValueProvider implements PropertyValueProvider<ElasticsearchPersistentProperty> { class ElasticsearchPropertyValueProvider implements PropertyValueProvider<ElasticsearchPersistentProperty> {
final MapValueAccessor mapValueAccessor; final MapValueAccessor mapValueAccessor;
ElasticsearchPropertyValueProvider(MapValueAccessor mapValueAccessor) {
this.mapValueAccessor = mapValueAccessor;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public <T> T getPropertyValue(ElasticsearchPersistentProperty property) { 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.core.convert.converter.Converter;
import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentProperty;
import org.springframework.lang.Nullable;
/** /**
* ElasticsearchPersistentProperty * ElasticsearchPersistentProperty
@ -60,7 +61,20 @@ public interface ElasticsearchPersistentProperty extends PersistentProperty<Elas
*/ */
boolean isParentProperty(); 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; INSTANCE;
@ -68,4 +82,17 @@ public interface ElasticsearchPersistentProperty extends PersistentProperty<Elas
return source.getFieldName(); 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; package org.springframework.data.elasticsearch.core.mapping;
import java.time.temporal.TemporalAccessor;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Field; 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.Parent;
import org.springframework.data.elasticsearch.annotations.Score; 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.Association;
import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentEntity;
@ -49,6 +53,7 @@ public class SimpleElasticsearchPersistentProperty extends
private final boolean isParent; private final boolean isParent;
private final boolean isId; private final boolean isId;
private final @Nullable String annotatedFieldName; private final @Nullable String annotatedFieldName;
private ElasticsearchPersistentPropertyConverter propertyConverter;
public SimpleElasticsearchPersistentProperty(Property property, public SimpleElasticsearchPersistentProperty(Property property,
PersistentEntity<?, ElasticsearchPersistentProperty> owner, SimpleTypeHolder simpleTypeHolder) { PersistentEntity<?, ElasticsearchPersistentProperty> owner, SimpleTypeHolder simpleTypeHolder) {
@ -72,6 +77,58 @@ public class SimpleElasticsearchPersistentProperty extends
if (isParent && !getType().equals(String.class)) { if (isParent && !getType().equals(String.class)) {
throw new MappingException(String.format("Parent property %s must be of type String!", property.getName())); 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 @Nullable

View File

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

View File

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

View File

@ -15,17 +15,26 @@
*/ */
package org.springframework.data.elasticsearch.core.query; package org.springframework.data.elasticsearch.core.query;
import org.springframework.util.Assert;
/** /**
* The most trivial implementation of a Field * The most trivial implementation of a Field
* *
* @author Rizwan Idrees * @author Rizwan Idrees
* @author Mohsin Husen * @author Mohsin Husen
* @author Peter-Josef Meisch
*/ */
public class SimpleField implements Field { public class SimpleField implements Field {
private final String name; private String name;
public SimpleField(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; 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.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHitSupport; import org.springframework.data.elasticsearch.core.SearchHitSupport;
import org.springframework.data.elasticsearch.core.SearchHits; 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.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery; 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 static final int DEFAULT_STREAM_BATCH_SIZE = 500;
private final PartTree tree; private final PartTree tree;
private final ElasticsearchConverter elasticsearchConverter;
private final MappingContext<?, ElasticsearchPersistentProperty> mappingContext; private final MappingContext<?, ElasticsearchPersistentProperty> mappingContext;
public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations) { public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations) {
super(method, elasticsearchOperations); super(method, elasticsearchOperations);
this.tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType()); this.tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType());
this.mappingContext = elasticsearchOperations.getElasticsearchConverter().getMappingContext(); this.elasticsearchConverter = elasticsearchOperations.getElasticsearchConverter();
this.mappingContext = elasticsearchConverter.getMappingContext();
} }
@Override @Override
@ -58,7 +61,10 @@ public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery
ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters);
CriteriaQuery query = createQuery(accessor); CriteriaQuery query = createQuery(accessor);
Assert.notNull(query, "unsupported query"); Assert.notNull(query, "unsupported query");
Class<?> clazz = queryMethod.getEntityInformation().getJavaType(); Class<?> clazz = queryMethod.getEntityInformation().getJavaType();
elasticsearchConverter.updateQuery(query, clazz);
IndexCoordinates index = elasticsearchOperations.getIndexCoordinatesFor(clazz); IndexCoordinates index = elasticsearchOperations.getIndexCoordinatesFor(clazz);
Object result = null; Object result = null;

View File

@ -65,7 +65,8 @@ public class ElasticsearchQueryCreator extends AbstractQueryCreator<CriteriaQuer
PersistentPropertyPath<ElasticsearchPersistentProperty> path = context PersistentPropertyPath<ElasticsearchPersistentProperty> path = context
.getPersistentPropertyPath(part.getProperty()); .getPersistentPropertyPath(part.getProperty());
return new CriteriaQuery(from(part, return new CriteriaQuery(from(part,
new Criteria(path.toDotPath(ElasticsearchPersistentProperty.PropertyToFieldNameConverter.INSTANCE)), iterator)); new Criteria(path.toDotPath(ElasticsearchPersistentProperty.QueryPropertyToFieldNameConverter.INSTANCE)),
iterator));
} }
@Override @Override
@ -76,7 +77,8 @@ public class ElasticsearchQueryCreator extends AbstractQueryCreator<CriteriaQuer
PersistentPropertyPath<ElasticsearchPersistentProperty> path = context PersistentPropertyPath<ElasticsearchPersistentProperty> path = context
.getPersistentPropertyPath(part.getProperty()); .getPersistentPropertyPath(part.getProperty());
return base.addCriteria(from(part, return base.addCriteria(from(part,
new Criteria(path.toDotPath(ElasticsearchPersistentProperty.PropertyToFieldNameConverter.INSTANCE)), iterator)); new Criteria(path.toDotPath(ElasticsearchPersistentProperty.QueryPropertyToFieldNameConverter.INSTANCE)),
iterator));
} }
@Override @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; package org.springframework.data.elasticsearch.core.convert;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
@ -26,16 +27,17 @@ import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import org.json.JSONException;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.convert.ConversionService; 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.annotation.TypeAlias;
import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter; 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.annotations.GeoPointField;
import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.geo.GeoPoint; import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; 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.Box;
import org.springframework.data.geo.Circle; import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Point; import org.springframework.data.geo.Point;
@ -294,7 +301,7 @@ public class MappingElasticsearchConverterUnitTests {
public void writesNestedEntity() { public void writesNestedEntity() {
Person person = new Person(); Person person = new Person();
person.birthdate = new Date(); person.birthDate = LocalDate.now();
person.gender = Gender.MAN; person.gender = Gender.MAN;
person.address = observatoryRoad; person.address = observatoryRoad;
@ -574,6 +581,45 @@ public class MappingElasticsearchConverterUnitTests {
assertThat(target.address).isEqualTo(bigBunsCafe); 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) { private String pointTemplate(String name, Point point) {
return String.format(Locale.ENGLISH, "\"%s\":{\"lat\":%.1f,\"lon\":%.1f}", name, point.getX(), point.getY()); 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; @Id String id;
String name; 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; Gender gender;
Address address; Address address;
@ -759,5 +808,4 @@ public class MappingElasticsearchConverterUnitTests {
@GeoPointField private double[] pointD; @GeoPointField private double[] pointD;
} }
} }

View File

@ -33,6 +33,7 @@ import java.lang.Boolean;
import java.lang.Double; import java.lang.Double;
import java.lang.Integer; import java.lang.Integer;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
@ -957,7 +958,7 @@ public class MappingBuilderTests extends MappingContextBaseTests {
@Field(copyTo = { "foo", "bar" }) private String copyTo; @Field(copyTo = { "foo", "bar" }) private String copyTo;
@Field(ignoreAbove = 42) private String ignoreAbove; @Field(ignoreAbove = 42) private String ignoreAbove;
@Field(type = FieldType.Integer) private String type; @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(analyzer = "ana", searchAnalyzer = "sana", normalizer = "norma") private String analyzers;
@Field(type = Keyword, docValues = true) private String docValuesTrue; @Field(type = Keyword, docValues = true) private String docValuesTrue;
@Field(type = Keyword, docValues = false) private String docValuesFalse; @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 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.junit.jupiter.api.Test;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Field; 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.elasticsearch.annotations.Score;
import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.MappingException;
@ -62,6 +68,47 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
assertThat(persistentProperty.getFieldName()).isEqualTo("by-value"); 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 { static class InvalidScoreProperty {
@Score String scoreProperty; @Score String scoreProperty;
} }
@ -73,4 +120,10 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
static class FieldValueProperty { static class FieldValueProperty {
@Field(value = "by-value") String fieldProperty; @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;
}
} }