diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java b/src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java index 4fd08f1d2..8ee7b7dcb 100644 --- a/src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/DateFormat.java @@ -16,58 +16,73 @@ package org.springframework.data.elasticsearch.annotations; /** - * Values based on reference doc - https://www.elastic.co/guide/reference/mapping/date-format/ - * + * Values based on reference doc - https://www.elastic.co/guide/reference/mapping/date-format/. The patterns are taken + * from this documentation and slightly adapted so that a Java {@link java.time.format.DateTimeFormatter} produces the + * same values as the Elasticsearch formatter. + * * @author Jakub Vavrik * @author Tim te Beek * @author Peter-Josef Meisch */ public enum DateFormat { - none, // - custom, // - basic_date, // - basic_date_time, // - basic_date_time_no_millis, // - basic_ordinal_date, // - basic_ordinal_date_time, // - basic_ordinal_date_time_no_millis, // - basic_time, // - basic_time_no_millis, // - basic_t_time, // - basic_t_time_no_millis, // - basic_week_date, // - basic_week_date_time, // - basic_week_date_time_no_millis, // - date, // - date_hour, // - date_hour_minute, // - date_hour_minute_second, // - date_hour_minute_second_fraction, // - date_hour_minute_second_millis, // - date_optional_time, // - date_time, // - date_time_no_millis, // - epoch_millis, // - epoch_second, // - hour, // - hour_minute, // - hour_minute_second, // - hour_minute_second_fraction, // - hour_minute_second_millis, // - ordinal_date, // - ordinal_date_time, // - ordinal_date_time_no_millis, // - time, // - time_no_millis, // - t_time, // - t_time_no_millis, // - week_date, // - week_date_time, // - week_date_time_no_millis, // - weekyear, // - weekyear_week, // - weekyear_week_day, // - year, // - year_month, // - year_month_day // + none(""), // + custom(""), // + basic_date("uuuuMMdd"), // + basic_date_time("uuuuMMdd'T'HHmmss.SSSXXX"), // + basic_date_time_no_millis("uuuuMMdd'T'HHmmssXXX"), // + basic_ordinal_date("uuuuDDD"), // + basic_ordinal_date_time("yyyyDDD'T'HHmmss.SSSXXX"), // + basic_ordinal_date_time_no_millis("yyyyDDD'T'HHmmssXXX"), // + basic_time("HHmmss.SSSXXX"), // + basic_time_no_millis("HHmmssXXX"), // + basic_t_time("'T'HHmmss.SSSXXX"), // + basic_t_time_no_millis("'T'HHmmssXXX"), // + basic_week_date("YYYY'W'wwe"), // week-based-year! + basic_week_date_time("YYYY'W'wwe'T'HHmmss.SSSX"), // here Elasticsearch uses a different zone format + basic_week_date_time_no_millis("YYYY'W'wwe'T'HHmmssX"), // + date("uuuu-MM-dd"), // + date_hour("uuuu-MM-dd'T'HH"), // + date_hour_minute("uuuu-MM-dd'T'HH:mm"), // + date_hour_minute_second("uuuu-MM-dd'T'HH:mm:ss"), // + date_hour_minute_second_fraction("uuuu-MM-dd'T'HH:mm:ss.SSS"), // + date_hour_minute_second_millis("uuuu-MM-dd'T'HH:mm:ss.SSS"), // + date_optional_time("uuuu-MM-dd['T'HH:mm:ss.SSSXXX]"), // + date_time("uuuu-MM-dd'T'HH:mm:ss.SSSXXX"), // + date_time_no_millis("uuuu-MM-dd'T'HH:mm:ssVV"), // here Elasticsearch uses the zone-id in it's implementation + epoch_millis("epoch_millis"), // + epoch_second("epoch_second"), // + hour("HH"), // + hour_minute("HH:mm"), // + hour_minute_second("HH:mm:ss"), // + hour_minute_second_fraction("HH:mm:ss.SSS"), // + hour_minute_second_millis("HH:mm:ss.SSS"), // + ordinal_date("uuuu-DDD"), // + ordinal_date_time("uuuu-DDD'T'HH:mm:ss.SSSXXX"), // + ordinal_date_time_no_millis("uuuu-DDD'T'HH:mm:ssXXX"), // + time("HH:mm:ss.SSSXXX"), // + time_no_millis("HH:mm:ssXXX"), // + t_time("'T'HH:mm:ss.SSSXXX"), // + t_time_no_millis("'T'HH:mm:ssXXX"), // + week_date("YYYY-'W'ww-e"), // + week_date_time("YYYY-'W'ww-e'T'HH:mm:ss.SSSXXX"), // + week_date_time_no_millis("YYYY-'W'ww-e'T'HH:mm:ssXXX"), // + weekyear(""), // no TemporalAccessor available for these 3 + weekyear_week(""), // + weekyear_week_day(""), // + year("uuuu"), // + year_month("uuuu-MM"), // + year_month_day("uuuu-MM-dd"); // + + private final String pattern; + + DateFormat(String pattern) { + this.pattern = pattern; + } + + /** + * @since 4.2 + */ + public String getPattern() { + return pattern; + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/DateFormatter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/DateFormatter.java new file mode 100644 index 000000000..141f9f8d5 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/DateFormatter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 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.time.temporal.TemporalAccessor; + +/** + * Interface to convert from and to {@link TemporalAccessor}s. + * + * @author Peter-Josef Meisch + * @since 4.2 + */ +public interface DateFormatter { + /** + * Formats a {@link TemporalAccessor} into a String. + * + * @param accessor must not be {@literal null} + * @return the formatted String + */ + String format(TemporalAccessor accessor); + + /** + * Parses a String into a {@link TemporalAccessor}. + * + * @param input the String to parse, must not be {@literal null} + * @param type the class of T + * @param the {@link TemporalAccessor} implementation + * @return the parsed instance + */ + T parse(String input, Class type); +} 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 index dbaebc7f7..2b82089bc 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverter.java @@ -18,13 +18,17 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQuery; import java.util.Date; +import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; -import org.elasticsearch.common.time.DateFormatter; -import org.elasticsearch.common.time.DateFormatters; import org.springframework.data.elasticsearch.annotations.DateFormat; import org.springframework.util.Assert; @@ -61,9 +65,13 @@ public static ElasticsearchDateConverter of(DateFormat dateFormat) { * @return converter */ public static ElasticsearchDateConverter of(String pattern) { + Assert.notNull(pattern, "pattern must not be null"); + Assert.hasText(pattern, "pattern must not be empty"); + + String[] subPatterns = pattern.split("\\|\\|"); - return converters.computeIfAbsent(pattern, p -> new ElasticsearchDateConverter(DateFormatter.forPattern(p))); + return converters.computeIfAbsent(subPatterns[0].trim(), p -> new ElasticsearchDateConverter(forPattern(p))); } private ElasticsearchDateConverter(DateFormatter dateFormatter) { @@ -71,14 +79,20 @@ private ElasticsearchDateConverter(DateFormatter dateFormatter) { } /** - * Formats the given {@link TemporalAccessor} int a String - * + * Formats the given {@link TemporalAccessor} into a String. + * * @param accessor must not be {@literal null} * @return the formatted object */ public String format(TemporalAccessor accessor) { - Assert.notNull(accessor, "accessor must not be null"); + Assert.notNull("accessor", "accessor must not be null"); + + if (accessor instanceof Instant) { + Instant instant = (Instant) accessor; + ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")); + return dateFormatter.format(zonedDateTime); + } return dateFormatter.format(accessor); } @@ -97,24 +111,15 @@ public String format(Date date) { } /** - * Parses a String into an object - * + * Parses a String into a TemporalAccessor. + * * @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) { - ZonedDateTime zonedDateTime = DateFormatters.from(dateFormatter.parse(input)); - try { - Method method = type.getMethod("from", TemporalAccessor.class); - Object o = method.invoke(null, zonedDateTime); - 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); - } + return dateFormatter.parse(input, type); } /** @@ -124,7 +129,171 @@ public T parse(String input, Class type) { * @return the new created object */ public Date parse(String input) { - ZonedDateTime zonedDateTime = DateFormatters.from(dateFormatter.parse(input)); - return new Date(Instant.from(zonedDateTime).toEpochMilli()); + return new Date(dateFormatter.parse(input, Instant.class).toEpochMilli()); + } + + /** + * Creates a {@link DateFormatter} for a given pattern. The pattern can be the name of a {@link DateFormat} enum value + * or a literal pattern. + * + * @param pattern the pattern to use + * @return DateFormatter + */ + private static DateFormatter forPattern(String pattern) { + + String resolvedPattern = pattern; + + if (DateFormat.epoch_millis.getPattern().equals(pattern)) { + return new EpochMillisDateFormatter(); + } + + if (DateFormat.epoch_second.getPattern().equals(pattern)) { + return new EpochSecondDateFormatter(); + } + + // check the enum values + for (DateFormat dateFormat : DateFormat.values()) { + + switch (dateFormat) { + case weekyear: + case weekyear_week: + case weekyear_week_day: + case custom: + continue; + } + + if (dateFormat.name().equals(pattern)) { + resolvedPattern = dateFormat.getPattern(); + break; + } + } + + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(resolvedPattern); + return new PatternDateFormatter(dateTimeFormatter); + } + + private static TemporalQuery getTemporalQuery(Class type) { + return temporal -> { + try { + Method method = type.getMethod("from", TemporalAccessor.class); + Object o = method.invoke(null, temporal); + 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); + } + }; + } // endregion + + /** + * a DateFormatter to convert epoch milliseconds + */ + static class EpochMillisDateFormatter implements DateFormatter { + + @Override + public String format(TemporalAccessor accessor) { + + Assert.notNull(accessor, "accessor must not be null"); + + return Long.toString(Instant.from(accessor).toEpochMilli()); + } + + @Override + public T parse(String input, Class type) { + + Assert.notNull(input, "input must not be null"); + Assert.notNull(type, "type must not be null"); + + Instant instant = Instant.ofEpochMilli(Long.parseLong(input)); + TemporalQuery query = getTemporalQuery(type); + return query.queryFrom(instant); + } + } + + /** + * a DateFormatter to convert epoch seconds. Elasticsearch's formatter uses double values, so do we + */ + static class EpochSecondDateFormatter implements DateFormatter { + + @Override + public String format(TemporalAccessor accessor) { + + Assert.notNull(accessor, "accessor must not be null"); + + long epochMilli = Instant.from(accessor).toEpochMilli(); + long fraction = epochMilli % 1_000; + if (fraction == 0) { + return Long.toString(epochMilli / 1_000); + } else { + Double d = ((double) epochMilli) / 1_000; + return String.format(Locale.ROOT, "%.03f", d); + } + } + + @Override + public T parse(String input, Class type) { + + Assert.notNull(input, "input must not be null"); + Assert.notNull(type, "type must not be null"); + + Double epochMilli = Double.parseDouble(input) * 1_000; + Instant instant = Instant.ofEpochMilli(epochMilli.longValue()); + TemporalQuery query = getTemporalQuery(type); + return query.queryFrom(instant); + } + } + + static class PatternDateFormatter implements DateFormatter { + + private final DateTimeFormatter dateTimeFormatter; + + PatternDateFormatter(DateTimeFormatter dateTimeFormatter) { + this.dateTimeFormatter = dateTimeFormatter; + } + + @Override + public String format(TemporalAccessor accessor) { + + Assert.notNull(accessor, "accessor must not be null"); + + try { + return dateTimeFormatter.format(accessor); + } catch (Exception e) { + if (accessor instanceof Instant) { + // as alternatives try to format a ZonedDateTime or LocalDateTime + return dateTimeFormatter.format(ZonedDateTime.ofInstant((Instant) accessor, ZoneId.of("UTC"))); + } else { + throw e; + } + } + } + + @Override + public T parse(String input, Class type) { + + Assert.notNull(input, "input must not be null"); + Assert.notNull(type, "type must not be null"); + + try { + return dateTimeFormatter.parse(input, getTemporalQuery(type)); + } catch (Exception e) { + + if (type.equals(Instant.class)) { + // as alternatives try to parse a ZonedDateTime or LocalDateTime + try { + ZonedDateTime zonedDateTime = dateTimeFormatter.parse(input, getTemporalQuery(ZonedDateTime.class)); + // noinspection unchecked + return (T) zonedDateTime.toInstant(); + } catch (Exception exception) { + LocalDateTime localDateTime = dateTimeFormatter.parse(input, getTemporalQuery(LocalDateTime.class)); + // noinspection unchecked + return (T) localDateTime.toInstant(ZoneOffset.UTC); + } + } else { + throw e; + } + } + } } } 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 7ee30ff0b..a3bafcc36 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 @@ -179,7 +179,7 @@ private void initDateConverter() { return; } - ElasticsearchDateConverter converter; + ElasticsearchDateConverter converter = null; if (dateFormat == DateFormat.custom) { String pattern = field.pattern(); @@ -192,33 +192,47 @@ private void initDateConverter() { converter = ElasticsearchDateConverter.of(pattern); } else { - converter = ElasticsearchDateConverter.of(dateFormat); + + switch (dateFormat) { + 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"); + break; + default: + converter = ElasticsearchDateConverter.of(dateFormat); + break; + } } - propertyConverter = new ElasticsearchPersistentPropertyConverter() { - final ElasticsearchDateConverter dateConverter = converter; - - @Override - public String write(Object property) { - if (isTemporalAccessor && TemporalAccessor.class.isAssignableFrom(property.getClass())) { - return dateConverter.format((TemporalAccessor) property); - } else if (isDate && Date.class.isAssignableFrom(property.getClass())) { - return dateConverter.format((Date) property); - } else { - return property.toString(); + if (converter != null) { + ElasticsearchDateConverter finalConverter = converter; + propertyConverter = new ElasticsearchPersistentPropertyConverter() { + final ElasticsearchDateConverter dateConverter = finalConverter; + + @Override + public String write(Object property) { + if (isTemporalAccessor && TemporalAccessor.class.isAssignableFrom(property.getClass())) { + return dateConverter.format((TemporalAccessor) property); + } else if (isDate && Date.class.isAssignableFrom(property.getClass())) { + return dateConverter.format((Date) property); + } else { + return property.toString(); + } } - } - @SuppressWarnings("unchecked") - @Override - public Object read(String s) { - if (isTemporalAccessor) { - return dateConverter.parse(s, (Class) actualType); - } else { // must be date - return dateConverter.parse(s); + @SuppressWarnings("unchecked") + @Override + public Object read(String s) { + if (isTemporalAccessor) { + return dateConverter.parse(s, (Class) actualType); + } else { // must be date + return dateConverter.parse(s); + } } - } - }; + }; + } } } 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 deleted file mode 100644 index 95ce82c08..000000000 --- a/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverterTests.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.springframework.data.elasticsearch.core.convert; - -import static org.assertj.core.api.Assertions.*; - -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.Date; -import java.util.GregorianCalendar; - -import org.junit.jupiter.api.DisplayName; -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 - * @author Tim te Beek - */ -class ElasticsearchDateConverterTests { - - @ParameterizedTest // DATAES-716 - @EnumSource(DateFormat.class) - void shouldCreateConvertersForAllKnownFormats(DateFormat dateFormat) { - - if (dateFormat == DateFormat.none) { - return; - } - String pattern = (dateFormat != DateFormat.custom) ? dateFormat.name() : "dd.MM.uuuu"; - - ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(pattern); - - assertThat(converter).isNotNull(); - } - - @Test // DATAES-716 - void shouldConvertTemporalAccessorToString() { - LocalDate localDate = LocalDate.of(2019, 12, 27); - ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.basic_date); - - String formatted = converter.format(localDate); - - assertThat(formatted).isEqualTo("20191227"); - } - - @Test // DATAES-716 - void shouldParseTemporalAccessorFromString() { - 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); - } - - @Test // DATAES-792 - void shouldConvertLegacyDateToString() { - GregorianCalendar calendar = GregorianCalendar - .from(ZonedDateTime.of(LocalDateTime.of(2020, 4, 19, 19, 44), ZoneId.of("UTC"))); - Date legacyDate = calendar.getTime(); - ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.basic_date_time); - - String formatted = converter.format(legacyDate); - - assertThat(formatted).isEqualTo("20200419T194400.000Z"); - } - - @Test // DATAES-792 - void shouldParseLegacyDateFromString() { - GregorianCalendar calendar = GregorianCalendar - .from(ZonedDateTime.of(LocalDateTime.of(2020, 4, 19, 19, 44), ZoneId.of("UTC"))); - Date legacyDate = calendar.getTime(); - ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.basic_date_time); - - Date parsed = converter.parse("20200419T194400.000Z"); - - assertThat(parsed).isEqualTo(legacyDate); - } - - @Test // DATAES-847 - void shouldParseEpochMillisString() { - Instant instant = Instant.ofEpochMilli(1234568901234L); - ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.epoch_millis); - - Date parsed = converter.parse("1234568901234"); - - assertThat(parsed.toInstant()).isEqualTo(instant); - } - - @Test // DATAES-847 - void shouldConvertInstantToString() { - Instant instant = Instant.ofEpochMilli(1234568901234L); - ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.epoch_millis); - - String formatted = converter.format(instant); - - assertThat(formatted).isEqualTo("1234568901234"); - } - - @Test // DATAES-953 - @DisplayName("should write and read Date with custom format") - void shouldWriteAndReadDateWithCustomFormat() { - - // only seconds as the format string does not store millis - long currentTimeSeconds = System.currentTimeMillis() / 1_000; - Date date = new Date(currentTimeSeconds * 1_000); - - ElasticsearchDateConverter converter = ElasticsearchDateConverter.of("uuuu-MM-dd HH:mm:ss"); - - String formatted = converter.format(date); - Date parsed = converter.parse(formatted); - - assertThat(parsed).isEqualTo(date); - } - - @Test // DATAES-953 - @DisplayName("should write and read Instant with custom format") - void shouldWriteAndReadInstantWithCustomFormat() { - - // only seconds as the format string does not store millis - long currentTimeSeconds = System.currentTimeMillis() / 1_000; - Instant instant = Instant.ofEpochSecond(currentTimeSeconds); - - ElasticsearchDateConverter converter = ElasticsearchDateConverter.of("uuuu-MM-dd HH:mm:ss"); - - String formatted = converter.format(instant); - Instant parsed = converter.parse(formatted, Instant.class); - - assertThat(parsed).isEqualTo(instant); - } -} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverterUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverterUnitTests.java new file mode 100644 index 000000000..673e3d22f --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/convert/ElasticsearchDateConverterUnitTests.java @@ -0,0 +1,411 @@ +package org.springframework.data.elasticsearch.core.convert; + +import static org.assertj.core.api.Assertions.*; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAccessor; +import java.util.Date; +import java.util.GregorianCalendar; + +import org.junit.jupiter.api.DisplayName; +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 + * @author Tim te Beek + */ +class ElasticsearchDateConverterUnitTests { + + private ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Europe/Berlin")); + + @ParameterizedTest // DATAES-716 + @EnumSource(DateFormat.class) + void shouldCreateConvertersForAllKnownFormats(DateFormat dateFormat) { + + switch (dateFormat) { + case none: + case weekyear: + case weekyear_week: + case weekyear_week_day: + return; + } + + String pattern = (dateFormat != DateFormat.custom) ? dateFormat.name() : "dd.MM.uuuu"; + + ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(pattern); + + assertThat(converter).isNotNull(); + } + + @Test // DATAES-716 + void shouldConvertTemporalAccessorToString() { + LocalDate localDate = LocalDate.of(2019, 12, 27); + ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.basic_date); + + String formatted = converter.format(localDate); + + assertThat(formatted).isEqualTo("20191227"); + } + + @Test // DATAES-716 + void shouldParseTemporalAccessorFromString() { + 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); + } + + @Test // DATAES-792 + void shouldConvertLegacyDateToString() { + GregorianCalendar calendar = GregorianCalendar + .from(ZonedDateTime.of(LocalDateTime.of(2020, 4, 19, 19, 44), ZoneId.of("UTC"))); + Date legacyDate = calendar.getTime(); + ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.basic_date_time); + + String formatted = converter.format(legacyDate); + + assertThat(formatted).isEqualTo("20200419T194400.000Z"); + } + + @Test // DATAES-792 + void shouldParseLegacyDateFromString() { + GregorianCalendar calendar = GregorianCalendar + .from(ZonedDateTime.of(LocalDateTime.of(2020, 4, 19, 19, 44), ZoneId.of("UTC"))); + Date legacyDate = calendar.getTime(); + ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.basic_date_time); + + Date parsed = converter.parse("20200419T194400.000Z"); + + assertThat(parsed).isEqualTo(legacyDate); + } + + @Test // DATAES-847 + void shouldParseEpochMillisString() { + Instant instant = Instant.ofEpochMilli(1234568901234L); + ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.epoch_millis); + + Date parsed = converter.parse("1234568901234"); + + assertThat(parsed.toInstant()).isEqualTo(instant); + } + + @Test // DATAES-847 + void shouldConvertInstantToString() { + Instant instant = Instant.ofEpochMilli(1234568901234L); + ElasticsearchDateConverter converter = ElasticsearchDateConverter.of(DateFormat.epoch_millis); + + String formatted = converter.format(instant); + + assertThat(formatted).isEqualTo("1234568901234"); + } + + @Test // DATAES-953 + @DisplayName("should write and read Date with custom format") + void shouldWriteAndReadDateWithCustomFormat() { + + // only seconds as the format string does not store millis + long currentTimeSeconds = System.currentTimeMillis() / 1_000; + Date date = new Date(currentTimeSeconds * 1_000); + + ElasticsearchDateConverter converter = ElasticsearchDateConverter.of("uuuu-MM-dd HH:mm:ss"); + + String formatted = converter.format(date); + Date parsed = converter.parse(formatted); + + assertThat(parsed).isEqualTo(date); + } + + @Test // DATAES-953 + @DisplayName("should write and read Instant with custom format") + void shouldWriteAndReadInstantWithCustomFormat() { + + // only seconds as the format string does not store millis + long currentTimeSeconds = System.currentTimeMillis() / 1_000; + Instant instant = Instant.ofEpochSecond(currentTimeSeconds); + + ElasticsearchDateConverter converter = ElasticsearchDateConverter.of("uuuu-MM-dd HH:mm:ss"); + + String formatted = converter.format(instant); + Instant parsed = converter.parse(formatted, Instant.class); + + assertThat(parsed).isEqualTo(instant); + } + + @Test // #1647 + @DisplayName("should convert basic_date") + void shouldConvertBasicDate() { + check(ElasticsearchDateConverter.of(DateFormat.basic_date), LocalDate.class); + } + + @Test // #1647 + @DisplayName("should convert basic_date_time") + void shouldConvertBasicDateTime() { + check(ElasticsearchDateConverter.of(DateFormat.basic_date_time), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert basic_date_time_no_millis") + void shouldConvertBasicDateTimeNoMillis() { + check(ElasticsearchDateConverter.of(DateFormat.basic_date_time_no_millis), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert basic_ordinal_date") + void shouldConvertBasicOrdinalDate() { + check(ElasticsearchDateConverter.of(DateFormat.basic_ordinal_date), LocalDate.class); + } + + @Test // #1647 + @DisplayName("should convert basic_ordinal_date_time") + void shouldConvertBasicOrdinalDateTime() { + check(ElasticsearchDateConverter.of(DateFormat.basic_ordinal_date_time), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert basic_ordinal_date_time_no_millis") + void shouldConvertBasicOrdinalDateTimeNoMillis() { + check(ElasticsearchDateConverter.of(DateFormat.basic_ordinal_date_time_no_millis), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert basic_time") + void shouldConvertBasicTime() { + check(ElasticsearchDateConverter.of(DateFormat.basic_time), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert basic_time_no_millis") + void shouldConvertBasicTimeNoMillis() { + check(ElasticsearchDateConverter.of(DateFormat.basic_time_no_millis), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert basic_t_time") + void shouldConvertBasicTTime() { + check(ElasticsearchDateConverter.of(DateFormat.basic_t_time), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert basic_t_time_no_millis") + void shouldConvertBasicTTimeNoMillis() { + check(ElasticsearchDateConverter.of(DateFormat.basic_t_time_no_millis), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert basic_week_date") + void shouldConvertBasicWeekDate() { + check(ElasticsearchDateConverter.of(DateFormat.basic_week_date), LocalDate.class); + } + + @Test // #1647 + @DisplayName("should convert basic_week_date_time") + void shouldConvertBasicWeekDateTime() { + check(ElasticsearchDateConverter.of(DateFormat.basic_week_date_time), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert basic_week_date_time_no_millis") + void shouldConvertBasicWeekDateTimeNoMillis() { + check(ElasticsearchDateConverter.of(DateFormat.basic_week_date_time_no_millis), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert date") + void shouldConvertDate() { + check(ElasticsearchDateConverter.of(DateFormat.date), LocalDate.class); + } + + @Test // #1647 + @DisplayName("should convert date_hour") + void shouldConvertDateHour() { + check(ElasticsearchDateConverter.of(DateFormat.date_hour), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert date_hour_minute") + void shouldConvertDateHourMinute() { + check(ElasticsearchDateConverter.of(DateFormat.date_hour_minute), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert date_hour_minute_second") + void shouldConvertDateHourMinuteSecond() { + check(ElasticsearchDateConverter.of(DateFormat.date_hour_minute_second), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert date_hour_minute_second_fraction") + void shouldConvertDateHourMinuteSecondFraction() { + check(ElasticsearchDateConverter.of(DateFormat.date_hour_minute_second_fraction), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert date_hour_minute_second_millis") + void shouldConvertDateHourMinuteSecondMillis() { + check(ElasticsearchDateConverter.of(DateFormat.date_hour_minute_second_millis), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert date_optional_time") + void shouldConvertDateOptionalTime() { + check(ElasticsearchDateConverter.of(DateFormat.date_optional_time), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert date_time") + void shouldConvertDateTime() { + check(ElasticsearchDateConverter.of(DateFormat.date_time), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert date_time_no_millis") + void shouldConvertDateTimeNoMillis() { + check(ElasticsearchDateConverter.of(DateFormat.date_time_no_millis), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert epoch_millis") + void shouldConvertEpochMillis() { + check(ElasticsearchDateConverter.of(DateFormat.epoch_millis), Instant.class); + } + + @Test // #1647 + @DisplayName("should convert epoch_second") + void shouldConvertEpochSecond() { + check(ElasticsearchDateConverter.of(DateFormat.epoch_second), Instant.class); + } + + @Test // #1647 + @DisplayName("should convert hour") + void shouldConvertHour() { + check(ElasticsearchDateConverter.of(DateFormat.hour), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert hour_minute") + void shouldConvertHourMinute() { + check(ElasticsearchDateConverter.of(DateFormat.hour_minute), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert hour_minute_second") + void shouldConvertHourMinuteSecond() { + check(ElasticsearchDateConverter.of(DateFormat.hour_minute_second), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert hour_minute_second_fraction") + void shouldConvertHourMinuteSecondFraction() { + check(ElasticsearchDateConverter.of(DateFormat.hour_minute_second_fraction), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert hour_minute_second_millis") + void shouldConvertHourMinuteSecondMillis() { + check(ElasticsearchDateConverter.of(DateFormat.hour_minute_second_millis), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert ordinal_date") + void shouldConvertOrdinalDate() { + check(ElasticsearchDateConverter.of(DateFormat.ordinal_date), LocalDate.class); + } + + @Test // #1647 + @DisplayName("should convert ordinal_date_time") + void shouldConvertOrdinalDateTime() { + check(ElasticsearchDateConverter.of(DateFormat.ordinal_date_time), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert ordinal_date_time_no_millis") + void shouldConvertOrdinalDateTimeNoMillis() { + check(ElasticsearchDateConverter.of(DateFormat.ordinal_date_time_no_millis), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert time") + void shouldConvertTime() { + check(ElasticsearchDateConverter.of(DateFormat.time), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert time_no_millis") + void shouldConvertTimeNoMillis() { + check(ElasticsearchDateConverter.of(DateFormat.time_no_millis), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert t_time") + void shouldConvertTTime() { + check(ElasticsearchDateConverter.of(DateFormat.t_time), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert t_time_no_millis") + void shouldConvertTTimeNoMillis() { + check(ElasticsearchDateConverter.of(DateFormat.t_time_no_millis), LocalTime.class); + } + + @Test // #1647 + @DisplayName("should convert week_date") + void shouldConvertWeekDate() { + check(ElasticsearchDateConverter.of(DateFormat.week_date), LocalDate.class); + } + + @Test // #1647 + @DisplayName("should convert week_date_time") + void shouldConvertWeekDateTime() { + check(ElasticsearchDateConverter.of(DateFormat.week_date_time), LocalDateTime.class); + } + + @Test // #1647 + @DisplayName("should convert week_date_time_no_millis") + void shouldConvertWeekDateTimeNoMillis() { + check(ElasticsearchDateConverter.of(DateFormat.week_date_time_no_millis), LocalDate.class); + } + + @Test // #1647 + @DisplayName("should convert year") + void shouldConvertYear() { + check(ElasticsearchDateConverter.of(DateFormat.year), Year.class); + } + + @Test // #1647 + @DisplayName("should convert year_month") + void shouldConvertYearMonth() { + check(ElasticsearchDateConverter.of(DateFormat.year_month), YearMonth.class); + } + + @Test // #1647 + @DisplayName("should convert year_month_day") + void shouldConvertYearMonthDay() { + check(ElasticsearchDateConverter.of(DateFormat.year_month_day), LocalDate.class); + } + + @Test // #1647 + @DisplayName("should convert with combined patterns") + void shouldConvertWithCombinedPatterns() { + check(ElasticsearchDateConverter.of("basic_date_time ||invalid-pattern"), LocalDateTime.class); + } + + private void check(ElasticsearchDateConverter converter, Class type) { + + String formatted = converter.format(zdt); + T parsed = converter.parse(formatted, type); + + assertThat(parsed).isNotNull(); + } +}