mirror of
https://github.com/spring-projects/spring-data-elasticsearch.git
synced 2025-06-22 12:02:10 +00:00
Dataes 716 - Add value mapping to the ElasticsearchMappingConverter.
Original PR: #366
This commit is contained in:
parent
d2b7df87f4
commit
a68c6ba5d7
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 = typeHint.getType().equals(entityType)
|
||||
? mappingContext.getRequiredPersistentEntity(typeHint)
|
||||
ElasticsearchPersistentEntity<?> entity = type.getType().equals(entityType)
|
||||
? mappingContext.getRequiredPersistentEntity(type)
|
||||
: 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;
|
||||
|
||||
TypeInformation<?> componentType = collectionTypeInformation.getComponentType() != null //
|
||||
? collectionTypeInformation.getComponentType() //
|
||||
: ClassTypeInformation.OBJECT;
|
||||
|
||||
return collectionTypeInformation.getType().isArray() //
|
||||
? new ArrayList<>(size) //
|
||||
: CollectionFactory.createCollection(collectionType, componentType.getType(), size);
|
||||
}
|
||||
|
||||
Object mappedResult = read(type, document);
|
||||
private ElasticsearchPersistentEntity<?> computeGenericValueTypeForRead(ElasticsearchPersistentProperty property,
|
||||
Object value) {
|
||||
|
||||
if (mappedResult == null) {
|
||||
return (T) null;
|
||||
}
|
||||
|
||||
return type.isInterface() || !ClassUtils.isAssignableValue(type, mappedResult)
|
||||
? getProjectionFactory().createProjection(type, mappedResult)
|
||||
: type.cast(mappedResult);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user