web-dev-qa-db-ja.com

p:calendarでJava.time.ZonedDateTime / LocalDateTimeを使用する方法

Java EEアプリケーションで日時操作にJoda Timeを使用していたが、関連付けられたクライアントから送信された日時の文字列表現が、送信前に次の変換ルーチンを使用して変換された。 JSFコンバーターのgetAsObject()メソッド内のデータベースへ。

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));

指定されたローカルタイムゾーンは、UTC/GMTより5時間30分進んでいます。したがって、UTCへの変換では、与えられた日時から5時間30分差し引く必要があります。これは、Joda Timeを使用して正しく行われます。期待どおりに次の出力が表示されます。

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

+0530の代わりにタイムゾーンオフセット+05:30が使用されています。これは、この形式でゾーンオフセットを送信する<p:calendar>に依存しているためです。 <p:calendar>のこの動作を変更することは不可能のようです(この質問自体は、それ以外の場合は必要ありませんでした)。


ただし、Java Time API in 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

Zzに置き換えられ、+0530+05:30に置き換えられました。

この点でこれら2つのAPIの動作が異なる理由は、この質問ではひたすら無視されています。

<p:calendar>が内部的に使用しているものの、<p:calendar>およびJavaでの時間Java 8が一貫して一貫して機能するために必要な時間) 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>

タイムゾーンは、ユーザーの現在のタイムゾーンに完全に透過的に依存しています。

単一のプロパティ以外に何もないBean。

@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.
    }
}

これは、最初の3つのコードスニペットの最後から2番目の例/中央に示されているように、予期しない方法で機能します。

具体的には、05-Jan-2016 12:00:00 AM +0530と入力すると、コンバータでの05-Jan-2016 05:30:00 AM ISTからUTCへの元の変換が失敗するため、05-Jan-2016 12:00:00 AM +0530が再表示されます。

オフセットが+05:30のローカルタイムゾーンからUTCへの変換、およびUTCからそのタイムゾーンへの変換は、カレンダーコンポーネントを通じて入力されたものと同じ日時を再表示する必要があります。これは、与えられたコンバーターの基本的な機能です。


更新:

Java.sql.TimestampおよびJava.time.ZonedDateTimeとの間で変換を行うJPAコンバーター。

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);
    }
}
23
Tiny

具体的な問題は、Jodaのゾーンレス日付時刻インスタンス DateTime から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. タイムゾーン(オフセット)は、データベースを含むすべてのレイヤーに渡る必要があります。ただし、データベースに「タイムゾーンのない日付時刻」列タイプがある場合、タイムゾーン情報は永続化中に失われ、データベースからサービスを提供するときに問題が発生します。

    使用しているDBは不明ですが、一部のDBは Oracle および PostgreSQL DBから知られている_TIMESTAMP WITH TIME ZONE_列タイプをサポートしていないことに注意してください。たとえば、 MySQLはサポートしていません です。 2列目が必要です。

これらの変更が受け入れられない場合は、LocalDateTimeに戻り、データベースを含むすべてのレイヤー全体で固定/事前定義されたタイムゾーンに依存する必要があります。通常これにはUTCが使用されます。


JSFおよびJPAでのZonedDateTimeの取り扱い

適切な_TIMESTAMP WITH TIME ZONE_ DB列タイプでZonedDateTimeを使用する場合、以下のJSFコンバーターを使用して、UIのStringとモデルのZonedDateTimeの間で変換します。このコンバーターは、親コンポーネントからpatternおよびlocale属性を検索します。親コンポーネントがpatternまたはlocale属性をネイティブでサポートしていない場合は、単にそれらを_<f:attribute name="..." value="...">_として追加します。 locale属性がない場合は、代わりに(デフォルト)_<f:view locale>_が使用されます。上記の#1で説明した理由により、notimeZone属性があります。

_@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とJDBCの_Java.util.Calendar_の間で変換します(適切な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());
    }

}
_

JSFおよびJPAでのLocalDateTimeの取り扱い

UTCベースのLocalDateTimeを適切なUTCベースのTIMESTAMP(タイムゾーンなし)で使用する場合、DB列タイプは、以下のJSFコンバーターを使用して、UIのStringと_の間で変換します。 [$ var] _モデル内。このコンバーターは、親コンポーネントからLocalDateTimepattern、およびtimeZone属性を検索します。親コンポーネントがlocalepattern、および/またはtimeZone属性をネイティブにサポートしていない場合は、単にそれらを_<f:attribute name="..." value="...">_として追加します。 locale属性は、入力文字列のフォールバックタイムゾーン(timeZoneにタイムゾーンが含まれていない場合)および出力文字列のタイムゾーンを表す必要があります。

_@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コンバーターを使用して、モデルのpatternとJDBCの_Java.sql.Timestamp_の間で変換します(適切なJDBCドライバーはLocalDateTime型付き列にそれを必要/使用します):

_@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();
    }

}
_

_<p:calendar>_を使用して特定のケースにTIMESTAMPを適用する

以下を変更する必要があります:

  1. _<p:calendar>_はLocalDateTimeConverterでコンバーターを検索しないため、_<converter><converter-id>localDateTimeConverter_の_faces-config.xml_に再登録するか、以下のように注釈を変更する必要があります

    _@FacesConverter("localDateTimeConverter")
    _
  2. _<p:calendar>_なしの_timeOnly="true"_はforClassを無視し、ポップアップでそれを編集するオプションを提供するので、コンバーターを回避するためにtimeZone属性を削除する必要があります混乱します(この属性は、タイムゾーンがtimeZoneにない場合にのみ必要です)。

  3. 出力時に必要な表示pattern属性を指定する必要があります(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_に登録する必要があります: 属性付きのコンバーター用のカスタムタグの作成

_<h:outputText id="display" value="#{bean.dateTime}">
    <my:convertLocalDateTime pattern="dd-MMM-yyyy hh:mm:ss a Z" 
                             timeZone="Asia/Kolkata" />
</h:outputText>
_
39
BalusC