Skip to content

Commit dbc4ce9

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

File tree

6 files changed

+99
-51
lines changed

6 files changed

+99
-51
lines changed

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ See https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-type
6464
* `@GeoPoint`: marks a field as _geo_point_ datatype.
6565
Can be omitted if the field is an instance of the `GeoPoint` class.
6666

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. +
67+
NOTE: Properties that derive from `TemporalAccessor` or are of type `java.util.Date` must either have a `@Field` annotation of type `FieldType.Date`.
6968
If you are using a custom date format, you need to use _uuuu_ for the year instead of _yyyy_.
7069
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].
7170

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

+5
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@
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 multiple formats in the @Field annotation.
31+
*/
32+
@Deprecated
2833
none(""), //
2934
custom(""), //
3035
basic_date("uuuuMMdd"), //

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

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

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

2123
import org.elasticsearch.common.Nullable;
2224
import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -41,6 +43,7 @@
4143
* @author Aleksei Arsenev
4244
* @author Brian Kimmig
4345
* @author Morgan Lutz
46+
* @author Sascha Woo
4447
* @since 4.0
4548
*/
4649
public final class MappingParameters {
@@ -78,12 +81,12 @@ public final class MappingParameters {
7881
private final String analyzer;
7982
private final boolean coerce;
8083
@Nullable private final String[] copyTo;
81-
private final String datePattern;
84+
private final DateFormat[] dateFormats;
85+
private final String[] dateFormatPatterns;
8286
private final boolean docValues;
8387
private final boolean eagerGlobalOrdinals;
8488
private final boolean enabled;
8589
private final boolean fielddata;
86-
private final DateFormat format;
8790
@Nullable private final Integer ignoreAbove;
8891
private final boolean ignoreMalformed;
8992
private final boolean index;
@@ -129,8 +132,8 @@ private MappingParameters(Field field) {
129132
store = field.store();
130133
fielddata = field.fielddata();
131134
type = field.type();
132-
format = field.format();
133-
datePattern = field.pattern();
135+
dateFormats = field.format();
136+
dateFormatPatterns = field.pattern();
134137
analyzer = field.analyzer();
135138
searchAnalyzer = field.searchAnalyzer();
136139
normalizer = field.normalizer();
@@ -171,8 +174,8 @@ private MappingParameters(InnerField field) {
171174
store = field.store();
172175
fielddata = field.fielddata();
173176
type = field.type();
174-
format = field.format();
175-
datePattern = field.pattern();
177+
dateFormats = field.format();
178+
dateFormatPatterns = field.pattern();
176179
analyzer = field.analyzer();
177180
searchAnalyzer = field.searchAnalyzer();
178181
normalizer = field.normalizer();
@@ -226,8 +229,22 @@ public void writeTypeAndParametersTo(XContentBuilder builder) throws IOException
226229

227230
if (type != FieldType.Auto) {
228231
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());
232+
233+
if (type == FieldType.Date && dateFormats.length > 0) {
234+
List<String> formats = new ArrayList<>();
235+
236+
for (int i = 0; i < dateFormats.length; i++) {
237+
DateFormat dateFormat = dateFormats[i];
238+
if (dateFormat == DateFormat.custom) {
239+
for (int ii = 0; ii < dateFormatPatterns.length; ii++) {
240+
formats.add(dateFormatPatterns[ii]);
241+
}
242+
} else {
243+
formats.add(dateFormat.toString());
244+
}
245+
}
246+
247+
builder.field(FIELD_PARAM_FORMAT, String.join("||", formats));
231248
}
232249
}
233250

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

+63-37
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,86 @@ 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+
DateFormat[] dateFormats = field.format();
158159

159160
String property = getOwner().getType().getSimpleName() + "." + getName();
160161

161-
if (dateFormat == DateFormat.none) {
162+
if (dateFormats.length == 0) {
162163
LOGGER.warn(
163-
String.format("No DateFormat defined for property %s. Make sure you have a Converter registered for %s",
164+
String.format("Property %s is annotated with FieldType.%s but has no format defined. Make sure you have a converter registered for %s.",
164165
property, actualType.getSimpleName()));
165166
return;
166167
}
167168

168-
ElasticsearchDateConverter converter = null;
169+
List<ElasticsearchDateConverter> converters = new ArrayList<>();
169170

170-
if (dateFormat == DateFormat.custom) {
171-
String pattern = field.pattern();
171+
for (int i = 0; i < dateFormats.length; i++) {
172+
DateFormat dateFormat = dateFormats[i];
172173

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-
}
174+
if (dateFormat == DateFormat.custom) {
175+
String[] dateFormatPatterns = field.pattern();
176+
177+
if (dateFormatPatterns.length == 0) {
178+
throw new MappingException(String.format(
179+
"Property %s is annotated with FieldType.%s and a custom format but has no pattern defined.", property,
180+
field.type().name()));
181+
}
178182

179-
converter = ElasticsearchDateConverter.of(pattern);
180-
} else {
181-
182-
switch (dateFormat) {
183-
case weekyear:
184-
case weekyear_week:
185-
case weekyear_week_day:
186-
LOGGER.warn("no Converter available for " + actualType.getName() + " and date format " + dateFormat.name()
187-
+ ". Use a custom converter instead");
188-
break;
189-
default:
190-
converter = ElasticsearchDateConverter.of(dateFormat);
191-
break;
183+
for (int j = 0; j < dateFormatPatterns.length; j++) {
184+
String pattern = dateFormatPatterns[j];
185+
if (!StringUtils.hasText(pattern)) {
186+
throw new MappingException(
187+
String.format("Date pattern of property %s must not be empty", property, field.type().name()));
188+
}
189+
converters.add(ElasticsearchDateConverter.of(pattern));
190+
}
191+
} else {
192+
193+
switch (dateFormat) {
194+
case weekyear:
195+
case weekyear_week:
196+
case weekyear_week_day:
197+
LOGGER.warn("No default converter available for " + actualType.getName() + " and date format "
198+
+ dateFormat.name() + ". Use a custom converter instead.");
199+
break;
200+
default:
201+
converters.add(ElasticsearchDateConverter.of(dateFormat));
202+
break;
203+
}
192204
}
205+
193206
}
194207

195-
if (converter != null) {
196-
ElasticsearchDateConverter finalConverter = converter;
208+
if (!converters.isEmpty()) {
197209
propertyConverter = new ElasticsearchPersistentPropertyConverter() {
198-
final ElasticsearchDateConverter dateConverter = finalConverter;
210+
final List<ElasticsearchDateConverter> dateConverters = converters;
211+
212+
@SuppressWarnings("unchecked")
213+
@Override
214+
public Object read(String s) {
215+
if (isTemporalAccessor) {
216+
for (ElasticsearchDateConverter dateConverter : dateConverters) {
217+
try {
218+
return dateConverter.parse(s, (Class<? extends TemporalAccessor>) actualType);
219+
} catch (Exception e) {
220+
}
221+
}
222+
} else { // must be date
223+
for (ElasticsearchDateConverter dateConverter : dateConverters) {
224+
try {
225+
return dateConverter.parse(s);
226+
} catch (Exception e) {
227+
}
228+
}
229+
}
230+
231+
throw new RuntimeException(String
232+
.format("Unable to parse date value '%s' of property '%s' with configured converters", s, property));
233+
}
199234

200235
@Override
201236
public String write(Object property) {
237+
ElasticsearchDateConverter dateConverter = dateConverters.get(0);
202238
if (isTemporalAccessor && TemporalAccessor.class.isAssignableFrom(property.getClass())) {
203239
return dateConverter.format((TemporalAccessor) property);
204240
} else if (isDate && Date.class.isAssignableFrom(property.getClass())) {
@@ -207,16 +243,6 @@ public String write(Object property) {
207243
return property.toString();
208244
}
209245
}
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-
}
220246
};
221247
}
222248
}

0 commit comments

Comments
 (0)