8085887: java.time.format.FormatStyle.LONG or FULL causes unchecked exception
authorrriggs
Wed, 16 Mar 2016 13:16:14 -0400
changeset 36638 bc1438c48f1b
parent 36637 ee1538b62f5d
child 36639 7b0617f87656
8085887: java.time.format.FormatStyle.LONG or FULL causes unchecked exception 8076528: LocalTime.format() throws exception when FormatStyle is LONG or FULL Reviewed-by: sherman, scolebourne
jdk/src/java.base/share/classes/java/time/format/DateTimeFormatter.java
jdk/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java
jdk/src/java.base/share/classes/java/time/format/DateTimePrintContext.java
jdk/src/java.base/share/classes/java/time/temporal/TemporalQueries.java
jdk/test/java/time/test/java/time/format/TestDateTimeFormatter.java
--- a/jdk/src/java.base/share/classes/java/time/format/DateTimeFormatter.java	Tue Mar 15 14:49:37 2016 -0700
+++ b/jdk/src/java.base/share/classes/java/time/format/DateTimeFormatter.java	Wed Mar 16 13:16:14 2016 -0400
@@ -619,6 +619,9 @@
      * The returned formatter has a chronology of ISO set to ensure dates in
      * other calendar systems are correctly converted.
      * It has no override zone and uses the {@link ResolverStyle#SMART SMART} resolver style.
+     * The {@code FULL} and {@code LONG} styles typically require a time-zone.
+     * When formatting using these styles, a {@code ZoneId} must be available,
+     * either by using {@code ZonedDateTime} or {@link DateTimeFormatter#withZone}.
      *
      * @param timeStyle  the formatter style to obtain, not null
      * @return the time formatter, not null
@@ -647,6 +650,9 @@
      * The returned formatter has a chronology of ISO set to ensure dates in
      * other calendar systems are correctly converted.
      * It has no override zone and uses the {@link ResolverStyle#SMART SMART} resolver style.
+     * The {@code FULL} and {@code LONG} styles typically require a time-zone.
+     * When formatting using these styles, a {@code ZoneId} must be available,
+     * either by using {@code ZonedDateTime} or {@link DateTimeFormatter#withZone}.
      *
      * @param dateTimeStyle  the formatter style to obtain, not null
      * @return the date-time formatter, not null
@@ -675,6 +681,9 @@
      * The returned formatter has a chronology of ISO set to ensure dates in
      * other calendar systems are correctly converted.
      * It has no override zone and uses the {@link ResolverStyle#SMART SMART} resolver style.
+     * The {@code FULL} and {@code LONG} styles typically require a time-zone.
+     * When formatting using these styles, a {@code ZoneId} must be available,
+     * either by using {@code ZonedDateTime} or {@link DateTimeFormatter#withZone}.
      *
      * @param dateStyle  the date formatter style to obtain, not null
      * @param timeStyle  the time formatter style to obtain, not null
--- a/jdk/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java	Tue Mar 15 14:49:37 2016 -0700
+++ b/jdk/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java	Wed Mar 16 13:16:14 2016 -0400
@@ -1256,6 +1256,9 @@
      * During formatting, the chronology is obtained from the temporal object
      * being formatted, which may have been overridden by
      * {@link DateTimeFormatter#withChronology(Chronology)}.
+     * The {@code FULL} and {@code LONG} styles typically require a time-zone.
+     * When formatting using these styles, a {@code ZoneId} must be available,
+     * either by using {@code ZonedDateTime} or {@link DateTimeFormatter#withZone}.
      * <p>
      * During parsing, if a chronology has already been parsed, then it is used.
      * Otherwise the default from {@code DateTimeFormatter.withChronology(Chronology)}
--- a/jdk/src/java.base/share/classes/java/time/format/DateTimePrintContext.java	Tue Mar 15 14:49:37 2016 -0700
+++ b/jdk/src/java.base/share/classes/java/time/format/DateTimePrintContext.java	Wed Mar 16 13:16:14 2016 -0400
@@ -218,6 +218,13 @@
                 }
                 return query.queryFrom(this);
             }
+
+            @Override
+            public String toString() {
+                return temporal +
+                        (effectiveChrono != null ? " with chronology " + effectiveChrono : "") +
+                        (effectiveZone != null ? " with zone " + effectiveZone : "");
+            }
         };
     }
 
@@ -279,7 +286,8 @@
     <R> R getValue(TemporalQuery<R> query) {
         R result = temporal.query(query);
         if (result == null && optional == 0) {
-            throw new DateTimeException("Unable to extract value: " + temporal.getClass());
+            throw new DateTimeException("Unable to extract " +
+                    query + " from temporal " + temporal);
         }
         return result;
     }
--- a/jdk/src/java.base/share/classes/java/time/temporal/TemporalQueries.java	Tue Mar 15 14:49:37 2016 -0700
+++ b/jdk/src/java.base/share/classes/java/time/temporal/TemporalQueries.java	Wed Mar 16 13:16:14 2016 -0400
@@ -341,58 +341,118 @@
     /**
      * A strict query for the {@code ZoneId}.
      */
-    static final TemporalQuery<ZoneId> ZONE_ID = (temporal) ->
-        temporal.query(TemporalQueries.ZONE_ID);
+    static final TemporalQuery<ZoneId> ZONE_ID = new TemporalQuery<>() {
+        @Override
+        public ZoneId queryFrom(TemporalAccessor temporal) {
+            return temporal.query(TemporalQueries.ZONE_ID);
+        }
+
+        @Override
+        public String toString() {
+            return "ZoneId";
+        }
+    };
 
     /**
      * A query for the {@code Chronology}.
      */
-    static final TemporalQuery<Chronology> CHRONO = (temporal) ->
-        temporal.query(TemporalQueries.CHRONO);
+    static final TemporalQuery<Chronology> CHRONO = new TemporalQuery<>() {
+        @Override
+        public Chronology queryFrom(TemporalAccessor temporal) {
+            return temporal.query(TemporalQueries.CHRONO);
+        }
+
+        @Override
+        public String toString() {
+            return "Chronology";
+        }
+    };
+
 
     /**
      * A query for the smallest supported unit.
      */
-    static final TemporalQuery<TemporalUnit> PRECISION = (temporal) ->
-        temporal.query(TemporalQueries.PRECISION);
+    static final TemporalQuery<TemporalUnit> PRECISION = new TemporalQuery<>() {
+        @Override
+        public TemporalUnit queryFrom(TemporalAccessor temporal) {
+            return temporal.query(TemporalQueries.PRECISION);
+        }
+
+        @Override
+        public String toString() {
+            return "Precision";
+        }
+    };
 
     //-----------------------------------------------------------------------
     /**
      * A query for {@code ZoneOffset} returning null if not found.
      */
-    static final TemporalQuery<ZoneOffset> OFFSET = (temporal) -> {
-        if (temporal.isSupported(OFFSET_SECONDS)) {
-            return ZoneOffset.ofTotalSeconds(temporal.get(OFFSET_SECONDS));
+    static final TemporalQuery<ZoneOffset> OFFSET = new TemporalQuery<>() {
+        @Override
+        public ZoneOffset queryFrom(TemporalAccessor temporal) {
+            if (temporal.isSupported(OFFSET_SECONDS)) {
+                return ZoneOffset.ofTotalSeconds(temporal.get(OFFSET_SECONDS));
+            }
+            return null;
         }
-        return null;
+
+        @Override
+        public String toString() {
+            return "ZoneOffset";
+        }
     };
 
     /**
      * A lenient query for the {@code ZoneId}, falling back to the {@code ZoneOffset}.
      */
-    static final TemporalQuery<ZoneId> ZONE = (temporal) -> {
-        ZoneId zone = temporal.query(ZONE_ID);
-        return (zone != null ? zone : temporal.query(OFFSET));
+    static final TemporalQuery<ZoneId> ZONE = new TemporalQuery<>() {
+        @Override
+        public ZoneId queryFrom(TemporalAccessor temporal) {
+            ZoneId zone = temporal.query(ZONE_ID);
+            return (zone != null ? zone : temporal.query(OFFSET));
+        }
+
+        @Override
+        public String toString() {
+            return "Zone";
+        }
     };
 
     /**
      * A query for {@code LocalDate} returning null if not found.
      */
-    static final TemporalQuery<LocalDate> LOCAL_DATE = (temporal) -> {
-        if (temporal.isSupported(EPOCH_DAY)) {
-            return LocalDate.ofEpochDay(temporal.getLong(EPOCH_DAY));
+    static final TemporalQuery<LocalDate> LOCAL_DATE = new TemporalQuery<>() {
+        @Override
+        public LocalDate queryFrom(TemporalAccessor temporal) {
+            if (temporal.isSupported(EPOCH_DAY)) {
+                return LocalDate.ofEpochDay(temporal.getLong(EPOCH_DAY));
+            }
+            return null;
         }
-        return null;
+
+        @Override
+        public String toString() {
+            return "LocalDate";
+        }
     };
 
     /**
      * A query for {@code LocalTime} returning null if not found.
      */
-    static final TemporalQuery<LocalTime> LOCAL_TIME = (temporal) -> {
-        if (temporal.isSupported(NANO_OF_DAY)) {
-            return LocalTime.ofNanoOfDay(temporal.getLong(NANO_OF_DAY));
+    static final TemporalQuery<LocalTime> LOCAL_TIME = new TemporalQuery<>() {
+        @Override
+        public LocalTime queryFrom(TemporalAccessor temporal) {
+            if (temporal.isSupported(NANO_OF_DAY)) {
+                return LocalTime.ofNanoOfDay(temporal.getLong(NANO_OF_DAY));
+            }
+            return null;
         }
-        return null;
+
+        @Override
+        public String toString() {
+            return "LocalTime";
+        }
     };
 
 }
--- a/jdk/test/java/time/test/java/time/format/TestDateTimeFormatter.java	Tue Mar 15 14:49:37 2016 -0700
+++ b/jdk/test/java/time/test/java/time/format/TestDateTimeFormatter.java	Wed Mar 16 13:16:14 2016 -0400
@@ -79,19 +79,24 @@
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
+import java.time.chrono.Chronology;
 import java.time.chrono.ThaiBuddhistChronology;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatterBuilder;
 import java.time.format.DecimalStyle;
 import java.time.format.SignStyle;
+import java.time.format.TextStyle;
+import java.time.temporal.Temporal;
 import java.time.temporal.TemporalAccessor;
 import java.util.Locale;
 import java.util.function.Function;
 
+import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 /**
  * Test DateTimeFormatter.
+ * @bug 8085887
  */
 @Test
 public class TestDateTimeFormatter {
@@ -196,4 +201,75 @@
         assertTrue(msg.contains("11:30:56"), msg);
     }
 
+    @DataProvider(name="nozone_exception_cases")
+    Object[][] exceptionCases() {
+        return new Object[][] {
+                {LocalDateTime.of(2000, 1, 1, 1, 1), "z", "ZoneId"},
+                {OffsetDateTime.of(2000, 1, 1, 3, 3, 3, 0, ZoneOffset.ofTotalSeconds(60)), "z", "ZoneId"},
+        };
+    }
+
+    // Test cases that should throw an exception with a cogent message
+    // containing the Type being queried and the Temporal being queried.
+    @Test(dataProvider="nozone_exception_cases")
+    public void test_throws_message(Temporal temporal, String pattern, String queryName) {
+        DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern);
+        try {
+            fmt.format(temporal);
+            fail("Format using \"" + pattern + "\" with " +
+                    temporal + " should have failed");
+        } catch (DateTimeException dte) {
+            String msg = dte.getMessage();
+            // Verify message contains the type that is missing and the temporal value
+            assertTrue(msg.contains(queryName),
+                    String.format("\"%s\" missing from %s", queryName, msg));
+            String s = temporal.toString();
+            assertTrue(msg.contains(s),
+                    String.format("\"%s\" missing from %s", s, msg));
+        }
+
+    }
+
+    // Test cases that should throw an exception with a cogent message when missing the Chronology
+    @Test
+    public void test_throws_message_chrono() {
+        Chronology chrono = ThaiBuddhistChronology.INSTANCE;
+        DateTimeFormatter fmt = new DateTimeFormatterBuilder().appendZoneId().toFormatter()
+                .withChronology(chrono);
+        LocalTime now = LocalTime.now();
+        try {
+            fmt.format(now);
+            fail("Format using appendZoneId() should have failed");
+        } catch (DateTimeException dte) {
+            String msg = dte.getMessage();
+            // Verify message contains the type that is missing and the temporal value
+            assertTrue(msg.contains("ZoneId"),
+                    String.format("\"%s\" missing from %s", "ZoneId", msg));
+            assertTrue(msg.contains(chrono.toString()),
+                    String.format("\"%s\" missing from %s", chrono.toString(), msg));
+        }
+
+    }
+
+    // Test cases that should throw an exception with a cogent message when missing the ZoneId
+    @Test
+    public void test_throws_message_zone() {
+        ZoneId zone = ZoneId.of("Pacific/Honolulu");
+        DateTimeFormatter fmt = new DateTimeFormatterBuilder().appendChronologyId().toFormatter()
+                .withZone(zone);
+        LocalTime now = LocalTime.now();
+        try {
+            fmt.format(now);
+            fail("Format using appendChronologyId() should have failed");
+        } catch (DateTimeException dte) {
+            String msg = dte.getMessage();
+            // Verify message contains the type that is missing and the temporal value
+            assertTrue(msg.contains("Chronology"),
+                    String.format("\"%s\" missing from %s", "Chronology", msg));
+            assertTrue(msg.contains(zone.toString()),
+                    String.format("\"%s\" missing from %s", zone.toString(), msg));
+        }
+
+    }
+
 }