Allow multiple date formats for date fields.

Original Pull Request #1728 
Closes #1727
This commit is contained in:
Sascha Woo 2021-03-17 22:39:12 +01:00 committed by GitHub
parent db39b9e27c
commit 31b488d08f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 123 additions and 61 deletions

View File

@ -56,16 +56,18 @@ Default value is _EXTERNAL_.
Constructor arguments are mapped by name to the key values in the retrieved Document.
* `@Field`: Applied at the field level and defines properties of the field, most of the attributes map to the respective https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html[Elasticsearch Mapping] definitions (the following list is not complete, check the annotation Javadoc for a complete reference):
** `name`: The name of the field as it will be represented in the Elasticsearch document, if not set, the Java field name is used.
** `type`: the field type, can be one of _Text, Keyword, Long, Integer, Short, Byte, Double, Float, Half_Float, Scaled_Float, Date, Date_Nanos, Boolean, Binary, Integer_Range, Float_Range, Long_Range, Double_Range, Date_Range, Ip_Range, Object, Nested, Ip, TokenCount, Percolator, Flattened, Search_As_You_Type_.
** `type`: The field type, can be one of _Text, Keyword, Long, Integer, Short, Byte, Double, Float, Half_Float, Scaled_Float, Date, Date_Nanos, Boolean, Binary, Integer_Range, Float_Range, Long_Range, Double_Range, Date_Range, Ip_Range, Object, Nested, Ip, TokenCount, Percolator, Flattened, Search_As_You_Type_.
See https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html[Elasticsearch Mapping Types]
** `format` and `pattern` definitions for the _Date_ type.
** `format`: One or more built-in formats, default value is _strict_date_optional_time_ and _epoch_millis_.
See https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html#built-in-date-formats[Elasticsearch Built In Formats]
** `pattern`: One or more custom date formats. NOTE: If you want to use only custom date formats, you must set the `format` property to empty `{}`.
See https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html#custom-date-formats[Elasticsearch Custom Date Formats]
** `store`: Flag whether the original field value should be store in Elasticsearch, default value is _false_.
** `analyzer`, `searchAnalyzer`, `normalizer` for specifying custom analyzers and normalizer.
* `@GeoPoint`: marks a field as _geo_point_ datatype.
* `@GeoPoint`: Marks a field as _geo_point_ datatype.
Can be omitted if the field is an instance of the `GeoPoint` class.
NOTE: Properties that derive from `TemporalAccessor` or are of type `java.util.Date` must either have a `@Field` annotation of type `FieldType.Date` and a
format different from `DateFormat.none` or a custom converter must be registered for this type. +
NOTE: Properties that derive from `TemporalAccessor` or are of type `java.util.Date` must either have a `@Field` annotation of type `FieldType.Date`.
If you are using a custom date format, you need to use _uuuu_ for the year instead of _yyyy_.
This is due to a https://www.elastic.co/guide/en/elasticsearch/reference/current/migrate-to-java-time.html#java-time-migration-incompatible-date-formats[change in Elasticsearch 7].

View File

@ -23,9 +23,21 @@ package org.springframework.data.elasticsearch.annotations;
* @author Jakub Vavrik
* @author Tim te Beek
* @author Peter-Josef Meisch
* @author Sascha Woo
*/
public enum DateFormat {
/**
* @deprecated since 4.2, will be removed in a future version. Use <code>format = {}</code> to disable built-in date
* formats in the @Field annotation.
*/
@Deprecated
none(""), //
/**
* @deprecated since 4.2, will be removed in a future version.It is no longer required for using a custom date format
* pattern. If you want to use only a custom date format pattern, you must set the <code>format</code>
* property to empty <code>{}</code>.
*/
@Deprecated
custom(""), //
basic_date("uuuuMMdd"), //
basic_date_time("uuuuMMdd'T'HHmmss.SSSXXX"), //

View File

@ -36,6 +36,7 @@ import org.springframework.core.annotation.AliasFor;
* @author Aleksei Arsenev
* @author Brian Kimmig
* @author Morgan Lutz
* @author Sascha Woo
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@ -65,9 +66,9 @@ public @interface Field {
boolean index() default true;
DateFormat format() default DateFormat.none;
DateFormat[] format() default { DateFormat.date_optional_time, DateFormat.epoch_millis };
String pattern() default "";
String[] pattern() default {};
boolean store() default false;

View File

@ -40,9 +40,9 @@ public @interface InnerField {
boolean index() default true;
DateFormat format() default DateFormat.none;
DateFormat[] format() default { DateFormat.date_optional_time, DateFormat.epoch_millis };
String pattern() default "";
String[] pattern() default {};
boolean store() default false;

View File

@ -17,6 +17,9 @@ package org.springframework.data.elasticsearch.core.index;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.xcontent.XContentBuilder;
@ -41,6 +44,7 @@ import org.springframework.util.StringUtils;
* @author Aleksei Arsenev
* @author Brian Kimmig
* @author Morgan Lutz
* @author Sascha Woo
* @since 4.0
*/
public final class MappingParameters {
@ -78,12 +82,12 @@ public final class MappingParameters {
private final String analyzer;
private final boolean coerce;
@Nullable private final String[] copyTo;
private final String datePattern;
private final DateFormat[] dateFormats;
private final String[] dateFormatPatterns;
private final boolean docValues;
private final boolean eagerGlobalOrdinals;
private final boolean enabled;
private final boolean fielddata;
private final DateFormat format;
@Nullable private final Integer ignoreAbove;
private final boolean ignoreMalformed;
private final boolean index;
@ -129,8 +133,8 @@ public final class MappingParameters {
store = field.store();
fielddata = field.fielddata();
type = field.type();
format = field.format();
datePattern = field.pattern();
dateFormats = field.format();
dateFormatPatterns = field.pattern();
analyzer = field.analyzer();
searchAnalyzer = field.searchAnalyzer();
normalizer = field.normalizer();
@ -171,8 +175,8 @@ public final class MappingParameters {
store = field.store();
fielddata = field.fielddata();
type = field.type();
format = field.format();
datePattern = field.pattern();
dateFormats = field.format();
dateFormatPatterns = field.pattern();
analyzer = field.analyzer();
searchAnalyzer = field.searchAnalyzer();
normalizer = field.normalizer();
@ -226,8 +230,24 @@ public final class MappingParameters {
if (type != FieldType.Auto) {
builder.field(FIELD_PARAM_TYPE, type.name().toLowerCase());
if (type == FieldType.Date && format != DateFormat.none) {
builder.field(FIELD_PARAM_FORMAT, format == DateFormat.custom ? datePattern : format.toString());
if (type == FieldType.Date) {
List<String> formats = new ArrayList<>();
// built-in formats
for (DateFormat dateFormat : dateFormats) {
if (dateFormat == DateFormat.none || dateFormat == DateFormat.custom) {
continue;
}
formats.add(dateFormat.toString());
}
// custom date formats
Collections.addAll(formats, dateFormatPatterns);
if (!formats.isEmpty()) {
builder.field(FIELD_PARAM_FORMAT, String.join("||", formats));
}
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.data.elasticsearch.core.mapping;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@ -154,51 +155,74 @@ public class SimpleElasticsearchPersistentProperty extends
if (field != null && (field.type() == FieldType.Date || field.type() == FieldType.Date_Nanos)
&& (isTemporalAccessor || isDate)) {
DateFormat dateFormat = field.format();
DateFormat[] dateFormats = field.format();
String[] dateFormatPatterns = field.pattern();
String property = getOwner().getType().getSimpleName() + "." + getName();
if (dateFormat == DateFormat.none) {
if (dateFormats.length == 0 && dateFormatPatterns.length == 0) {
LOGGER.warn(
String.format("No DateFormat defined for property %s. Make sure you have a Converter registered for %s",
property, actualType.getSimpleName()));
"Property '{}' has @Field type '{}' but has no built-in format or custom date pattern defined. Make sure you have a converter registered for type {}.",
property, field.type().name(), actualType.getSimpleName());
return;
}
ElasticsearchDateConverter converter = null;
if (dateFormat == DateFormat.custom) {
String pattern = field.pattern();
if (!StringUtils.hasLength(pattern)) {
throw new MappingException(
String.format("Property %s is annotated with FieldType.%s and a custom format but has no pattern defined",
property, field.type().name()));
}
converter = ElasticsearchDateConverter.of(pattern);
} else {
List<ElasticsearchDateConverter> converters = new ArrayList<>();
// register converters for built-in formats
for (DateFormat dateFormat : dateFormats) {
switch (dateFormat) {
case none:
case custom:
break;
case weekyear:
case weekyear_week:
case weekyear_week_day:
LOGGER.warn("no Converter available for " + actualType.getName() + " and date format " + dateFormat.name()
+ ". Use a custom converter instead");
LOGGER.warn("No default converter available for '{}' and date format '{}'. Use a custom converter instead.",
actualType.getName(), dateFormat.name());
break;
default:
converter = ElasticsearchDateConverter.of(dateFormat);
converters.add(ElasticsearchDateConverter.of(dateFormat));
break;
}
}
if (converter != null) {
ElasticsearchDateConverter finalConverter = converter;
// register converters for custom formats
for (String dateFormatPattern : dateFormatPatterns) {
if (!StringUtils.hasText(dateFormatPattern)) {
throw new MappingException(
String.format("Date pattern of property '%s' must not be empty", property));
}
converters.add(ElasticsearchDateConverter.of(dateFormatPattern));
}
if (!converters.isEmpty()) {
propertyConverter = new ElasticsearchPersistentPropertyConverter() {
final ElasticsearchDateConverter dateConverter = finalConverter;
final List<ElasticsearchDateConverter> dateConverters = converters;
@SuppressWarnings("unchecked")
@Override
public Object read(String s) {
for (ElasticsearchDateConverter dateConverter : dateConverters) {
try {
if (isTemporalAccessor) {
return dateConverter.parse(s, (Class<? extends TemporalAccessor>) actualType);
} else { // must be date
return dateConverter.parse(s);
}
} catch (Exception e) {
LOGGER.trace(e.getMessage(), e);
}
}
throw new RuntimeException(String
.format("Unable to parse date value '%s' of property '%s' with configured converters", s, property));
}
@Override
public String write(Object property) {
ElasticsearchDateConverter dateConverter = dateConverters.get(0);
if (isTemporalAccessor && TemporalAccessor.class.isAssignableFrom(property.getClass())) {
return dateConverter.format((TemporalAccessor) property);
} else if (isDate && Date.class.isAssignableFrom(property.getClass())) {
@ -207,16 +231,6 @@ public class SimpleElasticsearchPersistentProperty extends
return property.toString();
}
}
@SuppressWarnings("unchecked")
@Override
public Object read(String s) {
if (isTemporalAccessor) {
return dateConverter.parse(s, (Class<? extends TemporalAccessor>) actualType);
} else { // must be date
return dateConverter.parse(s);
}
}
};
}
}

View File

@ -48,6 +48,7 @@ import org.springframework.lang.Nullable;
* {@link CriteriaQueryProcessor} as this is needed to get the String representation to assert.
*
* @author Peter-Josef Meisch
* @author Sascha Woo
*/
public class CriteriaQueryMappingUnitTests {
@ -346,8 +347,7 @@ public class CriteriaQueryMappingUnitTests {
@Nullable @Field(name = "first-name") String firstName;
@Nullable @Field(name = "last-name") String lastName;
@Nullable @Field(name = "created-date", type = FieldType.Date, format = DateFormat.epoch_millis) Date createdDate;
@Nullable @Field(name = "birth-date", type = FieldType.Date, format = DateFormat.custom,
pattern = "dd.MM.uuuu") LocalDate birthDate;
@Nullable @Field(name = "birth-date", type = FieldType.Date, format = {}, pattern = "dd.MM.uuuu") LocalDate birthDate;
}
static class GeoShapeEntity {

View File

@ -23,6 +23,7 @@ import org.springframework.data.elasticsearch.annotations.DateFormat;
/**
* @author Peter-Josef Meisch
* @author Tim te Beek
* @author Sascha Woo
*/
class ElasticsearchDateConverterUnitTests {
@ -34,16 +35,28 @@ class ElasticsearchDateConverterUnitTests {
switch (dateFormat) {
case none:
case custom:
case weekyear:
case weekyear_week:
case weekyear_week_day:
return;
}
String pattern = (dateFormat != DateFormat.custom) ? dateFormat.name() : "dd.MM.uuuu";
ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(dateFormat.name());
assertThat(converter).isNotNull();
}
@Test // DATAES-716
void shouldCreateConvertersForDateFormatPattern() {
// given
String pattern = "dd.MM.uuuu";
// when
ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(pattern);
// then
assertThat(converter).isNotNull();
}

View File

@ -83,6 +83,7 @@ import org.springframework.lang.Nullable;
* @author Peter-Josef Meisch
* @author Konrad Kurdej
* @author Roman Puchkovskiy
* @author Sascha Woo
*/
public class MappingElasticsearchConverterUnitTests {
@ -1218,8 +1219,7 @@ public class MappingElasticsearchConverterUnitTests {
String name;
@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.uuuu") LocalDate birthDate;
@Field(name = "birth-date", type = FieldType.Date, format = {}, pattern = "dd.MM.uuuu") LocalDate birthDate;
Gender gender;
Address address;

View File

@ -921,7 +921,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
@Nullable @Field(copyTo = { "foo", "bar" }) private String copyTo;
@Nullable @Field(ignoreAbove = 42) private String ignoreAbove;
@Nullable @Field(type = FieldType.Integer) private String type;
@Nullable @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "YYYYMMDD") private LocalDate date;
@Nullable @Field(type = FieldType.Date, format = {}, pattern = "YYYYMMDD") private LocalDate date;
@Nullable @Field(analyzer = "ana", searchAnalyzer = "sana", normalizer = "norma") private String analyzers;
@Nullable @Field(type = Keyword) private String docValuesTrue;
@Nullable @Field(type = Keyword, docValues = false) private String docValuesFalse;

View File

@ -36,6 +36,7 @@ import org.springframework.data.elasticsearch.core.MappingContextBaseTests;
* @author Mohsin Husen
* @author Don Wellington
* @author Peter-Josef Meisch
* @author Sascha Woo
*/
public class SimpleElasticsearchDateMappingTests extends MappingContextBaseTests {
@ -62,8 +63,7 @@ public class SimpleElasticsearchDateMappingTests extends MappingContextBaseTests
@Field(type = Text, index = false, store = true, analyzer = "standard") private String message;
@Field(type = Date, format = DateFormat.custom,
pattern = "dd.MM.uuuu hh:mm") private LocalDateTime customFormatDate;
@Field(type = Date, format = {}, pattern = "dd.MM.uuuu hh:mm") private LocalDateTime customFormatDate;
@Field(type = FieldType.Date, format = DateFormat.basic_date) private LocalDateTime basicFormatDate;
}

View File

@ -50,6 +50,7 @@ import org.springframework.util.ReflectionUtils;
* @author Oliver Gierke
* @author Peter-Josef Meisch
* @author Roman Puchkovskiy
* @author Sascha Woo
*/
public class SimpleElasticsearchPersistentPropertyUnitTests {
@ -257,11 +258,10 @@ public class SimpleElasticsearchPersistentPropertyUnitTests {
}
static class DatesProperty {
@Nullable @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "dd.MM.uuuu") LocalDate localDate;
@Nullable @Field(type = FieldType.Date, format = {}, pattern = "dd.MM.uuuu") LocalDate localDate;
@Nullable @Field(type = FieldType.Date, format = DateFormat.basic_date_time) LocalDateTime localDateTime;
@Nullable @Field(type = FieldType.Date, format = DateFormat.basic_date_time) Date legacyDate;
@Nullable @Field(type = FieldType.Date, format = DateFormat.custom,
pattern = "dd.MM.uuuu") List<LocalDate> localDateList;
@Nullable @Field(type = FieldType.Date, format = {}, pattern = "dd.MM.uuuu") List<LocalDate> localDateList;
}
@Data