4

In Vaadin Flow version 14.1, I find only two implementations of a renderer for date-time types:

The first is for a date-only value in the LocalDate class, without a time-of-day and without a time zone. That is good.

The second is for LocalDateTime class representing a date with time-of-day, but purposely lacking the context of a time zone or a offset-from-UTC. Well enough.

The problem is that I can find no other renderers for the several other java.time data types. Here is a chart I made of the various date-time types, the modern java.time types, as well as the legacy date-time classes they supplant, and a list of the SQL-standard equivalent data-types.

Table of date-time types in Java (both legacy and modern) and in standard SQL

Specifically, in business app we tend to use LocalDateTime less frequently, mainly for booking future appointments when the definition of a time zone can be changed by politicians (which they have been shown to do quite frequently, around the world). That LocalDateTime class cannot represent a moment. For example, take January 23rd this year at 3 PM. Without the context of a time zone or offset-from-UTC, we do not know if this means 3 PM in Tokyo Japan, 3 PM in Toulouse France, or 3 PM in Toledo Ohio US — three very different moments several hours apart.

To represent a moment, we must use the Instant, OffsetDateTime, or ZonedDateTime classes. An Instant is a moment in UTC, always in UTC by definition. A OffsetDateTime represents a moment with an offset-from-UTC of some number of hours-minutes-seconds. A ZonedDateTime is a moment as seen through the wall-clock time used by the people of a particular region, a time zone. Such a time zone is a history of the past, present, and future changes to the offset used in that region.

➥ Does Vaadin 14 provide renderers for any of these other types? If not, is there a workaround, or a way to make a renderer?

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154

1 Answers1

1

My InstantRenderer class

You can easily create your own implementation of a renderer.

Here is the renderer I wrote to handle a Grid widget displaying objects that include an Instant object. An Instant is a moment, a specific point on the timeline, as seen in UTC (an offset of zero hours-minutes-seconds). The Instant class is the basic building block class used in the java.time framework.

The idea here is that we take the Instant object, apply a specified 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 object's human language and cultural norms.

diagram showing how the <code>DateTimeFormatter</code> object provides a <code>ZoneId</code> object used by the <code>InstantRenderer</code> to produce a <code>ZonedDateTime</code> which in turn uses the <code>DateTimeFormatter</code> to generate automatically localized text for presentation to the user

The ZoneId and the Locale are attached to the DateTimeFormatter passed by the calling programmer.

My code here is based on the code published by the Vaadin Ltd company for their LocalDateTimeRenderer class’ source-code found on their GitHub site.

I pruned the API of that class. Their API allowed for passing a formatting-pattern string rather than a DateTimeFormatter object. I do not believe it should be the responsibility of the renderer to produce a formatter object from such a string, and therefore too handle any resulting error conditions. And their API allowed for passing a Locale object. The Locale object can be attached to the DateTimeFormatter object being passed by the calling programmer. I do not see how this renderer class should be getting needlessly involved in assigning the passed locale to the passed formatter. The calling programming can do that assignment before passing the formatter to our renderer.

Here is typical usage in defining a InstantRenderer for rendering a Instant object for display within a Grid in Vaadin 14.

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

Specify a proper time zone name in the format of Continent/Region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 2-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

Be aware that the java.time classes use immutable objects. The withZone and withLocale methods produce a new fresh DateTimeFormatter rather than altering the original. So you might want to keep a global singleton DateTimeFormatter with your preference for short date and longer time-of-day.

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

Then elsewhere in your code, apply each user’s own preferred zone and locale. You get another, specialized, DateTimeFormatter object while the original remains unaffected due to the immutable objects pattern used in java.time.

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

By the way, there is a third optional argument to the constructors: a String to be used in case the Instant object being rendered is null. The default is to present no text at all to the user, an empty "" string. You may pass some other string if you wish, such as null or void.

And here is the source-code of my class. Notice that I put some discussion in the Javadoc near the top.

I am using the same Apache License 2 as does Vaadin Ltd, so you may use and change this code for yourself. Your feedback is welcome.

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

Perhaps I can later do something similar for the other java.time types seen listed in the Question.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154