Как использовать java.time.ZonedDateTime/LocalDateTime в p:calendar

Я использовал Joda Time для манипулирования датой и временем в приложении Java EE, в котором строковое представление даты и времени, отправленное соответствующим клиентом, было преобразовано с использованием следующей процедуры преобразования перед отправкой его в базу данных, т.е. в методе getAsObject() в конвертер JSF.

org.joda.time.format.DateTimeFormatter formatter = org.joda.time.format.DateTimeFormat.forPattern("dd-MMM-yyyy hh:mm:ss a Z").withZone(DateTimeZone.UTC);
DateTime dateTime = formatter.parseDateTime("05-Jan-2016 03:04:44 PM +0530");

System.out.println(formatter.print(dateTime));

Указанный местный часовой пояс на 5 часов и 30 минут опережает UTC / GMT. Следовательно, преобразование в UTC должно вычесть 5 часов и 30 минут из заданной даты и времени, что происходит правильно с использованием времени Joda. Он отображает следующий вывод, как и ожидалось.

05-Jan-2016 09:34:44 AM +0000

► Было принято смещение часового пояса +0530 вместо +05:30, поскольку оно зависит от <p:calendar>, который отправляет смещение пояса в этом формате. Не представляется возможным изменить такое поведение <p:calendar> (иначе сам этот вопрос был бы не нужен).


Однако то же самое не работает, если попытаться использовать Java Time API в Java 8.

java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("dd-MMM-yyyy hh:mm:ss a Z").withZone(ZoneOffset.UTC);
ZonedDateTime dateTime = ZonedDateTime.parse("05-Jan-2016 03:04:44 PM +0530", formatter);

System.out.println(formatter.format(dateTime));

Он неожиданно отображает следующий неверный вывод.

05-Jan-2016 03:04:44 PM +0000

Очевидно, что дата-время конвертируется не в соответствии с UTC, в который предполагается конвертировать.

Для корректной работы требуется внести следующие изменения.

java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("dd-MMM-yyyy hh:mm:ss a z").withZone(ZoneOffset.UTC);
ZonedDateTime dateTime = ZonedDateTime.parse("05-Jan-2016 03:04:44 PM +05:30", formatter);

System.out.println(formatter.format(dateTime));

Что, в свою очередь, отображает следующее.

05-Jan-2016 09:34:44 AM Z

Z заменено на z, а +0530 заменено на +05:30.

Почему эти два API ведут себя по-разному в этом отношении, в этом вопросе полностью игнорируется.

Какой промежуточный подход можно рассмотреть для того, чтобы <p:calendar> и Java Time в Java 8 работали согласованно и согласованно, хотя <p:calendar> внутри использует SimpleDateFormat вместе с java.util.Date?


Неудачный тестовый сценарий в JSF.

Преобразователь:

@FacesConverter("dateTimeConverter")
public class DateTimeConverter implements Converter {

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        if (value == null || value.isEmpty()) {
            return null;
        }

        try {
            return ZonedDateTime.parse(value, DateTimeFormatter.ofPattern("dd-MMM-yyyy hh:mm:ss a Z").withZone(ZoneOffset.UTC));
        } catch (IllegalArgumentException | DateTimeException e) {
            throw new ConverterException(new FacesMessage(FacesMessage.SEVERITY_ERROR, null, "Message"), e);
        }
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        if (value == null) {
            return "";
        }

        if (!(value instanceof ZonedDateTime)) {
            throw new ConverterException("Message");
        }

        return DateTimeFormatter.ofPattern("dd-MMM-yyyy hh:mm:ss a z").withZone(ZoneId.of("Asia/Kolkata")).format(((ZonedDateTime) value));
        // According to a time zone of a specific user.
    }
}

XHTML с <p:calendar>.

<p:calendar  id="dateTime"
             timeZone="Asia/Kolkata"
             pattern="dd-MMM-yyyy hh:mm:ss a Z"
             value="#{bean.dateTime}"
             showOn="button"
             required="true"
             showButtonPanel="true"
             navigator="true">
    <f:converter converterId="dateTimeConverter"/>
</p:calendar>

<p:message for="dateTime"/>

<p:commandButton value="Submit" update="display" actionListener="#{bean.action}"/><br/><br/>

<h:outputText id="display" value="#{bean.dateTime}">
    <f:converter converterId="dateTimeConverter"/>
</h:outputText>

Часовой пояс полностью зависит от текущего часового пояса пользователя.

Компонент, не имеющий ничего, кроме одного свойства.

@ManagedBean
@ViewScoped
public class Bean implements Serializable {

    private ZonedDateTime dateTime; // Getter and setter.
    private static final long serialVersionUID = 1L;

    public Bean() {}

    public void action() {
        // Do something.
    }
}

Это будет работать неожиданным образом, как показано в предпоследнем примере/посередине первых трех фрагментов кода.

В частности, если вы введете 05-Jan-2016 12:00:00 AM +0530, будет повторно отображено 05-Jan-2016 05:30:00 AM IST, потому что исходное преобразование 05-Jan-2016 12:00:00 AM +0530 в UTC в конвертере не удалось.

Преобразование из местного часового пояса со смещением +05:30 в UTC, а затем преобразование из UTC обратно в этот часовой пояс, очевидно, должно повторно отображать ту же дату-время, что и введенное через компонент календаря, который является элементарной функциональностью данного преобразователя.


Обновление:

Конвертер JPA, конвертирующий в java.sql.Timestamp и java.time.ZonedDateTime и обратно.

import java.sql.Timestamp;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public final class JodaDateTimeConverter implements AttributeConverter<ZonedDateTime, Timestamp> {

    @Override
    public Timestamp convertToDatabaseColumn(ZonedDateTime dateTime) {
        return dateTime == null ? null : Timestamp.from(dateTime.toInstant());
    }

    @Override
    public ZonedDateTime convertToEntityAttribute(Timestamp timestamp) {
        return timestamp == null ? null : ZonedDateTime.ofInstant(timestamp.toInstant(), ZoneOffset.UTC);
    }
}

person Tiny    schedule 19.01.2016    source источник


Ответы (1)


Ваша конкретная проблема заключается в том, что вы мигрировали с экземпляра даты и времени без зоны Joda DateTime< /a> в зонированный экземпляр даты и времени Java8 ZonedDateTime вместо экземпляра даты и времени без зоны в Java8 LocalDateTime .

Использование ZonedDateTime (или OffsetDateTime) вместо LocalDateTime требует как минимум 2 дополнительных изменения:

  1. Не задавайте часовой пояс (смещение) при преобразовании даты и времени. Вместо этого часовой пояс входной строки, если он есть, будет использоваться во время синтаксического анализа, а часовой пояс, хранящийся в экземпляре ZonedDateTime, должен использоваться во время форматирования.

    DateTimeFormatter#withZone() будет давать запутанные результаты только с ZonedDateTime, поскольку он будет действовать как запасной вариант во время синтаксического анализа (он используется только тогда, когда часовой пояс отсутствует во входной строке или шаблоне формата), и он будет действовать как переопределение во время форматирования (часовой пояс хранится в ZonedDateTime полностью игнорируется). Это основная причина вашей наблюдаемой проблемы. Простое опускание withZone() при создании средства форматирования должно исправить это.

    Обратите внимание, что если вы указали преобразователь и не имеете timeOnly="true", вам не нужно указывать <p:calendar timeZone>. Даже если вы это сделаете, вы предпочтете использовать TimeZone.getTimeZone(zonedDateTime.getZone()) вместо жесткого кодирования.

  2. Вам необходимо перенести часовой пояс (смещение) на все слои, включая базу данных. Однако, если ваша база данных имеет дату и время без типа столбца часового пояса, тогда информация о часовом поясе будет потеряна во время сохранения, и вы столкнетесь с проблемами при возврате из базы данных.

    Неясно, какую БД вы используете, но имейте в виду, что некоторые БД не поддерживают тип столбца TIMESTAMP WITH TIME ZONE, известный из Oracle и PostgreSQL БД. Например, MySQL не поддерживает это . Вам понадобится вторая колонка.

Если эти изменения неприемлемы, вам нужно вернуться к LocalDateTime и полагаться на фиксированный/предопределенный часовой пояс во всех слоях, включая базу данных. Обычно для этого используется UTC.


Работа с ZonedDateTime в JSF и JPA

При использовании ZonedDateTime с соответствующим типом столбца TIMESTAMP WITH TIME ZONE DB используйте приведенный ниже преобразователь JSF для преобразования между String в пользовательском интерфейсе и ZonedDateTime в модели. Этот преобразователь будет искать атрибуты pattern и locale в родительском компоненте. Если родительский компонент изначально не поддерживает атрибут pattern или locale, просто добавьте их как <f:attribute name="..." value="...">. Если атрибут locale отсутствует, вместо него будет использоваться (по умолчанию) <f:view locale>. Атрибута нет timeZone по причине, описанной в пункте 1 выше.

@FacesConverter(forClass=ZonedDateTime.class)
public class ZonedDateTimeConverter implements Converter {

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object modelValue) {
        if (modelValue == null) {
            return "";
        }

        if (modelValue instanceof ZonedDateTime) {
            return getFormatter(context, component).format((ZonedDateTime) modelValue);
        } else {
            throw new ConverterException(new FacesMessage(modelValue + " is not a valid ZonedDateTime"));
        }
    }

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String submittedValue) {
        if (submittedValue == null || submittedValue.isEmpty()) {
            return null;
        }

        try {
            return ZonedDateTime.parse(submittedValue, getFormatter(context, component));
        } catch (DateTimeParseException e) {
            throw new ConverterException(new FacesMessage(submittedValue + " is not a valid zoned date time"), e);
        }
    }

    private DateTimeFormatter getFormatter(FacesContext context, UIComponent component) {
        return DateTimeFormatter.ofPattern(getPattern(component), getLocale(context, component));
    }

    private String getPattern(UIComponent component) {
        String pattern = (String) component.getAttributes().get("pattern");

        if (pattern == null) {
            throw new IllegalArgumentException("pattern attribute is required");
        }

        return pattern;
    }

    private Locale getLocale(FacesContext context, UIComponent component) {
        Object locale = component.getAttributes().get("locale");
        return (locale instanceof Locale) ? (Locale) locale
            : (locale instanceof String) ? new Locale((String) locale)
            : context.getViewRoot().getLocale();
    }

}

И используйте приведенный ниже преобразователь JPA для преобразования между ZonedDateTime в модели и java.util.Calendar в JDBC (приличный драйвер JDBC потребует/использует его для типизированного столбца TIMESTAMP WITH TIME ZONE):

@Converter(autoApply=true)
public class ZonedDateTimeAttributeConverter implements AttributeConverter<ZonedDateTime, Calendar> {

    @Override
    public Calendar convertToDatabaseColumn(ZonedDateTime entityAttribute) {
        if (entityAttribute == null) {
            return null;
        }

        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(entityAttribute.toInstant().toEpochMilli());
        calendar.setTimeZone(TimeZone.getTimeZone(entityAttribute.getZone()));
        return calendar;
    }

    @Override
    public ZonedDateTime convertToEntityAttribute(Calendar databaseColumn) {
        if (databaseColumn == null) {
            return null;
        }

        return ZonedDateTime.ofInstant(databaseColumn.toInstant(), databaseColumn.getTimeZone().toZoneId());
    }

}

Работа с LocalDateTime в JSF и JPA

При использовании LocalDateTime на основе UTC с соответствующим типом столбца базы данных TIMESTAMP на основе UTC (без часового пояса!) используйте приведенный ниже преобразователь JSF для преобразования между String в пользовательском интерфейсе и LocalDateTime в модели. Этот преобразователь будет искать атрибуты pattern, timeZone и locale в родительском компоненте. Если родительский компонент изначально не поддерживает атрибуты pattern, timeZone и/или locale, просто добавьте их как <f:attribute name="..." value="...">. Атрибут timeZone должен представлять резервный часовой пояс входной строки (если pattern не содержит часовой пояс) и часовой пояс выходной строки.

@FacesConverter(forClass=LocalDateTime.class)
public class LocalDateTimeConverter implements Converter {

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object modelValue) {
        if (modelValue == null) {
            return "";
        }

        if (modelValue instanceof LocalDateTime) {
            return getFormatter(context, component).format(ZonedDateTime.of((LocalDateTime) modelValue, ZoneOffset.UTC));
        } else {
            throw new ConverterException(new FacesMessage(modelValue + " is not a valid LocalDateTime"));
        }
    }

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String submittedValue) {
        if (submittedValue == null || submittedValue.isEmpty()) {
            return null;
        }

        try {
            return ZonedDateTime.parse(submittedValue, getFormatter(context, component)).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();
        } catch (DateTimeParseException e) {
            throw new ConverterException(new FacesMessage(submittedValue + " is not a valid local date time"), e);
        }
    }

    private DateTimeFormatter getFormatter(FacesContext context, UIComponent component) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(getPattern(component), getLocale(context, component));
        ZoneId zone = getZoneId(component);
        return (zone != null) ? formatter.withZone(zone) : formatter;
    }

    private String getPattern(UIComponent component) {
        String pattern = (String) component.getAttributes().get("pattern");

        if (pattern == null) {
            throw new IllegalArgumentException("pattern attribute is required");
        }

        return pattern;
    }

    private Locale getLocale(FacesContext context, UIComponent component) {
        Object locale = component.getAttributes().get("locale");
        return (locale instanceof Locale) ? (Locale) locale
            : (locale instanceof String) ? new Locale((String) locale)
            : context.getViewRoot().getLocale();
    }

    private ZoneId getZoneId(UIComponent component) {
        Object timeZone = component.getAttributes().get("timeZone");
        return (timeZone instanceof TimeZone) ? ((TimeZone) timeZone).toZoneId()
            : (timeZone instanceof String) ? ZoneId.of((String) timeZone)
            : null;
    }

}

И используйте приведенный ниже преобразователь JPA для преобразования между LocalDateTime в модели и java.sql.Timestamp в JDBC (приличный драйвер JDBC потребует/использует его для типизированного столбца TIMESTAMP):

@Converter(autoApply=true)
public class LocalDateTimeAttributeConverter implements AttributeConverter<LocalDateTime, Timestamp> {

    @Override
    public Timestamp convertToDatabaseColumn(LocalDateTime entityAttribute) {
        if (entityAttribute == null) {
            return null;
        }

        return Timestamp.valueOf(entityAttribute);
    }

    @Override
    public LocalDateTime convertToEntityAttribute(Timestamp databaseColumn) {
        if (databaseColumn == null) {
            return null;
        }

        return databaseColumn.toLocalDateTime();
    }

}

Применение LocalDateTimeConverter к вашему конкретному делу с помощью <p:calendar>

Вам нужно изменить следующее:

  1. Поскольку <p:calendar> не выполняет поиск конвертеров по forClass, вам нужно либо перерегистрировать его с помощью <converter><converter-id>localDateTimeConverter в faces-config.xml, либо изменить аннотацию, как показано ниже.

     @FacesConverter("localDateTimeConverter")
    
  2. Поскольку <p:calendar> без timeOnly="true" игнорирует timeZone и предлагает во всплывающем окне возможность его редактирования, вам необходимо удалить атрибут timeZone, чтобы не путать конвертер (этот атрибут требуется только в том случае, если часовой пояс отсутствует в pattern) .

  3. Вам нужно указать желаемый атрибут отображения timeZone во время вывода (этот атрибут не требуется при использовании ZonedDateTimeConverter, так как он уже сохранен в ZonedDateTime).

Вот полный рабочий фрагмент:

<p:calendar id="dateTime"
            pattern="dd-MMM-yyyy hh:mm:ss a Z"
            value="#{bean.dateTime}"
            showOn="button"
            required="true"
            showButtonPanel="true"
            navigator="true">
    <f:converter converterId="localDateTimeConverter" />
</p:calendar>

<p:message for="dateTime" autoUpdate="true" />

<p:commandButton value="Submit" update="display" action="#{bean.action}" /><br/><br/>

<h:outputText id="display" value="#{bean.dateTime}">
    <f:converter converterId="localDateTimeConverter" />
    <f:attribute name="pattern" value="dd-MMM-yyyy hh:mm:ss a Z" />
    <f:attribute name="timeZone" value="Asia/Kolkata" />
</h:outputText>

Если вы собираетесь создать свой собственный <my:convertLocalDateTime> с атрибутами, вам нужно будет добавить их как bean-подобные свойства с геттерами/сеттерами в класс преобразователя и зарегистрировать его в *.taglib.xml, как показано в этом ответе: Создание пользовательского тега для Converter с атрибутами

<h:outputText id="display" value="#{bean.dateTime}">
    <my:convertLocalDateTime pattern="dd-MMM-yyyy hh:mm:ss a Z" 
                             timeZone="Asia/Kolkata" />
</h:outputText>
person BalusC    schedule 21.01.2016
comment
Это работает. Спасибо. Ответ заслуживает по крайней мере одной награды, так как это заняло значительное количество времени. Завтра начну баунти. - person Tiny; 22.01.2016
comment
В случае, если пользователь изменит смещение зоны на что-то другое, чем смещение по умолчанию в <p:calendar>, например, -0500 (America/New_York), это приведет к запутанному результату, поскольку конвертер продолжает работать на основе выбранной пользователем зоны. Зона, измененная в <p:calendar>, не будет иметь никакого эффекта. Если введено 07-Jan-2016 12:00:00 AM -0500, ожидаемая дата-время для вставки в базу данных будет 07-Jan-2016 05:00:00 AM ET, но вместо этого будет вставлено 06-Jan-2016 06:30:00 PM ET в зависимости от выбранной зоны, +0530. Календарь и конвертер можно как-то синхронизировать? - person Tiny; 24.01.2016
comment
Извините, не могу воспроизвести. Вы также изменили время ввода после смены часового пояса? - person BalusC; 24.01.2016
comment
Дата и время не изменяются после изменения часового пояса в <p:calendar>. Что касается среды Java SE, преобразователь, по сути, делает - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy hh:mm:ss a Z").withZone(ZoneId.of("Asia/Kolkata")); LocalDateTime localDateTime = ZonedDateTime.parse("07-Jan-2016 12:00:00 AM -0500", formatter).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();, и в этом случае formatter.format(localDateTime) возвращает 06-Jan-2016 06:30:00 PM ET, в то время как ожидается, что он вернет 07-Jan-2016 05:00:00 AM ET, преобразуя -0500 в -0000. - person Tiny; 24.01.2016
comment
Похоже, вы все еще установили timeZone на <p:calendar>. Его следует удалить, как показано в рабочем фрагменте. - person BalusC; 24.01.2016
comment
Атрибут timeZone больше не доступен в <p:calendar>, а конфигурация сервера ServerTimezone не установлена ​​на UTC. И то, и другое молотит постоянно из приложения/сервера. - person Tiny; 24.01.2016
comment
В вашем примере Java SE этот шаг withZone() следует пропустить. Интересно, откуда это идет. LocalDateTimeConverter в моем ответе добавляет его только тогда, когда для компонента указан атрибут timeZone. - person BalusC; 24.01.2016
comment
Проблема возникла из-за того, что я модифицировал код конвертера, внеся в него изюминку. Конвертер был значительно упрощен с помощью taglib. Спасибо. - person Tiny; 24.01.2016
comment
О, рад, что снова все в порядке :) - person BalusC; 24.01.2016
comment
Какой языковой формат принимает этот конструктор new Locale(String language)? Другой перегруженный конструктор new Locale("hi", "IN") работает, но этот new Locale("hi") молча не работает, сбрасывая языковой стандарт на значение по умолчанию. Тем не менее, это больше не нужно нигде в моих вариантах использования, как я делаю locale="#{facesContext.application.defaultLocale}" в <p:calendar>. - person Tiny; 25.01.2016
comment
Вы можете поставить hi_IN как language. - person BalusC; 25.01.2016
comment
Забыл упомянуть об этом в предыдущем комментарии. new Locale("hi_IN") имеет тот же эффект, что и new Locale("hi") — он также автоматически не работает, возвращаясь к локали по умолчанию. В настоящее время я тестирую консольное приложение Java SE. - person Tiny; 25.01.2016
comment
Вы правы, это интерпретируется как вариант, а не как страна. Неудобные вещи. Подумайте, что локаль лучше не указывать в виде строки, иначе вам придется делать разделение и тому подобное. - person BalusC; 25.01.2016
comment
Для JPA я использую проект ThreeTen JPA. Я запросил включение JSF - person Gilberto; 07.04.2016
comment
@Gilberto: JSF 2.3 будет поставляться со встроенной поддержкой через f:convertDateTime. - person BalusC; 07.04.2016