Your concrete problem is that you migrated from Joda’s zoneless date time instance DateTime
to Java8’s zoned date time instance ZonedDateTime
instead of Java8’s zoneless date time instance LocalDateTime
.
Using ZonedDateTime
(or OffsetDateTime
) instead of LocalDateTime
requires at least 2 additional changes:
-
Do not force a time zone (offset) during date time conversion. Instead, the time zone of the input string, if any, will be used during parsing, and the time zone stored in
ZonedDateTime
instance must be used during formatting.The
DateTimeFormatter#withZone()
will only give confusing results withZonedDateTime
as it will act as fallback during parsing (it’s only used when time zone is absent in input string or format pattern), and it will act as override during formatting (the time zone stored inZonedDateTime
is entirely ignored). This is the root cause of your observable problem. Just omittingwithZone()
while creating the formatter should fix it.Do note that when you have specified a converter, and don’t have
timeOnly="true"
, then you don’t need to specify<p:calendar timeZone>
. Even when you do, you’d rather like to useTimeZone.getTimeZone(zonedDateTime.getZone())
instead of hardcoding it. -
You need to carry the time zone (offset) along over all layers, including the database. If your database, however, has a “date time without time zone” column type, then the time zone information gets lost during persist and you will run into trouble when serving back from database.
It’s unclear which DB you’re using, but keep in mind that some DBs doesn’t support a
TIMESTAMP WITH TIME ZONE
column type as known from Oracle and PostgreSQL DBs. For example, MySQL does not support it. You’d need a second column.
If those changes are not acceptable, then you need to step back to LocalDateTime
and rely on fixed/predefined time zone throughout all layers, including the database. Usually UTC is used for this.
Dealing with ZonedDateTime
in JSF and JPA
When using ZonedDateTime
with an appropriate TIMESTAMP WITH TIME ZONE
DB column type, use the below JSF converter to convert between String
in the UI and ZonedDateTime
in the model. This converter will lookup the pattern
and locale
attributes from the parent component. If the parent component doesn’t natively support a pattern
or locale
attribute, simply add them as <f:attribute name="..." value="...">
. If the locale
attribute is absent, the (default) <f:view locale>
will be used instead. There is no timeZone
attribute for the reason as explained in #1 here above.
@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();
}
}
And use the below JPA converter to convert between ZonedDateTime
in the model and java.util.Calendar
in JDBC (the decent JDBC driver will require/use it for TIMESTAMP WITH TIME ZONE
typed column):
@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());
}
}
Dealing with LocalDateTime
in JSF and JPA
When using UTC based LocalDateTime
with an appropriate UTC based TIMESTAMP
(without time zone!) DB column type, use the below JSF converter to convert between String
in the UI and LocalDateTime
in the model. This converter will lookup the pattern
, timeZone
and locale
attributes from the parent component. If the parent component doesn’t natively support a pattern
, timeZone
and/or locale
attribute, simply add them as <f:attribute name="..." value="...">
. The timeZone
attribute must represent the fallback time zone of the input string (when the pattern
doesn’t contain a time zone), and the time zone of the output string.
@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;
}
}
And use the below JPA converter to convert between LocalDateTime
in the model and java.sql.Timestamp
in JDBC (the decent JDBC driver will require/use it for TIMESTAMP
typed column):
@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();
}
}
Applying LocalDateTimeConverter
to your specific case with <p:calendar>
You need to change the below:
-
As the
<p:calendar>
doesn’t lookup converters byforClass
, you’d either need to re-register it with<converter><converter-id>localDateTimeConverter
infaces-config.xml
, or to alter the annotation as below@FacesConverter("localDateTimeConverter")
-
As the
<p:calendar>
withouttimeOnly="true"
ignores thetimeZone
, and offers in the popup an option to edit it, you need to remove thetimeZone
attribute to avoid that the converter gets confused (this attribute is only required when the time zone is absent in thepattern
). -
You need to specify the desired display
timeZone
attribute during output (this attribute is not required when usingZonedDateTimeConverter
as it’s already stored inZonedDateTime
).
Here’s the full working snippet:
<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>
In case you intend to create your own <my:convertLocalDateTime>
with attributes, you’d need to add them as bean-like properties with getters/setters to the converter class and register it in *.taglib.xml
as demonstrated in this answer: Creating custom tag for Converter with attributes
<h:outputText id="display" value="#{bean.dateTime}">
<my:convertLocalDateTime pattern="dd-MMM-yyyy hh:mm:ss a Z"
timeZone="Asia/Kolkata" />
</h:outputText>