diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/ConversionException.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/ConversionException.java new file mode 100644 index 000000000..33a504610 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/ConversionException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.convert; + +/** + * @author Peter-Josef Meisch + */ +public class ConversionException extends RuntimeException { + public ConversionException() { + super(); + } + + public ConversionException(String message) { + super(message); + } + + public ConversionException(String message, Throwable cause) { + super(message, cause); + } + + public ConversionException(Throwable cause) { + super(cause); + } + + protected ConversionException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java index e377d8962..ed3d27af9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchConverter.java @@ -16,6 +16,7 @@ package org.springframework.data.elasticsearch.core.convert; import java.util.List; +import java.util.stream.Collectors; import org.springframework.data.convert.EntityConverter; import org.springframework.data.domain.Pageable; @@ -27,6 +28,7 @@ import org.springframework.data.elasticsearch.core.document.SearchDocument; import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.lang.Nullable; @@ -41,28 +43,6 @@ import org.springframework.util.Assert; public interface ElasticsearchConverter extends EntityConverter, ElasticsearchPersistentProperty, Object, Document> { - /** - * Convert a given {@literal idValue} to its {@link String} representation taking potentially registered - * {@link org.springframework.core.convert.converter.Converter Converters} into account. - * - * @param idValue must not be {@literal null}. - * @return never {@literal null}. - * @since 3.2 - */ - default String convertId(Object idValue) { - - Assert.notNull(idValue, "idValue must not be null!"); - - if (!getConversionService().canConvert(idValue.getClass(), String.class)) { - return idValue.toString(); - } - - return getConversionService().convert(idValue, String.class); - } - - AggregatedPage> mapResults(SearchDocumentResponse response, Class clazz, - @Nullable Pageable pageable); - /** * Get the configured {@link ProjectionFactory}.
* NOTE Should be overwritten in implementation to make use of the type cache. @@ -73,20 +53,35 @@ public interface ElasticsearchConverter return new SpelAwareProxyProjectionFactory(); } + // region read /** * Map a single {@link Document} to an instance of the given type. * * @param document the document to map * @param type must not be {@literal null}. - * @param + * @param the class of type * @return can be {@literal null} if the document is null or {@link Document#isEmpty()} is true. * @since 4.0 */ @Nullable T mapDocument(@Nullable Document document, Class type); + /** + * Map a list of {@link Document}s to a list of instance of the given type. + * + * @param documents must not be {@literal null}. + * @param type must not be {@literal null}. + * @param the class of type + * @return a list obtained by calling {@link #mapDocument(Document, Class)} on the elements of the list. + * @since 4.0 + */ + default List mapDocuments(List documents, Class type) { + return documents.stream().map(document -> mapDocument(document, type)).collect(Collectors.toList()); + } + /** * builds a {@link SearchHits} from a {@link SearchDocumentResponse}. + * * @param the clazz of the type, must not be {@literal null}. * @param type the type of the returned data, must not be {@literal null}. * @param searchDocumentResponse the response to read from, must not be {@literal null}. @@ -106,22 +101,53 @@ public interface ElasticsearchConverter */ SearchHit read(Class type, SearchDocument searchDocument); + AggregatedPage> mapResults(SearchDocumentResponse response, Class clazz, + @Nullable Pageable pageable); + + // endregion + + // region write /** - * Map a list of {@link Document}s to alist of instance of the given type. + * Convert a given {@literal idValue} to its {@link String} representation taking potentially registered + * {@link org.springframework.core.convert.converter.Converter Converters} into account. * - * @param documents must not be {@literal null}. - * @param type must not be {@literal null}. - * @param - * @return a list obtained by calling {@link #mapDocument(Document, Class)} on the elements of the list. - * @since 4.0 + * @param idValue must not be {@literal null}. + * @return never {@literal null}. + * @since 3.2 */ - List mapDocuments(List documents, Class type); + default String convertId(Object idValue) { + + Assert.notNull(idValue, "idValue must not be null!"); + + if (!getConversionService().canConvert(idValue.getClass(), String.class)) { + return idValue.toString(); + } + + return getConversionService().convert(idValue, String.class); + } /** * Map an object to a {@link Document}. * - * @param source + * @param source the object to map * @return will not be {@literal null}. */ - Document mapObject(Object source); + default Document mapObject(@Nullable Object source) { + Document target = Document.create(); + write(source, target); + return target; + } + // endregion + + /** + * Updates a query by renaming the property names in the query to the correct mapped field names and the values to the + * converted values if the {@link ElasticsearchPersistentProperty} for a property has a + * {@link org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentPropertyConverter}. + * + * @param criteriaQuery the query that is internally updated + * @param domainClass the class of the object that is searched with the query + */ + // region query + void updateQuery(CriteriaQuery criteriaQuery, Class domainClass); + // endregion } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverter.java new file mode 100644 index 000000000..0d6d25240 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverter.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.convert; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.time.temporal.TemporalAccessor; +import java.util.concurrent.ConcurrentHashMap; + +import org.elasticsearch.common.time.DateFormatter; +import org.springframework.data.elasticsearch.annotations.DateFormat; +import org.springframework.util.Assert; + +/** + * Provides Converter instances to convert to and from Dates in the different date and time formats that elasticsearch + * understands. + * + * @author Peter-Josef Meisch + * @since 4.0 + */ +final public class ElasticsearchDateConverter { + + private static final ConcurrentHashMap converters = new ConcurrentHashMap<>(); + + private final DateFormatter dateFormatter; + + /** + * Creates an ElasticsearchDateConverter for the given {@link DateFormat}. + * + * @param dateFormat must not be @{literal null} + * @return converter + */ + public static ElasticsearchDateConverter of(DateFormat dateFormat) { + + Assert.notNull(dateFormat, "dateFormat must not be null"); + + return of(dateFormat.name()); + } + + /** + * Creates an ElasticsearchDateConverter for the given pattern. + * + * @param pattern must not be {@literal null} + * @return converter + */ + public static ElasticsearchDateConverter of(String pattern) { + Assert.notNull(pattern, "pattern must not be null"); + + return converters.computeIfAbsent(pattern, p -> new ElasticsearchDateConverter(DateFormatter.forPattern(p))); + } + + private ElasticsearchDateConverter(DateFormatter dateFormatter) { + this.dateFormatter = dateFormatter; + } + + /** + * Formats the given {@link TemporalAccessor} int a String + * + * @param accessor must not be {@literal null} + * @return the formatted object + */ + public String format(TemporalAccessor accessor) { + return dateFormatter.format(accessor); + } + + /** + * Parses a String into an object + * + * @param input the String to parse, must not be {@literal null}. + * @param type the class to return + * @param the class of type + * @return the new created object + */ + public T parse(String input, Class type) { + TemporalAccessor accessor = dateFormatter.parse(input); + try { + Method method = type.getMethod("from", TemporalAccessor.class); + Object o = method.invoke(null, accessor); + return type.cast(o); + } catch (NoSuchMethodException e) { + throw new ConversionException("no 'from' factory method found in class " + type.getName()); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new ConversionException("could not create object of class " + type.getName(), e); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java index 2a7243d47..b383c346b 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java @@ -15,8 +15,6 @@ */ package org.springframework.data.elasticsearch.core.convert; -import lombok.RequiredArgsConstructor; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -54,6 +52,8 @@ import org.springframework.data.elasticsearch.core.document.SearchDocument; import org.springframework.data.elasticsearch.core.document.SearchDocumentResponse; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; +import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentPropertyConverter; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; @@ -133,40 +133,86 @@ public class MappingElasticsearchConverter this.conversions = conversions; } - /** - * Set the {@link ElasticsearchTypeMapper} to use for reading / writing type hints. - * - * @param typeMapper must not be {@literal null}. - */ - public void setTypeMapper(ElasticsearchTypeMapper typeMapper) { - this.typeMapper = typeMapper; - } - /* * (non-Javadoc) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ @Override public void afterPropertiesSet() { - DateFormatterRegistrar.addDateConverters(conversionService); conversions.registerConvertersIn(conversionService); } - @SuppressWarnings("unchecked") + // region read + + @Override + public AggregatedPage> mapResults(SearchDocumentResponse response, Class type, + Pageable pageable) { + + List> results = response.getSearchDocuments().stream() // + .map(searchDocument -> read(type, searchDocument)) // + .collect(Collectors.toList()); + + return new AggregatedPageImpl<>(results, pageable, response); + } + + @Override + public SearchHits read(Class type, SearchDocumentResponse searchDocumentResponse) { + + Assert.notNull(type, "type must not be null"); + Assert.notNull(searchDocumentResponse, "searchDocumentResponse must not be null"); + + long totalHits = searchDocumentResponse.getTotalHits(); + float maxScore = searchDocumentResponse.getMaxScore(); + String scrollId = searchDocumentResponse.getScrollId(); + List> searchHits = searchDocumentResponse.getSearchDocuments().stream() // + .map(searchDocument -> read(type, searchDocument)) // + .collect(Collectors.toList()); + Aggregations aggregations = searchDocumentResponse.getAggregations(); + return new SearchHits(totalHits, maxScore, scrollId, searchHits, aggregations); + } + + public SearchHit read(Class type, SearchDocument searchDocument) { + + Assert.notNull(type, "type must not be null"); + Assert.notNull(searchDocument, "searchDocument must not be null"); + + String id = searchDocument.hasId() ? searchDocument.getId() : null; + float score = searchDocument.getScore(); + Object[] sortValues = searchDocument.getSortValues(); + T content = mapDocument(searchDocument, type); + + return new SearchHit(id, score, sortValues, content); + } + @Override @Nullable - public R read(Class type, Document source) { - return doRead(source, ClassTypeInformation.from((Class) ClassUtils.getUserClass(type))); + public T mapDocument(@Nullable Document document, Class type) { + + if (document == null) { + return null; + } + + T mappedResult = read(type, document); + + return type.isInterface() || !ClassUtils.isAssignableValue(type, mappedResult) + ? getProjectionFactory().createProjection(type, mappedResult) + : type.cast(mappedResult); } @SuppressWarnings("unchecked") - @Nullable - protected R doRead(Document source, TypeInformation typeHint) { + @Override + public R read(Class type, Document source) { + TypeInformation typeHint = ClassTypeInformation.from((Class) ClassUtils.getUserClass(type)); typeHint = (TypeInformation) typeMapper.readType(source, typeHint); if (conversions.hasCustomReadTarget(Map.class, typeHint.getType())) { - return conversionService.convert(source, typeHint.getType()); + R converted = conversionService.convert(source, typeHint.getType()); + if (converted == null) { + // EntityReader.read is defined as non nullable , so we cannot return null + throw new ConversionException("conversion service to type " + typeHint.getType().getName() + " returned null"); + } + return converted; } if (typeHint.isMap() || ClassTypeInformation.OBJECT.equals(typeHint)) { @@ -177,7 +223,6 @@ public class MappingElasticsearchConverter return readEntity(entity, source); } - @SuppressWarnings("unchecked") protected R readEntity(ElasticsearchPersistentEntity entity, Map source) { ElasticsearchPersistentEntity targetEntity = computeClosestEntity(entity, source); @@ -187,6 +232,7 @@ public class MappingElasticsearchConverter EntityInstantiator instantiator = instantiators.getInstantiatorFor(targetEntity); + @SuppressWarnings("unchecked") R instance = (R) instantiator.createInstance(targetEntity, new PersistentEntityParameterValueProvider<>(targetEntity, propertyValueProvider, null)); @@ -248,7 +294,7 @@ public class MappingElasticsearchConverter Object value = valueProvider.getPropertyValue(prop); if (value != null) { - accessor.setProperty(prop, valueProvider.getPropertyValue(prop)); + accessor.setProperty(prop, value); } } @@ -256,6 +302,7 @@ public class MappingElasticsearchConverter } @SuppressWarnings("unchecked") + @Nullable protected R readValue(@Nullable Object source, ElasticsearchPersistentProperty property, TypeInformation targetType) { @@ -263,11 +310,15 @@ public class MappingElasticsearchConverter return null; } + if (property.hasPropertyConverter() && String.class.isAssignableFrom(source.getClass())) { + source = property.getPropertyConverter().read((String) source); + } + Class rawType = targetType.getType(); if (conversions.hasCustomReadTarget(source.getClass(), rawType)) { return rawType.cast(conversionService.convert(source, rawType)); } else if (source instanceof List) { - return readCollectionValue((List) source, property, targetType); + return readCollectionValue((List) source, property, targetType); } else if (source instanceof Map) { return readMapValue((Map) source, property, targetType); } @@ -275,11 +326,40 @@ public class MappingElasticsearchConverter return (R) readSimpleValue(source, targetType); } + @SuppressWarnings("unchecked") + @Nullable + private R readCollectionValue(@Nullable List source, ElasticsearchPersistentProperty property, + TypeInformation targetType) { + + if (source == null) { + return null; + } + + Collection target = createCollectionForValue(targetType, source.size()); + + for (Object value : source) { + + if (isSimpleType(value)) { + target.add( + readSimpleValue(value, targetType.getComponentType() != null ? targetType.getComponentType() : targetType)); + } else { + + if (value instanceof List) { + target.add(readValue(value, property, property.getTypeInformation().getActualType())); + } else { + target.add(readEntity(computeGenericValueTypeForRead(property, value), (Map) value)); + } + } + } + + return (R) target; + } + @SuppressWarnings("unchecked") private R readMapValue(@Nullable Map source, ElasticsearchPersistentProperty property, TypeInformation targetType) { - TypeInformation information = typeMapper.readType(source); + TypeInformation information = typeMapper.readType(source); if (property.isEntity() && !property.isMap() || information != null) { ElasticsearchPersistentEntity targetEntity = information != null @@ -300,9 +380,9 @@ public class MappingElasticsearchConverter if (targetEntity.getTypeInformation().isMap()) { - Map valueMap = (Map) entry.getValue(); + Map valueMap = (Map) entry.getValue(); if (typeMapper.containsTypeInformation(valueMap)) { - target.put(entry.getKey(), readEntity(targetEntity, (Map) entry.getValue())); + target.put(entry.getKey(), readEntity(targetEntity, valueMap)); } else { target.put(entry.getKey(), readValue(valueMap, property, targetEntity.getTypeInformation())); } @@ -311,7 +391,7 @@ public class MappingElasticsearchConverter target.put(entry.getKey(), readValue(entry.getValue(), property, targetEntity.getTypeInformation().getActualType())); } else { - target.put(entry.getKey(), readEntity(targetEntity, (Map) entry.getValue())); + target.put(entry.getKey(), readEntity(targetEntity, (Map) entry.getValue())); } } } @@ -319,40 +399,13 @@ public class MappingElasticsearchConverter return (R) target; } - @SuppressWarnings("unchecked") - private R readCollectionValue(@Nullable List source, ElasticsearchPersistentProperty property, - TypeInformation targetType) { - - if (source == null) { - return null; - } - - Collection target = createCollectionForValue(targetType, source.size()); - - for (Object value : source) { - - if (isSimpleType(value)) { - target.add( - readSimpleValue(value, targetType.getComponentType() != null ? targetType.getComponentType() : targetType)); - } else { - - if (value instanceof List) { - target.add(readValue(value, property, property.getTypeInformation().getActualType())); - } else { - target.add(readEntity(computeGenericValueTypeForRead(property, value), (Map) value)); - } - } - } - - return (R) target; - } - - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Nullable private Object readSimpleValue(@Nullable Object value, TypeInformation targetType) { Class target = targetType.getType(); - if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) { + if (value == null || ClassUtils.isAssignableValue(target, value)) { return value; } @@ -367,16 +420,38 @@ public class MappingElasticsearchConverter return conversionService.convert(value, target); } - @SuppressWarnings("unchecked") - @Override - public void write(@Nullable Object source, Document sink) { - - if (source == null) { - return; + private void populateScriptFields(T result, SearchDocument searchDocument) { + Map> fields = searchDocument.getFields(); + if (!fields.isEmpty()) { + for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) { + ScriptedField scriptedField = field.getAnnotation(ScriptedField.class); + if (scriptedField != null) { + String name = scriptedField.name().isEmpty() ? field.getName() : scriptedField.name(); + Object value = searchDocument.getFieldValue(name); + if (value != null) { + field.setAccessible(true); + try { + field.set(result, value); + } catch (IllegalArgumentException e) { + throw new ElasticsearchException("failed to set scripted field: " + name + " with value: " + value, e); + } catch (IllegalAccessException e) { + throw new ElasticsearchException("failed to access scripted field: " + name, e); + } + } + } + } } + } + // endregion + + // region write + @Override + public void write(Object source, Document sink) { + + Assert.notNull(source, "source to map must not be null"); if (source instanceof Map) { - + // noinspection unchecked sink.putAll((Map) source); return; } @@ -388,41 +463,22 @@ public class MappingElasticsearchConverter typeMapper.writeType(source.getClass(), sink); } - doWrite(source, sink, type); - } - - protected void doWrite(@Nullable Object source, Document sink, @Nullable TypeInformation typeHint) { - - if (source == null) { - return; - } - - Class entityType = source.getClass(); Optional> customTarget = conversions.getCustomWriteTarget(entityType, Map.class); if (customTarget.isPresent()) { - sink.putAll(conversionService.convert(source, Map.class)); return; } - if (typeHint != null) { + ElasticsearchPersistentEntity entity = type.getType().equals(entityType) + ? mappingContext.getRequiredPersistentEntity(type) + : mappingContext.getRequiredPersistentEntity(entityType); - ElasticsearchPersistentEntity entity = typeHint.getType().equals(entityType) - ? mappingContext.getRequiredPersistentEntity(typeHint) - : mappingContext.getRequiredPersistentEntity(entityType); - - writeEntity(entity, source, sink, null); - return; - } - - // write Entity - ElasticsearchPersistentEntity entity = mappingContext.getRequiredPersistentEntity(entityType); writeEntity(entity, source, sink, null); } protected void writeEntity(ElasticsearchPersistentEntity entity, Object source, Document sink, - @Nullable TypeInformation containingStructure) { + @Nullable TypeInformation containingStructure) { PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); @@ -448,10 +504,18 @@ public class MappingElasticsearchConverter continue; } + if (property.hasPropertyConverter()) { + ElasticsearchPersistentPropertyConverter propertyConverter = property.getPropertyConverter(); + value = propertyConverter.write(value); + } + if (!isSimpleType(value)) { writeProperty(property, value, sink); } else { - sink.set(property, getWriteSimpleValue(value)); + Object writeSimpleValue = getWriteSimpleValue(value); + if (writeSimpleValue != null) { + sink.set(property, writeSimpleValue); + } } } } @@ -461,7 +525,6 @@ public class MappingElasticsearchConverter Optional> customWriteTarget = conversions.getCustomWriteTarget(value.getClass()); if (customWriteTarget.isPresent()) { - Class writeTarget = customWriteTarget.get(); sink.set(property, conversionService.convert(value, writeTarget)); return; @@ -484,12 +547,8 @@ public class MappingElasticsearchConverter sink.set(property, getWriteComplexValue(property, typeHint, value)); } + @Nullable protected Object getWriteSimpleValue(Object value) { - - if (value == null) { - return null; - } - Optional> customTarget = conversions.getCustomWriteTarget(value.getClass()); if (customTarget.isPresent()) { @@ -583,67 +642,30 @@ public class MappingElasticsearchConverter } return target; } + // endregion - @Override - @Nullable - public T mapDocument(@Nullable Document document, Class type) { + // region helper methods + private Collection createCollectionForValue(TypeInformation collectionTypeInformation, int size) { - if (document == null) { - return null; - } + Class collectionType = collectionTypeInformation.isSubTypeOf(Collection.class) // + ? collectionTypeInformation.getType() // + : List.class; - Object mappedResult = read(type, document); + TypeInformation componentType = collectionTypeInformation.getComponentType() != null // + ? collectionTypeInformation.getComponentType() // + : ClassTypeInformation.OBJECT; - if (mappedResult == null) { - return (T) null; - } - - return type.isInterface() || !ClassUtils.isAssignableValue(type, mappedResult) - ? getProjectionFactory().createProjection(type, mappedResult) - : type.cast(mappedResult); + return collectionTypeInformation.getType().isArray() // + ? new ArrayList<>(size) // + : CollectionFactory.createCollection(collectionType, componentType.getType(), size); } - @Override - public SearchHits read(Class type, SearchDocumentResponse searchDocumentResponse) { + private ElasticsearchPersistentEntity computeGenericValueTypeForRead(ElasticsearchPersistentProperty property, + Object value) { - Assert.notNull(type, "type must not be null"); - Assert.notNull(searchDocumentResponse, "searchDocumentResponse must not be null"); - - long totalHits = searchDocumentResponse.getTotalHits(); - float maxScore = searchDocumentResponse.getMaxScore(); - String scrollId = searchDocumentResponse.getScrollId(); - List> searchHits = searchDocumentResponse.getSearchDocuments().stream() // - .map(searchDocument -> read(type, searchDocument)) // - .collect(Collectors.toList()); - Aggregations aggregations = searchDocumentResponse.getAggregations(); - return new SearchHits(totalHits, maxScore, scrollId, searchHits, aggregations); - } - - @Override - public SearchHit read(Class type, SearchDocument searchDocument) { - - Assert.notNull(type, "type must not be null"); - Assert.notNull(searchDocument, "searchDocument must not be null"); - - String id = searchDocument.hasId() ? searchDocument.getId() : null; - float score = searchDocument.getScore(); - Object[] sortValues = searchDocument.getSortValues(); - T content = mapDocument(searchDocument, type); - - return new SearchHit(id, score, sortValues, content); - } - - @Override - public List mapDocuments(List documents, Class type) { - return documents.stream().map(it -> mapDocument(it, type)).collect(Collectors.toList()); - } - - @Override - public Document mapObject(Object source) { - - Document target = Document.create(); - write(source, target); - return target; + return ClassTypeInformation.OBJECT.equals(property.getTypeInformation().getActualType()) + ? mappingContext.getRequiredPersistentEntity(value.getClass()) + : mappingContext.getRequiredPersistentEntity(property.getTypeInformation().getActualType()); } private boolean requiresTypeHint(TypeInformation type, Class actualType, @@ -697,29 +719,6 @@ public class MappingElasticsearchConverter return mappingContext.getRequiredPersistentEntity(typeToUse); } - private ElasticsearchPersistentEntity computeGenericValueTypeForRead(ElasticsearchPersistentProperty property, - Object value) { - - return ClassTypeInformation.OBJECT.equals(property.getTypeInformation().getActualType()) - ? mappingContext.getRequiredPersistentEntity(value.getClass()) - : mappingContext.getRequiredPersistentEntity(property.getTypeInformation().getActualType()); - } - - private Collection createCollectionForValue(TypeInformation collectionTypeInformation, int size) { - - Class collectionType = collectionTypeInformation.isSubTypeOf(Collection.class) // - ? collectionTypeInformation.getType() // - : List.class; - - TypeInformation componentType = collectionTypeInformation.getComponentType() != null // - ? collectionTypeInformation.getComponentType() // - : ClassTypeInformation.OBJECT; - - return collectionTypeInformation.getType().isArray() // - ? new ArrayList<>(size) // - : CollectionFactory.createCollection(collectionType, componentType.getType(), size); - } - private boolean isSimpleType(Object value) { return isSimpleType(value.getClass()); } @@ -727,40 +726,40 @@ public class MappingElasticsearchConverter private boolean isSimpleType(Class type) { return conversions.isSimpleType(type); } + // endregion + // region queries @Override - public AggregatedPage> mapResults(SearchDocumentResponse response, Class type, - Pageable pageable) { + public void updateQuery(CriteriaQuery criteriaQuery, Class domainClass) { + ElasticsearchPersistentEntity persistentEntity = mappingContext.getPersistentEntity(domainClass); - List> results = response.getSearchDocuments().stream() // - .map(searchDocument -> read(type, searchDocument)) // - .collect(Collectors.toList()); + if (persistentEntity != null) { + criteriaQuery.getCriteria().getCriteriaChain().forEach(criteria -> { + String name = criteria.getField().getName(); + ElasticsearchPersistentProperty property = persistentEntity.getPersistentProperty(name); - return new AggregatedPageImpl<>(results, pageable, response); - } + if (property != null && property.getName().equals(name)) { + criteria.getField().setName(property.getFieldName()); - private void populateScriptFields(T result, SearchDocument searchDocument) { - Map> fields = searchDocument.getFields(); - if (!fields.isEmpty()) { - for (java.lang.reflect.Field field : result.getClass().getDeclaredFields()) { - ScriptedField scriptedField = field.getAnnotation(ScriptedField.class); - if (scriptedField != null) { - String name = scriptedField.name().isEmpty() ? field.getName() : scriptedField.name(); - Object value = searchDocument.getFieldValue(name); - if (value != null) { - field.setAccessible(true); - try { - field.set(result, value); - } catch (IllegalArgumentException e) { - throw new ElasticsearchException("failed to set scripted field: " + name + " with value: " + value, e); - } catch (IllegalAccessException e) { - throw new ElasticsearchException("failed to access scripted field: " + name, e); - } + if (property.hasPropertyConverter()) { + ElasticsearchPersistentPropertyConverter propertyConverter = property.getPropertyConverter(); + criteria.getQueryCriteriaEntries().forEach(criteriaEntry -> { + Object value = criteriaEntry.getValue(); + if (value.getClass().isArray()) { + Object[] objects = (Object[]) value; + for (int i = 0; i < objects.length; i++) { + objects[i] = propertyConverter.write(objects[i]); + } + } else { + criteriaEntry.setValue(propertyConverter.write(value)); + } + }); } } - } + }); } } + // endregion static class MapValueAccessor { @@ -770,6 +769,7 @@ public class MappingElasticsearchConverter this.target = target; } + @Nullable public Object get(ElasticsearchPersistentProperty property) { if (target instanceof Document) { @@ -801,7 +801,7 @@ public class MappingElasticsearchConverter Map source = target; Object result = null; - while (source != null && parts.hasNext()) { + while (parts.hasNext()) { result = source.get(parts.next()); @@ -826,22 +826,25 @@ public class MappingElasticsearchConverter target.put(property.getFieldName(), value); } - @SuppressWarnings("unchecked") private Map getAsMap(Object result) { if (result instanceof Map) { - return (Map) result; + // noinspection unchecked + return (Map) result; } throw new IllegalArgumentException(String.format("%s is not a Map.", result)); } } - @RequiredArgsConstructor class ElasticsearchPropertyValueProvider implements PropertyValueProvider { final MapValueAccessor mapValueAccessor; + ElasticsearchPropertyValueProvider(MapValueAccessor mapValueAccessor) { + this.mapValueAccessor = mapValueAccessor; + } + @SuppressWarnings("unchecked") @Override public T getPropertyValue(ElasticsearchPersistentProperty property) { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java index 170c39f8d..e62052741 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java @@ -17,6 +17,7 @@ package org.springframework.data.elasticsearch.core.mapping; import org.springframework.core.convert.converter.Converter; import org.springframework.data.mapping.PersistentProperty; +import org.springframework.lang.Nullable; /** * ElasticsearchPersistentProperty @@ -60,7 +61,20 @@ public interface ElasticsearchPersistentProperty extends PersistentProperty { + /** + * @return true if an {@link ElasticsearchPersistentPropertyConverter} is available for this instance. + * @since 4.0 + */ + boolean hasPropertyConverter(); + + /** + * @return the {@link ElasticsearchPersistentPropertyConverter} for this instance. + * @since 4.0 + */ + @Nullable + ElasticsearchPersistentPropertyConverter getPropertyConverter(); + + enum PropertyToFieldNameConverter implements Converter { INSTANCE; @@ -68,4 +82,17 @@ public interface ElasticsearchPersistentProperty extends PersistentProperty { + + INSTANCE; + + public String convert(ElasticsearchPersistentProperty source) { + return source.getName(); + } + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentPropertyConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentPropertyConverter.java new file mode 100644 index 000000000..24da65fe5 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentPropertyConverter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.mapping; + +/** + * Interface defining methods to convert a property value to a String and back. + * + * @author Peter-Josef Meisch + */ +public interface ElasticsearchPersistentPropertyConverter { + + /** + * converts the property value to a String. + * + * @param property the property value to convert, must not be {@literal null} + * @return String representation. + */ + String write(Object property); + + /** + * converts a property value from a String. + * + * @param s the property to convert, must not be {@literal null} + * @return property value + */ + Object read(String s); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java index cdedcaad3..ee0825e52 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java @@ -15,12 +15,16 @@ */ package org.springframework.data.elasticsearch.core.mapping; +import java.time.temporal.TemporalAccessor; import java.util.Arrays; import java.util.List; +import org.springframework.data.elasticsearch.annotations.DateFormat; import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.Parent; import org.springframework.data.elasticsearch.annotations.Score; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchDateConverter; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; @@ -49,6 +53,7 @@ public class SimpleElasticsearchPersistentProperty extends private final boolean isParent; private final boolean isId; private final @Nullable String annotatedFieldName; + private ElasticsearchPersistentPropertyConverter propertyConverter; public SimpleElasticsearchPersistentProperty(Property property, PersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { @@ -72,6 +77,58 @@ public class SimpleElasticsearchPersistentProperty extends if (isParent && !getType().equals(String.class)) { throw new MappingException(String.format("Parent property %s must be of type String!", property.getName())); } + + initDateConverter(); + } + + @Override + public boolean hasPropertyConverter() { + return propertyConverter != null; + } + + @Override + public ElasticsearchPersistentPropertyConverter getPropertyConverter() { + return propertyConverter; + } + + /** + * Initializes an {@link ElasticsearchPersistentPropertyConverter} if this property is annotated as a Field with type + * {@link FieldType#Date}, has a {@link DateFormat} set and if the type of the property is one of the Java8 temporal + * classes. + */ + private void initDateConverter() { + Field field = findAnnotation(Field.class); + if (field != null && field.type() == FieldType.Date && TemporalAccessor.class.isAssignableFrom(getType())) { + DateFormat dateFormat = field.format(); + + ElasticsearchDateConverter converter = null; + + if (dateFormat == DateFormat.custom) { + String pattern = field.pattern(); + + if (StringUtils.hasLength(pattern)) { + converter = ElasticsearchDateConverter.of(pattern); + } + } else if (dateFormat != DateFormat.none) { + converter = ElasticsearchDateConverter.of(dateFormat); + } + + if (converter != null) { + ElasticsearchDateConverter dateConverter = converter; + propertyConverter = new ElasticsearchPersistentPropertyConverter() { + @Override + public String write(Object property) { + return dateConverter.format((TemporalAccessor) property); + } + + @SuppressWarnings("unchecked") + @Override + public Object read(String s) { + return dateConverter.parse(s, (Class) getType()); + } + }; + } + } } @Nullable diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java index 83ae04429..3a7bc43c4 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Criteria.java @@ -624,6 +624,7 @@ public class Criteria { public static class CriteriaEntry { private OperationKey key; + private Object value; CriteriaEntry(OperationKey key, Object value) { @@ -635,6 +636,10 @@ public class Criteria { return key; } + public void setValue(Object value) { + this.value = value; + } + public Object getValue() { return value; } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/Field.java b/src/main/java/org/springframework/data/elasticsearch/core/query/Field.java index e41ddbd3b..936445231 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/Field.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/Field.java @@ -20,13 +20,11 @@ package org.springframework.data.elasticsearch.core.query; * * @author Rizwan Idrees * @author Mohsin Husen + * @author Peter-Josef Meisch */ public interface Field { - /** - * Get the name of the field used in schema.xml of elasticsearch server - * - * @return - */ + void setName(String name); + String getName(); } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java index 5dc6f6cb8..42dac159d 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SimpleField.java @@ -15,17 +15,26 @@ */ package org.springframework.data.elasticsearch.core.query; +import org.springframework.util.Assert; + /** * The most trivial implementation of a Field * * @author Rizwan Idrees * @author Mohsin Husen + * @author Peter-Josef Meisch */ public class SimpleField implements Field { - private final String name; + private String name; public SimpleField(String name) { + setName(name); + } + + @Override + public void setName(String name) { + Assert.notNull(name, "name must not be null"); this.name = name; } diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java index d2b870e09..38d75d05e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/ElasticsearchPartQuery.java @@ -19,6 +19,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHitSupport; import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.query.CriteriaQuery; @@ -45,12 +46,14 @@ public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery private static final int DEFAULT_STREAM_BATCH_SIZE = 500; private final PartTree tree; + private final ElasticsearchConverter elasticsearchConverter; private final MappingContext mappingContext; public ElasticsearchPartQuery(ElasticsearchQueryMethod method, ElasticsearchOperations elasticsearchOperations) { super(method, elasticsearchOperations); this.tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType()); - this.mappingContext = elasticsearchOperations.getElasticsearchConverter().getMappingContext(); + this.elasticsearchConverter = elasticsearchOperations.getElasticsearchConverter(); + this.mappingContext = elasticsearchConverter.getMappingContext(); } @Override @@ -58,7 +61,10 @@ public class ElasticsearchPartQuery extends AbstractElasticsearchRepositoryQuery ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); CriteriaQuery query = createQuery(accessor); Assert.notNull(query, "unsupported query"); + Class clazz = queryMethod.getEntityInformation().getJavaType(); + elasticsearchConverter.updateQuery(query, clazz); + IndexCoordinates index = elasticsearchOperations.getIndexCoordinatesFor(clazz); Object result = null; diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java index f7f327efd..4f4d9b707 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/ElasticsearchQueryCreator.java @@ -65,7 +65,8 @@ public class ElasticsearchQueryCreator extends AbstractQueryCreator path = context .getPersistentPropertyPath(part.getProperty()); return new CriteriaQuery(from(part, - new Criteria(path.toDotPath(ElasticsearchPersistentProperty.PropertyToFieldNameConverter.INSTANCE)), iterator)); + new Criteria(path.toDotPath(ElasticsearchPersistentProperty.QueryPropertyToFieldNameConverter.INSTANCE)), + iterator)); } @Override @@ -76,7 +77,8 @@ public class ElasticsearchQueryCreator extends AbstractQueryCreator path = context .getPersistentPropertyPath(part.getProperty()); return base.addCriteria(from(part, - new Criteria(path.toDotPath(ElasticsearchPersistentProperty.PropertyToFieldNameConverter.INSTANCE)), iterator)); + new Criteria(path.toDotPath(ElasticsearchPersistentProperty.QueryPropertyToFieldNameConverter.INSTANCE)), + iterator)); } @Override diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/package-info.java b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/package-info.java new file mode 100644 index 000000000..3086bad78 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/repository/query/parser/package-info.java @@ -0,0 +1,5 @@ +/** + * Infrastructure for the Elasticsearch document-to-object mapping subsystem. + */ +@org.springframework.lang.NonNullApi +package org.springframework.data.elasticsearch.repository.query.parser; diff --git a/src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryMappingTests.java b/src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryMappingTests.java new file mode 100644 index 000000000..3176498e4 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryMappingTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core; + +import static org.skyscreamer.jsonassert.JSONAssert.*; + +import java.time.LocalDate; +import java.util.Collections; + +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.DateFormat; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; +import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; + +/** + * Tests for the mapping of {@link CriteriaQuery} by a + * {@link org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter}. In the same package as + * {@link CriteriaQueryProcessor} as this is needed to get the String represenation to assert. + * + * @author Peter-Josef Meisch + */ +public class CriteriaQueryMappingTests { + + MappingElasticsearchConverter mappingElasticsearchConverter; + + @BeforeEach + void setUp() { + SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext(); + mappingContext.setInitialEntitySet(Collections.singleton(Person.class)); + mappingContext.afterPropertiesSet(); + + mappingElasticsearchConverter = new MappingElasticsearchConverter(mappingContext, new GenericConversionService()); + mappingElasticsearchConverter.afterPropertiesSet(); + + } + + @Test + void shouldMapNamesAndConvertValuesInCriteriaQuery() throws JSONException { + + // use POJO properties and types in the query building + CriteriaQuery criteriaQuery = new CriteriaQuery( + new Criteria("birthDate").between(LocalDate.of(1989, 11, 9), LocalDate.of(1990, 11, 9)).or("birthDate").is(LocalDate.of(2019, 12, 28))); + + // mapped field name and converted parameter + String expected = '{' + // + " \"bool\" : {" + // + " \"should\" : [" + // + " {" + // + " \"range\" : {" + // + " \"birth-date\" : {" + // + " \"from\" : \"09.11.1989\"," + // + " \"to\" : \"09.11.1990\"," + // + " \"include_lower\" : true," + // + " \"include_upper\" : true" + // + " }" + // + " }" + // + " }," + // + " {" + // + " \"query_string\" : {" + // + " \"query\" : \"28.12.2019\"," + // + " \"fields\" : [" + // + " \"birth-date^1.0\"" + // + " ]" + // + " }" + // + " }" + // + " ]" + // + " }" + // + '}'; // + + mappingElasticsearchConverter.updateQuery(criteriaQuery, Person.class); + String queryString = new CriteriaQueryProcessor().createQueryFromCriteria(criteriaQuery.getCriteria()).toString(); + + assertEquals(expected, queryString, false); + } + + static class Person { + @Id String id; + @Field(name = "first-name") String firstName; + @Field(name = "last-name") String lastName; + @Field(name = "birth-date", type = FieldType.Date, format = DateFormat.custom, + pattern = "dd.MM.yyyy") LocalDate birthDate; + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverterTests.java b/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverterTests.java new file mode 100644 index 000000000..50fe532f3 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverterTests.java @@ -0,0 +1,50 @@ +package org.springframework.data.elasticsearch.core.convert; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.data.elasticsearch.annotations.DateFormat; + +/** + * @author Peter-Josef Meisch + */ +class ElasticsearchDateConverterTests { + + @ParameterizedTest + @EnumSource(DateFormat.class) + void shouldCreateConvertersForAllKnownFormats(DateFormat dateFormat) { + + if (dateFormat == DateFormat.none) { + return; + } + String pattern = (dateFormat != DateFormat.custom) ? dateFormat.name() : "dd.MM.yyyy"; + + ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(pattern); + + assertThat(converter).isNotNull(); + } + + @Test + void shouldConvertToString() { + LocalDate localDate = LocalDate.of(2019, 12, 27); + ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.basic_date); + + String formatted = converter.format(localDate); + + assertThat(formatted).isEqualTo("20191227"); + } + + @Test + void shouldParseFromString() { + LocalDate localDate = LocalDate.of(2019, 12, 27); + ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.basic_date); + + LocalDate parsed = converter.parse("20191227", LocalDate.class); + + assertThat(parsed).isEqualTo(localDate); + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java index adea4003d..009618c8d 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.elasticsearch.core.convert; import static org.assertj.core.api.Assertions.*; +import static org.skyscreamer.jsonassert.JSONAssert.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -26,16 +27,17 @@ import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import java.io.IOException; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import org.json.JSONException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.convert.ConversionService; @@ -47,10 +49,15 @@ import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; +import org.springframework.data.elasticsearch.annotations.DateFormat; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.GeoPointField; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.geo.GeoPoint; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; import org.springframework.data.geo.Box; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Point; @@ -294,7 +301,7 @@ public class MappingElasticsearchConverterUnitTests { public void writesNestedEntity() { Person person = new Person(); - person.birthdate = new Date(); + person.birthDate = LocalDate.now(); person.gender = Gender.MAN; person.address = observatoryRoad; @@ -574,6 +581,45 @@ public class MappingElasticsearchConverterUnitTests { assertThat(target.address).isEqualTo(bigBunsCafe); } + @Test // DATAES-716 + void shouldWriteLocalDate() throws JSONException { + Person person = new Person(); + person.id = "4711"; + person.firstName = "John"; + person.lastName = "Doe"; + person.birthDate = LocalDate.of(2000, 8, 22); + person.gender = Gender.MAN; + + String expected = '{' + // + " \"id\": \"4711\"," + // + " \"first-name\": \"John\"," + // + " \"last-name\": \"Doe\"," + // + " \"birth-date\": \"22.08.2000\"," + // + " \"gender\": \"MAN\"" + // + '}'; + Document document = Document.create(); + mappingElasticsearchConverter.write(person, document); + String json = document.toJson(); + + assertEquals(expected, json, false); + } + + @Test + void shouldReadLocalDate() { + Document document = Document.create(); + document.put("id", "4711"); + document.put("first-name", "John"); + document.put("last-name", "Doe"); + document.put("birth-date", "22.08.2000"); + document.put("gender", "MAN"); + + Person person = mappingElasticsearchConverter.read(Person.class, document); + + assertThat(person.getId()).isEqualTo("4711"); + assertThat(person.getBirthDate()).isEqualTo(LocalDate.of(2000, 8, 22)); + assertThat(person.getGender()).isEqualTo(Gender.MAN); + } + private String pointTemplate(String name, Point point) { return String.format(Locale.ENGLISH, "\"%s\":{\"lat\":%.1f,\"lon\":%.1f}", name, point.getX(), point.getY()); } @@ -598,7 +644,10 @@ public class MappingElasticsearchConverterUnitTests { @Id String id; String name; - Date birthdate; + @Field(name = "first-name") String firstName; + @Field(name = "last-name") String lastName; + @Field(name = "birth-date", type = FieldType.Date, format = DateFormat.custom, + pattern = "dd.MM.yyyy") LocalDate birthDate; Gender gender; Address address; @@ -759,5 +808,4 @@ public class MappingElasticsearchConverterUnitTests { @GeoPointField private double[] pointD; } - } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderTests.java index 6cc8d75d1..508a64f81 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderTests.java @@ -33,6 +33,7 @@ import java.lang.Boolean; import java.lang.Double; import java.lang.Integer; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -957,7 +958,7 @@ public class MappingBuilderTests extends MappingContextBaseTests { @Field(copyTo = { "foo", "bar" }) private String copyTo; @Field(ignoreAbove = 42) private String ignoreAbove; @Field(type = FieldType.Integer) private String type; - @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "YYYYMMDD") private String date; + @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "YYYYMMDD") private LocalDate date; @Field(analyzer = "ana", searchAnalyzer = "sana", normalizer = "norma") private String analyzers; @Field(type = Keyword, docValues = true) private String docValuesTrue; @Field(type = Keyword, docValues = false) private String docValuesFalse; diff --git a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentPropertyUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentPropertyUnitTests.java index 88f7355d7..38da81b2a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentPropertyUnitTests.java @@ -17,8 +17,14 @@ package org.springframework.data.elasticsearch.core.mapping; import static org.assertj.core.api.Assertions.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Date; + import org.junit.jupiter.api.Test; +import org.springframework.data.elasticsearch.annotations.DateFormat; import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.Score; import org.springframework.data.mapping.MappingException; @@ -62,6 +68,47 @@ public class SimpleElasticsearchPersistentPropertyUnitTests { assertThat(persistentProperty.getFieldName()).isEqualTo("by-value"); } + @Test + // DATAES-716 + void shouldSetPropertyConverters() { + SimpleElasticsearchPersistentEntity persistentEntity = context.getRequiredPersistentEntity(DatesProperty.class); + + ElasticsearchPersistentProperty persistentProperty = persistentEntity.getRequiredPersistentProperty("date"); + assertThat(persistentProperty.hasPropertyConverter()).isFalse(); + + persistentProperty = persistentEntity.getRequiredPersistentProperty("localDate"); + assertThat(persistentProperty.hasPropertyConverter()).isTrue(); + assertThat(persistentProperty.getPropertyConverter()).isNotNull(); + + persistentProperty = persistentEntity.getRequiredPersistentProperty("localDateTime"); + assertThat(persistentProperty.hasPropertyConverter()).isTrue(); + assertThat(persistentProperty.getPropertyConverter()).isNotNull(); + } + + @Test + // DATAES-716 + void shouldConvertFromLocalDate() { + SimpleElasticsearchPersistentEntity persistentEntity = context.getRequiredPersistentEntity(DatesProperty.class); + ElasticsearchPersistentProperty persistentProperty = persistentEntity.getRequiredPersistentProperty("localDate"); + LocalDate localDate = LocalDate.of(2019, 12, 27); + + String converted = persistentProperty.getPropertyConverter().write(localDate); + + assertThat(converted).isEqualTo("27.12.2019"); + } + + @Test + // DATAES-716 + void shouldConvertToLocalDate() { + SimpleElasticsearchPersistentEntity persistentEntity = context.getRequiredPersistentEntity(DatesProperty.class); + ElasticsearchPersistentProperty persistentProperty = persistentEntity.getRequiredPersistentProperty("localDate"); + + Object converted = persistentProperty.getPropertyConverter().read("27.12.2019"); + + assertThat(converted).isInstanceOf(LocalDate.class); + assertThat(converted).isEqualTo(LocalDate.of(2019, 12, 27)); + } + static class InvalidScoreProperty { @Score String scoreProperty; } @@ -73,4 +120,10 @@ public class SimpleElasticsearchPersistentPropertyUnitTests { static class FieldValueProperty { @Field(value = "by-value") String fieldProperty; } + + static class DatesProperty { + @Field(type = FieldType.Date, format = DateFormat.basic_date) Date date; + @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "dd.MM.yyyy") LocalDate localDate; + @Field(type = FieldType.Date, format = DateFormat.basic_date_time) LocalDateTime localDateTime; + } }