TimeConverterRegistrar.java
/*
* Copyright 2017-2020 original 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 io.micronaut.runtime.converters.time;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NextMajorVersion;
import io.micronaut.core.annotation.TypeHint;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.convert.MutableConversionService;
import io.micronaut.core.convert.TypeConverterRegistrar;
import io.micronaut.core.convert.format.Format;
import io.micronaut.core.util.StringUtils;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Period;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.SignStyle;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalQuery;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.YEAR;
/**
* Registers data time converters.
*
* @author Graeme Rocher
* @since 1.0
*/
@TypeHint(
value = {
Duration.class,
TemporalAmount.class,
Instant.class,
LocalTime.class,
LocalDate.class,
LocalDateTime.class,
MonthDay.class,
OffsetDateTime.class,
OffsetTime.class,
Period.class,
Year.class,
YearMonth.class,
ZonedDateTime.class,
ZoneId.class,
ZoneOffset.class
},
accessType = TypeHint.AccessType.ALL_PUBLIC
)
@Internal
public class TimeConverterRegistrar implements TypeConverterRegistrar {
private static final Pattern PERIOD_MATCHER = Pattern.compile("^(-?\\d+)([unywmd])(s?)$");
private static final Pattern DURATION_MATCHER = Pattern.compile("^(-?\\d+)([unsmhd])(s?)$");
private static final int MILLIS = 3;
/**
* Copy of java.time.Year.PARSER DateTimeFormatter.
*/
private static final DateTimeFormatter ISO_YEAR = new DateTimeFormatterBuilder()
.parseLenient()
.appendValue(YEAR, 1, 10, SignStyle.NORMAL)
.toFormatter();
/**
* Copy of java.time.YearMonth.PARSER DateTimeFormatter.
*/
private static final DateTimeFormatter ISO_YEAR_MONTH = new DateTimeFormatterBuilder()
.appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
.appendLiteral('-')
.appendValue(MONTH_OF_YEAR, 2)
.toFormatter();
/**
* Copy of java.time.MonthDay.PARSER DateTimeFormatter.
*/
private static final DateTimeFormatter ISO_MONTH_DAY = new DateTimeFormatterBuilder()
.appendLiteral("--")
.appendValue(MONTH_OF_YEAR, 2)
.appendLiteral('-')
.appendValue(DAY_OF_MONTH, 2)
.toFormatter();
private final Map<String, DateTimeFormatter> formattersCache = new ConcurrentHashMap<>();
@NextMajorVersion("Consider deletion of LocalDate and LocalDateTime converters")
@Override
public void register(MutableConversionService conversionService) {
final BiFunction<CharSequence, ConversionContext, Optional<Duration>> durationConverter = durationConverter();
// CharSequence -> Duration
conversionService.addConverter(
CharSequence.class,
Duration.class,
(object, targetType, context) -> durationConverter.apply(object, context)
);
// Integer -> Duration
conversionService.addConverter(
Integer.class,
Duration.class,
(integer, targetType, context) -> durationConverter.apply(integer.toString(), context)
);
// CharSequence -> TemporalAmount
conversionService.addConverter(
CharSequence.class,
TemporalAmount.class,
(object, targetType, context) -> durationConverter.apply(object, context).map(TemporalAmount.class::cast)
);
final BiFunction<CharSequence, ConversionContext, Optional<Period>> periodConverter = periodConverter();
// CharSequence -> Period
conversionService.addConverter(
CharSequence.class,
Period.class,
(object, targetType, context) -> periodConverter.apply(object, context)
);
// Integer -> Period
conversionService.addConverter(
Integer.class,
Period.class,
(integer, targetType, context) -> periodConverter.apply(integer.toString(), context)
);
// CharSequence -> TemporalAmount
conversionService.addConverter(
CharSequence.class,
TemporalAmount.class,
(object, targetType, context) -> periodConverter.apply(object, context).map(TemporalAmount.class::cast)
);
addTemporalStringConverters(conversionService, Instant.class, DateTimeFormatter.ISO_INSTANT, Instant::from);
addTemporalStringConverters(conversionService, LocalDate.class, DateTimeFormatter.ISO_LOCAL_DATE, LocalDate::from);
addTemporalStringConverters(conversionService, LocalDateTime.class, DateTimeFormatter.ISO_LOCAL_DATE_TIME, LocalDateTime::from);
addTemporalStringConverters(conversionService, OffsetTime.class, DateTimeFormatter.ISO_OFFSET_TIME, OffsetTime::from);
addTemporalStringConverters(conversionService, OffsetDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME, OffsetDateTime::from);
addTemporalStringConverters(conversionService, ZonedDateTime.class, DateTimeFormatter.ISO_ZONED_DATE_TIME, ZonedDateTime::from);
addTemporalStringConverters(conversionService, YearMonth.class, ISO_YEAR_MONTH, YearMonth::from);
addTemporalStringConverters(conversionService, Year.class, ISO_YEAR, Year::from);
addTemporalIntegerConverters(conversionService, Year.class, ISO_YEAR, Year::from);
addTemporalStringConverters(conversionService, MonthDay.class, ISO_MONTH_DAY, MonthDay::from);
addTemporalStringConverters(conversionService, LocalTime.class, DateTimeFormatter.ISO_LOCAL_TIME, LocalTime::from);
conversionService.addConverter(CharSequence.class, ZoneId.class, (object, targetType, context) -> {
if (StringUtils.isEmpty(object)) {
return Optional.empty();
}
try {
ZoneId result = ZoneId.of(object.toString());
return Optional.of(result);
} catch (DateTimeParseException e) {
context.reject(object, e);
return Optional.empty();
}
});
conversionService.addConverter(ZoneId.class, CharSequence.class, (object, targetType, context) -> {
if (Objects.isNull(object)) {
return Optional.empty();
}
return Optional.of(object.toString());
});
// java.time -> Date
addTemporalToDateConverter(conversionService, Instant.class, Function.identity());
addTemporalToDateConverter(conversionService, OffsetDateTime.class, OffsetDateTime::toInstant);
addTemporalToDateConverter(conversionService, ZonedDateTime.class, ZonedDateTime::toInstant);
// these two are a bit icky, but required for yaml parsing compatibility
// TODO Micronaut 4 Consider deletion
addTemporalToDateConverter(conversionService, LocalDate.class, ld -> ld.atTime(0, 0).toInstant(ZoneOffset.UTC));
addTemporalToDateConverter(conversionService, LocalDateTime.class, ldt -> ldt.toInstant(ZoneOffset.UTC));
}
private <T extends TemporalAccessor> void addTemporalStringConverters(MutableConversionService conversionService, Class<T> temporalType, DateTimeFormatter isoFormatter, TemporalQuery<T> query) {
conversionService.addConverter(CharSequence.class, temporalType, (CharSequence object, Class<T> targetType, ConversionContext context) -> {
if (StringUtils.isEmpty(object)) {
return Optional.empty();
}
// try explicit format first
Optional<String> format = context.getAnnotationMetadata().stringValue(Format.class);
if (format.isPresent()) {
DateTimeFormatter formatter = getFormatter(format.get(), context);
try {
T converted = formatter.parse(object, query);
return Optional.of(converted);
} catch (DateTimeParseException e) {
context.reject(object, e);
return Optional.empty();
}
} else {
try {
T converted = isoFormatter.parse(object, query);
return Optional.of(converted);
} catch (DateTimeParseException ignored) {
}
}
// fall back to RFC 1123 date time for compatibility
try {
T result = DateTimeFormatter.RFC_1123_DATE_TIME.parse(object, query);
return Optional.of(result);
} catch (DateTimeParseException e) {
context.reject(object, e);
return Optional.empty();
}
});
conversionService.addConverter(temporalType, CharSequence.class, (object, targetType, context) -> {
if (Objects.isNull(object)) {
return Optional.empty();
}
// try explicit format first
Optional<String> format = context.getAnnotationMetadata().stringValue(Format.class);
if (format.isPresent()) {
DateTimeFormatter formatter = getFormatter(format.get(), context);
try {
CharSequence converted = formatter.format(object);
return Optional.of(converted);
} catch (DateTimeException e) {
context.reject(object, e);
return Optional.empty();
}
} else {
try {
CharSequence converted = isoFormatter.format(object);
return Optional.of(converted);
} catch (DateTimeException ignored) {
}
}
// fall back to RFC 1123 date time for compatibility
try {
CharSequence converted = DateTimeFormatter.RFC_1123_DATE_TIME.format(object);
return Optional.of(converted);
} catch (DateTimeException e) {
context.reject(object, e);
return Optional.empty();
}
});
}
private <T extends TemporalAccessor> void addTemporalIntegerConverters(MutableConversionService conversionService, Class<T> temporalType, DateTimeFormatter isoFormatter, TemporalQuery<T> query) {
conversionService.addConverter(Integer.class, temporalType, (Integer object, Class<T> targetType, ConversionContext context) -> {
if (Objects.isNull(object)) {
return Optional.empty();
}
// try explicit format first
Optional<String> format = context.getAnnotationMetadata().stringValue(Format.class);
if (format.isPresent()) {
DateTimeFormatter formatter = getFormatter(format.get(), context);
try {
T converted = formatter.parse(object.toString(), query);
return Optional.of(converted);
} catch (DateTimeParseException e) {
context.reject(object, e);
return Optional.empty();
}
} else {
try {
T converted = isoFormatter.parse(object.toString(), query);
return Optional.of(converted);
} catch (DateTimeParseException ignored) {
}
}
// fall back to RFC 1123 date time for compatibility
try {
T result = DateTimeFormatter.RFC_1123_DATE_TIME.parse(object.toString(), query);
return Optional.of(result);
} catch (DateTimeParseException e) {
context.reject(object, e);
return Optional.empty();
}
});
}
private BiFunction<CharSequence, ConversionContext, Optional<Duration>> durationConverter() {
return (object, context) -> {
String value = object.toString().trim();
if (value.startsWith("P")) {
try {
return Optional.of(Duration.parse(value));
} catch (DateTimeParseException e) {
context.reject(value, e);
return Optional.empty();
}
} else {
Matcher matcher = DURATION_MATCHER.matcher(value);
if (matcher.find()) {
String amount = matcher.group(1);
final String g2 = matcher.group(2);
char type = g2.charAt(0);
try {
switch (type) {
case 's' -> {
return Optional.of(Duration.ofSeconds(Integer.parseInt(amount)));
}
case 'm' -> {
String ms = matcher.group(MILLIS);
if (StringUtils.hasText(ms)) {
return Optional.of(Duration.ofMillis(Integer.parseInt(amount)));
} else {
return Optional.of(Duration.ofMinutes(Integer.parseInt(amount)));
}
}
case 'h' -> {
return Optional.of(Duration.ofHours(Integer.parseInt(amount)));
}
case 'd' -> {
return Optional.of(Duration.ofDays(Integer.parseInt(amount)));
}
default -> {
final String seq = g2 + matcher.group(3);
if (seq.equals("ns")) {
return Optional.of(Duration.ofNanos(Integer.parseInt(amount)));
}
context.reject(
value,
new DateTimeParseException("Unparseable date format (" + value + "). Should either be a ISO-8601 duration or a round number followed by the unit type", value, 0)
);
return Optional.empty();
}
}
} catch (NumberFormatException e) {
context.reject(value, e);
}
} else {
context.reject(
value,
new DateTimeParseException("Unparseable date format (" + value + "). Should either be a ISO-8601 duration or a round number followed by the unit type", value, 0)
);
}
}
return Optional.empty();
};
}
private BiFunction<CharSequence, ConversionContext, Optional<Period>> periodConverter() {
return (object, context) -> {
String value = object.toString().trim();
if (value.startsWith("P")) {
try {
return Optional.of(Period.parse(value));
} catch (DateTimeParseException e) {
context.reject(value, e);
return Optional.empty();
}
} else {
Matcher matcher = PERIOD_MATCHER.matcher(value);
if (matcher.find()) {
String amount = matcher.group(1);
final String g2 = matcher.group(2);
char type = g2.charAt(0);
try {
switch (type) {
case 'y' -> {
return Optional.of(Period.ofYears(Integer.parseInt(amount)));
}
case 'm' -> {
return Optional.of(Period.ofMonths(Integer.parseInt(amount)));
}
case 'w' -> {
return Optional.of(Period.ofWeeks(Integer.parseInt(amount)));
}
case 'd' -> {
return Optional.of(Period.ofDays(Integer.parseInt(amount)));
}
default -> {
context.reject(
value,
new DateTimeParseException("Unparseable date format (" + value + "). Should either be a ISO-8601 duration or a round number followed by the unit type", value, 0)
);
return Optional.empty();
}
}
} catch (NumberFormatException e) {
context.reject(value, e);
}
} else {
context.reject(
value,
new DateTimeParseException("Unparseable date format (" + value + "). Should either be a ISO-8601 duration or a round number followed by the unit type", value, 0)
);
}
}
return Optional.empty();
};
}
private DateTimeFormatter getFormatter(String pattern, ConversionContext context) {
var key = pattern + context.getLocale();
var cachedFormatter = formattersCache.get(key);
if (cachedFormatter != null) {
return cachedFormatter;
}
var formatter = DateTimeFormatter.ofPattern(pattern, context.getLocale());
formattersCache.put(key, formatter);
return formatter;
}
private <T extends TemporalAccessor> void addTemporalToDateConverter(MutableConversionService conversionService, Class<T> temporalType, Function<T, Instant> toInstant) {
conversionService.addConverter(temporalType, Date.class, (T object, Class<Date> targetType, ConversionContext context) -> Optional.of(Date.from(toInstant.apply(object))));
}
}