Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8235238

Parsing a time string ignores any custom TimeZoneNameProvider

    XMLWordPrintable

Details

    • b02
    • x86_64
    • generic
    • Not verified

    Description

      ADDITIONAL SYSTEM INFORMATION :
      I have seen the same problem in Java 1.8.0_212, 11.0.3 and 13.0.1

      A DESCRIPTION OF THE PROBLEM :
      I created a custom timezone with a custom name for that time zone.
      Using the DateTimeFormatter I can create a String representation of that moment which includes the custom defined name. However it is not possible to parse the same string back into an Instant again.

      Note that the same code works as expected with any of the built in timezones, but not with a custom defined timezone.


      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      I create a custom ZoneRulesProvider and TimeZoneNameProvider for that new timezone. Via SPI I enable both. For the TimeZoneNameProvider the additional -Djava.locale.providers=SPI,CLDR,COMPAT commandline option is needed.

      DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzzz");
      Instant instant = Instant.ofEpochMilli(1521918000000L);
      ZonedDateTime custZoneDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("Custom/Timezone"));

      fmt.format(custZoneDateTime) --> "2018-03-24 20:00:00 Custom Winter Time"

      So the name is recognized and used when creating a datetime String.

      fmt.parse("2018-03-24 20:00:00 Custom Winter Time", Instant::from)

      java.time.format.DateTimeParseException: Text '2018-03-24 20:00:00 Custom Winter Time' could not be parsed at index 20

      Apparently the SPI based naming is not used by the parsing code.


      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      An Instant of the same moment in time as described by the String.
      ACTUAL -
      java.time.format.DateTimeParseException: Text '2018-03-24 20:00:00 Custom Winter Time' could not be parsed at index 20

      ---------- BEGIN SOURCE ----------
      ==============================
      $ cat pom.xml
      ==============================
      <?xml version="1.0" encoding="UTF-8"?>
      <project xmlns="http://maven.apache.org/POM/4.0.0"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
          <modelVersion>4.0.0</modelVersion>

          <packaging>jar</packaging>
          <groupId>nl.example.timezone</groupId>
          <version>1.0-SNAPSHOT</version>
          <artifactId>custom-timezone</artifactId>
          <name>Custom Timezone Library</name>

          <properties>
              <targetJdkVersion>13</targetJdkVersion>
              <maven.compiler.source>${targetJdkVersion}</maven.compiler.source>
              <maven.compiler.target>${targetJdkVersion}</maven.compiler.target>
              <maven.compiler.testSource>${targetJdkVersion}</maven.compiler.testSource>
              <maven.compiler.testTarget>${targetJdkVersion}</maven.compiler.testTarget>

              <defaultFileEncoding>UTF-8</defaultFileEncoding>
              <project.build.sourceEncoding>${defaultFileEncoding}</project.build.sourceEncoding>
              <project.reporting.outputEncoding>${defaultFileEncoding}</project.reporting.outputEncoding>

              <slf4j.version>1.7.26</slf4j.version>
              <junit.version>4.12</junit.version>
          </properties>

          <dependencies>
              <dependency>
                  <groupId>org.slf4j</groupId>
                  <artifactId>slf4j-api</artifactId>
                  <version>${slf4j.version}</version>
              </dependency>

              <!-- Test dependencies -->
              <dependency>
                  <groupId>org.slf4j</groupId>
                  <artifactId>slf4j-simple</artifactId>
                  <version>${slf4j.version}</version>
                  <scope>test</scope>
              </dependency>
              <dependency>
                  <groupId>junit</groupId>
                  <artifactId>junit</artifactId>
                  <version>${junit.version}</version>
                  <scope>test</scope>
              </dependency>
          </dependencies>
      </project>

      ==============================
      $ cat src/main/java/nl/example/timezone/CustomTimezoneRulesProvider.java
      ==============================
      package nl.example.timezone;

      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;

      import java.time.ZoneId;
      import java.time.zone.ZoneRules;
      import java.time.zone.ZoneRulesProvider;
      import java.util.Collections;
      import java.util.NavigableMap;
      import java.util.Set;
      import java.util.TreeMap;

      public class CustomTimezoneRulesProvider extends ZoneRulesProvider {
          private static final Logger LOG = LoggerFactory.getLogger(CustomTimezoneRulesProvider.class);

          public CustomTimezoneRulesProvider() {
              LOG.info("CustomTimezoneRulesProvider : Constructor");
          }

          static {
              LOG.info("CustomTimezoneRulesProvider : Static init");
          }


          private static final String BASE_ZONE_ID = "Europe/Amsterdam";
          public static final String ZONE_ID = "Custom/Timezone";

          @Override
          protected Set<String> provideZoneIds() {
              return Collections.singleton(ZONE_ID);
          }

          @Override
          protected ZoneRules provideRules(String zoneId, boolean forCaching) {
              // returns the ZoneRules for the custom timezone
              if (ZONE_ID.equals(zoneId)) {
                  ZoneId baseZone = ZoneId.of(BASE_ZONE_ID);
                  return baseZone.getRules();
              }
              return null;
          }

          @Override
          protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) {
              TreeMap<String, ZoneRules> map = new TreeMap<>();
              ZoneRules rules = getRules(zoneId, false);
              if (rules != null) {
                  map.put(zoneId, rules);
              }
              return map;
          }

      }

      ==============================
      $ cat src/main/java/nl/example/timezone/CustomTimezoneNameProvider.java
      ==============================
      package nl.example.timezone;

      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;

      import java.util.Locale;
      import java.util.TimeZone;
      import java.util.spi.TimeZoneNameProvider;

      import static nl.example.timezone.CustomTimezoneRulesProvider.ZONE_ID;


      public class CustomTimezoneNameProvider extends TimeZoneNameProvider {

          private static final Logger LOG = LoggerFactory.getLogger(CustomTimezoneNameProvider.class);

          public CustomTimezoneNameProvider() {
              LOG.info("CustomTimezoneNameProvider : Constructor");
          }

          static {
              LOG.info("CustomTimezoneNameProvider : Static init");
          }

          @Override
          public String getDisplayName(String ID, boolean daylight, int style, Locale locale) {
              if (ZONE_ID.equals(ID)) {
                  switch (style) {
                      case TimeZone.SHORT:
                          if (daylight) {
                              return "CUST_ST";
                          } else {
                              return "CUST_WT";
                          }
                      case TimeZone.LONG:
                          if (daylight) {
                              return "Custom Summer Time";
                          } else {
                              return "Custom Winter Time";
                          }
                  }
              }
              return null;
          }

          @Override
          public String getGenericDisplayName(String ID, int style, Locale locale) {
              if (ZONE_ID.equals(ID)) {
                  switch (style) {
                      case TimeZone.SHORT:
                          return "Custom";
                      case TimeZone.LONG:
                          return "My Own Custom Timezone";
                  }
              }
              return null;
          }

          @Override
          public boolean isSupportedLocale(Locale locale) {
              return true;
          }

          @Override
          public Locale[] getAvailableLocales() {
              return new Locale[]{
                  Locale.getDefault()
              };
          }
      }


      ==============================
      $ cat src/main/resources/META-INF/services/java.time.zone.ZoneRulesProvider
      ==============================
      nl.example.timezone.CustomTimezoneRulesProvider

      ==============================
      $ cat src/main/resources/META-INF/services/java.util.spi.TimeZoneNameProvider
      ==============================
      nl.example.timezone.CustomTimezoneNameProvider

      ==============================
      $ cat src/test/java/nl/example/timezone/date/TestTimezoneNaming.java
      ==============================
      package nl.example.timezone.date;

      import org.junit.Test;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;

      import java.time.Instant;
      import java.time.ZoneId;
      import java.time.ZonedDateTime;
      import java.time.format.DateTimeFormatter;

      import static org.junit.Assert.assertEquals;

      public class TestTimezoneNaming {
          private static final Logger LOG = LoggerFactory.getLogger(TestTimezoneNaming.class);

          private static final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
          private static final DateTimeFormatter fmtl = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzzz");

          @Test
          public void testCustomZoneNames() {
              Instant instant = Instant.ofEpochMilli(1521918000000L);
              ZonedDateTime utcZoneDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"));
              ZonedDateTime asdZoneDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("Europe/Amsterdam"));
              ZonedDateTime custZoneDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("Custom/Timezone"));
              LOG.info("UTC : {}", fmt.format(utcZoneDateTime));
              LOG.info("UTC : {}", fmtl.format(utcZoneDateTime));
              LOG.info("ASD : {}", fmt.format(asdZoneDateTime));
              LOG.info("ASD : {}", fmtl.format(asdZoneDateTime));
              LOG.info("CUST: {}", fmt.format(custZoneDateTime));
              LOG.info("CUST: {}", fmtl.format(custZoneDateTime));

              assertEquals("2018-03-24 19:00:00 UTC", fmt.format(utcZoneDateTime));
              assertEquals("2018-03-24 19:00:00 Coordinated Universal Time", fmtl.format(utcZoneDateTime));
              assertEquals("2018-03-24 20:00:00 CET", fmt.format(asdZoneDateTime));
              assertEquals("2018-03-24 20:00:00 Central European Standard Time", fmtl.format(asdZoneDateTime));
              assertEquals("2018-03-24 20:00:00 CUST_WT", fmt.format(custZoneDateTime));
              assertEquals("2018-03-24 20:00:00 Custom Winter Time", fmtl.format(custZoneDateTime));
          }

          @Test
          public void testCustomZoneParsingShort() {
              String[] sameMomentInTime = {
                  "2018-03-24 19:00:00 UTC",
                  "2018-03-24 20:00:00 CET",
                  "2018-03-24 20:00:00 CUST_WT",
              };

              for (String timeString: sameMomentInTime) {
                  Instant parsedInstant = fmt.parse(timeString, Instant::from);
                  assertEquals("Parse error: " + timeString, 1521918000000L, parsedInstant.toEpochMilli());
              }
          }

          @Test
          public void testCustomZoneParsingLong() {
              String[] sameMomentInTime = {
                  "2018-03-24 19:00:00 Coordinated Universal Time",
                  "2018-03-24 20:00:00 Central European Time",
                  "2018-03-24 20:00:00 Custom Winter Time"
              };

              for (String timeString: sameMomentInTime) {
                  Instant parsedInstant = fmtl.parse(timeString, Instant::from);
                  assertEquals("Parse error: " + timeString, 1521918000000L, parsedInstant.toEpochMilli());
              }
          }

      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      No known workaround.

      FREQUENCY : always


      Attachments

        1. pom.xml
          2 kB
        2. src.zip
          5 kB

        Activity

          People

            naoto Naoto Sato
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

            Dates

              Created:
              Updated:
              Resolved: