DATAES-530 - Polishing.

Fix generics in ElasticsearchEntityMapper. Extract methods. Move enum conversion handling to simple type handling for symmetric implementation. Swap comparison of nullable types to avoid potential null dereference.

Rename ElasticsearchDefaultTypeMapper to DefaultElasticsearchTypeMapper. Move MapTypeAliasAccessor to DefaultElasticsearchTypeMapper.

Introduce SearchResultMapperAdapter to avoid empty method implementations in anonymous classes. Javadoc, reference docs, formatting.

Original Pull Request: #237
This commit is contained in:
Mark Paluch 2019-02-07 13:34:31 +01:00 committed by Christoph Strobl
parent a64af54e26
commit c3e7056d7b
16 changed files with 373 additions and 302 deletions

View File

@ -66,13 +66,26 @@ for `Converter` registration.
<3> Optionally set `CustomConversions` if applicable.
====
[[elasticsearch.mapping.meta-model.annotations]]
=== Mapping Annotation Overview
The `ElasticsearchEntityMapper` can use metadata to drive the mapping of objects to documents. The following annotations are available:
* `@Id`: Applied at the field level to mark the field used for identity purpose.
* `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database. You can specify the index name and index type where the document will be stored.
* `@Transient`: By default all private fields are mapped to the document, this annotation excludes the field where it is applied from being stored in the database
* `@PersistenceConstructor`: Marks a given constructor - even a package protected one - to use when instantiating the object from the database. Constructor arguments are mapped by name to the key values in the retrieved Document.
* `@Field`: Applied at the field level and described the name of the field as it will be represented in the Elasticsearch document thus allowing the name to be different than the fieldname of the class.
The mapping metadata infrastructure is defined in a separate spring-data-commons project that is technology agnostic.
[[elasticsearch.mapping.meta-model.rules]]
=== Mapping Rules
==== Type Hints
Mapping uses _type hints_ embedded in the document sent to the server to allow generic type mapping. Those type hints are
represented as `_class` attributes within the document on the server and will be written for each aggregate root.
Mapping uses _type hints_ embedded in the document sent to the server to allow generic type mapping.
Those type hints are represented as `_class` attributes within the document and are written for each aggregate root.
.Type Hints
====
@ -123,8 +136,7 @@ public class Person {
<1> The configured alias is used when writing the entity.
====
NOTE: Type hints will not be written for nested Objects unless the properties type is `Object`, an interface or the
actual value type does not match the properties declaration.
NOTE: Type hints will not be written for nested Objects unless the properties type is `Object`, an interface or the actual value type does not match the properties declaration.
==== Geospatial Types
@ -152,8 +164,7 @@ public class Address {
==== Collections
For values inside Collections apply the same mapping rules as for aggregate roots when it comes to _type hints_
and <<elasticsearch.mapping.meta-model.conversions>>.
For values inside Collections apply the same mapping rules as for aggregate roots when it comes to _type hints_ and <<elasticsearch.mapping.meta-model.conversions>>.
.Collections
====
@ -179,8 +190,7 @@ public class Person {
==== Maps
For values inside Maps apply the same mapping rules as for aggregate roots when it comes to _type hints_
and <<elasticsearch.mapping.meta-model.conversions>>.
For values inside Maps apply the same mapping rules as for aggregate roots when it comes to _type hints_ and <<elasticsearch.mapping.meta-model.conversions>>.
However the Map key needs to a String to be processed by Elasticsearch.
.Collections
@ -214,8 +224,7 @@ public class Person {
[[elasticsearch.mapping.meta-model.conversions]]
=== Custom Conversions
Looking at the `Configuration` from the <<elasticsearch.mapping.meta-model, previous section>> `ElasticsearchCustomConversions`
allows to register specific rules for mapping domain and simple types.
Looking at the `Configuration` from the <<elasticsearch.mapping.meta-model, previous section>> `ElasticsearchCustomConversions` allows registering specific rules for mapping domain and simple types.
.Meta Model Object Mapping Configuration
====

View File

@ -72,15 +72,14 @@ public class ElasticsearchConfigurationSupport {
}
/**
* Returns the {@link EntityMapper} used for mapping source &lt;&gt; DomainType. <br />
* Returns the {@link EntityMapper} used for mapping between the source and domain type. <br />
* <strong>Hint</strong>: you can use {@link org.springframework.data.elasticsearch.core.ElasticsearchEntityMapper} as
* an alternative to the {@link DefaultEntityMapper}.
*
* <pre>{@code
* <pre class="code">
* ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(elasticsearchMappingContext(),
* new DefaultConversionService());
* new DefaultConversionService());
* entityMapper.setConversions(elasticsearchCustomConversions());
* }
* </pre>
*
* @return never {@literal null}.

View File

@ -28,14 +28,20 @@ public abstract class AbstractResultMapper implements ResultsMapper {
private final EntityMapper entityMapper;
private final ProjectionFactory projectionFactory;
/**
* Create a new {@link AbstractResultMapper}.
*
* @param entityMapper must not be {@literal null}.
*/
public AbstractResultMapper(EntityMapper entityMapper) {
this(entityMapper, new SpelAwareProxyProjectionFactory());
}
/**
* Create a new {@link AbstractResultMapper}.
*
* @param entityMapper
* @param projectionFactory
* @param entityMapper must not be {@literal null}.
* @param projectionFactory must not be {@literal null}.
* @since 3.2
*/
public AbstractResultMapper(EntityMapper entityMapper, ProjectionFactory projectionFactory) {
@ -47,6 +53,10 @@ public abstract class AbstractResultMapper implements ResultsMapper {
this.projectionFactory = projectionFactory;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.ResultsMapper#getEntityMapper()
*/
@Override
public EntityMapper getEntityMapper() {
return this.entityMapper;
@ -58,6 +68,6 @@ public abstract class AbstractResultMapper implements ResultsMapper {
*/
@Override
public ProjectionFactory getProjectionFactory() {
return projectionFactory;
return this.projectionFactory;
}
}

View File

@ -88,12 +88,11 @@ public class DefaultResultMapper extends AbstractResultMapper {
this.mappingContext = mappingContext;
}
static EntityMapper initEntityMapper(
private static EntityMapper initEntityMapper(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
Assert.notNull(mappingContext, "MappingContext must not be null!");
return new DefaultEntityMapper(mappingContext);
}
@Override

View File

@ -15,6 +15,8 @@
*/
package org.springframework.data.elasticsearch.core;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
@ -34,7 +36,6 @@ import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomCo
import org.springframework.data.elasticsearch.core.convert.ElasticsearchTypeMapper;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
@ -76,7 +77,49 @@ public class ElasticsearchEntityMapper implements
this.mappingContext = mappingContext;
this.conversionService = conversionService != null ? conversionService : new DefaultConversionService();
this.typeMapper = ElasticsearchTypeMapper.defaultTypeMapper(mappingContext);
this.typeMapper = ElasticsearchTypeMapper.create(mappingContext);
}
// --> GETTERS / SETTERS
@Override
public MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> getMappingContext() {
return mappingContext;
}
@Override
public ConversionService getConversionService() {
return conversionService;
}
/**
* Set the {@link CustomConversions} to be applied during the mapping process. <br />
* Conversions are registered after {@link #afterPropertiesSet() bean initialization}.
*
* @param conversions must not be {@literal null}.
*/
public void setConversions(CustomConversions 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)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
DateFormatterRegistrar.addDateConverters(conversionService);
conversions.registerConvertersIn(conversionService);
}
// --> READ
@ -86,12 +129,14 @@ public class ElasticsearchEntityMapper implements
return read(targetType, source);
}
@SuppressWarnings("unchecked")
@Override
@Nullable
public <R> R read(Class<R> type, Map<String, Object> source) {
return doRead(source, ClassTypeInformation.from((Class<R>) ClassUtils.getUserClass(type)));
}
@SuppressWarnings("unchecked")
@Nullable
protected <R> R doRead(Map<String, Object> source, TypeInformation<R> typeHint) {
@ -113,6 +158,7 @@ public class ElasticsearchEntityMapper implements
return readEntity(entity, source);
}
@SuppressWarnings("unchecked")
protected <R> R readEntity(ElasticsearchPersistentEntity<?> entity, Map<String, Object> source) {
ElasticsearchPersistentEntity<?> targetEntity = computeClosestEntity(entity, source);
@ -125,11 +171,8 @@ public class ElasticsearchEntityMapper implements
R instance = (R) instantiator.createInstance(targetEntity,
new PersistentEntityParameterValueProvider<>(targetEntity, propertyValueProvider, null));
if (targetEntity.requiresPropertyPopulation()) {
return readProperties(targetEntity, instance, propertyValueProvider);
}
return instance;
return targetEntity.requiresPropertyPopulation() ? readProperties(targetEntity, instance, propertyValueProvider)
: instance;
}
protected <R> R readProperties(ElasticsearchPersistentEntity<?> entity, R instance,
@ -153,6 +196,7 @@ public class ElasticsearchEntityMapper implements
return accessor.getBean();
}
@SuppressWarnings("unchecked")
protected <R> R readValue(@Nullable Object source, ElasticsearchPersistentProperty property,
TypeInformation<R> targetType) {
@ -160,20 +204,19 @@ public class ElasticsearchEntityMapper implements
return null;
}
Class<?> rawType = targetType.getType();
Class<R> rawType = targetType.getType();
if (conversions.hasCustomReadTarget(source.getClass(), rawType)) {
return (R) conversionService.convert(source, rawType);
return rawType.cast(conversionService.convert(source, rawType));
} else if (source instanceof List) {
return readCollectionValue((List) source, property, targetType);
} else if (source instanceof Map) {
return readMapValue((Map<String, Object>) source, property, targetType);
} else if (Enum.class.isAssignableFrom(rawType)) {
return (R) Enum.valueOf((Class<Enum>) rawType, source.toString());
}
return (R) readSimpleValue(source, targetType);
}
@SuppressWarnings("unchecked")
private <R> R readMapValue(@Nullable Map<String, Object> source, ElasticsearchPersistentProperty property,
TypeInformation<R> targetType) {
@ -186,10 +229,10 @@ public class ElasticsearchEntityMapper implements
return readEntity(targetEntity, source);
}
Map<String, Object> target = new LinkedHashMap();
Map<String, Object> target = new LinkedHashMap<>();
for (Entry<String, Object> entry : source.entrySet()) {
if (conversions.isSimpleType(entry.getValue().getClass())) {
if (isSimpleType(entry.getValue())) {
target.put(entry.getKey(),
readSimpleValue(entry.getValue(), targetType.isMap() ? targetType.getComponentType() : targetType));
} else {
@ -211,13 +254,13 @@ public class ElasticsearchEntityMapper implements
} else {
target.put(entry.getKey(), readEntity(targetEntity, (Map) entry.getValue()));
}
}
}
return (R) target;
}
@SuppressWarnings("unchecked")
private <R> R readCollectionValue(@Nullable List<?> source, ElasticsearchPersistentProperty property,
TypeInformation<R> targetType) {
@ -225,12 +268,11 @@ public class ElasticsearchEntityMapper implements
return null;
}
List<?> sourceList = source;
Collection target = createCollectionForValue(targetType, sourceList.size());
Collection<Object> target = createCollectionForValue(targetType, source.size());
for (Object value : sourceList) {
for (Object value : source) {
if (conversions.isSimpleType(value.getClass())) {
if (isSimpleType(value)) {
target.add(
readSimpleValue(value, targetType.getComponentType() != null ? targetType.getComponentType() : targetType));
} else {
@ -246,23 +288,24 @@ public class ElasticsearchEntityMapper implements
return (R) target;
}
protected Object readSimpleValue(@Nullable Object value, TypeInformation<?> targetType) {
@SuppressWarnings("unchecked")
private Object readSimpleValue(@Nullable Object value, TypeInformation<?> targetType) {
if (value == null) {
return null;
}
Class<?> target = targetType.getType();
if (ClassTypeInformation.OBJECT.equals(targetType)
|| (targetType != null && value.getClass().equals(targetType.getActualType()))) {
if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) {
return value;
}
if (conversionService.canConvert(value.getClass(), targetType.getType())) {
return conversionService.convert(value, targetType.getType());
if (conversions.hasCustomReadTarget(value.getClass(), target)) {
return conversionService.convert(value, target);
}
throw new MappingException(
String.format("Unable to map %s of type %s to %s", value, value.getClass(), targetType.getType()));
if (Enum.class.isAssignableFrom(target)) {
return Enum.valueOf((Class<Enum>) target, value.toString());
}
return conversionService.convert(value, target);
}
// --> WRITE
@ -275,6 +318,7 @@ public class ElasticsearchEntityMapper implements
return target;
}
@SuppressWarnings("unchecked")
@Override
public void write(@Nullable Object source, Map<String, Object> sink) {
@ -284,12 +328,12 @@ public class ElasticsearchEntityMapper implements
if (source instanceof Map) {
sink.putAll((Map) source);
sink.putAll((Map<String, Object>) source);
return;
}
Class<?> entityType = ClassUtils.getUserClass(source.getClass());
TypeInformation<? extends Object> type = ClassTypeInformation.from(entityType);
TypeInformation<?> type = ClassTypeInformation.from(entityType);
if (requiresTypeHint(type, source.getClass(), null)) {
typeMapper.writeType(source.getClass(), sink);
@ -337,6 +381,7 @@ public class ElasticsearchEntityMapper implements
if (requiresTypeHint(entity.getTypeInformation(), source.getClass(), containingStructure)) {
typeMapper.writeType(source.getClass(), sink);
}
writeProperties(entity, accessor, sink);
}
@ -355,7 +400,7 @@ public class ElasticsearchEntityMapper implements
continue;
}
if (!conversions.isSimpleType(value.getClass())) {
if (!isSimpleType(value)) {
writeProperty(property, value, sink);
} else {
sink.put(property.getFieldName(), getWriteSimpleValue(value));
@ -393,15 +438,20 @@ public class ElasticsearchEntityMapper implements
protected Object getWriteSimpleValue(Object value) {
Optional<Class<?>> customWriteTarget = conversions.getCustomWriteTarget(value.getClass());
if (!customWriteTarget.isPresent()) {
return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value;
if (value == null) {
return null;
}
return conversionService.convert(value, customWriteTarget.get());
Optional<Class<?>> customTarget = conversions.getCustomWriteTarget(value.getClass());
if (customTarget.isPresent()) {
return conversionService.convert(value, customTarget.get());
}
return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value;
}
@SuppressWarnings("unchecked")
protected Object getWriteComplexValue(ElasticsearchPersistentProperty property, TypeInformation<?> typeHint,
Object value) {
@ -412,7 +462,7 @@ public class ElasticsearchEntityMapper implements
return writeMapValue((Map<String, Object>) value, property, typeHint);
}
if (property.isEntity() || !conversions.isSimpleType(value.getClass())) {
if (property.isEntity() || !isSimpleType(value)) {
return writeEntity(value, property, typeHint);
}
@ -433,7 +483,7 @@ public class ElasticsearchEntityMapper implements
Streamable<Entry<String, Object>> mapSource = Streamable.of(value.entrySet());
if (!typeHint.getActualType().getType().equals(Object.class)
&& conversions.isSimpleType(typeHint.getMapValueType().getType())) {
&& isSimpleType(typeHint.getMapValueType().getType())) {
mapSource.forEach(it -> target.put(it.getKey(), getWriteSimpleValue(it.getValue())));
} else {
@ -442,7 +492,7 @@ public class ElasticsearchEntityMapper implements
Object converted = null;
if (it.getValue() != null) {
if (conversions.isSimpleType(it.getValue().getClass())) {
if (isSimpleType(it.getValue())) {
converted = getWriteSimpleValue(it.getValue());
} else {
converted = getWriteComplexValue(property, ClassTypeInformation.from(it.getValue().getClass()),
@ -451,7 +501,6 @@ public class ElasticsearchEntityMapper implements
}
target.put(it.getKey(), converted);
});
}
@ -461,23 +510,21 @@ public class ElasticsearchEntityMapper implements
private Object writeCollectionValue(Object value, ElasticsearchPersistentProperty property,
TypeInformation<?> typeHint) {
Streamable collectionSource = value instanceof Iterable ? Streamable.of((Iterable) value)
Streamable<?> collectionSource = value instanceof Iterable ? Streamable.of((Iterable<?>) value)
: Streamable.of(ObjectUtils.toObjectArray(value));
List<Object> target = new ArrayList<>();
if (!typeHint.getActualType().getType().equals(Object.class)
&& conversions.isSimpleType(typeHint.getActualType().getType())) {
if (!typeHint.getActualType().getType().equals(Object.class) && isSimpleType(typeHint.getActualType().getType())) {
collectionSource.map(this::getWriteSimpleValue).forEach(target::add);
} else {
Streamable.of((Iterable) value).map(it -> {
collectionSource.map(it -> {
if (it == null) {
return null;
}
if (conversions.isSimpleType(it.getClass())) {
if (isSimpleType(it)) {
return getWriteSimpleValue(it);
}
@ -488,48 +535,6 @@ public class ElasticsearchEntityMapper implements
return target;
}
// --> GETTERS / SETTERS
@Override
public MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> getMappingContext() {
return mappingContext;
}
@Override
public ConversionService getConversionService() {
return conversionService;
}
/**
* Set the {@link CustomConversions} to be applied during the mapping process. <br />
* Conversions are registered after {@link #afterPropertiesSet() bean initialization}.
*
* @param conversions must not be {@literal null}.
*/
public void setConversions(CustomConversions 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)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
DateFormatterRegistrar.addDateConverters(conversionService);
conversions.registerConvertersIn(conversionService);
}
// --> LEGACY
@Override
@ -549,21 +554,23 @@ public class ElasticsearchEntityMapper implements
// --> PRIVATE HELPERS
private boolean requiresTypeHint(TypeInformation<?> type, Class<?> actualType,
@Nullable TypeInformation<?> containingStructure) {
@Nullable TypeInformation<?> container) {
if (containingStructure != null) {
if (container != null) {
if (containingStructure.isCollectionLike()) {
if (containingStructure.getActualType().equals(type) && type.getType().equals(actualType)) {
if (container.isCollectionLike()) {
if (type.equals(container.getActualType()) && type.getType().equals(actualType)) {
return false;
}
}
if (containingStructure.isMap()) {
if (containingStructure.getMapValueType().equals(type) && type.getType().equals(actualType)) {
if (container.isMap()) {
if (type.equals(container.getMapValueType()) && type.getType().equals(actualType)) {
return false;
}
}
if (containingStructure.equals(type) && type.getType().equals(actualType)) {
if (container.equals(type) && type.getType().equals(actualType)) {
return false;
}
}
@ -574,7 +581,7 @@ public class ElasticsearchEntityMapper implements
/**
* Compute the type to use by checking the given entity against the store type;
*
*
* @param entity
* @param source
* @return
@ -600,12 +607,12 @@ public class ElasticsearchEntityMapper implements
private ElasticsearchPersistentEntity<?> computeGenericValueTypeForRead(ElasticsearchPersistentProperty property,
Object value) {
return property.getTypeInformation().getActualType().equals(ClassTypeInformation.OBJECT)
return ClassTypeInformation.OBJECT.equals(property.getTypeInformation().getActualType())
? mappingContext.getRequiredPersistentEntity(value.getClass())
: mappingContext.getRequiredPersistentEntity(property.getTypeInformation().getActualType());
}
private Collection<?> createCollectionForValue(TypeInformation<?> collectionTypeInformation, int size) {
private Collection<Object> createCollectionForValue(TypeInformation<?> collectionTypeInformation, int size) {
Class<?> collectionType = collectionTypeInformation.isSubTypeOf(Collection.class) //
? collectionTypeInformation.getType() //
@ -618,19 +625,24 @@ public class ElasticsearchEntityMapper implements
return collectionTypeInformation.getType().isArray() //
? new ArrayList<>(size) //
: CollectionFactory.createCollection(collectionType, componentType.getType(), size);
}
private boolean isSimpleType(Object value) {
return isSimpleType(value.getClass());
}
private boolean isSimpleType(Class<?> type) {
return conversions.isSimpleType(type);
}
// --> OHTER STUFF
@RequiredArgsConstructor
class ElasticsearchPropertyValueProvider implements PropertyValueProvider<ElasticsearchPersistentProperty> {
final MapValueAccessor mapValueAccessor;
public ElasticsearchPropertyValueProvider(MapValueAccessor mapValueAccessor) {
this.mapValueAccessor = mapValueAccessor;
}
@SuppressWarnings("unchecked")
@Override
public <T> T getPropertyValue(ElasticsearchPersistentProperty property) {
return (T) readValue(mapValueAccessor.get(property), property, property.getTypeInformation());
@ -670,13 +682,14 @@ public class ElasticsearchEntityMapper implements
return result;
}
@SuppressWarnings("unchecked")
private Map<String, Object> getAsMap(Object result) {
if (result instanceof Map) {
return (Map) result;
}
throw new IllegalArgumentException(String.format("%s is no Map.", result));
throw new IllegalArgumentException(String.format("%s is not a Map.", result));
}
}

View File

@ -24,16 +24,7 @@ import static org.springframework.util.StringUtils.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.action.ActionFuture;
@ -877,7 +868,7 @@ public class ElasticsearchRestTemplate
SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(deleteQuery.getQuery()).withIndices(indexName)
.withTypes(typeName).withPageable(PageRequest.of(0, pageSize)).build();
SearchResultMapper onlyIdResultMapper = new SearchResultMapper() {
SearchResultMapper onlyIdResultMapper = new SearchResultMapperAdapter() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<String> result = new ArrayList<String>();
@ -886,14 +877,9 @@ public class ElasticsearchRestTemplate
result.add(id);
}
if (result.size() > 0) {
return new AggregatedPageImpl<T>((List<T>) result, response.getScrollId());
return new AggregatedPageImpl<>((List<T>) result, response.getScrollId());
}
return new AggregatedPageImpl<T>(Collections.EMPTY_LIST, response.getScrollId());
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
return new AggregatedPageImpl<>(Collections.emptyList(), response.getScrollId());
}
};
@ -1458,7 +1444,7 @@ public class ElasticsearchRestTemplate
/**
* It takes two steps to create a List<AliasMetadata> from the elasticsearch http response because the aliases field
* is actually a Map by alias name, but the alias name is on the AliasMetadata.
*
*
* @param aliasResponse
* @return
*/

View File

@ -755,7 +755,7 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(deleteQuery.getQuery()).withIndices(indexName)
.withTypes(typeName).withPageable(PageRequest.of(0, pageSize)).build();
SearchResultMapper onlyIdResultMapper = new SearchResultMapper() {
SearchResultMapper onlyIdResultMapper = new SearchResultMapperAdapter() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<String> result = new ArrayList<String>();
@ -766,12 +766,7 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
if (result.size() > 0) {
return new AggregatedPageImpl<T>((List<T>) result, response.getScrollId());
}
return new AggregatedPageImpl<T>(Collections.EMPTY_LIST, response.getScrollId());
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
return new AggregatedPageImpl<T>(Collections.emptyList(), response.getScrollId());
}
};

View File

@ -86,14 +86,14 @@ public interface ResultsMapper extends SearchResultMapper, GetResultMapper, Mult
Object mappedResult = getEntityMapper().readObject(source, type);
if (mappedResult == null) {
return (T) mappedResult;
return (T) null;
}
if (type.isInterface() || !ClassUtils.isAssignableValue(type, mappedResult)) {
return getProjectionFactory().createProjection(type, mappedResult);
}
return (T) mappedResult;
return type.cast(mappedResult);
}
/**
@ -120,13 +120,13 @@ public interface ResultsMapper extends SearchResultMapper, GetResultMapper, Mult
Object mappedResult = getEntityMapper().readObject(source, type);
if (mappedResult == null) {
return (T) mappedResult;
return null;
}
if (type.isInterface()) {
return getProjectionFactory().createProjection(type, mappedResult);
}
return (T) mappedResult;
return type.cast(mappedResult);
}
}

View File

@ -0,0 +1,49 @@
/*
* 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
*
* http://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 org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.search.SearchHit;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
/**
* Adapter utility for {@link SearchResultMapper} that wish to implement a subset of mapping methods. Default
* implementations throw {@link UnsupportedOperationException}.
*
* @author Mark Paluch
* @since 3.2
*/
abstract class SearchResultMapperAdapter implements SearchResultMapper {
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.SearchResultMapper#mapResults(org.elasticsearch.action.search.SearchResponse, java.lang.Class, org.springframework.data.domain.Pageable)
*/
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
throw new UnsupportedOperationException();
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.SearchResultMapper#mapSearchHit(org.elasticsearch.search.SearchHit, java.lang.Class)
*/
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
throw new UnsupportedOperationException();
}
}

View File

@ -30,7 +30,6 @@ import org.springframework.core.convert.converter.Converter;
* @author Rizwan Idrees
* @author Mohsin Husen
*/
public final class DateTimeConverters {
private static DateTimeFormatter formatter = ISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC);

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
*
* http://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.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.data.convert.DefaultTypeMapper;
import org.springframework.data.convert.SimpleTypeInformationMapper;
import org.springframework.data.convert.TypeAliasAccessor;
import org.springframework.data.convert.TypeInformationMapper;
import org.springframework.data.mapping.Alias;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.lang.Nullable;
/**
* Elasticsearch specific {@link org.springframework.data.convert.TypeMapper} implementation.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 3.2
*/
public class DefaultElasticsearchTypeMapper extends DefaultTypeMapper<Map<String, Object>>
implements ElasticsearchTypeMapper {
private final @Nullable String typeKey;
public DefaultElasticsearchTypeMapper(@Nullable String typeKey) {
this(typeKey, Collections.singletonList(new SimpleTypeInformationMapper()));
}
public DefaultElasticsearchTypeMapper(@Nullable String typeKey,
MappingContext<? extends PersistentEntity<?, ?>, ?> mappingContext) {
this(typeKey, new MapTypeAliasAccessor(typeKey), mappingContext,
Collections.singletonList(new SimpleTypeInformationMapper()));
}
public DefaultElasticsearchTypeMapper(@Nullable String typeKey, List<? extends TypeInformationMapper> mappers) {
this(typeKey, new MapTypeAliasAccessor(typeKey), null, mappers);
}
public DefaultElasticsearchTypeMapper(@Nullable String typeKey, TypeAliasAccessor<Map<String, Object>> accessor,
@Nullable MappingContext<? extends PersistentEntity<?, ?>, ?> mappingContext,
List<? extends TypeInformationMapper> mappers) {
super(accessor, mappingContext, mappers);
this.typeKey = typeKey;
}
@Override
public boolean isTypeKey(String key) {
return typeKey != null && typeKey.equals(key);
}
/**
* {@link TypeAliasAccessor} to store aliases in a {@link Map}.
*
* @author Christoph Strobl
*/
public static class MapTypeAliasAccessor implements TypeAliasAccessor<Map<String, Object>> {
private final @Nullable String typeKey;
public MapTypeAliasAccessor(@Nullable String typeKey) {
this.typeKey = typeKey;
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.TypeAliasAccessor#readAliasFrom(java.lang.Object)
*/
public Alias readAliasFrom(Map<String, Object> source) {
return Alias.ofNullable(source.get(typeKey));
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.TypeAliasAccessor#writeTypeTo(java.lang.Object, java.lang.Object)
*/
public void writeTypeTo(Map<String, Object> sink, Object alias) {
if (typeKey == null) {
return;
}
sink.put(typeKey, alias);
}
}
}

View File

@ -43,6 +43,7 @@ public class ElasticsearchCustomConversions extends CustomConversions {
static {
List<Object> converters = new ArrayList<>();
converters.addAll(GeoConverters.getConvertersToRegister());
converters.add(StringToUUIDConverter.INSTANCE);
converters.add(UUIDToStringConverter.INSTANCE);
@ -62,6 +63,9 @@ public class ElasticsearchCustomConversions extends CustomConversions {
super(STORE_CONVERSIONS, converters);
}
/**
* {@link Converter} to read a {@link UUID} from its {@link String} representation.
*/
@ReadingConverter
enum StringToUUIDConverter implements Converter<String, UUID> {
@ -73,6 +77,9 @@ public class ElasticsearchCustomConversions extends CustomConversions {
}
}
/**
* {@link Converter} to write a {@link UUID} to its {@link String} representation.
*/
@WritingConverter
enum UUIDToStringConverter implements Converter<UUID, String> {
@ -84,6 +91,9 @@ public class ElasticsearchCustomConversions extends CustomConversions {
}
}
/**
* {@link Converter} to read a {@link BigDecimal} from a {@link Double} value.
*/
@ReadingConverter
enum DoubleToBigDecimalConverter implements Converter<Double, BigDecimal> {
@ -95,6 +105,9 @@ public class ElasticsearchCustomConversions extends CustomConversions {
}
}
/**
* {@link Converter} to write a {@link BigDecimal} to a {@link Double} value.
*/
@WritingConverter
enum BigDecimalToDoubleConverter implements Converter<BigDecimal, Double> {

View File

@ -1,47 +0,0 @@
/*
* 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
*
* http://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.util.List;
import java.util.Map;
import org.springframework.data.convert.DefaultTypeMapper;
import org.springframework.data.convert.TypeAliasAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.lang.Nullable;
/**
* Elasticsearch specific {@link org.springframework.data.convert.TypeMapper} implementation.
*
* @author Christoph Strobl
* @since 3.2
*/
class ElasticsearchDefaultTypeMapper extends DefaultTypeMapper<Map<String, Object>> implements ElasticsearchTypeMapper {
private final @Nullable String typeKey;
ElasticsearchDefaultTypeMapper(@Nullable String typeKey, TypeAliasAccessor accessor,
@Nullable MappingContext mappingContext, List additionalMappers) {
super(accessor, mappingContext, additionalMappers);
this.typeKey = typeKey;
}
@Override
public boolean isTypeKey(String key) {
return typeKey != null && typeKey.equals(key);
}
}

View File

@ -15,17 +15,12 @@
*/
package org.springframework.data.elasticsearch.core.convert;
import java.util.Collections;
import java.util.Map;
import org.springframework.data.convert.SimpleTypeInformationMapper;
import org.springframework.data.convert.TypeAliasAccessor;
import org.springframework.data.convert.TypeMapper;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.mapping.Alias;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.lang.Nullable;
/**
* Elasticsearch specific {@link TypeMapper} definition.
@ -48,45 +43,15 @@ public interface ElasticsearchTypeMapper extends TypeMapper<Map<String, Object>>
return readType(source) != null;
}
static ElasticsearchTypeMapper defaultTypeMapper(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
return new ElasticsearchDefaultTypeMapper(DEFAULT_TYPE_KEY, new MapTypeAliasAccessor(DEFAULT_TYPE_KEY),
mappingContext, Collections.singletonList(new SimpleTypeInformationMapper()));
}
/**
* {@link TypeAliasAccessor} to store aliases in a {@link Map}.
* Creates a new default {@link ElasticsearchTypeMapper}.
*
* @author Christoph Strobl
* @param mappingContext the mapping context.
* @return a new default {@link ElasticsearchTypeMapper}.
* @see DefaultElasticsearchTypeMapper
*/
final class MapTypeAliasAccessor implements TypeAliasAccessor<Map<String, Object>> {
private final @Nullable String typeKey;
public MapTypeAliasAccessor(@Nullable String typeKey) {
this.typeKey = typeKey;
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.TypeAliasAccessor#readAliasFrom(java.lang.Object)
*/
public Alias readAliasFrom(Map<String, Object> source) {
return Alias.ofNullable(source.get(typeKey));
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.TypeAliasAccessor#writeTypeTo(java.lang.Object, java.lang.Object)
*/
public void writeTypeTo(Map<String, Object> sink, Object alias) {
if (typeKey == null) {
return;
}
sink.put(typeKey, alias);
}
static ElasticsearchTypeMapper create(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
return new DefaultElasticsearchTypeMapper(DEFAULT_TYPE_KEY, mappingContext);
}
}

View File

@ -35,14 +35,17 @@ import org.springframework.util.NumberUtils;
*/
class GeoConverters {
static Collection<? extends Object> getConvertersToRegister() {
static Collection<Object> getConvertersToRegister() {
return Arrays.asList(PointToMapConverter.INSTANCE, MapToPointConverter.INSTANCE, GeoPointToMapConverter.INSTANCE,
MapToGeoPointConverter.INSTANCE);
}
/**
* {@link Converter} to write a {@link Point} to {@link Map} using {@code lat/long} properties.
*/
@WritingConverter
enum PointToMapConverter implements Converter<Point, Map> {
enum PointToMapConverter implements Converter<Point, Map<String, Object>> {
INSTANCE;
@ -56,8 +59,11 @@ class GeoConverters {
}
}
/**
* {@link Converter} to write a {@link GeoPoint} to {@link Map} using {@code lat/long} properties.
*/
@WritingConverter
enum GeoPointToMapConverter implements Converter<GeoPoint, Map> {
enum GeoPointToMapConverter implements Converter<GeoPoint, Map<String, Object>> {
INSTANCE;
@ -70,8 +76,11 @@ class GeoConverters {
}
}
/**
* {@link Converter} to read a {@link Point} from {@link Map} using {@code lat/long} properties.
*/
@ReadingConverter
enum MapToPointConverter implements Converter<Map, Point> {
enum MapToPointConverter implements Converter<Map<String, Object>, Point> {
INSTANCE;
@ -84,8 +93,11 @@ class GeoConverters {
}
}
/**
* {@link Converter} to read a {@link GeoPoint} from {@link Map} using {@code lat/long} properties.
*/
@ReadingConverter
enum MapToGeoPointConverter implements Converter<Map, GeoPoint> {
enum MapToGeoPointConverter implements Converter<Map<String, Object>, GeoPoint> {
INSTANCE;

View File

@ -15,6 +15,12 @@
*/
package org.springframework.data.elasticsearch.core;
import static org.apache.commons.lang.RandomStringUtils.*;
import static org.elasticsearch.index.query.QueryBuilders.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.elasticsearch.utils.IndexBuilder.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -62,11 +68,6 @@ import org.springframework.data.elasticsearch.entities.SampleEntity;
import org.springframework.data.elasticsearch.entities.SampleMappingEntity;
import org.springframework.data.elasticsearch.entities.UseServerConfigurationEntity;
import org.springframework.data.util.CloseableIterator;
import static org.apache.commons.lang.RandomStringUtils.*;
import static org.elasticsearch.index.query.QueryBuilders.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.elasticsearch.utils.IndexBuilder.*;
/**
* Base for testing rest/transport templates
@ -808,7 +809,7 @@ public class ElasticsearchTemplateTests {
SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()).withIndices(INDEX_NAME)
.withTypes(TYPE_NAME).withFields("message").build();
// when
Page<String> page = elasticsearchTemplate.queryForPage(searchQuery, String.class, new SearchResultMapper() {
Page<String> page = elasticsearchTemplate.queryForPage(searchQuery, String.class, new SearchResultMapperAdapter() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<String> values = new ArrayList<>();
@ -817,11 +818,6 @@ public class ElasticsearchTemplateTests {
}
return new AggregatedPageImpl<>((List<T>) values);
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
// then
assertThat(page, is(notNullValue()));
@ -941,10 +937,10 @@ public class ElasticsearchTemplateTests {
}
final SearchResultMapper searchResultMapper = new SearchResultMapper() {
final SearchResultMapper searchResultMapper = new SearchResultMapperAdapter() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<SampleEntity> result = new ArrayList<SampleEntity>();
List<SampleEntity> result = new ArrayList<>();
for (SearchHit searchHit : response.getHits()) {
if (response.getHits().getHits().length <= 0) {
return new AggregatedPageImpl<T>(Collections.EMPTY_LIST, response.getScrollId());
@ -959,12 +955,7 @@ public class ElasticsearchTemplateTests {
if (result.size() > 0) {
return new AggregatedPageImpl<T>((List<T>) result, response.getScrollId());
}
return new AggregatedPageImpl<T>(Collections.EMPTY_LIST, response.getScrollId());
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
return new AggregatedPageImpl<T>(Collections.emptyList(), response.getScrollId());
}
};
@ -1347,7 +1338,8 @@ public class ElasticsearchTemplateTests {
.withHighlightFields(message.toArray(new HighlightBuilder.Field[message.size()]))
.build();
Page<SampleEntity> sampleEntities = elasticsearchTemplate.queryForPage(searchQuery, SampleEntity.class, new SearchResultMapper() {
Page<SampleEntity> sampleEntities = elasticsearchTemplate.queryForPage(searchQuery, SampleEntity.class,
new SearchResultMapperAdapter() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<SampleEntity> chunk = new ArrayList<>();
@ -1366,11 +1358,6 @@ public class ElasticsearchTemplateTests {
}
return null;
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
assertThat(sampleEntities.getContent().get(0).getHighlightedMessage(), is(highlightedMessage));
@ -1409,7 +1396,7 @@ public class ElasticsearchTemplateTests {
.build();
// when
elasticsearchTemplate.queryForPage(searchQuery, SampleEntity.class, new SearchResultMapper() {
elasticsearchTemplate.queryForPage(searchQuery, SampleEntity.class, new SearchResultMapperAdapter() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
for (SearchHit searchHit : response.getHits()) {
@ -1425,11 +1412,6 @@ public class ElasticsearchTemplateTests {
}
return null;
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
}
@ -1458,7 +1440,7 @@ public class ElasticsearchTemplateTests {
.withHighlightFields(new HighlightBuilder.Field("message"))
.build();
// when
elasticsearchTemplate.queryForPage(searchQuery, SampleEntity.class, new SearchResultMapper() {
elasticsearchTemplate.queryForPage(searchQuery, SampleEntity.class, new SearchResultMapperAdapter() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
for (SearchHit searchHit : response.getHits()) {
@ -1471,11 +1453,6 @@ public class ElasticsearchTemplateTests {
}
return null;
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
}
@ -1621,7 +1598,8 @@ public class ElasticsearchTemplateTests {
.withTypes(TYPE_NAME)
.build();
// then
Page<SampleEntity> page = elasticsearchTemplate.queryForPage(searchQuery, SampleEntity.class, new SearchResultMapper() {
Page<SampleEntity> page = elasticsearchTemplate.queryForPage(searchQuery, SampleEntity.class,
new SearchResultMapperAdapter() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<SampleEntity> values = new ArrayList<>();
@ -1633,11 +1611,6 @@ public class ElasticsearchTemplateTests {
}
return new AggregatedPageImpl<>((List<T>) values);
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
assertThat(page, is(notNullValue()));
assertThat(page.getContent().size(), is(1));
@ -1826,7 +1799,8 @@ public class ElasticsearchTemplateTests {
// then
SearchQuery searchQuery = new NativeSearchQueryBuilder().withIndices(INDEX_NAME)
.withTypes(TYPE_NAME).withQuery(matchAllQuery()).build();
Page<Map> sampleEntities = elasticsearchTemplate.queryForPage(searchQuery, Map.class, new SearchResultMapper() {
Page<Map> sampleEntities = elasticsearchTemplate.queryForPage(searchQuery, Map.class,
new SearchResultMapperAdapter() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<Map> chunk = new ArrayList<>();
@ -1847,11 +1821,6 @@ public class ElasticsearchTemplateTests {
}
return null;
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
assertThat(sampleEntities.getTotalElements(), is(equalTo(2L)));
assertThat(sampleEntities.getContent().get(0).get("userId"), is(person1.get("userId")));
@ -2400,7 +2369,8 @@ public class ElasticsearchTemplateTests {
// When
SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()).withTypes("hetro").withIndices(INDEX_1_NAME, INDEX_2_NAME).build();
Page<ResultAggregator> page = elasticsearchTemplate.queryForPage(searchQuery, ResultAggregator.class, new SearchResultMapper() {
Page<ResultAggregator> page = elasticsearchTemplate.queryForPage(searchQuery, ResultAggregator.class,
new SearchResultMapperAdapter() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<ResultAggregator> values = new ArrayList<>();
@ -2412,11 +2382,6 @@ public class ElasticsearchTemplateTests {
}
return new AggregatedPageImpl<>((List<T>) values);
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
assertThat(page.getTotalElements(), is(2l));