web-dev-qa-db-fra.com

Comment utiliser Java.time.ZonedDateTime / LocalDateTime dans p: calendar

J'utilisais Joda Time pour la manipulation date-heure dans une application Java EE dans laquelle une représentation sous forme de chaîne de la date-heure soumise par le client associé avait été convertie à l'aide de la routine de conversion suivante avant de la soumettre) à une base de données, c'est-à-dire dans la méthode getAsObject() dans un convertisseur 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));

Le fuseau horaire local donné est de 5 heures et 30 minutes d'avance sur UTC/GMT. Par conséquent, la conversion en UTC devrait déduire 5 heures et 30 minutes de la date-heure indiquée qui se déroule correctement avec Joda Time. Il affiche la sortie suivante comme prévu.

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

► Le décalage de fuseau horaire +0530 au lieu de +05:30 a été prise car elle dépend de <p:calendar> qui soumet un décalage de zone dans ce format. Il ne semble pas possible de modifier ce comportement de <p:calendar> (Cette question elle-même n'aurait pas été nécessaire autrement).


La même chose est cependant cassée, si vous tentez d'utiliser l'API Java Time dans 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));

Il affiche de manière inattendue la sortie incorrecte suivante.

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

De toute évidence, la date-heure convertie n'est pas conforme à UTC dans laquelle elle est censée se convertir.

Il nécessite que les modifications suivantes soient adoptées pour qu'il fonctionne correctement.

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

Qui à son tour affiche les éléments suivants.

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

Z a été remplacé par z et +0530 a été remplacé par +05:30.

Pourquoi ces deux API ont un comportement différent à cet égard a été totalement ignoré dans cette question.

Quelle approche intermédiaire peut être envisagée pour <p:calendar> et Java Temps en Java 8 pour fonctionner de manière cohérente et cohérente si <p:calendar> utilise en interne SimpleDateFormat avec Java.util.Date?


Le scénario de test infructueux dans JSF.

Le convertisseur:

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

Le fuseau horaire dépend de manière totalement transparente du fuseau horaire actuel de l'utilisateur.

Le haricot n'ayant rien d'autre qu'une propriété unique.

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

Cela fonctionnera de manière inattendue, comme démontré dans l'avant-dernier exemple/milieu des trois premiers extraits de code.

Plus précisément, si vous entrez 05-Jan-2016 12:00:00 AM +0530, il réaffichera 05-Jan-2016 05:30:00 AM IST car la conversion d'origine de 05-Jan-2016 12:00:00 AM +0530 à UTC dans le convertisseur échoue.

Conversion à partir d'un fuseau horaire local dont le décalage est +05:30 vers UTC puis la conversion de UTC vers ce fuseau horaire doit évidemment afficher de nouveau la même date-heure que celle entrée via le composant de calendrier qui est la fonctionnalité rudimentaire du convertisseur donné.


Mise à jour:

Le convertisseur JPA convertissant vers et depuis Java.sql.Timestamp et 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);
    }
}
23
Tiny

Votre problème concret est que vous avez migré de l'instance de date et d'heure sans zone de Joda DateTime vers l'instance de date et d'heure zonée de Java8 ZonedDateTime au lieu de la date sans zone de Java8 instance de temps LocalDateTime .

L'utilisation de ZonedDateTime (ou OffsetDateTime ) au lieu de LocalDateTime nécessite au moins 2 modifications supplémentaires:

  1. Ne forcez pas un fuseau horaire (décalage) pendant la conversion date-heure . Au lieu de cela, le fuseau horaire de la chaîne d'entrée, le cas échéant, sera utilisé pendant l'analyse, et le fuseau horaire stocké dans l'instance ZonedDateTime doit être utilisé pendant le formatage.

    DateTimeFormatter#withZone() ne donnera que des résultats confus avec ZonedDateTime car il agira comme solution de repli pendant l'analyse (il n'est utilisé que lorsque le fuseau horaire est absent dans la chaîne d'entrée ou le modèle de format ), et il agira comme un remplacement lors du formatage (le fuseau horaire stocké dans ZonedDateTime est entièrement ignoré). C'est la cause première de votre problème observable. L'omission de withZone() lors de la création du formateur devrait le corriger.

    Notez que lorsque vous avez spécifié un convertisseur et que vous n'avez pas timeOnly="true", Vous n'avez pas besoin de spécifier <p:calendar timeZone>. Même lorsque vous le faites, vous préférez utiliser TimeZone.getTimeZone(zonedDateTime.getZone()) au lieu de le coder en dur.

  2. Vous devez transporter le fuseau horaire (décalage) sur toutes les couches, y compris la base de données . Si votre base de données, cependant, a un type de colonne "date heure sans fuseau horaire", alors les informations de fuseau horaire sont perdues pendant la persistance et vous rencontrerez des problèmes lors du retour de la base de données.

    La base de données que vous utilisez n'est pas claire, mais gardez à l'esprit que certaines bases de données ne prennent pas en charge un type de colonne TIMESTAMP WITH TIME ZONE, Connu de Oracle et PostgreSQL DBs . Par exemple, MySQL ne le prend pas en charge . Vous auriez besoin d'une deuxième colonne.

Si ces modifications ne sont pas acceptables, vous devez revenir à LocalDateTime et vous fier à un fuseau horaire fixe/prédéfini dans toutes les couches, y compris la base de données. Généralement, UTC est utilisé pour cela.


Gérer ZonedDateTime dans JSF et JPA

Lorsque vous utilisez ZonedDateTime avec un type de colonne DB TIMESTAMP WITH TIME ZONE Approprié, utilisez le convertisseur JSF ci-dessous pour convertir entre String dans l'interface utilisateur et ZonedDateTime dans le modèle. Ce convertisseur recherchera les attributs pattern et locale du composant parent. Si le composant parent ne prend pas en charge nativement un attribut pattern ou locale, ajoutez-les simplement en tant que <f:attribute name="..." value="...">. Si l'attribut locale est absent, le (par défaut) <f:view locale> Sera utilisé à la place. Il n'y a aucun attribut timeZone pour la raison expliquée dans # 1 ci-dessus.

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

}

Et utilisez le convertisseur JPA ci-dessous pour convertir entre ZonedDateTime dans le modèle et Java.util.Calendar Dans JDBC (le pilote JDBC décent l'exigera/l'utilisera pour la colonne tapée 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());
    }

}

Gérer LocalDateTime dans JSF et JPA

Lorsque vous utilisez le type de colonne DB basé sur UTC LocalDateTime avec un type de base de données TIMESTAMP (sans fuseau horaire!) Approprié, utilisez le convertisseur JSF ci-dessous pour convertir entre String dans l'interface utilisateur et LocalDateTime dans le modèle. Ce convertisseur recherchera les attributs pattern, timeZone et locale du composant parent. Si le composant parent ne prend pas en charge nativement un attribut pattern, timeZone et/ou locale, ajoutez-les simplement en tant que <f:attribute name="..." value="...">. L'attribut timeZone doit représenter le fuseau horaire de secours de la chaîne d'entrée (lorsque le pattern ne contient pas de fuseau horaire) et le fuseau horaire de la chaîne de sortie.

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

}

Et utilisez le convertisseur JPA ci-dessous pour convertir entre LocalDateTime dans le modèle et Java.sql.Timestamp Dans JDBC (le pilote JDBC décent l'exigera/l'utilisera pour la colonne typée 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();
    }

}

Appliquer LocalDateTimeConverter à votre cas spécifique avec <p:calendar>

Vous devez modifier ce qui suit:

  1. Comme <p:calendar> Ne recherche pas les convertisseurs par forClass, vous devez soit le réenregistrer avec <converter><converter-id>localDateTimeConverter Dans faces-config.xml, Soit modifier le annotation comme ci-dessous

    @FacesConverter("localDateTimeConverter")
    
  2. Comme <p:calendar> Sans timeOnly="true" Ignore le timeZone et propose dans la fenêtre contextuelle une option pour le modifier, vous devez supprimer l'attribut timeZone pour éviter cela le convertisseur devient confus (cet attribut n'est requis que lorsque le fuseau horaire est absent dans le pattern).

  3. Vous devez spécifier l'attribut d'affichage timeZone souhaité lors de la sortie (cet attribut n'est pas requis lorsque vous utilisez ZonedDateTimeConverter car il est déjà stocké dans ZonedDateTime).

Voici l'extrait de travail complet:

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

Si vous avez l'intention de créer votre propre <my:convertLocalDateTime> Avec des attributs, vous devrez les ajouter en tant que propriétés de type bean avec des getters/setters à la classe du convertisseur et l'enregistrer dans *.taglib.xml Comme illustré dans cette réponse: Création d'une balise personnalisée pour Converter avec des attributs

<h:outputText id="display" value="#{bean.dateTime}">
    <my:convertLocalDateTime pattern="dd-MMM-yyyy hh:mm:ss a Z" 
                             timeZone="Asia/Kolkata" />
</h:outputText>
39
BalusC