diff --git a/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java b/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java index be6628d3bf..24d5d1f53e 100644 --- a/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java +++ b/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java @@ -22,264 +22,40 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; -import java.text.ParseException; -import java.text.ParsePosition; -import java.util.Calendar; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Date; -import java.util.GregorianCalendar; -import java.util.Locale; -import java.util.TimeZone; public final class UtcDateTypeAdapter extends TypeAdapter { - private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC"); + private final DateTimeFormatter FORMATTER = + DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneOffset.UTC); @Override public void write(JsonWriter out, Date date) throws IOException { if (date == null) { out.nullValue(); } else { - String value = format(date, true, UTC_TIME_ZONE); + String value = FORMATTER.format(date.toInstant()); out.value(value); } } @Override public Date read(JsonReader in) throws IOException { - try { - if (in.peek().equals(JsonToken.NULL)) { - in.nextNull(); - return null; - } else { - String date = in.nextString(); - // Instead of using iso8601Format.parse(value), we use Jackson's date parsing - // This is because Android doesn't support XXX because it is JDK 1.6 - return parse(date, new ParsePosition(0)); - } - } catch (ParseException e) { - throw new JsonParseException(e); - } - } - - // Date parsing code from Jackson databind ISO8601Utils.java - // https://github.com/FasterXML/jackson-databind/blob/2.8/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java - private static final String GMT_ID = "GMT"; - - /** - * Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] - * - * @param date the date to format - * @param millis true to include millis precision otherwise false - * @param tz timezone to use for the formatting (GMT will produce 'Z') - * @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] - */ - private static String format(Date date, boolean millis, TimeZone tz) { - Calendar calendar = new GregorianCalendar(tz, Locale.US); - calendar.setTime(date); - - // estimate capacity of buffer as close as we can (yeah, that's pedantic ;) - int capacity = "yyyy-MM-ddThh:mm:ss".length(); - capacity += millis ? ".sss".length() : 0; - capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".length(); - StringBuilder formatted = new StringBuilder(capacity); - - padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length()); - formatted.append('-'); - padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length()); - formatted.append('-'); - padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length()); - formatted.append('T'); - padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length()); - formatted.append(':'); - padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length()); - formatted.append(':'); - padInt(formatted, calendar.get(Calendar.SECOND), "ss".length()); - if (millis) { - formatted.append('.'); - padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length()); - } - - int offset = tz.getOffset(calendar.getTimeInMillis()); - if (offset != 0) { - int hours = Math.abs((offset / (60 * 1000)) / 60); - int minutes = Math.abs((offset / (60 * 1000)) % 60); - formatted.append(offset < 0 ? '-' : '+'); - padInt(formatted, hours, "hh".length()); - formatted.append(':'); - padInt(formatted, minutes, "mm".length()); + if (in.peek().equals(JsonToken.NULL)) { + in.nextNull(); + return null; } else { - formatted.append('Z'); - } - - return formatted.toString(); - } - - /** - * Zero pad a number to a specified length - * - * @param buffer buffer to use for padding - * @param value the integer value to pad if necessary. - * @param length the length of the string we should zero pad - */ - private static void padInt(StringBuilder buffer, int value, int length) { - String strValue = Integer.toString(value); - for (int i = length - strValue.length(); i > 0; i--) { - buffer.append('0'); - } - buffer.append(strValue); - } - - /** - * Parse a date from ISO-8601 formatted string. It expects a format - * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]] - * - * @param date ISO string to parse in the appropriate format. - * @param pos The position to start parsing from, updated to where parsing stopped. - * @return the parsed date - * @throws ParseException if the date is not in the appropriate format - */ - private static Date parse(String date, ParsePosition pos) throws ParseException { - Exception fail = null; - try { - int offset = pos.getIndex(); - - // extract year - int year = parseInt(date, offset, offset += 4); - if (checkOffset(date, offset, '-')) { - offset += 1; - } - - // extract month - int month = parseInt(date, offset, offset += 2); - if (checkOffset(date, offset, '-')) { - offset += 1; - } - - // extract day - int day = parseInt(date, offset, offset += 2); - // default time value - int hour = 0; - int minutes = 0; - int seconds = 0; - // always use 0 otherwise returned date will include millis of current time - int milliseconds = 0; - if (checkOffset(date, offset, 'T')) { - - // extract hours, minutes, seconds and milliseconds - hour = parseInt(date, offset += 1, offset += 2); - if (checkOffset(date, offset, ':')) { - offset += 1; - } - - minutes = parseInt(date, offset, offset += 2); - if (checkOffset(date, offset, ':')) { - offset += 1; - } - // second and milliseconds can be optional - if (date.length() > offset) { - char c = date.charAt(offset); - if (c != 'Z' && c != '+' && c != '-') { - seconds = parseInt(date, offset, offset += 2); - // milliseconds can be optional in the format - if (checkOffset(date, offset, '.')) { - milliseconds = parseInt(date, offset += 1, offset += 3); - } - } - } - } - - // extract timezone - String timezoneId; - if (date.length() <= offset) { - throw new IllegalArgumentException("No time zone indicator"); - } - char timezoneIndicator = date.charAt(offset); - if (timezoneIndicator == '+' || timezoneIndicator == '-') { - String timezoneOffset = date.substring(offset); - timezoneId = GMT_ID + timezoneOffset; - offset += timezoneOffset.length(); - } else if (timezoneIndicator == 'Z') { - timezoneId = GMT_ID; - offset += 1; - } else { - throw new IndexOutOfBoundsException("Invalid time zone indicator " + timezoneIndicator); - } - - TimeZone timezone = TimeZone.getTimeZone(timezoneId); - if (!timezone.getID().equals(timezoneId)) { - throw new IndexOutOfBoundsException(); - } - - Calendar calendar = new GregorianCalendar(timezone); - calendar.setLenient(false); - calendar.set(Calendar.YEAR, year); - calendar.set(Calendar.MONTH, month - 1); - calendar.set(Calendar.DAY_OF_MONTH, day); - calendar.set(Calendar.HOUR_OF_DAY, hour); - calendar.set(Calendar.MINUTE, minutes); - calendar.set(Calendar.SECOND, seconds); - calendar.set(Calendar.MILLISECOND, milliseconds); - - pos.setIndex(offset); - return calendar.getTime(); - // If we get a ParseException it'll already have the right message/offset. - // Other exception types can convert here. - } catch (IndexOutOfBoundsException e) { - fail = e; - } catch (NumberFormatException e) { - fail = e; - } catch (IllegalArgumentException e) { - fail = e; - } - String input = (date == null) ? null : ("'" + date + "'"); - throw new ParseException( - "Failed to parse date [" + input + "]: " + fail.getMessage(), pos.getIndex()); - } - - /** - * Check if the expected character exist at the given offset in the value. - * - * @param value the string to check at the specified offset - * @param offset the offset to look for the expected character - * @param expected the expected character - * @return true if the expected character exist at the given offset - */ - private static boolean checkOffset(String value, int offset, char expected) { - return (offset < value.length()) && (value.charAt(offset) == expected); - } - - /** - * Parse an integer located between 2 given offsets in a string - * - * @param value the string to parse - * @param beginIndex the start index for the integer in the string - * @param endIndex the end index for the integer in the string - * @return the int - * @throws NumberFormatException if the value is not a number - */ - private static int parseInt(String value, int beginIndex, int endIndex) - throws NumberFormatException { - if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) { - throw new NumberFormatException(value); - } - // use same logic as in Integer.parseInt() but less generic we're not supporting negative values - int i = beginIndex; - int result = 0; - int digit; - if (i < endIndex) { - digit = Character.digit(value.charAt(i++), 10); - if (digit < 0) { - throw new NumberFormatException("Invalid number: " + value); - } - result = -digit; - } - while (i < endIndex) { - digit = Character.digit(value.charAt(i++), 10); - if (digit < 0) { - throw new NumberFormatException("Invalid number: " + value); + String date = in.nextString(); + try { + // Parse the ISO 8601 string directly to Instant + Instant instant = ZonedDateTime.parse(date, FORMATTER).toInstant(); + return Date.from(instant); + } catch (Exception e) { + throw new JsonParseException("Failed to parse date: " + date, e); } - result *= 10; - result -= digit; } - return -result; } } diff --git a/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java index b5dffe24fb..5e67b3bf66 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java @@ -115,6 +115,7 @@ public final TypeAdapterFactory createAdapterFactory(int dateStyle, int timeStyl * List of 1 or more different date formats used for de-serialization attempts. The first of them * is used for serialization as well. */ + // TODO: It might be worth looking at the possibility of making a DateTimeFormatter list private final List dateFormats = new ArrayList<>(); private DefaultDateTypeAdapter(DateType dateType, String datePattern) { @@ -179,7 +180,7 @@ private Date deserializeToDate(JsonReader in) throws IOException { } try { - return ISO8601Utils.parse(s, new ParsePosition(0)); + return Date.from(ISO8601Utils.parse(s, new ParsePosition(0))); } catch (ParseException e) { throw new JsonSyntaxException( "Failed parsing '" + s + "' as Date; at path " + in.getPreviousPath(), e); diff --git a/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java b/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java index 6709b6e0b1..4496d7d010 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java +++ b/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java @@ -18,10 +18,12 @@ import java.text.ParseException; import java.text.ParsePosition; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.Locale; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.TimeZone; /** @@ -62,71 +64,43 @@ private ISO8601Utils() {} /** * Format a date into 'yyyy-MM-ddThh:mm:ssZ' (default timezone, no milliseconds precision) * - * @param date the date to format + * @param instant the Instant to format * @return the date formatted as 'yyyy-MM-ddThh:mm:ssZ' */ - public static String format(Date date) { - return format(date, false, TIMEZONE_UTC); + public static String format(Instant instant) { + return format(instant, false, TIMEZONE_UTC.toZoneId()); } /** * Format a date into 'yyyy-MM-ddThh:mm:ss[.sss]Z' (GMT timezone) * - * @param date the date to format + * @param instant the Instant to format * @param millis true to include millis precision otherwise false * @return the date formatted as 'yyyy-MM-ddThh:mm:ss[.sss]Z' */ - public static String format(Date date, boolean millis) { - return format(date, millis, TIMEZONE_UTC); + public static String format(Instant instant, boolean millis) { + return format(instant, millis, TIMEZONE_UTC.toZoneId()); } /** * Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] * - * @param date the date to format + * @param instant the Instant to format * @param millis true to include millis precision otherwise false - * @param tz timezone to use for the formatting (UTC will produce 'Z') + * @param zoneId ZoneId to use for the formatting (UTC will produce 'Z') * @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] */ - public static String format(Date date, boolean millis, TimeZone tz) { - Calendar calendar = new GregorianCalendar(tz, Locale.US); - calendar.setTime(date); + public static String format(Instant instant, boolean millis, ZoneId zoneId) { + ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, zoneId); - // estimate capacity of buffer as close as we can (yeah, that's pedantic ;) - int capacity = "yyyy-MM-ddThh:mm:ss".length(); - capacity += millis ? ".sss".length() : 0; - capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".length(); - StringBuilder formatted = new StringBuilder(capacity); - - padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length()); - formatted.append('-'); - padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length()); - formatted.append('-'); - padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length()); - formatted.append('T'); - padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length()); - formatted.append(':'); - padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length()); - formatted.append(':'); - padInt(formatted, calendar.get(Calendar.SECOND), "ss".length()); + DateTimeFormatter formatter; if (millis) { - formatted.append('.'); - padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length()); - } - - int offset = tz.getOffset(calendar.getTimeInMillis()); - if (offset != 0) { - int hours = Math.abs((offset / (60 * 1000)) / 60); - int minutes = Math.abs((offset / (60 * 1000)) % 60); - formatted.append(offset < 0 ? '-' : '+'); - padInt(formatted, hours, "hh".length()); - formatted.append(':'); - padInt(formatted, minutes, "mm".length()); + formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); } else { - formatted.append('Z'); + formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); } - return formatted.toString(); + return zdt.format(formatter); } /* @@ -141,246 +115,34 @@ public static String format(Date date, boolean millis, TimeZone tz) { * * @param date ISO string to parse in the appropriate format. * @param pos The position to start parsing from, updated to where parsing stopped. - * @return the parsed date + * @return the parsed instant * @throws ParseException if the date is not in the appropriate format */ - public static Date parse(String date, ParsePosition pos) throws ParseException { - Exception fail = null; + public static Instant parse(String date, ParsePosition pos) throws ParseException { try { int offset = pos.getIndex(); + Instant parsedInstant; - // extract year - int year = parseInt(date, offset, offset += 4); - if (checkOffset(date, offset, '-')) { - offset += 1; - } - - // extract month - int month = parseInt(date, offset, offset += 2); - if (checkOffset(date, offset, '-')) { - offset += 1; - } - - // extract day - int day = parseInt(date, offset, offset += 2); - - // default time value - int hour = 0; - int minutes = 0; - int seconds = 0; - - // always use 0 otherwise returned date will include millis of current time - int milliseconds = 0; - - // if the value has no time component (and no time zone), we are done - boolean hasT = checkOffset(date, offset, 'T'); - - if (!hasT && (date.length() <= offset)) { - Calendar calendar = new GregorianCalendar(year, month - 1, day); - calendar.setLenient(false); - - pos.setIndex(offset); - return calendar.getTime(); - } - - if (hasT) { - - // extract hours, minutes, seconds and milliseconds - hour = parseInt(date, offset += 1, offset += 2); - if (checkOffset(date, offset, ':')) { - offset += 1; - } - - minutes = parseInt(date, offset, offset += 2); - if (checkOffset(date, offset, ':')) { - offset += 1; - } - // second and milliseconds can be optional - if (date.length() > offset) { - char c = date.charAt(offset); - if (c != 'Z' && c != '+' && c != '-') { - seconds = parseInt(date, offset, offset += 2); - if (seconds > 59 && seconds < 63) { - seconds = 59; // truncate up to 3 leap seconds - } - // milliseconds can be optional in the format - if (checkOffset(date, offset, '.')) { - offset += 1; - int endOffset = indexOfNonDigit(date, offset + 1); // assume at least one digit - int parseEndOffset = Math.min(endOffset, offset + 3); // parse up to 3 digits - int fraction = parseInt(date, offset, parseEndOffset); - // compensate for "missing" digits - switch (parseEndOffset - offset) { // number of digits parsed - case 2: - milliseconds = fraction * 10; - break; - case 1: - milliseconds = fraction * 100; - break; - default: - milliseconds = fraction; - } - offset = endOffset; - } - } - } - } - - // extract timezone - if (date.length() <= offset) { - throw new IllegalArgumentException("No time zone indicator"); - } - - TimeZone timezone = null; - char timezoneIndicator = date.charAt(offset); + if (date.contains("T")) { + // if the value has time component and time zone - if (timezoneIndicator == 'Z') { - timezone = TIMEZONE_UTC; - offset += 1; - } else if (timezoneIndicator == '+' || timezoneIndicator == '-') { - String timezoneOffset = date.substring(offset); - - // When timezone has no minutes, we should append it, valid timezones are, for example: - // +00:00, +0000 and +00 - timezoneOffset = timezoneOffset.length() >= 5 ? timezoneOffset : timezoneOffset + "00"; - - offset += timezoneOffset.length(); - // 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00" - if (timezoneOffset.equals("+0000") || timezoneOffset.equals("+00:00")) { - timezone = TIMEZONE_UTC; - } else { - // 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC... - // not sure why, but that's the way it looks. Further, Javadocs for - // `java.util.TimeZone` specifically instruct use of GMT as base for - // custom timezones... odd. - String timezoneId = "GMT" + timezoneOffset; - // String timezoneId = "UTC" + timezoneOffset; - - timezone = TimeZone.getTimeZone(timezoneId); - - String act = timezone.getID(); - if (!act.equals(timezoneId)) { - /* 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be given - * one without. If so, don't sweat. - * Yes, very inefficient. Hopefully not hit often. - * If it becomes a perf problem, add 'loose' comparison instead. - */ - String cleaned = act.replace(":", ""); - if (!cleaned.equals(timezoneId)) { - throw new IndexOutOfBoundsException( - "Mismatching time zone indicator: " - + timezoneId - + " given, resolves to " - + timezone.getID()); - } - } - } + ZonedDateTime zdt = + ZonedDateTime.parse(date.substring(offset), DateTimeFormatter.ISO_ZONED_DATE_TIME); + pos.setIndex(date.length()); + parsedInstant = zdt.toInstant(); } else { - throw new IndexOutOfBoundsException( - "Invalid time zone indicator '" + timezoneIndicator + "'"); - } - - Calendar calendar = new GregorianCalendar(timezone); - calendar.setLenient(false); - calendar.set(Calendar.YEAR, year); - calendar.set(Calendar.MONTH, month - 1); - calendar.set(Calendar.DAY_OF_MONTH, day); - calendar.set(Calendar.HOUR_OF_DAY, hour); - calendar.set(Calendar.MINUTE, minutes); - calendar.set(Calendar.SECOND, seconds); - calendar.set(Calendar.MILLISECOND, milliseconds); + LocalDate localDate = + LocalDate.parse(date.substring(offset), DateTimeFormatter.ISO_LOCAL_DATE); + pos.setIndex(date.length()); - pos.setIndex(offset); - return calendar.getTime(); - // If we get a ParseException it'll already have the right message/offset. - // Other exception types can convert here. - } catch (IndexOutOfBoundsException | IllegalArgumentException e) { - fail = e; - } - String input = (date == null) ? null : ('"' + date + '"'); - String msg = fail.getMessage(); - if (msg == null || msg.isEmpty()) { - msg = "(" + fail.getClass().getName() + ")"; - } - ParseException ex = - new ParseException("Failed to parse date [" + input + "]: " + msg, pos.getIndex()); - ex.initCause(fail); - throw ex; - } - - /** - * Check if the expected character exist at the given offset in the value. - * - * @param value the string to check at the specified offset - * @param offset the offset to look for the expected character - * @param expected the expected character - * @return true if the expected character exist at the given offset - */ - private static boolean checkOffset(String value, int offset, char expected) { - return (offset < value.length()) && (value.charAt(offset) == expected); - } - - /** - * Parse an integer located between 2 given offsets in a string - * - * @param value the string to parse - * @param beginIndex the start index for the integer in the string - * @param endIndex the end index for the integer in the string - * @return the int - * @throws NumberFormatException if the value is not a number - */ - private static int parseInt(String value, int beginIndex, int endIndex) - throws NumberFormatException { - if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) { - throw new NumberFormatException(value); - } - // use same logic as in Integer.parseInt() but less generic we're not supporting negative values - int i = beginIndex; - int result = 0; - int digit; - if (i < endIndex) { - digit = Character.digit(value.charAt(i++), 10); - if (digit < 0) { - throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex)); + parsedInstant = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); } - result = -digit; - } - while (i < endIndex) { - digit = Character.digit(value.charAt(i++), 10); - if (digit < 0) { - throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex)); - } - result *= 10; - result -= digit; - } - return -result; - } - /** - * Zero pad a number to a specified length - * - * @param buffer buffer to use for padding - * @param value the integer value to pad if necessary. - * @param length the length of the string we should zero pad - */ - private static void padInt(StringBuilder buffer, int value, int length) { - String strValue = Integer.toString(value); - for (int i = length - strValue.length(); i > 0; i--) { - buffer.append('0'); - } - buffer.append(strValue); - } - - /** - * Returns the index of the first character in the string that is not a digit, starting at offset. - */ - private static int indexOfNonDigit(String string, int offset) { - for (int i = offset; i < string.length(); i++) { - char c = string.charAt(i); - if (c < '0' || c > '9') { - return i; - } + return parsedInstant; + } catch (DateTimeParseException e) { + String input = (date == null) ? null : ('"' + date + '"'); + throw new ParseException( + "Failed to parse date [" + input + "]: " + e.getMessage(), pos.getIndex()); } - return string.length(); } } diff --git a/gson/src/main/java/com/google/gson/internal/sql/SqlDateTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/sql/SqlDateTypeAdapter.java index 1991daefb0..ea00b8d7b8 100644 --- a/gson/src/main/java/com/google/gson/internal/sql/SqlDateTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/sql/SqlDateTypeAdapter.java @@ -25,16 +25,13 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; /** - * Adapter for java.sql.Date. Although this class appears stateless, it is not. DateFormat captures - * its time zone and locale when it is created, which gives this class state. DateFormat isn't - * thread safe either, so this class has to synchronize its read and write methods. + * Adapter for java.sql.Time. Although this class appears stateless, it is not. DateTimeFormatter + * captures its time zone and locale when it is created, which gives this class state. */ @SuppressWarnings("JavaUtilDate") final class SqlDateTypeAdapter extends TypeAdapter { @@ -49,7 +46,7 @@ public TypeAdapter create(Gson gson, TypeToken typeToken) { } }; - private final DateFormat format = new SimpleDateFormat("MMM d, yyyy"); + private final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy"); private SqlDateTypeAdapter() {} @@ -59,18 +56,15 @@ public java.sql.Date read(JsonReader in) throws IOException { in.nextNull(); return null; } + String s = in.nextString(); - synchronized (this) { - TimeZone originalTimeZone = format.getTimeZone(); // Save the original time zone - try { - Date utilDate = format.parse(s); - return new java.sql.Date(utilDate.getTime()); - } catch (ParseException e) { - throw new JsonSyntaxException( - "Failed parsing '" + s + "' as SQL Date; at path " + in.getPreviousPath(), e); - } finally { - format.setTimeZone(originalTimeZone); // Restore the original time zone after parsing - } + try { + LocalDate localDate = LocalDate.parse(s, FORMATTER); + + return java.sql.Date.valueOf(localDate); + } catch (DateTimeParseException e) { + throw new JsonSyntaxException( + "Failed parsing '" + s + "' as SQL Date; at path " + in.getPreviousPath(), e); } } @@ -80,10 +74,9 @@ public void write(JsonWriter out, java.sql.Date value) throws IOException { out.nullValue(); return; } - String dateString; - synchronized (this) { - dateString = format.format(value); - } + + String dateString = value.toLocalDate().format(FORMATTER); + out.value(dateString); } } diff --git a/gson/src/main/java/com/google/gson/internal/sql/SqlTimeTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/sql/SqlTimeTypeAdapter.java index d63ae0677e..d474d9cf6c 100644 --- a/gson/src/main/java/com/google/gson/internal/sql/SqlTimeTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/sql/SqlTimeTypeAdapter.java @@ -26,16 +26,13 @@ import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.sql.Time; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; /** - * Adapter for java.sql.Time. Although this class appears stateless, it is not. DateFormat captures - * its time zone and locale when it is created, which gives this class state. DateFormat isn't - * thread safe either, so this class has to synchronize its read and write methods. + * Adapter for java.sql.Time. Although this class appears stateless, it is not. DateTimeFormatter + * captures its time zone and locale when it is created, which gives this class state. */ @SuppressWarnings("JavaUtilDate") final class SqlTimeTypeAdapter extends TypeAdapter