org.springframework.data.elasticsearch.core.convert.ElasticsearchDateConverter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spring-data-elasticsearch Show documentation
Show all versions of spring-data-elasticsearch Show documentation
Spring Data Implementation for Elasticsearch
/*
* Copyright 2019-2023 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.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.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.hasText(pattern, "pattern must not be empty");
String[] subPatterns = pattern.split("\\|\\|");
return converters.computeIfAbsent(subPatterns[0].trim(), p -> new ElasticsearchDateConverter(forPattern(p)));
}
private ElasticsearchDateConverter(DateFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}
/**
* 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");
if (accessor instanceof Instant instant) {
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"));
return dateFormatter.format(zonedDateTime);
}
return dateFormatter.format(accessor);
}
/**
* Formats the given {@link TemporalAccessor} int a String
*
* @param date must not be {@literal null}
* @return the formatted object
*/
public String format(Date date) {
Assert.notNull(date, "accessor must not be null");
return dateFormatter.format(Instant.ofEpochMilli(date.getTime()));
}
/**
* 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) {
return dateFormatter.parse(input, type);
}
/**
* Parses a String into a Date.
*
* @param input the String to parse, must not be {@literal null}.
* @return the new created object
*/
public Date parse(String input) {
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:
continue;
}
if (dateFormat.name().equals(pattern)) {
resolvedPattern = dateFormat.getPattern();
break;
}
}
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(resolvedPattern);
return new PatternDateFormatter(dateTimeFormatter);
}
@SuppressWarnings("unchecked")
private static TemporalQuery getTemporalQuery(Class type) {
return temporal -> {
// no reflection for java.time classes (GraalVM native)
if (type == java.time.chrono.HijrahDate.class) {
return (T) java.time.chrono.HijrahDate.from(temporal);
}
if (type == java.time.chrono.JapaneseDate.class) {
return (T) java.time.chrono.JapaneseDate.from(temporal);
}
if (type == java.time.ZonedDateTime.class) {
return (T) java.time.ZonedDateTime.from(temporal);
}
if (type == java.time.LocalDateTime.class) {
return (T) java.time.LocalDateTime.from(temporal);
}
if (type == java.time.chrono.ThaiBuddhistDate.class) {
return (T) java.time.chrono.ThaiBuddhistDate.from(temporal);
}
if (type == java.time.LocalTime.class) {
return (T) java.time.LocalTime.from(temporal);
}
if (type == java.time.ZoneOffset.class) {
return (T) java.time.ZoneOffset.from(temporal);
}
if (type == java.time.OffsetTime.class) {
return (T) java.time.OffsetTime.from(temporal);
}
if (type == java.time.chrono.ChronoLocalDate.class) {
return (T) java.time.chrono.ChronoLocalDate.from(temporal);
}
if (type == java.time.Month.class) {
return (T) java.time.Month.from(temporal);
}
if (type == java.time.chrono.ChronoLocalDateTime.class) {
return (T) java.time.chrono.ChronoLocalDateTime.from(temporal);
}
if (type == java.time.MonthDay.class) {
return (T) java.time.MonthDay.from(temporal);
}
if (type == java.time.Instant.class) {
return (T) java.time.Instant.from(temporal);
}
if (type == java.time.OffsetDateTime.class) {
return (T) java.time.OffsetDateTime.from(temporal);
}
if (type == java.time.chrono.ChronoZonedDateTime.class) {
return (T) java.time.chrono.ChronoZonedDateTime.from(temporal);
}
if (type == java.time.chrono.MinguoDate.class) {
return (T) java.time.chrono.MinguoDate.from(temporal);
}
if (type == java.time.Year.class) {
return (T) java.time.Year.from(temporal);
}
if (type == java.time.DayOfWeek.class) {
return (T) java.time.DayOfWeek.from(temporal);
}
if (type == java.time.LocalDate.class) {
return (T) java.time.LocalDate.from(temporal);
}
if (type == java.time.YearMonth.class) {
return (T) java.time.YearMonth.from(temporal);
}
// for implementations not covered until here use reflection to check for the existence of a static
// from(TemporalAccessor) method
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;
}
}
}
}
}