DATAES-530 - Add converter based EntityMapper.

ElasticsearchEntityMapper provides an alternative to the Jackson based EntityMapper implementation.

Original Pull Request: #237
This commit is contained in:
Christoph Strobl 2019-01-17 10:52:32 +01:00
parent 1dc113d57b
commit a64af54e26
28 changed files with 2457 additions and 140 deletions

View File

@ -29,6 +29,7 @@ include::{spring-data-commons-docs}/repositories.adoc[]
:leveloffset: +1
include::reference/elasticsearch-clients.adoc[]
include::reference/elasticsearch-object-mapping.adoc[]
include::reference/data-elasticsearch.adoc[]
include::reference/reactive-elasticsearch-operations.adoc[]
include::reference/reactive-elasticsearch-repositories.adoc[]

View File

@ -0,0 +1,287 @@
[[elasticsearch.mapping]]
= Elasticsearch Object Mapping
Spring Data Elasticsearch allows to choose between two mapping implementations abstracted via the `EntityMapper` interface:
* <<elasticsearch.mapping.jackson2>>
* <<elasticsearch.mapping.meta-model>>
[[elasticsearch.mapping.jackson2]]
== Jackson Object Mapping
The Jackson2 based approach (used by default) utilizes a customized `ObjectMapper` instance with spring data specific modules.
Extensions to the actual mapping need to be customized via Jackson annotations like `@JsonInclude`.
.Jackson2 Object Mapping Configuration
====
[source,java]
----
@Configuration
public class Config extends AbstractElasticsearchConfiguration { <1>
@Override
public RestHighLevelClient elasticsearchClient() {
return RestClients.create(ClientConfiguration.create("localhost:9200")).rest()
}
}
----
<1> `AbstractElasticsearchConfiguration` already defines a Jackson2 based `entityMapper` via `ElasticsearchConfigurationSupport`.
====
WARNING: `CustomConversions`, `@ReadingConverter` & `@WritingConverter` cannot be applied when using the Jackson based `EntityMapper`.
[[elasticsearch.mapping.meta-model]]
== Meta Model Object Mapping
The Metamodel based approach uses domain type information for reading/writing from/to Elasticsearch.
This allows to register `Converter` instances for specific domain type mapping.
.Meta Model Object Mapping Configuration
====
[source,java]
----
@Configuration
public class Config extends AbstractElasticsearchConfiguration {
@Override
public RestHighLevelClient elasticsearchClient() {
return RestClients.create(ClientConfiguration.create("localhost:9200")).rest()
}
@Bean
@Override
public EntityMapper entityMapper() { <1>
ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(elasticsearchMappingContext(),
new DefaultConversionService()); <2>
entityMapper.setConversions(elasticsearchCustomConversions()); <3>
return entityMapper;
}
}
----
<1> Overwrite the default `EntityMapper` from `ElasticsearchConfigurationSupport` and expose it as bean.
<2> Use the provided `SimpleElasticsearchMappingContext` to avoid inconsistencies and provide a `GenericConversionService`
for `Converter` registration.
<3> Optionally set `CustomConversions` if applicable.
====
[[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.
.Type Hints
====
[source,java]
----
public class Person { <1>
@Id String id;
String firstname;
String lastname;
}
----
[source,json]
----
{
"_class" : "com.example.Person", <1>
"id" : "cb7bef",
"firstname" : "Sarah",
"lastname" : "Connor"
}
----
<1> By default the domain types class name is used for the type hint.
====
Type hints can be configured to hold custom information. Use the `@TypeAlias` annotation to do so.
NOTE: Make sure to add types with `@TypeAlias` to the initial entity set (`AbstractElasticsearchConfiguration#getInitialEntitySet`)
to already have entity information available when first reading data from the store.
.Type Hints with Alias
====
[source,java]
----
@TypeAlias("human") <1>
public class Person {
@Id String id;
// ...
}
----
[source,json]
----
{
"_class" : "human", <1>
"id" : ...
}
----
<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.
==== Geospatial Types
Geospatial types like `Point` & `GeoPoint` are converted into _lat/lon_ pairs.
.Geospatial types
====
[source,java]
----
public class Address {
String city, street;
Point location;
}
----
[source,json]
----
{
"city" : "Los Angeles",
"street" : "2800 East Observatory Road",
"location" : { "lat" : 34.118347, "lon" : -118.3026284 }
}
----
====
==== 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>>.
.Collections
====
[source,java]
----
public class Person {
// ...
List<Person> friends;
}
----
[source,json]
----
{
// ...
"friends" : [ { "firstname" : "Kyle", "lastname" : "Reese" } ]
}
----
====
==== 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>>.
However the Map key needs to a String to be processed by Elasticsearch.
.Collections
====
[source,java]
----
public class Person {
// ...
Map<String, Address> knownLocations;
}
----
[source,json]
----
{
// ...
"knownLocations" : {
"arrivedAt" : {
"city" : "Los Angeles",
"street" : "2800 East Observatory Road",
"location" : { "lat" : 34.118347, "lon" : -118.3026284 }
}
}
}
----
====
[[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.
.Meta Model Object Mapping Configuration
====
[source,java]
----
@Configuration
public class Config extends AbstractElasticsearchConfiguration {
@Override
public RestHighLevelClient elasticsearchClient() {
return RestClients.create(ClientConfiguration.create("localhost:9200")).rest()
}
@Bean
@Override
public EntityMapper entityMapper() {
ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(elasticsearchMappingContext(),
new DefaultConversionService());
entityMapper.setConversions(elasticsearchCustomConversions()); <1>
return entityMapper;
}
@Bean
@Override
public ElasticsearchCustomConversions elasticsearchCustomConversions() {
return new ElasticsearchCustomConversions(Arrays.asList(new AddressToMap(), new MapToAddress())); <2>
}
@WritingConverter <3>
static class AddressToMap implements Converter<Address, Map<String, Object>> {
@Override
public Map<String, Object> convert(Address source) {
LinkedHashMap<String, Object> target = new LinkedHashMap<>();
target.put("ciudad", source.getCity());
// ...
return target;
}
}
@ReadingConverter <4>
static class MapToAddress implements Converter<Map<String, Object>, Address> {
@Override
public Address convert(Map<String, Object> source) {
// ...
return address;
}
}
}
----
[source,json]
----
{
"ciudad" : "Los Angeles",
"calle" : "2800 East Observatory Road",
"localidad" : { "lat" : 34.118347, "lon" : -118.3026284 }
}
----
<1> Register `ElasticsearchCustomConversions` with the `EntityMapper`.
<2> Add `Converter` implementations.
<3> Set up the `Converter` used for writing `DomainType` to Elasticsearch.
<4> Set up the `Converter` used for reading `DomainType` from search result.
====

View File

@ -43,6 +43,6 @@ public abstract class AbstractElasticsearchConfiguration extends ElasticsearchCo
*/
@Bean
public ElasticsearchOperations elasticsearchOperations() {
return new ElasticsearchRestTemplate(elasticsearchClient(), elasticsearchConverter());
return new ElasticsearchRestTemplate(elasticsearchClient(), elasticsearchConverter(), resultsMapper());
}
}

View File

@ -50,7 +50,7 @@ public abstract class AbstractReactiveElasticsearchConfiguration extends Elastic
public ReactiveElasticsearchOperations reactiveElasticsearchTemplate() {
ReactiveElasticsearchTemplate template = new ReactiveElasticsearchTemplate(reactiveElasticsearchClient(),
elasticsearchConverter());
elasticsearchConverter(), resultsMapper());
template.setIndicesOptions(indicesOptions());
template.setRefreshPolicy(refreshPolicy());

View File

@ -30,6 +30,10 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.core.DefaultEntityMapper;
import org.springframework.data.elasticsearch.core.DefaultResultMapper;
import org.springframework.data.elasticsearch.core.EntityMapper;
import org.springframework.data.elasticsearch.core.ResultsMapper;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
@ -62,18 +66,48 @@ public class ElasticsearchConfigurationSupport {
SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
mappingContext.setInitialEntitySet(getInitialEntitySet());
mappingContext.setSimpleTypeHolder(customConversions().getSimpleTypeHolder());
mappingContext.setSimpleTypeHolder(elasticsearchCustomConversions().getSimpleTypeHolder());
return mappingContext;
}
/**
* Returns the {@link EntityMapper} used for mapping source &lt;&gt; DomainType. <br />
* <strong>Hint</strong>: you can use {@link org.springframework.data.elasticsearch.core.ElasticsearchEntityMapper} as
* an alternative to the {@link DefaultEntityMapper}.
*
* <pre>{@code
* ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(elasticsearchMappingContext(),
* new DefaultConversionService());
* entityMapper.setConversions(elasticsearchCustomConversions());
* }
* </pre>
*
* @return never {@literal null}.
*/
@Bean
public EntityMapper entityMapper() {
return new DefaultEntityMapper(elasticsearchMappingContext());
}
/**
* Returns the {@link ResultsMapper} to be used for search responses.
*
* @see #entityMapper()
* @return never {@literal null}.
*/
@Bean
public ResultsMapper resultsMapper() {
return new DefaultResultMapper(elasticsearchMappingContext(), entityMapper());
}
/**
* Register custom {@link Converter}s in a {@link ElasticsearchCustomConversions} object if required.
*
* @return never {@literal null}.
*/
@Bean
public ElasticsearchCustomConversions customConversions() {
public ElasticsearchCustomConversions elasticsearchCustomConversions() {
return new ElasticsearchCustomConversions(Collections.emptyList());
}

View File

@ -15,24 +15,49 @@
*/
package org.springframework.data.elasticsearch.core;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.util.Assert;
/**
* @author Artur Konczak
* @author Christoph Strobl
*/
public abstract class AbstractResultMapper implements ResultsMapper {
private EntityMapper entityMapper;
private final EntityMapper entityMapper;
private final ProjectionFactory projectionFactory;
public AbstractResultMapper(EntityMapper entityMapper) {
this(entityMapper, new SpelAwareProxyProjectionFactory());
}
/**
*
* @param entityMapper
* @param projectionFactory
* @since 3.2
*/
public AbstractResultMapper(EntityMapper entityMapper, ProjectionFactory projectionFactory) {
Assert.notNull(entityMapper, "EntityMapper must not be null!");
Assert.notNull(projectionFactory, "ProjectionFactory must not be null!");
this.entityMapper = entityMapper;
this.projectionFactory = projectionFactory;
}
@Override
public EntityMapper getEntityMapper() {
return this.entityMapper;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.ResultsMapper#getProjectionFactory()
*/
@Override
public ProjectionFactory getProjectionFactory() {
return projectionFactory;
}
}

View File

@ -17,12 +17,15 @@ package org.springframework.data.elasticsearch.core;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.elasticsearch.core.geo.CustomGeoModule;
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.context.MappingContext;
import org.springframework.util.Assert;
@ -40,6 +43,7 @@ import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
* @author Artur Konczak
* @author Petar Tahchiev
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class DefaultEntityMapper implements EntityMapper {
@ -52,14 +56,14 @@ public class DefaultEntityMapper implements EntityMapper {
*/
public DefaultEntityMapper(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> context) {
Assert.notNull(context, "MappingContext must not be null!");
objectMapper = new ObjectMapper();
objectMapper.registerModule(new SpringDataElasticsearchModule(context));
objectMapper.registerModule(new CustomGeoModule());
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
}
@ -73,6 +77,20 @@ public class DefaultEntityMapper implements EntityMapper {
return objectMapper.writeValueAsString(object);
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.EntityMapper#mapObject(java.lang.Object)
*/
@Override
public Map<String, Object> mapObject(Object source) {
try {
return objectMapper.readValue(mapToString(source), HashMap.class);
} catch (IOException e) {
throw new MappingException(e.getMessage(), e);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.EntityMapper#mapToObject(java.lang.String, java.lang.Class)
@ -82,6 +100,20 @@ public class DefaultEntityMapper implements EntityMapper {
return objectMapper.readValue(source, clazz);
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.EntityMapper#readObject(java.util.Map, java.lang.Class)
*/
@Override
public <T> T readObject (Map<String, Object> source, Class<T> targetType) {
try {
return mapToObject(mapToString(source), targetType);
} catch (IOException e) {
throw new MappingException(e.getMessage(), e);
}
}
/**
* A simple Jackson module to register the {@link SpringDataSerializerModifier}.
*
@ -101,7 +133,7 @@ public class DefaultEntityMapper implements EntityMapper {
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> context) {
Assert.notNull(context, "MappingContext must not be null!");
setSerializerModifier(new SpringDataSerializerModifier(context));
}

View File

@ -43,12 +43,13 @@ import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMa
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import org.springframework.util.StringUtils;
/**
* @author Artur Konczak
@ -59,24 +60,20 @@ import org.springframework.util.StringUtils;
* @author Mark Paluch
* @author Ilkang Na
* @author Sascha Woo
* @author Christoph Strobl
*/
public class DefaultResultMapper extends AbstractResultMapper {
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
private final ConversionService conversionService = new DefaultConversionService();
public DefaultResultMapper() {
this(new SimpleElasticsearchMappingContext());
}
public DefaultResultMapper(MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
super(new DefaultEntityMapper(mappingContext));
Assert.notNull(mappingContext, "MappingContext must not be null!");
this.mappingContext = mappingContext;
public DefaultResultMapper(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
this(mappingContext, initEntityMapper(mappingContext));
}
public DefaultResultMapper(EntityMapper entityMapper) {
@ -85,18 +82,23 @@ public class DefaultResultMapper extends AbstractResultMapper {
public DefaultResultMapper(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext,
EntityMapper entityMapper) {
super(entityMapper);
Assert.notNull(mappingContext, "MappingContext must not be null!");
@Nullable EntityMapper entityMapper) {
super(entityMapper != null ? entityMapper : initEntityMapper(mappingContext));
this.mappingContext = mappingContext;
}
static EntityMapper initEntityMapper(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
Assert.notNull(mappingContext, "MappingContext must not be null!");
return new DefaultEntityMapper(mappingContext);
}
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
long totalHits = response.getHits().getTotalHits();
float maxScore = response.getHits().getMaxScore();
@ -113,7 +115,7 @@ public class DefaultResultMapper extends AbstractResultMapper {
setPersistentEntityId(result, hit.getId(), clazz);
setPersistentEntityVersion(result, hit.getVersion(), clazz);
setPersistentEntityScore(result, hit.getScore(), clazz);
populateScriptFields(result, hit);
results.add(result);
}
@ -200,14 +202,14 @@ public class DefaultResultMapper extends AbstractResultMapper {
}
private <T> void setPersistentEntityId(T result, String id, Class<T> clazz) {
if (clazz.isAnnotationPresent(Document.class)) {
ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(clazz);
ElasticsearchPersistentProperty idProperty = persistentEntity.getIdProperty();
PersistentPropertyAccessor<T> accessor = new ConvertingPropertyAccessor<>(persistentEntity.getPropertyAccessor(result),
conversionService);
PersistentPropertyAccessor<T> accessor = new ConvertingPropertyAccessor<>(
persistentEntity.getPropertyAccessor(result), conversionService);
// Only deal with String because ES generated Ids are strings !
if (idProperty != null && idProperty.getType().isAssignableFrom(String.class)) {
@ -217,9 +219,9 @@ public class DefaultResultMapper extends AbstractResultMapper {
}
private <T> void setPersistentEntityVersion(T result, long version, Class<T> clazz) {
if (clazz.isAnnotationPresent(Document.class)) {
ElasticsearchPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(clazz);
ElasticsearchPersistentProperty versionProperty = persistentEntity.getVersionProperty();

View File

@ -0,0 +1,683 @@
/*
* 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 java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.EntityConverter;
import org.springframework.data.convert.EntityInstantiator;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.convert.EntityReader;
import org.springframework.data.convert.EntityWriter;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions;
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;
import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
import org.springframework.data.mapping.model.PropertyValueProvider;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.format.datetime.DateFormatterRegistrar;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Elasticsearch specific {@link EntityReader} & {@link EntityWriter} implementation based on domain type
* {@link ElasticsearchPersistentEntity metadata}.
*
* @author Christoph Strobl
* @since 3.2
*/
public class ElasticsearchEntityMapper implements
EntityConverter<ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty, Object, Map<String, Object>>,
EntityWriter<Object, Map<String, Object>>, EntityReader<Object, Map<String, Object>>, InitializingBean,
EntityMapper {
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
private final GenericConversionService conversionService;
private CustomConversions conversions = new ElasticsearchCustomConversions(Collections.emptyList());
private EntityInstantiators instantiators = new EntityInstantiators();
private ElasticsearchTypeMapper typeMapper;
public ElasticsearchEntityMapper(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext,
@Nullable GenericConversionService conversionService) {
this.mappingContext = mappingContext;
this.conversionService = conversionService != null ? conversionService : new DefaultConversionService();
this.typeMapper = ElasticsearchTypeMapper.defaultTypeMapper(mappingContext);
}
// --> READ
@Override
public <T> T readObject(Map<String, Object> source, Class<T> targetType) {
return read(targetType, source);
}
@Override
@Nullable
public <R> R read(Class<R> type, Map<String, Object> source) {
return doRead(source, ClassTypeInformation.from((Class<R>) ClassUtils.getUserClass(type)));
}
@Nullable
protected <R> R doRead(Map<String, Object> source, TypeInformation<R> typeHint) {
if (source == null) {
return null;
}
typeHint = (TypeInformation<R>) typeMapper.readType(source, typeHint);
if (conversions.hasCustomReadTarget(Map.class, typeHint.getType())) {
return conversionService.convert(source, typeHint.getType());
}
if (typeHint.isMap() || ClassTypeInformation.OBJECT.equals(typeHint)) {
return (R) source;
}
ElasticsearchPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(typeHint);
return readEntity(entity, source);
}
protected <R> R readEntity(ElasticsearchPersistentEntity<?> entity, Map<String, Object> source) {
ElasticsearchPersistentEntity<?> targetEntity = computeClosestEntity(entity, source);
ElasticsearchPropertyValueProvider propertyValueProvider = new ElasticsearchPropertyValueProvider(
new MapValueAccessor(source));
EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity);
R instance = (R) instantiator.createInstance(targetEntity,
new PersistentEntityParameterValueProvider<>(targetEntity, propertyValueProvider, null));
if (targetEntity.requiresPropertyPopulation()) {
return readProperties(targetEntity, instance, propertyValueProvider);
}
return instance;
}
protected <R> R readProperties(ElasticsearchPersistentEntity<?> entity, R instance,
ElasticsearchPropertyValueProvider valueProvider) {
PersistentPropertyAccessor<R> accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance),
conversionService);
for (ElasticsearchPersistentProperty prop : entity) {
if (entity.isConstructorArgument(prop) || prop.isScoreProperty()) {
continue;
}
Object value = valueProvider.getPropertyValue(prop);
if (value != null) {
accessor.setProperty(prop, valueProvider.getPropertyValue(prop));
}
}
return accessor.getBean();
}
protected <R> R readValue(@Nullable Object source, ElasticsearchPersistentProperty property,
TypeInformation<R> targetType) {
if (source == null) {
return null;
}
Class<?> rawType = targetType.getType();
if (conversions.hasCustomReadTarget(source.getClass(), rawType)) {
return (R) 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);
}
private <R> R readMapValue(@Nullable Map<String, Object> source, ElasticsearchPersistentProperty property,
TypeInformation<R> targetType) {
TypeInformation information = typeMapper.readType(source);
if (property.isEntity() && !property.isMap() || information != null) {
ElasticsearchPersistentEntity<?> targetEntity = information != null
? mappingContext.getRequiredPersistentEntity(information)
: mappingContext.getRequiredPersistentEntity(property);
return readEntity(targetEntity, source);
}
Map<String, Object> target = new LinkedHashMap();
for (Entry<String, Object> entry : source.entrySet()) {
if (conversions.isSimpleType(entry.getValue().getClass())) {
target.put(entry.getKey(),
readSimpleValue(entry.getValue(), targetType.isMap() ? targetType.getComponentType() : targetType));
} else {
ElasticsearchPersistentEntity<?> targetEntity = computeGenericValueTypeForRead(property, entry.getValue());
if (targetEntity.getTypeInformation().isMap()) {
Map<String, Object> valueMap = (Map) entry.getValue();
if (typeMapper.containsTypeInformation(valueMap)) {
target.put(entry.getKey(), readEntity(targetEntity, (Map) entry.getValue()));
} else {
target.put(entry.getKey(), readValue(valueMap, property, targetEntity.getTypeInformation()));
}
} else if (targetEntity.getTypeInformation().isCollectionLike()) {
target.put(entry.getKey(),
readValue(entry.getValue(), property, targetEntity.getTypeInformation().getActualType()));
} else {
target.put(entry.getKey(), readEntity(targetEntity, (Map) entry.getValue()));
}
}
}
return (R) target;
}
private <R> R readCollectionValue(@Nullable List<?> source, ElasticsearchPersistentProperty property,
TypeInformation<R> targetType) {
if (source == null) {
return null;
}
List<?> sourceList = source;
Collection target = createCollectionForValue(targetType, sourceList.size());
for (Object value : sourceList) {
if (conversions.isSimpleType(value.getClass())) {
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;
}
protected Object readSimpleValue(@Nullable Object value, TypeInformation<?> targetType) {
if (value == null) {
return null;
}
if (ClassTypeInformation.OBJECT.equals(targetType)
|| (targetType != null && value.getClass().equals(targetType.getActualType()))) {
return value;
}
if (conversionService.canConvert(value.getClass(), targetType.getType())) {
return conversionService.convert(value, targetType.getType());
}
throw new MappingException(
String.format("Unable to map %s of type %s to %s", value, value.getClass(), targetType.getType()));
}
// --> WRITE
@Override
public Map<String, Object> mapObject(Object source) {
LinkedHashMap<String, Object> target = new LinkedHashMap<>();
write(source, target);
return target;
}
@Override
public void write(@Nullable Object source, Map<String, Object> sink) {
if (source == null) {
return;
}
if (source instanceof Map) {
sink.putAll((Map) source);
return;
}
Class<?> entityType = ClassUtils.getUserClass(source.getClass());
TypeInformation<? extends Object> type = ClassTypeInformation.from(entityType);
if (requiresTypeHint(type, source.getClass(), null)) {
typeMapper.writeType(source.getClass(), sink);
}
doWrite(source, sink, type);
}
protected void doWrite(@Nullable Object source, Map<String, Object> 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)
: 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, Map<String, Object> sink,
@Nullable TypeInformation containingStructure) {
PersistentPropertyAccessor<?> accessor = entity.getPropertyAccessor(source);
if (requiresTypeHint(entity.getTypeInformation(), source.getClass(), containingStructure)) {
typeMapper.writeType(source.getClass(), sink);
}
writeProperties(entity, accessor, sink);
}
protected void writeProperties(ElasticsearchPersistentEntity<?> entity, PersistentPropertyAccessor<?> accessor,
Map<String, Object> sink) {
for (ElasticsearchPersistentProperty property : entity) {
if (!property.isWritable()) {
continue;
}
Object value = accessor.getProperty(property);
if (value == null) {
continue;
}
if (!conversions.isSimpleType(value.getClass())) {
writeProperty(property, value, sink);
} else {
sink.put(property.getFieldName(), getWriteSimpleValue(value));
}
}
}
protected void writeProperty(ElasticsearchPersistentProperty property, Object value, Map<String, Object> sink) {
Optional<Class<?>> customWriteTarget = conversions.getCustomWriteTarget(value.getClass());
if (customWriteTarget.isPresent()) {
Class<?> writeTarget = customWriteTarget.get();
sink.put(property.getFieldName(), conversionService.convert(value, writeTarget));
return;
}
TypeInformation<?> typeHint = property.getTypeInformation();
if (typeHint.equals(ClassTypeInformation.OBJECT)) {
if (value instanceof List) {
typeHint = ClassTypeInformation.LIST;
} else if (value instanceof Map) {
typeHint = ClassTypeInformation.MAP;
} else if (value instanceof Set) {
typeHint = ClassTypeInformation.SET;
} else if (value instanceof Collection) {
typeHint = ClassTypeInformation.COLLECTION;
}
}
sink.put(property.getFieldName(), getWriteComplexValue(property, typeHint, value));
}
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;
}
return conversionService.convert(value, customWriteTarget.get());
}
protected Object getWriteComplexValue(ElasticsearchPersistentProperty property, TypeInformation<?> typeHint,
Object value) {
if (typeHint.isCollectionLike() || value instanceof Iterable) {
return writeCollectionValue(value, property, typeHint);
}
if (typeHint.isMap()) {
return writeMapValue((Map<String, Object>) value, property, typeHint);
}
if (property.isEntity() || !conversions.isSimpleType(value.getClass())) {
return writeEntity(value, property, typeHint);
}
return value;
}
private Object writeEntity(Object value, ElasticsearchPersistentProperty property, TypeInformation<?> typeHint) {
Map<String, Object> target = new LinkedHashMap<>();
writeEntity(mappingContext.getRequiredPersistentEntity(value.getClass()), value, target,
property.getTypeInformation());
return target;
}
private Object writeMapValue(Map<String, Object> value, ElasticsearchPersistentProperty property,
TypeInformation<?> typeHint) {
Map<Object, Object> target = new LinkedHashMap<>();
Streamable<Entry<String, Object>> mapSource = Streamable.of(value.entrySet());
if (!typeHint.getActualType().getType().equals(Object.class)
&& conversions.isSimpleType(typeHint.getMapValueType().getType())) {
mapSource.forEach(it -> target.put(it.getKey(), getWriteSimpleValue(it.getValue())));
} else {
mapSource.forEach(it -> {
Object converted = null;
if (it.getValue() != null) {
if (conversions.isSimpleType(it.getValue().getClass())) {
converted = getWriteSimpleValue(it.getValue());
} else {
converted = getWriteComplexValue(property, ClassTypeInformation.from(it.getValue().getClass()),
it.getValue());
}
}
target.put(it.getKey(), converted);
});
}
return target;
}
private Object writeCollectionValue(Object value, ElasticsearchPersistentProperty property,
TypeInformation<?> typeHint) {
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())) {
collectionSource.map(this::getWriteSimpleValue).forEach(target::add);
} else {
Streamable.of((Iterable) value).map(it -> {
if (it == null) {
return null;
}
if (conversions.isSimpleType(it.getClass())) {
return getWriteSimpleValue(it);
}
return getWriteComplexValue(property, ClassTypeInformation.from(it.getClass()), it);
}).forEach(target::add);
}
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
public String mapToString(Object source) throws IOException {
Map<String, Object> sink = new LinkedHashMap<>();
write(source, sink);
return new ObjectMapper().writeValueAsString(sink);
}
@Override
public <T> T mapToObject(String source, Class<T> clazz) throws IOException {
return read(clazz, new ObjectMapper().readerFor(HashMap.class).readValue(source));
}
// --> PRIVATE HELPERS
private boolean requiresTypeHint(TypeInformation<?> type, Class<?> actualType,
@Nullable TypeInformation<?> containingStructure) {
if (containingStructure != null) {
if (containingStructure.isCollectionLike()) {
if (containingStructure.getActualType().equals(type) && type.getType().equals(actualType)) {
return false;
}
}
if (containingStructure.isMap()) {
if (containingStructure.getMapValueType().equals(type) && type.getType().equals(actualType)) {
return false;
}
}
if (containingStructure.equals(type) && type.getType().equals(actualType)) {
return false;
}
}
return !conversions.isSimpleType(type.getType()) && !type.isCollectionLike()
&& !conversions.hasCustomWriteTarget(type.getType());
}
/**
* Compute the type to use by checking the given entity against the store type;
*
* @param entity
* @param source
* @return
*/
private ElasticsearchPersistentEntity<?> computeClosestEntity(ElasticsearchPersistentEntity<?> entity,
Map<String, Object> source) {
TypeInformation<?> typeToUse = typeMapper.readType(source);
if (typeToUse == null) {
return entity;
}
if (!entity.getTypeInformation().getType().isInterface() && !entity.getTypeInformation().isCollectionLike()
&& !entity.getTypeInformation().isMap()
&& !ClassUtils.isAssignableValue(entity.getType(), typeToUse.getType())) {
return entity;
}
return mappingContext.getRequiredPersistentEntity(typeToUse);
}
private ElasticsearchPersistentEntity<?> computeGenericValueTypeForRead(ElasticsearchPersistentProperty property,
Object value) {
return property.getTypeInformation().getActualType().equals(ClassTypeInformation.OBJECT)
? mappingContext.getRequiredPersistentEntity(value.getClass())
: mappingContext.getRequiredPersistentEntity(property.getTypeInformation().getActualType());
}
private Collection<?> 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);
}
// --> OHTER STUFF
class ElasticsearchPropertyValueProvider implements PropertyValueProvider<ElasticsearchPersistentProperty> {
final MapValueAccessor mapValueAccessor;
public ElasticsearchPropertyValueProvider(MapValueAccessor mapValueAccessor) {
this.mapValueAccessor = mapValueAccessor;
}
@Override
public <T> T getPropertyValue(ElasticsearchPersistentProperty property) {
return (T) readValue(mapValueAccessor.get(property), property, property.getTypeInformation());
}
}
static class MapValueAccessor {
final Map<String, Object> target;
MapValueAccessor(Map<String, Object> target) {
this.target = target;
}
public Object get(ElasticsearchPersistentProperty property) {
String fieldName = property.getFieldName();
if (!fieldName.contains(".")) {
return target.get(fieldName);
}
Iterator<String> parts = Arrays.asList(fieldName.split("\\.")).iterator();
Map<String, Object> source = target;
Object result = null;
while (source != null && parts.hasNext()) {
result = source.get(parts.next());
if (parts.hasNext()) {
source = getAsMap(result);
}
}
return result;
}
private Map<String, Object> getAsMap(Object result) {
if (result instanceof Map) {
return (Map) result;
}
throw new IllegalArgumentException(String.format("%s is no Map.", result));
}
}
}

View File

@ -15,17 +15,25 @@
*/
package org.springframework.data.elasticsearch.core;
import static org.elasticsearch.client.Requests.refreshRequest;
import static org.elasticsearch.index.query.QueryBuilders.moreLikeThisQuery;
import static org.elasticsearch.index.query.QueryBuilders.wrapperQuery;
import static org.springframework.data.elasticsearch.core.MappingBuilder.buildMapping;
import static org.elasticsearch.client.Requests.*;
import static org.elasticsearch.index.query.QueryBuilders.*;
import static org.springframework.data.elasticsearch.core.MappingBuilder.*;
import static org.springframework.util.CollectionUtils.isEmpty;
import static org.springframework.util.StringUtils.hasText;
import static org.springframework.util.StringUtils.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
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 org.apache.http.util.EntityUtils;
import org.elasticsearch.action.ActionFuture;
@ -105,11 +113,11 @@ import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMa
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.data.util.CloseableIterator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.util.StringUtils;
/**
* ElasticsearchRestTemplate
@ -132,6 +140,7 @@ import org.springframework.util.StringUtils;
* @author Zetang Zeng
* @author Peter Nowak
* @author Ivan Greene
* @author Christoph Strobl
*/
public class ElasticsearchRestTemplate
implements ElasticsearchOperations, EsClient<RestHighLevelClient>, ApplicationContextAware {
@ -345,7 +354,8 @@ public class ElasticsearchRestTemplate
return queryForPage(queries, clazz, resultsMapper);
}
private <T> List<Page<T>> doMultiSearch(List<SearchQuery> queries, Class<T> clazz, MultiSearchRequest request, SearchResultMapper resultsMapper) {
private <T> List<Page<T>> doMultiSearch(List<SearchQuery> queries, Class<T> clazz, MultiSearchRequest request,
SearchResultMapper resultsMapper) {
MultiSearchResponse.Item[] items = getMultiSearchResult(request);
List<Page<T>> res = new ArrayList<>(queries.size());
int c = 0;
@ -355,7 +365,8 @@ public class ElasticsearchRestTemplate
return res;
}
private List<Page<?>> doMultiSearch(List<SearchQuery> queries, List<Class<?>> classes, MultiSearchRequest request, SearchResultMapper resultsMapper) {
private List<Page<?>> doMultiSearch(List<SearchQuery> queries, List<Class<?>> classes, MultiSearchRequest request,
SearchResultMapper resultsMapper) {
MultiSearchResponse.Item[] items = getMultiSearchResult(request);
List<Page<?>> res = new ArrayList<>(queries.size());
int c = 0;
@ -726,8 +737,7 @@ public class ElasticsearchRestTemplate
private UpdateRequest prepareUpdate(UpdateQuery query) {
String indexName = hasText(query.getIndexName()) ? query.getIndexName()
: getPersistentEntityFor(query.getClazz()).getIndexName();
String type = hasText(query.getType()) ? query.getType()
: getPersistentEntityFor(query.getClazz()).getIndexType();
String type = hasText(query.getType()) ? query.getType() : getPersistentEntityFor(query.getClazz()).getIndexType();
Assert.notNull(indexName, "No index defined for Query");
Assert.notNull(type, "No type define for Query");
Assert.notNull(query.getId(), "No Id define for Query");
@ -880,6 +890,11 @@ public class ElasticsearchRestTemplate
}
return new AggregatedPageImpl<T>(Collections.EMPTY_LIST, response.getScrollId());
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
};
Page<String> scrolledResult = startScroll(scrollTimeInMillis, searchQuery, String.class, onlyIdResultMapper);
@ -1132,7 +1147,7 @@ public class ElasticsearchRestTemplate
if (highlightBuilder == null) {
highlightBuilder = new HighlightBuilder();
}
if(searchQuery.getHighlightFields() != null) {
if (searchQuery.getHighlightFields() != null) {
for (HighlightBuilder.Field highlightField : searchQuery.getHighlightFields()) {
highlightBuilder.field(highlightField);
}
@ -1326,7 +1341,8 @@ public class ElasticsearchRestTemplate
String indexName = StringUtils.isEmpty(query.getIndexName())
? retrieveIndexNameFromPersistentEntity(query.getObject().getClass())[0]
: query.getIndexName();
String type = StringUtils.isEmpty(query.getType()) ? retrieveTypeFromPersistentEntity(query.getObject().getClass())[0]
String type = StringUtils.isEmpty(query.getType())
? retrieveTypeFromPersistentEntity(query.getObject().getClass())[0]
: query.getType();
IndexRequest indexRequest = null;

View File

@ -99,21 +99,7 @@ import org.springframework.data.elasticsearch.core.facet.FacetRequest;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.AliasQuery;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.DeleteQuery;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.GetQuery;
import org.springframework.data.elasticsearch.core.query.IndexBoost;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.MoreLikeThisQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.Query;
import org.springframework.data.elasticsearch.core.query.ScriptField;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.data.elasticsearch.core.query.SourceFilter;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.data.util.CloseableIterator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@ -138,6 +124,7 @@ import org.springframework.util.StringUtils;
* @author Jean-Baptiste Nizet
* @author Zetang Zeng
* @author Ivan Greene
* @author Christoph Strobl
*/
public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<Client>, ApplicationContextAware {
@ -223,8 +210,8 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
ElasticsearchPersistentProperty property = persistentEntity.getRequiredIdProperty();
xContentBuilder = buildMapping(clazz, persistentEntity.getIndexType(),
property.getFieldName(), persistentEntity.getParentType());
xContentBuilder = buildMapping(clazz, persistentEntity.getIndexType(), property.getFieldName(),
persistentEntity.getParentType());
} catch (Exception e) {
throw new ElasticsearchException("Failed to build mapping for " + clazz.getSimpleName(), e);
}
@ -332,7 +319,8 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
return doMultiSearch(queries, clazz, request, mapper);
}
private <T> List<Page<T>> doMultiSearch(List<SearchQuery> queries, Class<T> clazz, MultiSearchRequest request, SearchResultMapper resultsMapper) {
private <T> List<Page<T>> doMultiSearch(List<SearchQuery> queries, Class<T> clazz, MultiSearchRequest request,
SearchResultMapper resultsMapper) {
MultiSearchResponse.Item[] items = getMultiSearchResult(request);
List<Page<T>> res = new ArrayList<>(queries.size());
int c = 0;
@ -342,7 +330,8 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
return res;
}
private List<Page<?>> doMultiSearch(List<SearchQuery> queries, List<Class<?>> classes, MultiSearchRequest request, SearchResultMapper resultsMapper) {
private List<Page<?>> doMultiSearch(List<SearchQuery> queries, List<Class<?>> classes, MultiSearchRequest request,
SearchResultMapper resultsMapper) {
MultiSearchResponse.Item[] items = getMultiSearchResult(request);
List<Page<?>> res = new ArrayList<>(queries.size());
int c = 0;
@ -446,7 +435,8 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
@Override
public <T> CloseableIterator<T> stream(CriteriaQuery query, Class<T> clazz) {
final long scrollTimeInMillis = TimeValue.timeValueMinutes(1).millis();
return doStream(scrollTimeInMillis, (ScrolledPage<T>) startScroll(scrollTimeInMillis, query, clazz), clazz, resultsMapper);
return doStream(scrollTimeInMillis, (ScrolledPage<T>) startScroll(scrollTimeInMillis, query, clazz), clazz,
resultsMapper);
}
@Override
@ -457,10 +447,12 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
@Override
public <T> CloseableIterator<T> stream(SearchQuery query, final Class<T> clazz, final SearchResultMapper mapper) {
final long scrollTimeInMillis = TimeValue.timeValueMinutes(1).millis();
return doStream(scrollTimeInMillis, (ScrolledPage<T>) startScroll(scrollTimeInMillis, query, clazz, mapper), clazz, mapper);
return doStream(scrollTimeInMillis, (ScrolledPage<T>) startScroll(scrollTimeInMillis, query, clazz, mapper), clazz,
mapper);
}
private <T> CloseableIterator<T> doStream(final long scrollTimeInMillis, final ScrolledPage<T> page, final Class<T> clazz, final SearchResultMapper mapper) {
private <T> CloseableIterator<T> doStream(final long scrollTimeInMillis, final ScrolledPage<T> page,
final Class<T> clazz, final SearchResultMapper mapper) {
return new CloseableIterator<T>() {
/** As we couldn't retrieve single result with scroll, store current hits. */
@ -776,6 +768,11 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
}
return new AggregatedPageImpl<T>(Collections.EMPTY_LIST, response.getScrollId());
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
};
Page<String> scrolledResult = startScroll(scrollTimeInMillis, searchQuery, String.class, onlyIdResultMapper);
@ -784,8 +781,9 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
do {
ids.addAll(scrolledResult.getContent());
scrolledResult = continueScroll(((ScrolledPage<T>)scrolledResult).getScrollId(), scrollTimeInMillis, String.class, onlyIdResultMapper);
} while(scrolledResult.getContent().size() != 0);
scrolledResult = continueScroll(((ScrolledPage<T>) scrolledResult).getScrollId(), scrollTimeInMillis,
String.class, onlyIdResultMapper);
} while (scrolledResult.getContent().size() != 0);
for (String id : ids) {
bulkRequestBuilder.add(client.prepareDelete(indexName, typeName, id));
@ -821,12 +819,10 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
private SearchRequestBuilder prepareScroll(Query query, long scrollTimeInMillis) {
SearchRequestBuilder requestBuilder = client.prepareSearch(toArray(query.getIndices()))
.setTypes(toArray(query.getTypes()))
.setScroll(TimeValue.timeValueMillis(scrollTimeInMillis))
.setFrom(0)
.setTypes(toArray(query.getTypes())).setScroll(TimeValue.timeValueMillis(scrollTimeInMillis)).setFrom(0)
.setVersion(true);
if(query.getPageable().isPaged()){
if (query.getPageable().isPaged()) {
requestBuilder.setSize(query.getPageable().getPageSize());
}
@ -880,25 +876,28 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
return resultsMapper.mapResults(response, clazz, null);
}
public <T> Page<T> startScroll(long scrollTimeInMillis, SearchQuery searchQuery, Class<T> clazz, SearchResultMapper mapper) {
public <T> Page<T> startScroll(long scrollTimeInMillis, SearchQuery searchQuery, Class<T> clazz,
SearchResultMapper mapper) {
SearchResponse response = doScroll(prepareScroll(searchQuery, scrollTimeInMillis, clazz), searchQuery);
return mapper.mapResults(response, clazz, null);
}
public <T> Page<T> startScroll(long scrollTimeInMillis, CriteriaQuery criteriaQuery, Class<T> clazz, SearchResultMapper mapper) {
public <T> Page<T> startScroll(long scrollTimeInMillis, CriteriaQuery criteriaQuery, Class<T> clazz,
SearchResultMapper mapper) {
SearchResponse response = doScroll(prepareScroll(criteriaQuery, scrollTimeInMillis, clazz), criteriaQuery);
return mapper.mapResults(response, clazz, null);
}
public <T> Page<T> continueScroll(@Nullable String scrollId, long scrollTimeInMillis, Class<T> clazz) {
SearchResponse response = getSearchResponse(client.prepareSearchScroll(scrollId)
.setScroll(TimeValue.timeValueMillis(scrollTimeInMillis)).execute());
SearchResponse response = getSearchResponse(
client.prepareSearchScroll(scrollId).setScroll(TimeValue.timeValueMillis(scrollTimeInMillis)).execute());
return resultsMapper.mapResults(response, clazz, Pageable.unpaged());
}
public <T> Page<T> continueScroll(@Nullable String scrollId, long scrollTimeInMillis, Class<T> clazz, SearchResultMapper mapper) {
SearchResponse response = getSearchResponse(client.prepareSearchScroll(scrollId)
.setScroll(TimeValue.timeValueMillis(scrollTimeInMillis)).execute());
public <T> Page<T> continueScroll(@Nullable String scrollId, long scrollTimeInMillis, Class<T> clazz,
SearchResultMapper mapper) {
SearchResponse response = getSearchResponse(
client.prepareSearchScroll(scrollId).setScroll(TimeValue.timeValueMillis(scrollTimeInMillis)).execute());
return mapper.mapResults(response, clazz, Pageable.unpaged());
}
@ -911,15 +910,16 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
public <T> Page<T> moreLikeThis(MoreLikeThisQuery query, Class<T> clazz) {
ElasticsearchPersistentEntity persistentEntity = getPersistentEntityFor(clazz);
String indexName = !StringUtils.isEmpty(query.getIndexName()) ? query.getIndexName() : persistentEntity.getIndexName();
String indexName = !StringUtils.isEmpty(query.getIndexName()) ? query.getIndexName()
: persistentEntity.getIndexName();
String type = !StringUtils.isEmpty(query.getType()) ? query.getType() : persistentEntity.getIndexType();
Assert.notNull(indexName, "No 'indexName' defined for MoreLikeThisQuery");
Assert.notNull(type, "No 'type' defined for MoreLikeThisQuery");
Assert.notNull(query.getId(), "No document id defined for MoreLikeThisQuery");
MoreLikeThisQueryBuilder moreLikeThisQueryBuilder = moreLikeThisQuery(toArray(new MoreLikeThisQueryBuilder.Item(indexName, type, query.getId())));
MoreLikeThisQueryBuilder moreLikeThisQueryBuilder = moreLikeThisQuery(
toArray(new MoreLikeThisQueryBuilder.Item(indexName, type, query.getId())));
if (query.getMinTermFreq() != null) {
moreLikeThisQueryBuilder.minTermFreq(query.getMinTermFreq());
@ -978,7 +978,7 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
if (highlightBuilder == null) {
highlightBuilder = new HighlightBuilder();
}
if(searchQuery.getHighlightFields() != null) {
if (searchQuery.getHighlightFields() != null) {
for (HighlightBuilder.Field highlightField : searchQuery.getHighlightFields()) {
highlightBuilder.field(highlightField);
}
@ -1075,9 +1075,9 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
@Override
public Map getSetting(String indexName) {
Assert.notNull(indexName, "No index defined for getSettings");
Settings settings = client.admin().indices().getSettings(new GetSettingsRequest()).actionGet()
.getIndexToSettings().get(indexName);
return settings.keySet().stream().collect(Collectors.toMap((key)->key, (key)->settings.get(key)));
Settings settings = client.admin().indices().getSettings(new GetSettingsRequest()).actionGet().getIndexToSettings()
.get(indexName);
return settings.keySet().stream().collect(Collectors.toMap((key) -> key, (key) -> settings.get(key)));
}
private <T> SearchRequestBuilder prepareSearch(Query query, Class<T> clazz) {
@ -1091,9 +1091,7 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
int startRecord = 0;
SearchRequestBuilder searchRequestBuilder = client.prepareSearch(toArray(query.getIndices()))
.setSearchType(query.getSearchType())
.setTypes(toArray(query.getTypes()))
.setVersion(true)
.setSearchType(query.getSearchType()).setTypes(toArray(query.getTypes())).setVersion(true)
.setTrackScores(query.getTrackScores());
if (query.getSourceFilter() != null) {
@ -1152,7 +1150,8 @@ public class ElasticsearchTemplate implements ElasticsearchOperations, EsClient<
String indexName = StringUtils.isEmpty(query.getIndexName())
? retrieveIndexNameFromPersistentEntity(query.getObject().getClass())[0]
: query.getIndexName();
String type = StringUtils.isEmpty(query.getType()) ? retrieveTypeFromPersistentEntity(query.getObject().getClass())[0]
String type = StringUtils.isEmpty(query.getType())
? retrieveTypeFromPersistentEntity(query.getObject().getClass())[0]
: query.getType();
IndexRequestBuilder indexRequestBuilder = null;

View File

@ -16,6 +16,9 @@
package org.springframework.data.elasticsearch.core;
import java.io.IOException;
import java.util.Map;
import org.springframework.lang.Nullable;
/**
* DocumentMapper interface, it will allow to customize how we mapping object to json
@ -23,10 +26,32 @@ import java.io.IOException;
* @author Artur Konczak
* @author Rizwan Idrees
* @author Mohsin Husen
* @author Christoph Strobl
*/
public interface EntityMapper {
public String mapToString(Object object) throws IOException;
String mapToString(Object object) throws IOException;
public <T> T mapToObject(String source, Class<T> clazz) throws IOException;
<T> T mapToObject(String source, Class<T> clazz) throws IOException;
/**
* Map the given {@literal source} to {@link Map}.
*
* @param source must not be {@literal null}.
* @return never {@literal null}
* @since 3.2
*/
Map<String, Object> mapObject(Object source);
/**
* Map the given {@link Map} into an instance of the {@literal targetType}.
*
* @param source must not be {@literal null}.
* @param targetType must not be {@literal null}.
* @param <T>
* @return can be {@literal null}.
* @since 3.2
*/
@Nullable
<T> T readObject(Map<String, Object> source, Class<T> targetType);
}

View File

@ -16,12 +16,27 @@
package org.springframework.data.elasticsearch.core;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.index.get.GetResult;
import org.springframework.lang.Nullable;
/**
* @author Artur Konczak
* @author Mohsin Husen
* @author Christoph Strobl
*/
public interface GetResultMapper {
<T> T mapResult(GetResponse response, Class<T> clazz);
/**
* Map a single {@link GetResult} to the given {@link Class type}.
*
* @param getResult must not be {@literal null}.
* @param type must not be {@literal null}.
* @param <T>
* @return can be {@literal null}.
* @since 3.2
*/
@Nullable
<T> T mapGetResult(GetResult getResult, Class<T> type);
}

View File

@ -98,11 +98,17 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
}
public ReactiveElasticsearchTemplate(ReactiveElasticsearchClient client, ElasticsearchConverter converter) {
this(client, converter, new DefaultResultMapper(converter.getMappingContext()));
}
public ReactiveElasticsearchTemplate(ReactiveElasticsearchClient client, ElasticsearchConverter converter,
ResultsMapper resultsMapper) {
this.client = client;
this.converter = converter;
this.mappingContext = converter.getMappingContext();
this.resultMapper = new DefaultResultMapper(converter.getMappingContext());
this.resultMapper = resultsMapper;
this.exceptionTranslator = new ElasticsearchExceptionTranslator();
this.operations = new EntityOperations(this.mappingContext);
}
@ -184,7 +190,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
Assert.notNull(id, "Id must not be null!");
return doFindById(id, getPersistentEntity(entityType), index, type)
.map(it -> resultMapper.mapEntity(it, entityType));
.map(it -> resultMapper.mapGetResult(it, entityType));
}
private Mono<GetResult> doFindById(String id, ElasticsearchPersistentEntity<?> entity, @Nullable String index,
@ -201,6 +207,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations#exists(String, Class, String, String)
* @see org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations#exists(String, Class, String, String)
*/
@Override
public Mono<Boolean> exists(String id, Class<?> entityType, String index, String type) {
@ -230,7 +237,7 @@ public class ReactiveElasticsearchTemplate implements ReactiveElasticsearchOpera
Class<T> resultType) {
return doFind(query, getPersistentEntity(entityType), index, type)
.map(it -> resultMapper.mapEntity(it, resultType));
.map(it -> resultMapper.mapSearchHit(it, resultType));
}
private Flux<SearchHit> doFind(Query query, ElasticsearchPersistentEntity<?> entity, @Nullable String index,

View File

@ -16,11 +16,15 @@
package org.springframework.data.elasticsearch.core;
import java.io.IOException;
import java.util.Map;
import org.elasticsearch.index.get.GetResult;
import org.elasticsearch.search.SearchHit;
import org.springframework.data.elasticsearch.ElasticsearchException;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@ -35,6 +39,16 @@ public interface ResultsMapper extends SearchResultMapper, GetResultMapper, Mult
EntityMapper getEntityMapper();
/**
* Get the configured {@link ProjectionFactory}. <br />
* <strong>NOTE</strong> Should be overwritten in implementation to make use of the type cache.
*
* @since 3.2
*/
default ProjectionFactory getProjectionFactory() {
return new SpelAwareProxyProjectionFactory();
}
@Nullable
default <T> T mapEntity(String source, Class<T> clazz) {
@ -58,19 +72,28 @@ public interface ResultsMapper extends SearchResultMapper, GetResultMapper, Mult
* @since 3.2
*/
@Nullable
default <T> T mapEntity(GetResult getResult, Class<T> type) {
default <T> T mapGetResult(GetResult getResult, Class<T> type) {
if (getResult.isSourceEmpty()) {
return null;
}
String sourceString = getResult.sourceAsString();
if (sourceString.startsWith("{\"id\":null,")) {
sourceString = sourceString.replaceFirst("\"id\":null", "\"id\":\"" + getResult.getId() + "\"");
Map<String, Object> source = getResult.getSource();
if (!source.containsKey("id") || source.get("id") == null) {
source.put("id", getResult.getId());
}
return mapEntity(sourceString, type);
Object mappedResult = getEntityMapper().readObject(source, type);
if (mappedResult == null) {
return (T) mappedResult;
}
if (type.isInterface() || !ClassUtils.isAssignableValue(type, mappedResult)) {
return getProjectionFactory().createProjection(type, mappedResult);
}
return (T) mappedResult;
}
/**
@ -83,18 +106,27 @@ public interface ResultsMapper extends SearchResultMapper, GetResultMapper, Mult
* @since 3.2
*/
@Nullable
default <T> T mapEntity(SearchHit searchHit, Class<T> type) {
default <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
if (!searchHit.hasSource()) {
return null;
}
String sourceString = searchHit.getSourceAsString();
if (sourceString.startsWith("{\"id\":null,")) {
sourceString = sourceString.replaceFirst("\"id\":null", "\"id\":\"" + searchHit.getId() + "\"");
Map<String, Object> source = searchHit.getSourceAsMap();
if (!source.containsKey("id") || source.get("id") == null) {
source.put("id", searchHit.getId());
}
return mapEntity(sourceString, type);
Object mappedResult = getEntityMapper().readObject(source, type);
if (mappedResult == null) {
return (T) mappedResult;
}
if (type.isInterface()) {
return getProjectionFactory().createProjection(type, mappedResult);
}
return (T) mappedResult;
}
}

View File

@ -16,14 +16,29 @@
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;
import org.springframework.lang.Nullable;
/**
* @author Artur Konczak
* @author Petar Tahchiev
* @author Christoph Strobl
*/
public interface SearchResultMapper {
<T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable);
/**
* Map a single {@link SearchHit} to the given {@link Class type}.
*
* @param searchHit must not be {@literal null}.
* @param type must not be {@literal null}.
* @param <T>
* @return can be {@literal null}.
* @since 3.2
*/
@Nullable
<T> T mapSearchHit(SearchHit searchHit, Class<T> type);
}

View File

@ -15,22 +15,94 @@
*/
package org.springframework.data.elasticsearch.core.convert;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchSimpleTypes;
import org.springframework.util.NumberUtils;
/**
* Elasticsearch specific {@link CustomConversions}.
*
* @author Christoph Strobl
* @since 3.2
*/
public class ElasticsearchCustomConversions extends CustomConversions {
private static final StoreConversions STORE_CONVERSIONS;
private static final List<Object> STORE_CONVERTERS;
static {
List<Object> converters = new ArrayList<>();
converters.addAll(GeoConverters.getConvertersToRegister());
converters.add(StringToUUIDConverter.INSTANCE);
converters.add(UUIDToStringConverter.INSTANCE);
converters.add(BigDecimalToDoubleConverter.INSTANCE);
converters.add(DoubleToBigDecimalConverter.INSTANCE);
STORE_CONVERTERS = Collections.unmodifiableList(converters);
STORE_CONVERSIONS = StoreConversions.of(ElasticsearchSimpleTypes.HOLDER, STORE_CONVERTERS);
}
/**
* Creates a new {@link CustomConversions} instance registering the given converters.
*
* @param converters must not be {@literal null}.
*/
public ElasticsearchCustomConversions(Collection<?> converters) {
super(StoreConversions.NONE, converters);
super(STORE_CONVERSIONS, converters);
}
@ReadingConverter
enum StringToUUIDConverter implements Converter<String, UUID> {
INSTANCE;
@Override
public UUID convert(String source) {
return UUID.fromString(source);
}
}
@WritingConverter
enum UUIDToStringConverter implements Converter<UUID, String> {
INSTANCE;
@Override
public String convert(UUID source) {
return source.toString();
}
}
@ReadingConverter
enum DoubleToBigDecimalConverter implements Converter<Double, BigDecimal> {
INSTANCE;
@Override
public BigDecimal convert(Double source) {
return NumberUtils.convertNumberToTargetClass(source, BigDecimal.class);
}
}
@WritingConverter
enum BigDecimalToDoubleConverter implements Converter<BigDecimal, Double> {
INSTANCE;
@Override
public Double convert(BigDecimal source) {
return NumberUtils.convertNumberToTargetClass(source, Double.class);
}
}
}

View File

@ -0,0 +1,47 @@
/*
* 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

@ -0,0 +1,92 @@
/*
* 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.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.
*
* @author Christoph Strobl
* @since 3.2
*/
public interface ElasticsearchTypeMapper extends TypeMapper<Map<String, Object>> {
String DEFAULT_TYPE_KEY = "_class";
/**
* Returns whether the given key is the type key.
*
* @return {@literal true} if given {@literal key} is used as type hint key.
*/
boolean isTypeKey(String key);
default boolean containsTypeInformation(Map<String, Object> source) {
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}.
*
* @author Christoph Strobl
*/
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);
}
}
}

View File

@ -0,0 +1,100 @@
/*
* 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.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.geo.Point;
import org.springframework.util.NumberUtils;
/**
* Set of {@link Converter converters} specific to Elasticsearch Geo types.
*
* @author Christoph Strobl
* @since 3.2
*/
class GeoConverters {
static Collection<? extends Object> getConvertersToRegister() {
return Arrays.asList(PointToMapConverter.INSTANCE, MapToPointConverter.INSTANCE, GeoPointToMapConverter.INSTANCE,
MapToGeoPointConverter.INSTANCE);
}
@WritingConverter
enum PointToMapConverter implements Converter<Point, Map> {
INSTANCE;
@Override
public Map convert(Point source) {
Map<String, Object> target = new LinkedHashMap<>();
target.put("lat", source.getX());
target.put("lon", source.getY());
return target;
}
}
@WritingConverter
enum GeoPointToMapConverter implements Converter<GeoPoint, Map> {
INSTANCE;
@Override
public Map convert(GeoPoint source) {
Map<String, Object> target = new LinkedHashMap<>();
target.put("lat", source.getLat());
target.put("lon", source.getLon());
return target;
}
}
@ReadingConverter
enum MapToPointConverter implements Converter<Map, Point> {
INSTANCE;
@Override
public Point convert(Map source) {
Double x = NumberUtils.convertNumberToTargetClass((Number) source.get("lat"), Double.class);
Double y = NumberUtils.convertNumberToTargetClass((Number) source.get("lon"), Double.class);
return new Point(x, y);
}
}
@ReadingConverter
enum MapToGeoPointConverter implements Converter<Map, GeoPoint> {
INSTANCE;
@Override
public GeoPoint convert(Map source) {
Double x = NumberUtils.convertNumberToTargetClass((Number) source.get("lat"), Double.class);
Double y = NumberUtils.convertNumberToTargetClass((Number) source.get("lon"), Double.class);
return new GeoPoint(x, y);
}
}
}

View File

@ -0,0 +1,46 @@
/*
* 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.mapping;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.mapping.model.SimpleTypeHolder;
/**
* @author Christoph Strobl
* @since 3.2
*/
public class ElasticsearchSimpleTypes {
static final Set<Class<?>> AUTOGENERATED_ID_TYPES;
static {
Set<Class<?>> classes = new HashSet<>();
classes.add(String.class);
AUTOGENERATED_ID_TYPES = Collections.unmodifiableSet(classes);
Set<Class<?>> simpleTypes = new HashSet<>();
ELASTICSEARCH_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes);
}
private static final Set<Class<?>> ELASTICSEARCH_SIMPLE_TYPES;
public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(ELASTICSEARCH_SIMPLE_TYPES, true);
private ElasticsearchSimpleTypes() {}
}

View File

@ -25,12 +25,15 @@ import org.apache.commons.lang.ClassUtils;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient;
import org.springframework.data.elasticsearch.core.ElasticsearchEntityMapper;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.EntityMapper;
import org.springframework.data.elasticsearch.core.ReactiveElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
@ -89,6 +92,13 @@ public class ElasticsearchConfigurationSupportUnitTests {
assertThat(context.getBean(ReactiveElasticsearchTemplate.class)).isNotNull();
}
@Test // DATAES-530
public void usesConfiguredEntityMapper() {
AbstractApplicationContext context = new AnnotationConfigApplicationContext(EntityMapperConfig.class);
assertThat(context.getBean(EntityMapper.class)).isInstanceOf(ElasticsearchEntityMapper.class);
}
@Configuration
static class StubConfig extends ElasticsearchConfigurationSupport {
@ -112,6 +122,21 @@ public class ElasticsearchConfigurationSupportUnitTests {
}
}
@Configuration
static class EntityMapperConfig extends ElasticsearchConfigurationSupport {
@Bean
@Override
public EntityMapper entityMapper() {
ElasticsearchEntityMapper entityMapper = new ElasticsearchEntityMapper(elasticsearchMappingContext(),
new DefaultConversionService());
entityMapper.setConversions(elasticsearchCustomConversions());
return entityMapper;
}
}
@Document(indexName = "config-support-tests")
static class Entity {}
}

View File

@ -16,6 +16,7 @@
package org.springframework.data.elasticsearch.core;
import java.io.IOException;
import java.util.Map;
/**
* @author Artur Konczak
@ -38,4 +39,14 @@ public class CustomEntityMapper implements EntityMapper {
//mapping text to Object
return null;
}
@Override
public Map<String, Object> mapObject(Object source) {
return null;
}
@Override
public <T> T readObject(Map<String, Object> source, Class<T> targetType) {
return null;
}
}

View File

@ -15,14 +15,20 @@
*/
package org.springframework.data.elasticsearch.core;
import static java.util.Arrays.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import com.fasterxml.jackson.databind.util.ArrayIterator;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.get.MultiGetItemResponse;
import org.elasticsearch.action.get.MultiGetResponse;
@ -36,8 +42,12 @@ import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Document;
@ -46,11 +56,7 @@ import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMa
import org.springframework.data.elasticsearch.entities.Car;
import org.springframework.data.elasticsearch.entities.SampleEntity;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import com.fasterxml.jackson.databind.util.ArrayIterator;
/**
* @author Artur Konczak
@ -58,24 +64,43 @@ import static org.mockito.Mockito.*;
* @author Chris White
* @author Mark Paluch
* @author Ilkang Na
* @author Christoph Strobl
*/
@RunWith(Parameterized.class)
public class DefaultResultMapperTests {
private DefaultResultMapper resultMapper;
private SimpleElasticsearchMappingContext context;
private EntityMapper entityMapper;
@Mock
private SearchResponse response;
@Mock private SearchResponse response;
public DefaultResultMapperTests(SimpleElasticsearchMappingContext context, EntityMapper entityMapper) {
this.context = context;
this.entityMapper = entityMapper;
}
@Parameters
public static Collection<Object[]> data() {
SimpleElasticsearchMappingContext context = new SimpleElasticsearchMappingContext();
return Arrays.asList(new Object[] { context, new DefaultEntityMapper(context) },
new Object[] { context, new ElasticsearchEntityMapper(context, new DefaultConversionService()) });
}
@Before
public void init() {
MockitoAnnotations.initMocks(this);
resultMapper = new DefaultResultMapper(new SimpleElasticsearchMappingContext());
resultMapper = new DefaultResultMapper(context, entityMapper);
}
@Test
public void shouldMapAggregationsToPage() {
//Given
SearchHit[] hits = {createCarHit("Ford", "Grat"), createCarHit("BMW", "Arrow")};
// Given
SearchHit[] hits = { createCarHit("Ford", "Grat"), createCarHit("BMW", "Arrow") };
SearchHits searchHits = mock(SearchHits.class);
when(searchHits.getTotalHits()).thenReturn(2L);
when(searchHits.iterator()).thenReturn(new ArrayIterator(hits));
@ -84,10 +109,10 @@ public class DefaultResultMapperTests {
Aggregations aggregations = new Aggregations(asList(createCarAggregation()));
when(response.getAggregations()).thenReturn(aggregations);
//When
// When
AggregatedPage<Car> page = (AggregatedPage<Car>) resultMapper.mapResults(response, Car.class, Pageable.unpaged());
//Then
// Then
page.hasFacets();
assertThat(page.hasAggregations(), is(true));
assertThat(page.getAggregation("Diesel").getName(), is("Diesel"));
@ -95,17 +120,17 @@ public class DefaultResultMapperTests {
@Test
public void shouldMapSearchRequestToPage() {
//Given
SearchHit[] hits = {createCarHit("Ford", "Grat"), createCarHit("BMW", "Arrow")};
// Given
SearchHit[] hits = { createCarHit("Ford", "Grat"), createCarHit("BMW", "Arrow") };
SearchHits searchHits = mock(SearchHits.class);
when(searchHits.getTotalHits()).thenReturn(2L);
when(searchHits.iterator()).thenReturn(new ArrayIterator(hits));
when(response.getHits()).thenReturn(searchHits);
//When
// When
Page<Car> page = resultMapper.mapResults(response, Car.class, Pageable.unpaged());
//Then
// Then
assertThat(page.hasContent(), is(true));
assertThat(page.getTotalElements(), is(2L));
assertThat(page.getContent().get(0).getName(), is("Ford"));
@ -113,17 +138,17 @@ public class DefaultResultMapperTests {
@Test
public void shouldMapPartialSearchRequestToObject() {
//Given
SearchHit[] hits = {createCarPartialHit("Ford", "Grat"), createCarPartialHit("BMW", "Arrow")};
// Given
SearchHit[] hits = { createCarPartialHit("Ford", "Grat"), createCarPartialHit("BMW", "Arrow") };
SearchHits searchHits = mock(SearchHits.class);
when(searchHits.getTotalHits()).thenReturn(2L);
when(searchHits.iterator()).thenReturn(new ArrayIterator(hits));
when(response.getHits()).thenReturn(searchHits);
//When
// When
Page<Car> page = resultMapper.mapResults(response, Car.class, Pageable.unpaged());
//Then
// Then
assertThat(page.hasContent(), is(true));
assertThat(page.getTotalElements(), is(2L));
assertThat(page.getContent().get(0).getName(), is("Ford"));
@ -131,14 +156,14 @@ public class DefaultResultMapperTests {
@Test
public void shouldMapGetRequestToObject() {
//Given
// Given
GetResponse response = mock(GetResponse.class);
when(response.getSourceAsString()).thenReturn(createJsonCar("Ford", "Grat"));
//When
// When
Car result = resultMapper.mapResult(response, Car.class);
//Then
// Then
assertThat(result, notNullValue());
assertThat(result.getModel(), is("Grat"));
assertThat(result.getName(), is("Ford"));

View File

@ -0,0 +1,677 @@
/*
* 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 static org.assertj.core.api.Assertions.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
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.junit.Before;
import org.junit.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.ReadOnlyProperty;
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.core.convert.ElasticsearchCustomConversions;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.entities.Car;
import org.springframework.data.elasticsearch.entities.GeoEntity;
import org.springframework.data.geo.Point;
/**
* @author Christoph Strobl
*/
public class ElasticsearchEntityMapperUnitTests {
static final String JSON_STRING = "{\"_class\":\"org.springframework.data.elasticsearch.entities.Car\",\"name\":\"Grat\",\"model\":\"Ford\"}";
static final String CAR_MODEL = "Ford";
static final String CAR_NAME = "Grat";
ElasticsearchEntityMapper entityMapper;
Person sarahConnor;
Person kyleReese;
Person t800;
Inventory gun = new Gun("Glock 19", 33);
Inventory grenade = new Grenade("40 mm");
Inventory rifle = new Rifle("AR-18 Assault Rifle", 3.17, 40);
Inventory shotGun = new ShotGun("Ithaca 37 Pump Shotgun");
Address observatoryRoad;
Place bigBunsCafe;
Map<String, Object> sarahAsMap;
Map<String, Object> t800AsMap;
Map<String, Object> kyleAsMap;
Map<String, Object> gratiotAveAsMap;
Map<String, Object> locationAsMap;
Map<String, Object> gunAsMap;
Map<String, Object> grenadeAsMap;
Map<String, Object> rifleAsMap;
Map<String, Object> shotGunAsMap;
Map<String, Object> bigBunsCafeAsMap;
@Before
public void init() {
SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
mappingContext.setInitialEntitySet(Collections.singleton(Rifle.class));
mappingContext.afterPropertiesSet();
entityMapper = new ElasticsearchEntityMapper(mappingContext, new GenericConversionService());
entityMapper.setConversions(
new ElasticsearchCustomConversions(Arrays.asList(new ShotGunToMapConverter(), new MapToShotGunConverter())));
entityMapper.afterPropertiesSet();
sarahConnor = new Person();
sarahConnor.id = "sarah";
sarahConnor.name = "Sarah Connor";
sarahConnor.gender = Gender.MAN;
kyleReese = new Person();
kyleReese.id = "kyle";
kyleReese.gender = Gender.MAN;
kyleReese.name = "Kyle Reese";
t800 = new Person();
t800.id = "t800";
t800.name = "T-800";
t800.gender = Gender.MACHINE;
t800AsMap = new LinkedHashMap<>();
t800AsMap.put("id", "t800");
t800AsMap.put("name", "T-800");
t800AsMap.put("gender", "MACHINE");
t800AsMap.put("_class", "org.springframework.data.elasticsearch.core.ElasticsearchEntityMapperUnitTests$Person");
observatoryRoad = new Address();
observatoryRoad.city = "Los Angeles";
observatoryRoad.street = "2800 East Observatory Road";
observatoryRoad.location = new Point(34.118347D, -118.3026284D);
bigBunsCafe = new Place();
bigBunsCafe.name = "Big Buns Cafe";
bigBunsCafe.city = "Los Angeles";
bigBunsCafe.street = "15 South Fremont Avenue";
bigBunsCafe.location = new Point(34.0945637D, -118.1545845D);
sarahAsMap = new LinkedHashMap<>();
sarahAsMap.put("id", "sarah");
sarahAsMap.put("name", "Sarah Connor");
sarahAsMap.put("gender", "MAN");
sarahAsMap.put("_class", "org.springframework.data.elasticsearch.core.ElasticsearchEntityMapperUnitTests$Person");
kyleAsMap = new LinkedHashMap<>();
kyleAsMap.put("id", "kyle");
kyleAsMap.put("gender", "MAN");
kyleAsMap.put("name", "Kyle Reese");
locationAsMap = new LinkedHashMap<>();
locationAsMap.put("lat", 34.118347D);
locationAsMap.put("lon", -118.3026284D);
gratiotAveAsMap = new LinkedHashMap<>();
gratiotAveAsMap.put("city", "Los Angeles");
gratiotAveAsMap.put("street", "2800 East Observatory Road");
gratiotAveAsMap.put("location", locationAsMap);
bigBunsCafeAsMap = new LinkedHashMap<>();
bigBunsCafeAsMap.put("name", "Big Buns Cafe");
bigBunsCafeAsMap.put("city", "Los Angeles");
bigBunsCafeAsMap.put("street", "15 South Fremont Avenue");
bigBunsCafeAsMap.put("location", new LinkedHashMap<>());
((HashMap<String, Object>) bigBunsCafeAsMap.get("location")).put("lat", 34.0945637D);
((HashMap<String, Object>) bigBunsCafeAsMap.get("location")).put("lon", -118.1545845D);
bigBunsCafeAsMap.put("_class",
"org.springframework.data.elasticsearch.core.ElasticsearchEntityMapperUnitTests$Place");
gunAsMap = new LinkedHashMap<>();
gunAsMap.put("label", "Glock 19");
gunAsMap.put("shotsPerMagazine", 33);
gunAsMap.put("_class", Gun.class.getName());
grenadeAsMap = new LinkedHashMap<>();
grenadeAsMap.put("label", "40 mm");
grenadeAsMap.put("_class", Grenade.class.getName());
rifleAsMap = new LinkedHashMap<>();
rifleAsMap.put("label", "AR-18 Assault Rifle");
rifleAsMap.put("weight", 3.17D);
rifleAsMap.put("maxShotsPerMagazine", 40);
rifleAsMap.put("_class", "rifle");
shotGunAsMap = new LinkedHashMap<>();
shotGunAsMap.put("model", "Ithaca 37 Pump Shotgun");
shotGunAsMap.put("_class", ShotGun.class.getName());
}
@Test // DATAES-530
public void shouldMapObjectToJsonString() throws IOException {
// Given
// When
String jsonResult = entityMapper.mapToString(Car.builder().model(CAR_MODEL).name(CAR_NAME).build());
// Then
assertThat(jsonResult).isEqualTo(JSON_STRING);
}
@Test // DATAES-530
public void shouldMapJsonStringToObject() throws IOException {
// Given
// When
Car result = entityMapper.mapToObject(JSON_STRING, Car.class);
// Then
assertThat(result.getName()).isEqualTo(CAR_NAME);
assertThat(result.getModel()).isEqualTo(CAR_MODEL);
}
@Test // DATAES-530
public void shouldMapGeoPointElasticsearchNames() throws IOException {
// given
final Point point = new Point(10, 20);
final String pointAsString = point.getX() + "," + point.getY();
final double[] pointAsArray = { point.getX(), point.getY() };
final GeoEntity geoEntity = GeoEntity.builder().pointA(point).pointB(GeoPoint.fromPoint(point))
.pointC(pointAsString).pointD(pointAsArray).build();
// when
String jsonResult = entityMapper.mapToString(geoEntity);
// then
assertThat(jsonResult).contains(pointTemplate("pointA", point));
assertThat(jsonResult).contains(pointTemplate("pointB", point));
assertThat(jsonResult).contains(String.format(Locale.ENGLISH, "\"%s\":\"%s\"", "pointC", pointAsString));
assertThat(jsonResult)
.contains(String.format(Locale.ENGLISH, "\"%s\":[%.1f,%.1f]", "pointD", pointAsArray[0], pointAsArray[1]));
}
@Test // DATAES-530
public void ignoresReadOnlyProperties() throws IOException {
// given
Sample sample = new Sample();
sample.readOnly = "readOnly";
sample.property = "property";
sample.javaTransientProperty = "javaTransient";
sample.annotatedTransientProperty = "transient";
// when
String result = entityMapper.mapToString(sample);
// then
assertThat(result).contains("\"property\"");
assertThat(result).contains("\"javaTransient\"");
assertThat(result).doesNotContain("readOnly");
assertThat(result).doesNotContain("annotatedTransientProperty");
}
@Test // DATAES-530
public void writesNestedEntity() {
Person person = new Person();
person.birthdate = new Date();
person.gender = Gender.MAN;
person.address = observatoryRoad;
LinkedHashMap<String, Object> sink = writeToMap(person);
assertThat(sink.get("address")).isEqualTo(gratiotAveAsMap);
}
@Test // DATAES-530
public void writesConcreteList() throws IOException {
Person ginger = new Person();
ginger.id = "ginger";
ginger.gender = Gender.MAN;
sarahConnor.coWorkers = Arrays.asList(kyleReese, ginger);
LinkedHashMap<String, Object> target = writeToMap(sarahConnor);
assertThat((List) target.get("coWorkers")).hasSize(2).contains(kyleAsMap);
}
@Test // DATAES-530
public void writesInterfaceList() throws IOException {
Inventory gun = new Gun("Glock 19", 33);
Inventory grenade = new Grenade("40 mm");
sarahConnor.inventoryList = Arrays.asList(gun, grenade);
LinkedHashMap<String, Object> target = writeToMap(sarahConnor);
assertThat((List) target.get("inventoryList")).containsExactly(gunAsMap, grenadeAsMap);
}
@Test // DATAES-530
public void readTypeCorrectly() {
Person target = entityMapper.read(Person.class, sarahAsMap);
assertThat(target).isEqualTo(sarahConnor);
}
@Test // DATAES-530
public void readListOfConcreteTypesCorrectly() {
sarahAsMap.put("coWorkers", Arrays.asList(kyleAsMap));
Person target = entityMapper.read(Person.class, sarahAsMap);
assertThat(target.getCoWorkers()).contains(kyleReese);
}
@Test // DATAES-530
public void readListOfInterfacesTypesCorrectly() {
sarahAsMap.put("inventoryList", Arrays.asList(gunAsMap, grenadeAsMap));
Person target = entityMapper.read(Person.class, sarahAsMap);
assertThat(target.getInventoryList()).containsExactly(gun, grenade);
}
@Test // DATAES-530
public void writeMapOfConcreteType() {
sarahConnor.shippingAddresses = new LinkedHashMap<>();
sarahConnor.shippingAddresses.put("home", observatoryRoad);
LinkedHashMap<String, Object> target = writeToMap(sarahConnor);
assertThat(target.get("shippingAddresses")).isInstanceOf(Map.class);
assertThat(target.get("shippingAddresses")).isEqualTo(Collections.singletonMap("home", gratiotAveAsMap));
}
@Test // DATAES-530
public void writeMapOfInterfaceType() {
sarahConnor.inventoryMap = new LinkedHashMap<>();
sarahConnor.inventoryMap.put("glock19", gun);
sarahConnor.inventoryMap.put("40 mm grenade", grenade);
LinkedHashMap<String, Object> target = writeToMap(sarahConnor);
assertThat(target.get("inventoryMap")).isInstanceOf(Map.class);
assertThat((Map) target.get("inventoryMap")).containsEntry("glock19", gunAsMap).containsEntry("40 mm grenade",
grenadeAsMap);
}
@Test // DATAES-530
public void readConcreteMapCorrectly() {
sarahAsMap.put("shippingAddresses", Collections.singletonMap("home", gratiotAveAsMap));
Person target = entityMapper.read(Person.class, sarahAsMap);
assertThat(target.getShippingAddresses()).hasSize(1).containsEntry("home", observatoryRoad);
}
@Test // DATAES-530
public void readInterfaceMapCorrectly() {
sarahAsMap.put("inventoryMap", Collections.singletonMap("glock19", gunAsMap));
Person target = entityMapper.read(Person.class, sarahAsMap);
assertThat(target.getInventoryMap()).hasSize(1).containsEntry("glock19", gun);
}
@Test // DATAES-530
public void genericWriteList() {
Skynet skynet = new Skynet();
skynet.objectList = new ArrayList<>();
skynet.objectList.add(t800);
skynet.objectList.add(gun);
LinkedHashMap<String, Object> target = writeToMap(skynet);
assertThat((List<Object>) target.get("objectList")).containsExactly(t800AsMap, gunAsMap);
}
@Test // DATAES-530
public void readGenericList() {
LinkedHashMap<String, Object> source = new LinkedHashMap<>();
source.put("objectList", Arrays.asList(t800AsMap, gunAsMap));
Skynet target = entityMapper.read(Skynet.class, source);
assertThat(target.getObjectList()).containsExactly(t800, gun);
}
@Test // DATAES-530
public void genericWriteListWithList() {
Skynet skynet = new Skynet();
skynet.objectList = new ArrayList<>();
skynet.objectList.add(Arrays.asList(t800, gun));
LinkedHashMap<String, Object> target = writeToMap(skynet);
assertThat((List<Object>) target.get("objectList")).containsExactly(Arrays.asList(t800AsMap, gunAsMap));
}
@Test // DATAES-530
public void readGenericListList() {
LinkedHashMap<String, Object> source = new LinkedHashMap<>();
source.put("objectList", Arrays.asList(Arrays.asList(t800AsMap, gunAsMap)));
Skynet target = entityMapper.read(Skynet.class, source);
assertThat(target.getObjectList()).containsExactly(Arrays.asList(t800, gun));
}
@Test // DATAES-530
public void writeGenericMap() {
Skynet skynet = new Skynet();
skynet.objectMap = new LinkedHashMap<>();
skynet.objectMap.put("gun", gun);
skynet.objectMap.put("grenade", grenade);
LinkedHashMap<String, Object> target = writeToMap(skynet);
assertThat((Map<String, Object>) target.get("objectMap")).containsEntry("gun", gunAsMap).containsEntry("grenade",
grenadeAsMap);
}
@Test // DATAES-530
public void readGenericMap() {
LinkedHashMap<String, Object> source = new LinkedHashMap<>();
source.put("objectMap", Collections.singletonMap("glock19", gunAsMap));
Skynet target = entityMapper.read(Skynet.class, source);
assertThat(target.getObjectMap()).containsEntry("glock19", gun);
}
@Test // DATAES-530
public void writeGenericMapMap() {
Skynet skynet = new Skynet();
skynet.objectMap = new LinkedHashMap<>();
skynet.objectMap.put("inventory", Collections.singletonMap("glock19", gun));
LinkedHashMap<String, Object> target = writeToMap(skynet);
assertThat((Map<String, Object>) target.get("objectMap")).containsEntry("inventory",
Collections.singletonMap("glock19", gunAsMap));
}
@Test // DATAES-530
public void readGenericMapMap() {
LinkedHashMap<String, Object> source = new LinkedHashMap<>();
source.put("objectMap", Collections.singletonMap("inventory", Collections.singletonMap("glock19", gunAsMap)));
Skynet target = entityMapper.read(Skynet.class, source);
assertThat(target.getObjectMap()).containsEntry("inventory", Collections.singletonMap("glock19", gun));
}
@Test // DATAES-530
public void readsNestedEntity() {
sarahAsMap.put("address", gratiotAveAsMap);
Person target = entityMapper.read(Person.class, sarahAsMap);
assertThat(target.getAddress()).isEqualTo(observatoryRoad);
}
@Test // DATAES-530
public void readsNestedObjectEntity() {
LinkedHashMap<String, Object> source = new LinkedHashMap<>();
source.put("object", t800AsMap);
Skynet target = entityMapper.read(Skynet.class, source);
assertThat(target.getObject()).isEqualTo(t800);
}
@Test // DATAES-530
public void writesAliased() {
assertThat(writeToMap(rifle)).containsEntry("_class", "rifle").doesNotContainValue(Rifle.class.getName());
}
@Test // DATAES-530
public void writesNestedAliased() {
t800.inventoryList = Collections.singletonList(rifle);
LinkedHashMap<String, Object> target = writeToMap(t800);
assertThat((List) target.get("inventoryList")).contains(rifleAsMap);
}
@Test // DATAES-530
public void readsAliased() {
assertThat(entityMapper.read(Inventory.class, rifleAsMap)).isEqualTo(rifle);
}
@Test // DATAES-530
public void readsNestedAliased() {
t800AsMap.put("inventoryList", Collections.singletonList(rifleAsMap));
assertThat(entityMapper.read(Person.class, t800AsMap).getInventoryList()).containsExactly(rifle);
}
@Test // DATAES-530
public void appliesCustomConverterForWrite() {
assertThat(writeToMap(shotGun)).isEqualTo(shotGunAsMap);
}
@Test // DATAES-530
public void appliesCustomConverterForRead() {
assertThat(entityMapper.read(Inventory.class, shotGunAsMap)).isEqualTo(shotGun);
}
@Test // DATAES-530
public void writeSubTypeCorrectly() {
sarahConnor.address = bigBunsCafe;
LinkedHashMap<String, Object> target = writeToMap(sarahConnor);
assertThat(target.get("address")).isEqualTo(bigBunsCafeAsMap);
}
@Test // DATAES-530
public void readSubTypeCorrectly() {
sarahAsMap.put("address", bigBunsCafeAsMap);
Person target = entityMapper.read(Person.class, sarahAsMap);
assertThat(target.address).isEqualTo(bigBunsCafe);
}
private String pointTemplate(String name, Point point) {
return String.format(Locale.ENGLISH, "\"%s\":{\"lat\":%.1f,\"lon\":%.1f}", name, point.getX(), point.getY());
}
private LinkedHashMap<String, Object> writeToMap(Object source) {
LinkedHashMap<String, Object> sink = new LinkedHashMap<>();
entityMapper.write(source, sink);
return sink;
}
public static class Sample {
public @ReadOnlyProperty String readOnly;
public @Transient String annotatedTransientProperty;
public transient String javaTransientProperty;
public String property;
}
@Data
static class Person {
@Id String id;
String name;
Date birthdate;
Gender gender;
Address address;
List<Person> coWorkers;
List<Inventory> inventoryList;
Map<String, Address> shippingAddresses;
Map<String, Inventory> inventoryMap;
}
enum Gender {
MAN("1"), MACHINE("0");
String theValue;
Gender(String theValue) {
this.theValue = theValue;
}
public String getTheValue() {
return theValue;
}
}
interface Inventory {
String getLabel();
}
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
static class Gun implements Inventory {
final String label;
final int shotsPerMagazine;
@Override
public String getLabel() {
return label;
}
}
@RequiredArgsConstructor
@EqualsAndHashCode
static class Grenade implements Inventory {
final String label;
@Override
public String getLabel() {
return label;
}
}
@TypeAlias("rifle")
@EqualsAndHashCode
@RequiredArgsConstructor
static class Rifle implements Inventory {
final String label;
final double weight;
final int maxShotsPerMagazine;
@Override
public String getLabel() {
return label;
}
}
@EqualsAndHashCode
@RequiredArgsConstructor
static class ShotGun implements Inventory {
final String label;
@Override
public String getLabel() {
return label;
}
}
@Data
static class Address {
Point location;
String street;
String city;
}
@Data
static class Place extends Address {
String name;
}
@Data
static class Skynet {
Object object;
List<Object> objectList;
Map<String, Object> objectMap;
}
@WritingConverter
static class ShotGunToMapConverter implements Converter<ShotGun, Map<String, Object>> {
@Override
public Map<String, Object> convert(ShotGun source) {
LinkedHashMap<String, Object> target = new LinkedHashMap<>();
target.put("model", source.getLabel());
target.put("_class", ShotGun.class.getName());
return target;
}
}
@ReadingConverter
static class MapToShotGunConverter implements Converter<Map<String, Object>, ShotGun> {
@Override
public ShotGun convert(Map<String, Object> source) {
return new ShotGun(source.get("model").toString());
}
}
}

View File

@ -817,6 +817,11 @@ 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()));
@ -956,6 +961,11 @@ public class ElasticsearchTemplateTests {
}
return new AggregatedPageImpl<T>(Collections.EMPTY_LIST, response.getScrollId());
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
};
/*
@ -1356,6 +1366,11 @@ 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));
@ -1410,6 +1425,11 @@ public class ElasticsearchTemplateTests {
}
return null;
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
}
@ -1451,6 +1471,11 @@ public class ElasticsearchTemplateTests {
}
return null;
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
}
@ -1491,6 +1516,11 @@ public class ElasticsearchTemplateTests {
}
return null;
}
@Override
public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
return null;
}
});
}
@ -1603,6 +1633,11 @@ 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));
@ -1812,6 +1847,11 @@ 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")));
@ -2372,6 +2412,11 @@ 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));

View File

@ -101,7 +101,7 @@ public class MappingBuilderTests {
assertThat(xContentBuilderToString(xContentBuilder), is(expected));
}
@Test
@Test // DATAES-530
public void shouldAddStockPriceDocumentToIndex() throws IOException {
// Given
@ -112,8 +112,9 @@ public class MappingBuilderTests {
String symbol = "AU";
double price = 2.34;
String id = "abc";
elasticsearchTemplate
.index(buildIndex(StockPrice.builder().id(id).symbol(symbol).price(new BigDecimal(price)).build()));
.index(buildIndex(StockPrice.builder().id(id).symbol(symbol).price(BigDecimal.valueOf(price)).build()));
elasticsearchTemplate.refresh(StockPrice.class);
SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(matchAllQuery()).build();
@ -122,7 +123,7 @@ public class MappingBuilderTests {
assertThat(result.size(), is(1));
StockPrice entry = result.get(0);
assertThat(entry.getSymbol(), is(symbol));
assertThat(entry.getPrice(), is(new BigDecimal(price)));
assertThat(entry.getPrice(), is(BigDecimal.valueOf(price)));
}
@Test

View File

@ -38,6 +38,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.PageRequest;
@ -86,7 +87,9 @@ public class ReactiveElasticsearchTemplateTests {
restTemplate.putMapping(SampleEntity.class);
restTemplate.refresh(SampleEntity.class);
template = new ReactiveElasticsearchTemplate(TestUtils.reactiveClient());
template = new ReactiveElasticsearchTemplate(TestUtils.reactiveClient(), restTemplate.getElasticsearchConverter(),
new DefaultResultMapper(new ElasticsearchEntityMapper(
restTemplate.getElasticsearchConverter().getMappingContext(), new DefaultConversionService())));
}
@Test // DATAES-504