jsf primefaces calendar timezone java-time

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:

  1. 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 con ZonedDateTime 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 en ZonedDateTime se ignora por completo). Esta es la causa raíz de su problema observable. Solo omitiendo withZone() 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 usar TimeZone.getTimeZone(zonedDateTime.getZone()) lugar de codificarlo.

  2. 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:

  1. Como el <p:calendar> no forClass conversores por forClass , deberá volver a registrarlo con <converter><converter-id>localDateTimeConverter en faces-config.xml , o modificar la anotación como se muestra a continuación

    @FacesConverter("localDateTimeConverter")

  2. Como <p:calendar> without timeOnly="true" ignora la zona timeZone , y ofrece en la ventana emergente la opción de editarla, debe eliminar el atributo de zona timeZone para evitar que el convertidor se confunda (este atributo solo es necesario cuando la zona horaria está ausente en el pattern ).

  3. timeZone especificar el atributo de zona timeZone visualización deseado durante la salida (este atributo no es necesario cuando se usa ZonedDateTimeConverter ya que ya está almacenado en ZonedDateTime ).

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