Здравствуйте, подскажите, пожалуйста, как отображать даты и время в пользовательском интерфейсе с учетом часового пояса пользователя при условии, что в базе данных это все хранится в UTC?
Добрый день,
Можете более детально описать:
- какая версия Jmix
- Какой исходный тип даты вы хотите фарматировать
- какие то еще дополнительные детали проблемы
Версия Jmix 2.0.2
Хочу отформатировать LocalDateTime для того, чтобы в таблицах (и не только в них) отображалось время с учетом временной зоны, которую он непосредственно указал в настройках
Добрый день,
Извиняюсь за задержу.
Проблема: Связь с временными зонами
- LocalDateTime не имеет понятия о временных зонах. Это просто дата и время в локальном контексте.
-
ZonedDateTime связывает
LocalDateTime
с временной зоной, учитывая правила перехода на летнее/зимнее время. -
OffsetDateTime связывает
LocalDateTime
с фиксированным смещением от UTC, не учитывая правила перехода на летнее/зимнее время.
Решение
В Jmix есть 2 способа получить зону клиента.
Источник тайм зоны для логики отображения.
-
Spring-ый из SecurityContext.
currentAuthentication.getTimeZone().toZoneId()
Всегда статическое и получается из сессии.
-
из пользователя Jmix. Тут есть конфигурируемое поле в Users:
final User user = (User) currentAuthentication.getUser(); user.getTimeZoneId();
Получается из настроек и может быть null. А еще - если поменять, то только после пере-логина и форматы поменяются.
Логика форматирования:
Нам надо просто с использованием зоны и знаний LDT конвертировать его в строку.
private String convertUtcToUserTimezone(LocalDateTime utcTime, String userTimezone) {
if(utcTime == null) {
return "";
}
ZonedDateTime utcZonedDateTime = utcTime.atZone(ZoneId.of("UTC"));
ZonedDateTime userZonedDateTime = utcZonedDateTime.withZoneSameInstant(ZoneId.of(userTimezone));
// your pattern here, may be provided from some service or configuration
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
return userZonedDateTime.format(formatter);
}
UI
-
Таблицы. Получаем необходимую отформатировать колонку и даем ей
Renderer
:// здесь я специально опустил ZoneProvider тк решите сами как хотите получать зону public <T> ItemLabelGenerator<T> newGenerator(Function<T, LocalDateTime> ldtProvider) { return entity -> convertUtcToUserTimezone(ldtProvider.apply(entity), userZoneProvider.getCurrentUserZoneIdString()); } // добавляем рендерер на необходимую таблицу @Subscribe public void onBeforeShow(final BeforeShowEvent event) { usersDataGrid.getColumnByKey("ldtColumn") .setRenderer(new TextRenderer<>( localDateTimeLabelGeneratorProvider.newGenerator(User::getLdt))); }
-
филды:
ldtField.setZoneId(currentAuthentication.getTimeZone().toZoneId());
Domain-based solution
создаем вычисляемое поле типа OffsetDateTime.
@DependsOnProperties("ldt")
@JmixProperty
public OffsetDateTime getOffsetLdt() {
if(this.ldt == null) {
return null;
}
return TimeConverter.fromLocalDateTime(this.ldt);
}
@DependsOnProperties("ldt")
public void setOffsetLdt(OffsetDateTime offsetLdt) {
LocalDateTime localDateTime = TimeConverter.convertFromOffsetDateTime(offsetLdt);
// prevent recursive update due to collection value change listeners
if(!localDateTime.equals(this.ldt)){
// updating linked fields
setLdt(localDateTime);
}
}
public LocalDateTime getLdt() {
return ldt;
}
public void setLdt(LocalDateTime ldt) {
this.ldt = ldt;
}
Util class:
public final class TimeConverter {
// copied from io.jmix.core.security.CurrentAuthentication
public static TimeZone getCurrentTimeZone() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object details = authentication.getDetails();
Object principal = authentication.getPrincipal();
TimeZone timeZone = null;
if (principal instanceof HasTimeZone) {
String timeZoneId = ((HasTimeZone)principal).getTimeZoneId();
if (!Strings.isNullOrEmpty(timeZoneId)) {
timeZone = TimeZone.getTimeZone(timeZoneId);
}
} else if (details instanceof ClientDetails) {
timeZone = ((ClientDetails)details).getTimeZone();
}
return timeZone == null ? TimeZone.getDefault() : timeZone;
}
public static LocalDateTime convertFromOffsetDateTime(OffsetDateTime offsetDateTime) {
OffsetDateTime utcOffsetDateTime = offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);
return utcOffsetDateTime.toLocalDateTime();
}
public static OffsetDateTime fromLocalDateTime(LocalDateTime localDateTime) {
ZoneId zoneId = getCurrentTimeZone().toZoneId();
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
return zonedDateTime.toOffsetDateTime();
}
}
Плюсы:
- такое поле легко использовать в экранах - оно сразу с оффетом на зону UTC
- не учитывает переходы на зимнее и летнее время(не страшно для Европы и России)
Минусы:
- Бизнес логика в доменной области - смешивать императивный стиль бизнес сценариев в сервисах и бизнес в домене - не советую
- сложно для понимания
- Необходимо понимать работу VaueChange event-ов в Jmix - может привести к багам и StackOverflow
- я сделал связь только при изменениях offsetLdt - если его меняют, то поменяется и оригинальный ldt. Чтобы в обратную сторону сработало надо добавлять транзиентный флаг и при setLdt ставить его на true, прокидывать setOffsetDateTime() и потом снова на false. В общем - еще одна обработка SOF.
В рамках Jmix, все же советую использовать первый вариант и просто делегировать логику форматирования каким то сервисам. Полагаю, что у вас решение внутри россии, никаких впн нету и нигде сохранять ничего не надо. Так что не так уж сложно просто проставить везде форматтеры.
Общее решение.
На сколько мне известно, заставить LocalDateTime само-конвертироваться в зональное время в Jmix из коробки нету, тк есть OffsetDateTime и ZonedDateTime.
С уважением,
Дмитрий
UPD: Альтернативно можно создать кастомный тип данных и его маппить и юзать вместо LDT
https://docs.jmix.io/jmix/data-model/data-types.html#custom-type