Cómo usar java.time.ZonedDateTime / LocalDateTime en p: calendario

Utilicé Joda Time para la manipulación de fecha y hora en una aplicación Java EE en la que una representación de cadena de fecha y 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 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 por delante de UTC / GMT . Por lo tanto, la conversión a UTC debe deducir 5 horas y 30 minutos a partir de la fecha y hora indicadas, lo que sucede correctamente utilizando Joda Time. Muestra el siguiente resultado como se esperaba.

 05-Jan-2016 09:34:44 AM +0000 

► Se ha tomado la compensación de zona horaria +0530 en lugar de +05:30 porque depende de

que envía un desplazamiento de zona en este formato. No parece posible cambiar este comportamiento de

(de lo contrario, esta pregunta no habría sido necesaria).


Sin embargo, lo mismo está roto, si se intentó utilizar la API de tiempo de Java 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)); 

Muestra inesperadamente la siguiente salida incorrecta.

 05-Jan-2016 03:04:44 PM +0000 

Obviamente, la fecha y hora convertidas no está de acuerdo con el UTC en el que se supone que debe convertirse.

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 az").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 por z y +0530 ha sido reemplazado por +05:30 .

La razón por la cual estas dos API tienen un comportamiento diferente en este sentido ha sido ignorada de todo corazón en esta pregunta.

¿Qué enfoque de vía media se puede considerar para

y Java Time en Java 8 para trabajar de manera coherente y consistente aunque

internamente usa SimpleDateFormat junto con java.util.Date ?


El escenario de prueba fallido 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 az").withZone(ZoneId.of("Asia/Kolkata")).format(((ZonedDateTime) value)); // According to a time zone of a specific user. } } 

XHTML que tiene

.

 



La zona horaria depende completamente de forma transparente de la zona horaria actual del usuario.

El frijol no tiene otra cosa 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 forma inesperada, como se demuestra en el segundo último ejemplo / medio en los primeros tres fragmentos de código.

Específicamente, si ingresa el 05-Jan-2016 12:00:00 AM +0530 , se volverá a mostrar el 05-Jan-2016 05:30:00 AM IST porque la conversión original fue el 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 a esa zona horaria, obviamente, debe volver a mostrar la misma fecha y hora introducidas a través del componente de calendario, que es la funcionalidad rudimentaria del convertidor.


Actualizar:

El convertidor JPA convirtiendo 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 { @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); } } 

Su problema concreto es que migró de la instancia de fecha y hora sin fecha de Joda, DateTime a la instancia de fecha y hora ZonedDateTime de Java8 ZonedDateTime lugar de la instancia de hora de fecha sin zona de 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, se utilizará la zona horaria de la cadena de entrada, si la hay, durante el análisis, y la zona horaria almacenada en la instancia de ZonedDateTime debe usar durante el formateo.

    DateTimeFormatter#withZone() solo arrojará resultados confusos con ZonedDateTime ya que actuará como respaldo durante el análisis (solo se utiliza cuando no hay zona horaria en la cadena de entrada o en 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 omita withZone() mientras crea el formateador para solucionarlo.

    Tenga en cuenta que cuando ha especificado un convertidor y no tiene timeOnly="true" , entonces no necesita especificar . Incluso cuando lo haga, preferiría utilizar 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 . Si su base de datos, sin embargo, tiene un tipo de columna de “fecha y hora sin zona horaria”, entonces la información de zona horaria se pierde durante la persistencia y se encontrará con problemas cuando se le devuelva la información de la base de datos.

    No está claro qué DB está utilizando, pero tenga en cuenta que algunos DB no admiten un tipo de columna TIMESTAMP WITH TIME ZONE como se conoce de Oracle y DB de PostgreSQL . Por ejemplo, MySQL no lo admite . Necesitarías una segunda columna.

Si esos cambios no son aceptables, entonces debe volver a LocalDateTime y confiar en la zona horaria fija / predefinida en todas las capas, incluida la base de datos. Por lo general, UTC se usa para esto.


Tratando con ZonedDateTime en JSF y JPA

Al usar ZonedDateTime con un tipo de columna TIMESTAMP WITH TIME ZONE apropiado TIMESTAMP WITH TIME ZONE DB, use el convertidor JSF a continuación para convertir entre String en la UI y ZonedDateTime en el modelo. Este convertidor buscará los atributos de pattern y locale del componente primario. Si el componente principal no admite de forma nativa un pattern o atributo de locale , simplemente agréguelos como . Si el atributo de locale está ausente, se usará (por defecto) su lugar. No hay ningún atributo de zona timeZone por el motivo explicado en el n. ° 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 convertidor JPA a continuación 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 ZonedDateTime ):

 @Converter(autoApply=true) public class ZonedDateTimeAttributeConverter implements AttributeConverter { @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 utilice LocalDateTime basado en UTC con un tipo de columna DB de TIMESTAMP basado en UTC apropiado (sin huso horario), utilice el convertidor JSF a continuación para convertir String en la UI y LocalDateTime en el modelo. Este convertidor buscará los atributos pattern , timeZone y locale del componente principal. Si el componente principal no admite de forma nativa un pattern , un timeZone y / o un atributo de locale , simplemente agréguelos como . 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 convertidor JPA a continuación para convertir LocalDateTime en el modelo y java.sql.Timestamp en JDBC (el controlador JDBC decente lo requerirá / usará para la columna de tipo TIMESTAMP ):

 @Converter(autoApply=true) public class LocalDateTimeAttributeConverter implements AttributeConverter { @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 tu caso específico con

Debes cambiar el siguiente:

  1. Como no forClass convertidores por forClass , necesitaría volver a registrarlo con localDateTimeConverter en faces-config.xml , o para alterar la anotación como se muestra a continuación

     @FacesConverter("localDateTimeConverter") 
  2. Como without timeOnly="true" ignora la zona timeZone , y ofrece en la ventana emergente una opción para editarla, debe eliminar el atributo 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 visualización timeZone deseado durante la salida (este atributo no es necesario cuando se usa ZonedDateTimeConverter ya que está almacenado en ZonedDateTime ).

Aquí está el fragmento de trabajo completo:

     

En caso de que tenga la intención de crear su propio con atributos, deberá agregarlos como propiedades tipo bean con getters / setters a la clase de convertidor y registrarlo en *.taglib.xml como se demuestra en esta respuesta : Creando una etiqueta personalizada para Convertidor con atributos

    
    Intereting Posts