如何在p:calendar

时间:2016-01-19 17:28:19

标签: jsf primefaces calendar timezone java-time

我一直在Java EE应用程序中使用Joda Time进行日期时间操作,其中关联客户端提交的日期时间的字符串表示已使用以下转换例程转换,然后将其提交到数据库,即JSF转换器中的getAsObject()方法。

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

所提供的当地时区比UTC / GMT提前5小时30分钟。因此,转换为UTC应从使用Joda Time正确发生的日期时间开始扣除5小时30分钟。它按预期显示以下输出。

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

►时区偏移+0530已取代+05:30,因为它取决于以此格式提交区域偏移的<p:calendar>。似乎不可能改变<p:calendar>的这种行为(否则本身就不需要这个问题)。

如果尝试在Java 8中使用Java Time API,那么同样的事情就会被打破。

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

意外地显示以下错误输出。

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

显然,转换的日期时间不符合它应转换的UTC

它需要采取以下更改才能正常工作。

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

反过来显示以下内容。

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

Z已替换为z+0530已替换为+05:30

为什么这两个API在这方面有不同的行为在这个问题中被全心全意地忽略了。

虽然<p:calendar>内部使用<p:calendar>SimpleDateFormat,但{8}中的java.util.Date和Java Time可以考虑采用哪种中间方法来保持一致且一致的工作?

JSF中不成功的测试场景。

转换器:

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

时区完全透明地取决于用户的当前时区。

豆只有一个属性。

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

这将以意想不到的方式工作,如前三个代码段中的第二个示例/中间所示。

具体而言,如果您输入05-Jan-2016 12:00:00 AM +0530,则会重新显示05-Jan-2016 05:30:00 AM IST,因为转换器中05-Jan-2016 12:00:00 AM +0530UTC的原始转换失败。

从偏移量为+05:30的本地时区转换为UTC,然后从UTC转换回该时区,显然必须重新显示与通过日历输入的日期时间相同的日期时间组件,这是转换器的基本功能。

更新

JPA转换器转换为java.sql.Timestampjava.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);
    }
}

1 个答案:

答案 0 :(得分:32)

您的具体问题是您从Joda的无区域日期时间实例DateTime迁移到Java8的分区日期时间实例ZonedDateTime而不是Java8的无区域日期时间实例LocalDateTime

使用ZonedDateTime(或OffsetDateTime)代替LocalDateTime至少需要进行2次更改:

  1. 请勿在日期时间转换期间强制使用时区(偏移)。相反,在解析期间将使用输入字符串的时区(如果有),并且在格式化期间必须使用存储在ZonedDateTime实例中的时区。

    DateTimeFormatter#withZone()只会给ZonedDateTime带来令人困惑的结果,因为它在解析过程中会起到后退的作用(只有在输入字符串或格式模式中没有时区时才会使用它),以及它将在格式化期间充当覆盖(存储在ZonedDateTime中的时区完全被忽略)。这是您可观察到的问题的根本原因。只需在创建格式化程序时省略withZone()即可修复它。

    请注意,当您指定了转换器但没有timeOnly="true"时,您就不需要指定<p:calendar timeZone>。即使你这样做,也不想使用TimeZone.getTimeZone(zonedDateTime.getZone())代替硬编码。

  2. 您需要在所有图层上携带时区(偏移),包括数据库。但是,如果您的数据库具有没有时区的&#34;日期时间&#34;列类型,然后在持久化期间时区信息丢失,从数据库返回时会遇到麻烦。

    目前还不清楚您正在使用哪个数据库,但请注意,某些数据库不支持Oracle和{{{0}}已知的TIMESTAMP WITH TIME ZONE列类型3}} DBs。例如,PostgreSQL。您需要第二列。

  3. 如果这些更改不可接受,则需要返回LocalDateTime并依赖所有层(包括数据库)中的固定/预定义时区。通常使用UTC。

    在JSF和JPA中处理ZonedDateTime

    ZonedDateTime与适当的TIMESTAMP WITH TIME ZONE数据库列类型一起使用时,请使用以下JSF转换器在UI中的String和模型中的ZonedDateTime之间进行转换。此转换器将从父组件中查找patternlocale属性。如果父组件本身不支持patternlocale属性,只需将其添加为<f:attribute name="..." value="...">即可。如果缺少locale属性,则会使用(默认)<f:view locale>没有 timeZone属性的原因如上文#1中所述。

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

    并使用下面的JPA转换器在模型中的ZonedDateTime和JDBC中的java.util.Calendar之间进行转换(体面的JDBC驱动程序将需要/使用它用于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());
        }
    
    }
    

    在JSF和JPA中处理LocalDateTime

    当使用基于UTC的LocalDateTime和基于UTC的TIMESTAMP(没有时区!)DB列类型时,使用以下JSF转换器在UI中String之间转换{模型中的{1}}。此转换器将从父组件中查找LocalDateTimepatterntimeZone属性。如果父组件本身不支持localepattern和/或timeZone属性,只需将其添加为locale即可。 <f:attribute name="..." value="...">属性必须表示输入字符串的回退时区(当timeZone不包含时区时),以及输出字符串的时区。

    pattern

    并使用下面的JPA转换器在模型中的@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; } } 和JDBC中的LocalDateTime之间进行转换(体面的JDBC驱动程序将需要/使用它用于java.sql.Timestamp类型的列:)

    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(); } } 应用于LocalDateTimeConverter

    的特定案例

    您需要更改以下内容:

    1. 由于<p:calendar><p:calendar>之前没有查找转换器,您需要在forClass中使用<converter><converter-id>localDateTimeConverter重新注册它},或改变注释,如下所示

      faces-config.xml
    2. 由于@FacesConverter("localDateTimeConverter") 没有<p:calendar>会忽略timeOnly="true",并且在弹出窗口中提供了编辑它的选项,您需要将timeZone属性删除为避免转换器混淆(仅当timeZone中没有时区时才需要此属性。)

    3. 您需要在输出期间指定所需的显示pattern属性(使用timeZone时不需要此属性,因为它已存储在ZonedDateTimeConverter中)。

    4. 这是完整的工作片段:

      ZonedDateTime

      如果您打算使用属性创建自己的<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> ,则需要将它们作为类似bean的属性添加到转换器类中,并在<my:convertLocalDateTime>中注册它如答案所示:MySQL does not support it

      *.taglib.xml