Ответ 1
Ваша конкретная проблема заключается в том, что вы перенесли из экземпляра даты без даты года 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())
вместо hardcoding. -
Вам необходимо переносить часовой пояс (смещение) по всем слоям, включая базу данных. Однако, если ваша база данных имеет тип столбца "дата времени без часового пояса", информация о часовом поясе теряется во время сохранения, и при работе с базой данных возникают проблемы.
Неясно, какая БД вы используете, но имейте в виду, что некоторые DB не поддерживают тип столбца
TIMESTAMP WITH TIME ZONE
, как известно из Oracle и PostgreSQL DB. Например, MySQL не поддерживает его. Вам понадобится второй столбец.
Если эти изменения неприемлемы, вам необходимо вернуться к LocalDateTime
и полагаться на фиксированный/предопределенный часовой пояс на всех уровнях, включая базу данных. Обычно для этого используется UTC.
Работа с ZonedDateTime
в JSF и JPA
При использовании ZonedDateTime
с соответствующим типом столбца столбца TIMESTAMP WITH TIME ZONE
используйте приведенный ниже 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
При использовании UTC на основе LocalDateTime
с соответствующим типом столбца базы данных TIMESTAMP
(без часового пояса!) DBTC с использованием 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>
Вам нужно изменить ниже:
-
Поскольку
<p:calendar>
не ищет конвертеры поforClass
, вам нужно либо перерегистрировать его с помощью<converter><converter-id>localDateTimeConverter
вfaces-config.xml
, либо изменить аннотацию ниже@FacesConverter("localDateTimeConverter")
-
Поскольку
<p:calendar>
безtimeOnly="true"
игнорируетtimeZone
и предлагает во всплывающем окне возможность редактировать его, вам нужно удалить атрибутtimeZone
, чтобы избежать путаницы конвертера (это атрибут требуется только тогда, когда часовой пояс отсутствует вpattern
). -
Вам нужно указать желаемый атрибут display
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
, как показано в этом answer: Создание настраиваемого тега для конвертера с атрибутами
<h:outputText id="display" value="#{bean.dateTime}">
<my:convertLocalDateTime pattern="dd-MMM-yyyy hh:mm:ss a Z"
timeZone="Asia/Kolkata" />
</h:outputText>