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
Z
はz
に置き換えられ、+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);
}
}
具体的な問題は、Jodaのゾーンレス日付時刻インスタンス DateTime
からJava8のゾーンレス日付インスタンス ZonedDateTime
にJava8のゾーンレス日付ではなく移行したことです時間インスタンス LocalDateTime
。
ZonedDateTime
の代わりにOffsetDateTime
(または LocalDateTime
)を使用するには、少なくとも2つの追加の変更が必要です。
日時変換中にタイムゾーン(オフセット)を強制しないでください。代わりに、入力文字列のタイムゾーン(ある場合)が解析中に使用され、ZonedDateTime
インスタンスに格納されているタイムゾーンがフォーマット中に使用される必要があります。
DateTimeFormatter#withZone()
は、解析中にフォールバックとして機能するため、ZonedDateTime
で混乱する結果を与えるだけです(タイムゾーンが入力文字列またはフォーマットパターンに存在しない場合にのみ使用されます) )、フォーマット中にオーバーライドとして機能します(ZonedDateTime
に格納されているタイムゾーンは完全に無視されます)。これが、観察可能な問題の根本的な原因です。フォーマッタの作成中にwithZone()
を省略するだけで修正できます。
コンバータを指定していて、_timeOnly="true"
_がない場合は、_<p:calendar timeZone>
_を指定する必要がないことに注意してください。その場合でも、ハードコーディングする代わりにTimeZone.getTimeZone(zonedDateTime.getZone())
を使用したいとします。
タイムゾーン(オフセット)は、データベースを含むすべてのレイヤーに渡る必要があります。ただし、データベースに「タイムゾーンのない日付時刻」列タイプがある場合、タイムゾーン情報は永続化中に失われ、データベースからサービスを提供するときに問題が発生します。
使用しているDBは不明ですが、一部のDBは Oracle および PostgreSQL DBから知られている_TIMESTAMP WITH TIME ZONE
_列タイプをサポートしていないことに注意してください。たとえば、 MySQLはサポートしていません です。 2列目が必要です。
これらの変更が受け入れられない場合は、LocalDateTime
に戻り、データベースを含むすべてのレイヤー全体で固定/事前定義されたタイムゾーンに依存する必要があります。通常これにはUTCが使用されます。
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());
}
}
_
LocalDateTime
の取り扱いUTCベースのLocalDateTime
を適切なUTCベースのTIMESTAMP
(タイムゾーンなし)で使用する場合、DB列タイプは、以下のJSFコンバーターを使用して、UIのString
と_の間で変換します。 [$ var] _モデル内。このコンバーターは、親コンポーネントからLocalDateTime
、pattern
、およびtimeZone
属性を検索します。親コンポーネントがlocale
、pattern
、および/または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
を適用する以下を変更する必要があります:
_<p:calendar>
_はLocalDateTimeConverter
でコンバーターを検索しないため、_<converter><converter-id>localDateTimeConverter
_の_faces-config.xml
_に再登録するか、以下のように注釈を変更する必要があります
_@FacesConverter("localDateTimeConverter")
_
_<p:calendar>
_なしの_timeOnly="true"
_はforClass
を無視し、ポップアップでそれを編集するオプションを提供するので、コンバーターを回避するためにtimeZone
属性を削除する必要があります混乱します(この属性は、タイムゾーンがtimeZone
にない場合にのみ必要です)。
出力時に必要な表示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>
_