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