jsf - Cómo usar java.time.ZonedDateTime/LocalDateTime en p: calendar
primefaces timezone (1)
Su problema concreto es que migró de la instancia de Date Time de Zoda sin fecha de ZonedDateTime
instancia de ZonedDateTime
de zoned de Java8, ZonedDateTime
lugar de la instancia de fecha de zoneless de Java8, LocalDateTime
.
Usar ZonedDateTime
(o OffsetDateTime
) en lugar de LocalDateTime
requiere al menos 2 cambios adicionales:
No fuerce una zona horaria (desplazamiento) durante la conversión de fecha y hora . En su lugar, la zona horaria de la cadena de entrada, si la hubiera, se usará durante el análisis, y la zona horaria almacenada en la instancia de
ZonedDateTime
debe usarse durante el formateo.El
DateTimeFormatter#withZone()
solo dará resultados confusos conZonedDateTime
ya que actuará como reserva durante el análisis (solo se usa cuando la zona horaria está ausente en la cadena de entrada o el patrón de formato), y actuará como anulación durante el formateo (la zona horaria almacenado enZonedDateTime
se ignora por completo). Esta es la causa raíz de su problema observable. Solo omitiendowithZone()
al crear el formateador debería arreglarlo.Tenga en cuenta que cuando haya especificado un convertidor y no tenga
timeOnly="true"
, entonces no necesita especificar<p:calendar timeZone>
. Incluso cuando lo haces, te gustaría usarTimeZone.getTimeZone(zonedDateTime.getZone())
lugar de codificarlo.Debe llevar la zona horaria (desplazamiento) a lo largo de todas las capas, incluida la base de datos . Sin embargo, si su base de datos tiene un tipo de columna de "fecha y hora sin zona horaria", entonces la información de la zona horaria se pierde durante la persistencia y tendrá problemas al volver a servir desde la base de datos.
No está claro qué base de datos está utilizando, pero tenga en cuenta que algunas bases de datos no admiten el tipo de columna
TIMESTAMP WITH TIME ZONE
como se conoce en las Oracle de datos Oracle y PostgreSQL . Por ejemplo, MySQL no lo admite . Necesitarías una segunda columna.
Si esos cambios no son aceptables, entonces necesita volver a LocalDateTime
y confiar en la zona horaria fija / predefinida en todas las capas, incluida la base de datos. Usualmente se usa UTC para esto.
Tratando con ZonedDateTime
en JSF y JPA
Cuando use ZonedDateTime
con un tipo apropiado de columna de TIMESTAMP WITH TIME ZONE
DB, use el siguiente convertidor de JSF para convertir entre String
en la interfaz de usuario y ZonedDateTime
en el modelo. Este convertidor buscará los atributos de pattern
y locale
del componente principal. Si el componente principal no admite de forma nativa un pattern
o atributo de locale
, simplemente agréguelos como <f:attribute name="..." value="...">
. Si el atributo de locale
está ausente, se usará en su lugar el (predeterminado) <f:view locale>
. No hay timeZone
atributo de zona timeZone
por la razón como se explica en el # 1 aquí arriba.
@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();
}
}
Y use el siguiente convertidor de JPA para convertir entre ZonedDateTime
en el modelo y java.util.Calendar
en JDBC (el controlador JDBC decente lo requerirá / usará para TIMESTAMP WITH TIME ZONE
columna con tipo de 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());
}
}
Tratar con LocalDateTime
en JSF y JPA
Cuando use el tipo de columna DB basado en UTC LocalDateTime
con un UTC basado en UTC basado en UTC, use el siguiente convertidor de JSF para convertir entre String
en la UI y LocalDateTime
en el modelo. Este convertidor buscará los atributos de pattern
, zona timeZone
y locale
del componente principal. Si el componente principal no admite de forma nativa un pattern
, zona timeZone
y / o atributo de locale
, simplemente agréguelos como <f:attribute name="..." value="...">
. El atributo timeZone
debe representar la zona horaria alternativa de la cadena de entrada (cuando el pattern
no contiene una zona horaria) y la zona horaria de la cadena de salida.
@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;
}
}
Y use el siguiente conversor JPA para convertir entre LocalDateTime
en el modelo y java.sql.Timestamp
en JDBC (el controlador JDBC decente lo requerirá / usará para la columna con tipo de 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();
}
}
Aplicando LocalDateTimeConverter
a su caso específico con <p:calendar>
Necesitas cambiar lo siguiente:
Como el
<p:calendar>
noforClass
conversores porforClass
, deberá volver a registrarlo con<converter><converter-id>localDateTimeConverter
enfaces-config.xml
, o modificar la anotación como se muestra a continuación@FacesConverter("localDateTimeConverter")
Como
<p:calendar>
withouttimeOnly="true"
ignora la zonatimeZone
, y ofrece en la ventana emergente la opción de editarla, debe eliminar el atributo de zonatimeZone
para evitar que el convertidor se confunda (este atributo solo es necesario cuando la zona horaria está ausente en elpattern
).timeZone
especificar el atributo de zonatimeZone
visualización deseado durante la salida (este atributo no es necesario cuando se usaZonedDateTimeConverter
ya que ya está almacenado enZonedDateTime
).
Aquí está el fragmento de trabajo completo:
<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>
En caso de que <my:convertLocalDateTime>
crear su propio <my:convertLocalDateTime>
con atributos, deberá agregarlos como propiedades de tipo bean con getters / setters a la clase de convertidor y registrarlo en *.taglib.xml
como se muestra en esta respuesta. : Creando etiqueta personalizada para el convertidor con atributos
<h:outputText id="display" value="#{bean.dateTime}">
<my:convertLocalDateTime pattern="dd-MMM-yyyy hh:mm:ss a Z"
timeZone="Asia/Kolkata" />
</h:outputText>
Había estado utilizando Joda Time para la manipulación de la fecha y la hora en una aplicación Java EE en la que una representación de cadena de la fecha y la hora enviada por el cliente asociado se había convertido utilizando la siguiente rutina de conversión antes de enviarla a una base de datos, es decir, en el objeto getAsObject()
Método en un convertidor 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));
La zona horaria local dada es de 5 horas y 30 minutos antes de UTC
/ GMT
. Por lo tanto, la conversión a UTC
debe deducir 5 horas y 30 minutos a partir de la fecha y la hora indicadas, lo que sucede correctamente utilizando Joda Time. Muestra la siguiente salida como se esperaba.
05-Jan-2016 09:34:44 AM +0000
► Se ha tomado el desplazamiento de zona horaria +0530
en lugar de +05:30
porque depende de <p:calendar>
que envía un desplazamiento de zona en este formato. No parece posible cambiar este comportamiento de <p:calendar>
(esta pregunta en sí no habría sido necesaria de otra manera).
Sin embargo, se rompe lo mismo si se intenta utilizar la API de Java Time en 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));
Se muestra de forma inesperada la siguiente salida incorrecta.
05-Jan-2016 03:04:44 PM +0000
Obviamente, la fecha-hora convertida no está de acuerdo con la UTC
en la que se supone que debe convertir.
Requiere que se adopten los siguientes cambios para que funcione correctamente.
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));
Que a su vez muestra lo siguiente.
05-Jan-2016 09:34:44 AM Z
Z
ha sido reemplazado con z
y +0530
ha sido reemplazado con +05:30
.
Por qué estas dos API tienen un comportamiento diferente en este sentido se ha ignorado de todo corazón en esta pregunta.
¿Qué enfoque intermedio se puede considerar para que <p:calendar>
y Java Time en Java 8 funcionen de manera coherente y coherente a través de <p:calendar>
internamente utiliza SimpleDateFormat
junto con java.util.Date
?
El escenario de prueba no exitoso en JSF.
El convertidor:
@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 teniendo <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>
La zona horaria depende totalmente de forma transparente de la zona horaria actual del usuario.
El frijol no tiene nada más que una sola propiedad.
@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.
}
}
Esto funcionará de manera inesperada como se muestra en el segundo último ejemplo / medio en los tres primeros fragmentos de código.
Específicamente, si ingresa 05-Jan-2016 12:00:00 AM +0530
, se volverá a mostrar 05-Jan-2016 05:30:00 AM IST
porque la conversión original de 05-Jan-2016 12:00:00 AM +0530
a UTC
en el convertidor falla.
La conversión de una zona horaria local cuyo desplazamiento es +05:30
a UTC
y luego la conversión de UTC
nuevamente a esa zona horaria debe volver a mostrar la misma fecha y hora introducidas en el componente del calendario, que es la funcionalidad rudimentaria del convertidor dado.
Actualizar:
El convertidor JPA que convierte java.time.ZonedDateTime
desde java.sql.Timestamp
y 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);
}
}