Come usare java.time.ZonedDateTime / LocalDateTime in p: calendar

Stavo usando Joda Time per la manipolazione data-ora in un’applicazione Java EE in cui una rappresentazione di stringa di data-ora inviata dal client associato era stata convertita usando la seguente routine di conversione prima di getAsObject() ad un database, ad esempio in getAsObject() metodo in un convertitore 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)); 

Il fuso orario locale indicato è 5 ore e 30 minuti prima di UTC / GMT . Pertanto, la conversione in UTC deve dedurre 5 ore e 30 minuti dall’ora data indicata, che avviene correttamente utilizzando Joda Time. Visualizza il seguente output come previsto.

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

► L’offset fuso orario +0530 al posto di +05:30 è stato preso perché dipende da

che invia un offset di zona in questo formato. Non sembra ansible modificare questo comportamento di

(questa domanda non sarebbe stata necessaria altrimenti).


La stessa cosa è comunque infranta, se si tenta di utilizzare l’API Java Time in 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)); 

Visualizza in modo imprevisto il seguente output errato.

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

Ovviamente, la data-ora convertita non è in base UTC in cui deve essere convertita.

Richiede le seguenti modifiche da adottare perché funzioni correttamente.

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

Che a sua volta mostra quanto segue.

 05-Jan-2016 09:34:44 AM Z 

Z è stato sostituito con z e +0530 è stato sostituito con +05:30 .

Perché queste due API hanno un comportamento diverso in questo senso è stato ignorato con tutto il cuore in questa domanda.

Quale approccio di middle way può essere considerato per

e Java Time in Java 8 per funzionare in modo coerente e coerente sebbene

utilizzi internamente SimpleDateFormat insieme a java.util.Date ?


Lo scenario di test non riuscito in JSF.

Il convertitore:

 @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 con

.

 



Il fuso orario è completamente trasparente in base al fuso orario corrente dell’utente.

Il bean non ha altro che una singola proprietà.

 @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. } } 

Questo funzionerà in modo inaspettato come dimostrato nel penultimo esempio / medio nei primi tre frammenti di codice.

Nello specifico, se inserisci 05-Jan-2016 12:00:00 AM +0530 , verrà visualizzato di nuovo 05-Jan-2016 05:30:00 AM IST perché la conversione originale di 05-Jan-2016 12:00:00 AM +0530 a UTC nel convertitore non riesce.

Conversione da un fuso orario locale il cui offset è +05:30 in UTC e quindi la conversione da UTC a quel fuso orario deve ovviamente visualizzare nuovamente la stessa data-ora immessa nel componente del calendario, che è la funzionalità rudimentale del convertitore fornito.


Aggiornare:

Il convertitore JPA che converte da e verso java.sql.Timestamp e 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); } } 

Il tuo problema concreto è che sei passato dall’istanza data / ora zon senza data di Joda all’ora ZonedDateTime data di zonata di ZonedDateTime invece dell’ora di data / ora zoneless di Java8 LocalDateTime .

L’utilizzo di ZonedDateTime (o OffsetDateTime ) invece di LocalDateTime richiede almeno 2 ulteriori modifiche:

  1. Non forzare un fuso orario (offset) durante la conversione dell’ora . Invece, il fuso orario della stringa di input, se presente, verrà utilizzato durante l’analisi e il fuso orario memorizzato nell’istanza ZonedDateTime deve essere utilizzato durante la formattazione.

    DateTimeFormatter#withZone() restituirà risultati confusi solo con ZonedDateTime poiché agirà come fallback durante l’analisi (viene utilizzato solo quando il fuso orario è assente nella stringa di input o nel formato) e agirà come override durante la formattazione (il fuso orario memorizzato in ZonedDateTime è completamente ignorato). Questa è la causa principale del tuo problema osservabile. L’omissione di withZone() durante la creazione del formattatore dovrebbe risolverlo.

    Si noti che quando si specifica un convertitore e non si ha timeOnly="true" , non è necessario specificare . Anche quando lo fai, preferisci usare TimeZone.getTimeZone(zonedDateTime.getZone()) invece di codificarlo.

  2. È necessario trasportare il fuso orario (offset) su tutti i livelli, incluso il database . Se il database, tuttavia, ha un tipo di colonna “data e ora senza fuso orario”, le informazioni sul fuso orario vengono perse durante la persistenza e si verificheranno dei problemi durante la pubblicazione dal database.

    Non è chiaro quale DB stai usando, ma tieni presente che alcuni DB non supportano un tipo di colonna TIMESTAMP WITH TIME ZONE come noto dai DB Oracle e PostgreSQL . Ad esempio, MySQL non lo supporta . Avresti bisogno di una seconda colonna.

Se tali modifiche non sono accettabili, è necessario tornare a LocalDateTime e fare affidamento su un fuso orario fisso / predefinito in tutti i livelli, incluso il database. Di solito UTC è usato per questo.


Trattare con ZonedDateTime in JSF e JPA

Quando si utilizza ZonedDateTime con un tipo di colonna TIMESTAMP WITH TIME ZONE appropriato, utilizzare il convertitore JSF di seguito per convertire tra String nell’interfaccia utente e ZonedDateTime nel modello. Questo convertitore cercherà gli attributi pattern e locale dal componente principale. Se il componente padre non supporta in modo nativo un attributo pattern o locale , è sufficiente aggiungerli come . Se l’attributo locale è assente, verrà utilizzato il (predefinito) . Non esiste un attributo timeZone per la ragione spiegata al punto 1 qui sopra.

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

E usa il convertitore JPA qui sotto per convertire tra ZonedDateTime nel modello e java.util.Calendar in JDBC (il driver JDBC decente lo richiederà / userà per TIMESTAMP WITH TIME ZONE colonna tipizzata TIMESTAMP WITH TIME ZONE ):

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

Trattare con LocalDateTime in JSF e JPA

Quando si utilizza LocalDateTime basato su UTC con un tipo di colonna DB TIMESTAMP (senza fuso orario) basato su UTC appropriato, utilizzare il convertitore JSF di seguito per convertire tra String nell’interfaccia utente e LocalDateTime nel modello. Questo convertitore cercherà gli attributi pattern , timeZone e locale dal componente principale. Se il componente padre non supporta in modo nativo un timeZone pattern , timeZone e / o locale , è sufficiente aggiungerli come . L’attributo timeZone deve rappresentare il fuso orario di fallback della stringa di input (quando il pattern non contiene un fuso orario) e il fuso orario della stringa di output.

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

E usa il convertitore JPA qui sotto per convertire tra LocalDateTime nel modello e java.sql.Timestamp in JDBC (il driver JDBC decente lo richiederà / userà per la colonna tipizzata 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(); } } 

Applicando LocalDateTimeConverter al caso specifico con

È necessario modificare il seguente:

  1. Siccome il non forClass convertitori per forClass , dovresti registrare nuovamente con localDateTimeConverter in faces-config.xml , o per modificare l’annotazione come sotto

     @FacesConverter("localDateTimeConverter") 
  2. Poiché senza timeOnly="true" ignora il timeZone e offre nel popup un’opzione per modificarlo, è necessario rimuovere l’attributo timeZone per evitare che il convertitore venga confuso (questo attributo è richiesto solo quando il il fuso orario è assente nello pattern ).

  3. È necessario specificare l’attributo timeZone visualizzazione desiderato durante l’output (questo attributo non è richiesto quando si utilizza ZonedDateTimeConverter poiché è già memorizzato in ZonedDateTime ).

Ecco lo snippet di lavoro completo:

     

Nel caso in cui si intenda creare il proprio con attributi, è necessario aggiungerli come proprietà bean-like con getter / setter alla class del convertitore e registrarlo in *.taglib.xml come dimostrato in questa risposta : Creazione di tag personalizzati per il convertitore con attributi