java.time 日期时间类型的 Vaadin Flow 渲染器不仅限于 LocalDateTime 和 LocalDate 类

Vaadin Flow renderer for java.time date-time types beyond just LocalDateTime & LocalDate classes

在 Vaadin Flow 版本 14.1 中,我发现日期时间类型的渲染器只有两种实现:

第一个是 LocalDate class 中的仅日期值,没有时间和时区。不错。

第二个是 LocalDateTime class representing a date with time-of-day, but purposely lacking the context of a time zone or a offset-from-UTC。好吧。

问题是我找不到其他几种 java.time 数据类型的渲染器。这是我制作的各种日期时间类型的图表,现代 java.time 类型,以及它们取代的遗留日期时间 classes,以及 SQL 标准等效数据类型的列表。

具体来说,在商业应用程序中,我们倾向于使用 LocalDateTime 的频率较低,主要是为了预订未来的约会,因为政客可以更改时区的定义(他们经常这样做,世界各地)。那LocalDateTime class 不能代表一个时刻。例如,以今年 1 月 23 日下午 3 点为例。如果没有时区或与 UTC 的偏移量,我们不知道这是否意味着日本东京下午 3 点、法国图卢兹下午 3 点或美国俄亥俄州托莱多下午 3 点——三个非常不同的时刻相隔几个小时。

为了表示一个时刻,我们必须使用 Instant, OffsetDateTime, or ZonedDateTime classes. An Instant is a moment in UTC,根据定义始终采用 UTC。 OffsetDateTime 表示与 UTC 的偏移量为小时-分钟-秒的时刻。 ZonedDateTime 是通过特定地区、时区的人们使用的挂钟时间看到的时刻。这样的时区是该地区使用的偏移量的过去、现在和未来变化的历史。

➥ Vaadin 14 是否为这些其他类型提供渲染器?如果没有,是否有解决方法或制作渲染器的方法?

我的InstantRendererclass

您可以轻松创建自己的渲染器实现。

这是我编写的渲染器,用于处理 Grid 显示包含 Instant 对象的对象的小部件。 Instant 是一个时刻,时间线上的一个特定点,如 UTC 中所示(零小时-分钟-秒的偏移量)。 Instant class 是 java.time 框架中使用的基本构建块 class。

这里的想法是,我们以 Instant 对象,应用指定的 ZoneId to get a ZonedDateTime object. That ZonedDateTime object uses the specified DateTimeFormatter object to generate text in a String object. The text represents the contents of the ZonedDateTime object automatically localized to the specified Locale 对象的人类语言和文化规范。

ZoneIdLocale 附加到调用程序员传递的 DateTimeFormatter

我这里的代码是基于 Vaadin Ltd 公司为他们的 LocalDateTimeRenderer class’ source-code found on their GitHub site.

发布的代码

我修剪了 class 的 API。他们的 API 允许传递格式化模式字符串而不是 DateTimeFormatter 对象。我不认为渲染器有责任从这样的字符串生成格式化程序对象,因此也应该处理任何由此产生的错误情况。他们的 API 允许传递 Locale 对象。 Locale 对象可以附加到调用程序员传递的 DateTimeFormatter 对象。我看不出这个渲染器 class 应该如何不必要地参与将传递的语言环境分配给传递的格式化程序。调用程序可以在 之前 将格式化程序传递给我们的渲染器。

这是在 Vaadin 14 中定义 InstantRenderer 以呈现 Instant 对象以在 Grid 中显示的典型用法。

invoicesGrid
        .addColumn(
                new InstantRenderer <>( Invoice :: getWhenCreated ,
                        DateTimeFormatter
                                .ofLocalizedDateTime( FormatStyle.SHORT , FormatStyle.MEDIUM )
                                .withLocale( Locale.CANADA_FRENCH )
                                .withZone( ZoneId.of( "America/Montreal" ) )
                )
        )
        .setHeader( "Created" )
;

Continent/Region的格式指定一个proper time zone name,例如America/MontrealAfrica/CasablancaPacific/Auckland。切勿使用 ESTIST 等 2-4 字母缩写,因为它们 不是 真正的时区,未标准化,甚至不是唯一的(!)。

请注意 java.time classes 使用 immutable objects. The withZone and withLocale 方法生成新的 DateTimeFormatter 而不是改变原来的。因此,您可能希望根据您对较短日期和较长时间的偏好来保留全局单身人士 DateTimeFormatter

DateTimeFormatter f = DateTimeFormatter
                                .ofLocalizedDateTime( 
                                    FormatStyle.SHORT ,   // Length of date portion.
                                    FormatStyle.MEDIUM    // Length of time-of-day portion.
                                )
;

然后在代码的其他地方,应用每个用户自己的首选区域和语言环境。由于 java.time 中使用的不可变对象模式,您会得到另一个专门的 DateTimeFormatter 对象,而原始对象不受影响。

invoicesGrid
        .addColumn(
                new InstantRenderer <>( Invoice :: getWhenCreated ,
                        f
                                .withLocale( user.getPreferredLocale()  )
                                .withZone( user.getPreferredZone() )
                )
        )
        .setHeader( "Created" )
;

顺便说一句,构造函数还有第三个可选参数:String 用于在呈现的 Instant 对象为 null 的情况下。默认是不向用户显示任何文本,一个空 "" 字符串。如果愿意,您可以传递一些其他字符串,例如 nullvoid

这是我的 class 的源代码。请注意,我在靠近顶部的 Javadoc 中放置了一些讨论。

我使用与 Vaadin Ltd 相同的 Apache License 2,因此您可以自己使用和更改此代码。欢迎您的反馈。

package work.basil.example.ui;

/*
 * Copyright 2000-2020 Vaadin Ltd.
 * Copyright 2020 Basil Bourque.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */


import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
import java.util.Objects;

import com.vaadin.flow.data.renderer.BasicRenderer;
import com.vaadin.flow.function.ValueProvider;

/*
 * This class is based on source-code directly copied from
 * `LocalDateTimeRenderer.java` of Vaadin 14.1.x
 * as written and published by Vaadin Ltd. from their GitHub page.
 *
 * https://github.com/vaadin/flow/blob/master/flow-data/src/main/java/com/vaadin/flow/data/renderer/LocalDateTimeRenderer.java
 *
 * I re-purposed that class to handle `Instant` objects rather than `LocalDateTime`
 * objects. An `Instant` represents a moment, whereas `LocalDateTime` cannot because
 * of it lacking any concept of time zone or offset-from-UTC. In contrast, `Instant`
 * represents a moment in UTC (an offset-from-UTC of zero hours-minutes-seconds).
 *
 * By default, a `Instant` object renders in Vaadin by way of its `toString` method
 * generating text in standard ISO 8601 format YYYY-MM-DDTHH:MM:SS.SSSSSSSSSZ.
 *
 * If you want other than ISO 8601 format in UTC, use this class. In this class, we
 * apply  a time zone (`ZoneId`) to the `Instant` to adjust from UTC.
 *
 * The `ZoneId` object comes from one of three places:
 *  - Passed implicitly by being set as a property on a `DateTimeFormatter`
 *    object passed as an argument. This is the best case.
 *  - Defaults to calling `ZoneId.systemDefault` if  not found
 *    on the `DateTimeFormatter` object  (where `getZone` returns null).
 *
 * I deleted the constructors taking a formatting pattern string. Parsing such a string
 * and instantiating a `DateTimeFormatter` and handling resulting error conditions
 * should *not* be the job of this class. I believe the Vaadin team made a poor choice
 * in having constructors taking a string formatting pattern rather than just a
 * `DateTimeFormatter` object.
 *
 * Locale is another critical issue. A `Locale` object determines:
 *
 * (a) The human language used for translating items such as name of month and
 * name of day.
 *
 * (b) The cultural norms used in deciding localization issues such as the ordering
 * of elements (ex: day comes before or after month), abbreviation, capitalization,
 * punctuation, and so on.
 *
 * Again, I deleted the constructors taking a `Locale` object. The `DateTimeFormatter`
 * object passed by the calling programmer carries a `Locale`. That calling programmer
 * should have attached their intended locale object to that `DateTimeFormatter` object
 * by calling `DateTimeFormatter::withLocale`. Usually a `DateTimeFormatter` has a default
 * `Locale` assigned. But if found lacking, here we attach the JVM’s current default locale.
 *
 * Following the logic discussed above, I chose to not take a `ZoneId` as an argument.
 * A `ZoneId` can be attached to the `DateTimeFormatter` by calling `withZoneId`.
 * If the passed `DateTimeFormatter` is found lacking, here we attach the JVM’s current
 * default time zone.
 *
 * Typical usage, passing 2 arguments, a method reference and a `DateTimeFormatter` object
 * while omitting 3rd optional argument for null-representation to go with an blank empty string:
 *
 *     myGrid
 *          .addColumn(
 *                  new InstantRenderer <>( TheBusinessObject :: getWhenCreated ,
 *                          DateTimeFormatter
 *                                  .ofLocalizedDateTime( FormatStyle.SHORT , FormatStyle.MEDIUM )
 *                                  .withLocale( Locale.CANADA_FRENCH )
 *                                  .withZone( ZoneId.of( "America/Montreal" ) )
 *                  )
 *         )
 *
 * This code is written for Java 8 or later.
 *
 *  For criticisms and suggestions, contact me via LinkedIn at:  basilbourque
 */

/**
 * A template renderer for presenting {@code Instant} objects.
 *
 * @param <SOURCE> the type of the input item, from which the {@link Instant}
 *                 is extracted
 * @author Vaadin Ltd
 * @since 1.0.
 */
public class InstantRenderer < SOURCE >
        extends BasicRenderer < SOURCE, Instant >
{
    private DateTimeFormatter formatter;
    private String nullRepresentation;

    /**
     * Creates a new InstantRenderer.
     * <p>
     * The renderer is configured to render with the format style
     * {@code FormatStyle.LONG} for the date and {@code FormatStyle.SHORT} for
     * time, with an empty string as its null representation.
     *
     * @param valueProvider the callback to provide a {@link Instant} to the
     *                      renderer, not <code>null</code>
     * @see <a href=
     * "https://docs.oracle.com/javase/8/docs/api/java/time/format/FormatStyle.html#LONG">
     * FormatStyle.LONG</a>
     * @see <a href=
     * "https://docs.oracle.com/javase/8/docs/api/java/time/format/FormatStyle.html#SHORT">
     * FormatStyle.SHORT</a>
     */
    public InstantRenderer (
            ValueProvider < SOURCE, Instant > valueProvider )
    {
        this(
                valueProvider ,
                DateTimeFormatter
                        .ofLocalizedDateTime( FormatStyle.LONG )
                        .withZone( ZoneId.systemDefault() )
                        .withLocale( Locale.getDefault() ) ,
                ""
        );
    }

    /**
     * Creates a new InstantRenderer.
     * <p>
     * The renderer is configured to render with the given formatter, with the
     * empty string as its null representation.
     *
     * @param valueProvider the callback to provide a {@link Instant} to the
     *                      renderer, not <code>null</code>
     * @param formatter     the formatter to use, not <code>null</code>
     */
    public InstantRenderer (
            ValueProvider < SOURCE, Instant > valueProvider ,
            DateTimeFormatter formatter
    )
    {
        this(
                valueProvider ,
                formatter ,
                ""
        );
    }

    /**
     * Creates a new InstantRenderer.
     * <p>
     * The renderer is configured to render with the given formatter.
     *
     * @param valueProvider      the callback to provide a {@link Instant} to the
     *                           renderer, not <code>null</code>
     * @param formatter          the formatter to use, not <code>null</code>
     * @param nullRepresentation the textual representation of the <code>null</code> value
     */
    public InstantRenderer (
            final ValueProvider < SOURCE, Instant > valueProvider ,
            final DateTimeFormatter formatter ,
            final String nullRepresentation
    )
    {
        super( valueProvider );

        this.formatter = Objects.requireNonNull( formatter , "formatter may not be null" );
        this.nullRepresentation = Objects.requireNonNull( nullRepresentation , "null-representation may not be null" );

        // If the formatter provided by the calling programmer lacks a time zone, apply the JVM's current default zone.
        // This condition is less than ideal. The calling programmer should have set an appropriate zone.
        // Often the appropriate zone is one specifically chosen or confirmed by the user.
        if ( Objects.isNull( this.formatter.getZone() ) )
        {
            this.formatter = this.formatter.withZone( ZoneId.systemDefault() );
        }

        // If the formatter provided by the calling programmer lacks a locale, apply the JVM's current default locale.
        // This condition is less than ideal. The calling programmer should have set an appropriate locale.
        // Often the appropriate locale is one specifically chosen or confirmed by the user.
        if ( Objects.isNull( this.formatter.getLocale() ) )
        {
            this.formatter = this.formatter.withLocale( Locale.getDefault() );
        }
    }


    @Override
    protected String getFormattedValue ( final Instant instant )
    {
        // If null, return the null representation.
        // If not null, adjust the `Instant` from UTC into the time zone attached to the `DateTimeFormatter` object.
        // This adjustment, made by calling `Instant::atZone`, produces a `ZonedDateTime` object.
        // We then create a `String` with text representing the value of that `ZonedDateTime` object.
        // That text is automatically localized per the `Locale` attached to the `DateTimeFormatter` object.
        String s = Objects.isNull( instant ) ? nullRepresentation : formatter.format( instant.atZone( this.formatter.getZone() ) );
        return s;
    }
}

也许我以后可以对问题中列出的其他 java.time 类型做类似的事情。