Skip to content

Commit 7bac792

Browse files
committed
Allow multiple date formats for date fields
1 parent 120eed0 commit 7bac792

12 files changed

+122
-62
lines changed

Diff for: src/main/asciidoc/reference/elasticsearch-object-mapping.adoc

+7-5
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,18 @@ Default value is _EXTERNAL_.
5656
Constructor arguments are mapped by name to the key values in the retrieved Document.
5757
* `@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):
5858
** `name`: The name of the field as it will be represented in the Elasticsearch document, if not set, the Java field name is used.
59-
** `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_.
59+
** `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_.
6060
See https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html[Elasticsearch Mapping Types]
61-
** `format` and `pattern` definitions for the _Date_ type.
61+
** `format`: One or more built-in formats, default value is _strict_date_optional_time_ and _epoch_millis_.
62+
See https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html#built-in-date-formats[Elasticsearch Built In Formats]
63+
** `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 `{}`.
64+
See https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html#custom-date-formats[Elasticsearch Custom Date Formats]
6265
** `store`: Flag whether the original field value should be store in Elasticsearch, default value is _false_.
6366
** `analyzer`, `searchAnalyzer`, `normalizer` for specifying custom analyzers and normalizer.
64-
* `@GeoPoint`: marks a field as _geo_point_ datatype.
67+
* `@GeoPoint`: Marks a field as _geo_point_ datatype.
6568
Can be omitted if the field is an instance of the `GeoPoint` class.
6669

67-
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
68-
format different from `DateFormat.none` or a custom converter must be registered for this type. +
70+
NOTE: Properties that derive from `TemporalAccessor` or are of type `java.util.Date` must either have a `@Field` annotation of type `FieldType.Date`.
6971
If you are using a custom date format, you need to use _uuuu_ for the year instead of _yyyy_.
7072
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].
7173

Diff for: src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java

+9
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,18 @@
2323
* @author Jakub Vavrik
2424
* @author Tim te Beek
2525
* @author Peter-Josef Meisch
26+
* @author Sascha Woo
2627
*/
2728
public enum DateFormat {
29+
/**
30+
* @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.
31+
*/
32+
@Deprecated
2833
none(""), //
34+
/**
35+
* @deprecated since 4.2, will be removed in a future version. It is no longer necessary to use a date format pattern in the @Field annotation.
36+
*/
37+
@Deprecated
2938
custom(""), //
3039
basic_date("uuuuMMdd"), //
3140
basic_date_time("uuuuMMdd'T'HHmmss.SSSXXX"), //

Diff for: src/main/java/org/springframework/data/elasticsearch/annotations/Field.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
* @author Aleksei Arsenev
3737
* @author Brian Kimmig
3838
* @author Morgan Lutz
39+
* @author Sascha Woo
3940
*/
4041
@Retention(RetentionPolicy.RUNTIME)
4142
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@@ -65,9 +66,9 @@
6566

6667
boolean index() default true;
6768

68-
DateFormat format() default DateFormat.none;
69+
DateFormat[] format() default { DateFormat.date_optional_time, DateFormat.epoch_millis };
6970

70-
String pattern() default "";
71+
String[] pattern() default {};
7172

7273
boolean store() default false;
7374

Diff for: src/main/java/org/springframework/data/elasticsearch/annotations/InnerField.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@
4040

4141
boolean index() default true;
4242

43-
DateFormat format() default DateFormat.none;
43+
DateFormat[] format() default { DateFormat.date_optional_time, DateFormat.epoch_millis };
4444

45-
String pattern() default "";
45+
String[] pattern() default {};
4646

4747
boolean store() default false;
4848

Diff for: src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java

+28-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
import java.io.IOException;
1919
import java.lang.annotation.Annotation;
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.List;
2023

2124
import org.elasticsearch.common.Nullable;
2225
import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -41,6 +44,7 @@
4144
* @author Aleksei Arsenev
4245
* @author Brian Kimmig
4346
* @author Morgan Lutz
47+
* @author Sascha Woo
4448
* @since 4.0
4549
*/
4650
public final class MappingParameters {
@@ -78,12 +82,12 @@ public final class MappingParameters {
7882
private final String analyzer;
7983
private final boolean coerce;
8084
@Nullable private final String[] copyTo;
81-
private final String datePattern;
85+
private final DateFormat[] dateFormats;
86+
private final String[] dateFormatPatterns;
8287
private final boolean docValues;
8388
private final boolean eagerGlobalOrdinals;
8489
private final boolean enabled;
8590
private final boolean fielddata;
86-
private final DateFormat format;
8791
@Nullable private final Integer ignoreAbove;
8892
private final boolean ignoreMalformed;
8993
private final boolean index;
@@ -129,8 +133,8 @@ private MappingParameters(Field field) {
129133
store = field.store();
130134
fielddata = field.fielddata();
131135
type = field.type();
132-
format = field.format();
133-
datePattern = field.pattern();
136+
dateFormats = field.format();
137+
dateFormatPatterns = field.pattern();
134138
analyzer = field.analyzer();
135139
searchAnalyzer = field.searchAnalyzer();
136140
normalizer = field.normalizer();
@@ -171,8 +175,8 @@ private MappingParameters(InnerField field) {
171175
store = field.store();
172176
fielddata = field.fielddata();
173177
type = field.type();
174-
format = field.format();
175-
datePattern = field.pattern();
178+
dateFormats = field.format();
179+
dateFormatPatterns = field.pattern();
176180
analyzer = field.analyzer();
177181
searchAnalyzer = field.searchAnalyzer();
178182
normalizer = field.normalizer();
@@ -226,8 +230,24 @@ public void writeTypeAndParametersTo(XContentBuilder builder) throws IOException
226230

227231
if (type != FieldType.Auto) {
228232
builder.field(FIELD_PARAM_TYPE, type.name().toLowerCase());
229-
if (type == FieldType.Date && format != DateFormat.none) {
230-
builder.field(FIELD_PARAM_FORMAT, format == DateFormat.custom ? datePattern : format.toString());
233+
234+
if (type == FieldType.Date) {
235+
List<String> formats = new ArrayList<>();
236+
237+
// built-in formats
238+
for (DateFormat dateFormat : dateFormats) {
239+
if (dateFormat == DateFormat.none || dateFormat == DateFormat.custom) {
240+
continue;
241+
}
242+
formats.add(dateFormat.toString());
243+
}
244+
245+
// custom date formats
246+
Collections.addAll(formats, dateFormatPatterns);
247+
248+
if (!formats.isEmpty()) {
249+
builder.field(FIELD_PARAM_FORMAT, String.join("||", formats));
250+
}
231251
}
232252
}
233253

Diff for: src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java

+49-34
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.elasticsearch.core.mapping;
1717

1818
import java.time.temporal.TemporalAccessor;
19+
import java.util.ArrayList;
1920
import java.util.Arrays;
2021
import java.util.Date;
2122
import java.util.List;
@@ -154,51 +155,75 @@ private void initDateConverter() {
154155

155156
if (field != null && (field.type() == FieldType.Date || field.type() == FieldType.Date_Nanos)
156157
&& (isTemporalAccessor || isDate)) {
157-
DateFormat dateFormat = field.format();
158+
159+
DateFormat[] dateFormats = field.format();
160+
String[] dateFormatPatterns = field.pattern();
158161

159162
String property = getOwner().getType().getSimpleName() + "." + getName();
160163

161-
if (dateFormat == DateFormat.none) {
162-
LOGGER.warn(
163-
String.format("No DateFormat defined for property %s. Make sure you have a Converter registered for %s",
164-
property, actualType.getSimpleName()));
164+
if (dateFormats.length == 0 && dateFormatPatterns.length == 0) {
165+
LOGGER.warn(String.format(
166+
"Property '%s' has @Field type '%s' but has no built-in format or custom date pattern defined. Make sure you have a converter registered for type %s.",
167+
property, field.type().name(), actualType.getSimpleName()));
165168
return;
166169
}
167170

168-
ElasticsearchDateConverter converter = null;
169-
170-
if (dateFormat == DateFormat.custom) {
171-
String pattern = field.pattern();
172-
173-
if (!StringUtils.hasLength(pattern)) {
174-
throw new MappingException(
175-
String.format("Property %s is annotated with FieldType.%s and a custom format but has no pattern defined",
176-
property, field.type().name()));
177-
}
178-
179-
converter = ElasticsearchDateConverter.of(pattern);
180-
} else {
171+
List<ElasticsearchDateConverter> converters = new ArrayList<>();
181172

173+
// register converters for built-in formats
174+
for (DateFormat dateFormat : dateFormats) {
182175
switch (dateFormat) {
176+
case none:
177+
case custom:
178+
break;
183179
case weekyear:
184180
case weekyear_week:
185181
case weekyear_week_day:
186-
LOGGER.warn("no Converter available for " + actualType.getName() + " and date format " + dateFormat.name()
187-
+ ". Use a custom converter instead");
182+
LOGGER.warn(String.format(
183+
"No default converter available for '%s' and date format '%s'. Use a custom converter instead.",
184+
actualType.getName(), dateFormat.name()));
188185
break;
189186
default:
190-
converter = ElasticsearchDateConverter.of(dateFormat);
187+
converters.add(ElasticsearchDateConverter.of(dateFormat));
191188
break;
192189
}
193190
}
194191

195-
if (converter != null) {
196-
ElasticsearchDateConverter finalConverter = converter;
192+
// register converters for custom formats
193+
for (String dateFormatPattern : dateFormatPatterns) {
194+
if (!StringUtils.hasText(dateFormatPattern)) {
195+
throw new MappingException(
196+
String.format("Date pattern of property '%s' must not be empty", property));
197+
}
198+
converters.add(ElasticsearchDateConverter.of(dateFormatPattern));
199+
}
200+
201+
if (!converters.isEmpty()) {
197202
propertyConverter = new ElasticsearchPersistentPropertyConverter() {
198-
final ElasticsearchDateConverter dateConverter = finalConverter;
203+
final List<ElasticsearchDateConverter> dateConverters = converters;
204+
205+
@SuppressWarnings("unchecked")
206+
@Override
207+
public Object read(String s) {
208+
for (ElasticsearchDateConverter dateConverter : dateConverters) {
209+
try {
210+
if (isTemporalAccessor) {
211+
return dateConverter.parse(s, (Class<? extends TemporalAccessor>) actualType);
212+
} else { // must be date
213+
return dateConverter.parse(s);
214+
}
215+
} catch (Exception e) {
216+
LOGGER.trace(e.getMessage(), e);
217+
}
218+
}
219+
220+
throw new RuntimeException(String
221+
.format("Unable to parse date value '%s' of property '%s' with configured converters", s, property));
222+
}
199223

200224
@Override
201225
public String write(Object property) {
226+
ElasticsearchDateConverter dateConverter = dateConverters.get(0);
202227
if (isTemporalAccessor && TemporalAccessor.class.isAssignableFrom(property.getClass())) {
203228
return dateConverter.format((TemporalAccessor) property);
204229
} else if (isDate && Date.class.isAssignableFrom(property.getClass())) {
@@ -207,16 +232,6 @@ public String write(Object property) {
207232
return property.toString();
208233
}
209234
}
210-
211-
@SuppressWarnings("unchecked")
212-
@Override
213-
public Object read(String s) {
214-
if (isTemporalAccessor) {
215-
return dateConverter.parse(s, (Class<? extends TemporalAccessor>) actualType);
216-
} else { // must be date
217-
return dateConverter.parse(s);
218-
}
219-
}
220235
};
221236
}
222237
}

Diff for: src/test/java/org/springframework/data/elasticsearch/core/CriteriaQueryMappingUnitTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
* {@link CriteriaQueryProcessor} as this is needed to get the String representation to assert.
4949
*
5050
* @author Peter-Josef Meisch
51+
* @author Sascha Woo
5152
*/
5253
public class CriteriaQueryMappingUnitTests {
5354

@@ -346,8 +347,7 @@ static class Person {
346347
@Nullable @Field(name = "first-name") String firstName;
347348
@Nullable @Field(name = "last-name") String lastName;
348349
@Nullable @Field(name = "created-date", type = FieldType.Date, format = DateFormat.epoch_millis) Date createdDate;
349-
@Nullable @Field(name = "birth-date", type = FieldType.Date, format = DateFormat.custom,
350-
pattern = "dd.MM.uuuu") LocalDate birthDate;
350+
@Nullable @Field(name = "birth-date", type = FieldType.Date, format = {}, pattern = "dd.MM.uuuu") LocalDate birthDate;
351351
}
352352

353353
static class GeoShapeEntity {

Diff for: src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverterUnitTests.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
/**
2424
* @author Peter-Josef Meisch
2525
* @author Tim te Beek
26+
* @author Sascha Woo
2627
*/
2728
class ElasticsearchDateConverterUnitTests {
2829

@@ -34,16 +35,28 @@ void shouldCreateConvertersForAllKnownFormats(DateFormat dateFormat) {
3435

3536
switch (dateFormat) {
3637
case none:
38+
case custom:
3739
case weekyear:
3840
case weekyear_week:
3941
case weekyear_week_day:
4042
return;
4143
}
4244

43-
String pattern = (dateFormat != DateFormat.custom) ? dateFormat.name() : "dd.MM.uuuu";
45+
ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(dateFormat.name());
4446

47+
assertThat(converter).isNotNull();
48+
}
49+
50+
@Test // DATAES-716
51+
void shouldCreateConvertersForDateFormatPattern() {
52+
53+
// given
54+
String pattern = "dd.MM.uuuu";
55+
56+
// when
4557
ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(pattern);
4658

59+
// then
4760
assertThat(converter).isNotNull();
4861
}
4962

Diff for: src/test/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverterUnitTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
* @author Peter-Josef Meisch
8484
* @author Konrad Kurdej
8585
* @author Roman Puchkovskiy
86+
* @author Sascha Woo
8687
*/
8788
public class MappingElasticsearchConverterUnitTests {
8889

@@ -1218,8 +1219,7 @@ static class Person {
12181219
String name;
12191220
@Field(name = "first-name") String firstName;
12201221
@Field(name = "last-name") String lastName;
1221-
@Field(name = "birth-date", type = FieldType.Date, format = DateFormat.custom,
1222-
pattern = "dd.MM.uuuu") LocalDate birthDate;
1222+
@Field(name = "birth-date", type = FieldType.Date, format = {}, pattern = "dd.MM.uuuu") LocalDate birthDate;
12231223
Gender gender;
12241224
Address address;
12251225

Diff for: src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -921,7 +921,7 @@ static class FieldMappingParameters {
921921
@Nullable @Field(copyTo = { "foo", "bar" }) private String copyTo;
922922
@Nullable @Field(ignoreAbove = 42) private String ignoreAbove;
923923
@Nullable @Field(type = FieldType.Integer) private String type;
924-
@Nullable @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "YYYYMMDD") private LocalDate date;
924+
@Nullable @Field(type = FieldType.Date, format = {}, pattern = "YYYYMMDD") private LocalDate date;
925925
@Nullable @Field(analyzer = "ana", searchAnalyzer = "sana", normalizer = "norma") private String analyzers;
926926
@Nullable @Field(type = Keyword) private String docValuesTrue;
927927
@Nullable @Field(type = Keyword, docValues = false) private String docValuesFalse;

Diff for: src/test/java/org/springframework/data/elasticsearch/core/index/SimpleElasticsearchDateMappingTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
* @author Mohsin Husen
3737
* @author Don Wellington
3838
* @author Peter-Josef Meisch
39+
* @author Sascha Woo
3940
*/
4041
public class SimpleElasticsearchDateMappingTests extends MappingContextBaseTests {
4142

@@ -62,8 +63,7 @@ static class SampleDateMappingEntity {
6263

6364
@Field(type = Text, index = false, store = true, analyzer = "standard") private String message;
6465

65-
@Field(type = Date, format = DateFormat.custom,
66-
pattern = "dd.MM.uuuu hh:mm") private LocalDateTime customFormatDate;
66+
@Field(type = Date, format = {}, pattern = "dd.MM.uuuu hh:mm") private LocalDateTime customFormatDate;
6767

6868
@Field(type = FieldType.Date, format = DateFormat.basic_date) private LocalDateTime basicFormatDate;
6969
}

0 commit comments

Comments
 (0)