diff --git a/gson/src/main/java/com/google/gson/DefaultDateTypeAdapter.java b/gson/src/main/java/com/google/gson/DefaultDateTypeAdapter.java index 3ce97fe8..0cbf77ce 100644 --- a/gson/src/main/java/com/google/gson/DefaultDateTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/DefaultDateTypeAdapter.java @@ -22,13 +22,17 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.ParsePosition; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Locale; +import com.google.gson.internal.PreJava9DateFormatProvider; import com.google.gson.internal.bind.util.ISO8601Utils; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; +import com.google.gson.util.VersionUtils; /** * This type adapter supports three subclasses of date: Date, Timestamp, and @@ -42,42 +46,63 @@ final class DefaultDateTypeAdapter extends TypeAdapter { private static final String SIMPLE_NAME = "DefaultDateTypeAdapter"; private final Class dateType; - private final DateFormat enUsFormat; - private final DateFormat localFormat; - + + /** + * List of 1 or more different date formats used for de-serialization attempts. + * The first of them is used for serialization as well. + */ + private final List dateFormats = new ArrayList(); + DefaultDateTypeAdapter(Class dateType) { - this(dateType, - DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.US), - DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT)); + this.dateType = verifyDateType(dateType); + dateFormats.add(DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.US)); + if (!Locale.getDefault().equals(Locale.US)) { + dateFormats.add(DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT)); + } + if (VersionUtils.isJava9OrLater()) { + dateFormats.add(PreJava9DateFormatProvider.getUSDateTimeFormat(DateFormat.DEFAULT, DateFormat.DEFAULT)); + } } DefaultDateTypeAdapter(Class dateType, String datePattern) { - this(dateType, new SimpleDateFormat(datePattern, Locale.US), new SimpleDateFormat(datePattern)); + this.dateType = verifyDateType(dateType); + dateFormats.add(new SimpleDateFormat(datePattern, Locale.US)); + if (!Locale.getDefault().equals(Locale.US)) { + dateFormats.add(new SimpleDateFormat(datePattern)); + } } DefaultDateTypeAdapter(Class dateType, int style) { - this(dateType, DateFormat.getDateInstance(style, Locale.US), DateFormat.getDateInstance(style)); + this.dateType = verifyDateType(dateType); + dateFormats.add(DateFormat.getDateInstance(style, Locale.US)); + if (!Locale.getDefault().equals(Locale.US)) { + dateFormats.add(DateFormat.getDateInstance(style)); + } + if (VersionUtils.isJava9OrLater()) { + dateFormats.add(PreJava9DateFormatProvider.getUSDateFormat(style)); + } } public DefaultDateTypeAdapter(int dateStyle, int timeStyle) { - this(Date.class, - DateFormat.getDateTimeInstance(dateStyle, timeStyle, Locale.US), - DateFormat.getDateTimeInstance(dateStyle, timeStyle)); + this(Date.class, dateStyle, timeStyle); } public DefaultDateTypeAdapter(Class dateType, int dateStyle, int timeStyle) { - this(dateType, - DateFormat.getDateTimeInstance(dateStyle, timeStyle, Locale.US), - DateFormat.getDateTimeInstance(dateStyle, timeStyle)); + this.dateType = verifyDateType(dateType); + dateFormats.add(DateFormat.getDateTimeInstance(dateStyle, timeStyle, Locale.US)); + if (!Locale.getDefault().equals(Locale.US)) { + dateFormats.add(DateFormat.getDateTimeInstance(dateStyle, timeStyle)); + } + if (VersionUtils.isJava9OrLater()) { + dateFormats.add(PreJava9DateFormatProvider.getUSDateTimeFormat(dateStyle, timeStyle)); + } } - DefaultDateTypeAdapter(final Class dateType, DateFormat enUsFormat, DateFormat localFormat) { + private static Class verifyDateType(Class dateType) { if ( dateType != Date.class && dateType != java.sql.Date.class && dateType != Timestamp.class ) { throw new IllegalArgumentException("Date type must be one of " + Date.class + ", " + Timestamp.class + ", or " + java.sql.Date.class + " but was " + dateType); } - this.dateType = dateType; - this.enUsFormat = enUsFormat; - this.localFormat = localFormat; + return dateType; } // These methods need to be synchronized since JDK DateFormat classes are not thread-safe @@ -88,8 +113,8 @@ final class DefaultDateTypeAdapter extends TypeAdapter { out.nullValue(); return; } - synchronized (localFormat) { - String dateFormatAsString = enUsFormat.format(value); + synchronized(dateFormats) { + String dateFormatAsString = dateFormats.get(0).format(value); out.value(dateFormatAsString); } } @@ -114,13 +139,12 @@ final class DefaultDateTypeAdapter extends TypeAdapter { } private Date deserializeToDate(String s) { - synchronized (localFormat) { - try { - return localFormat.parse(s); - } catch (ParseException ignored) {} - try { - return enUsFormat.parse(s); - } catch (ParseException ignored) {} + synchronized (dateFormats) { + for (DateFormat dateFormat : dateFormats) { + try { + return dateFormat.parse(s); + } catch (ParseException ignored) {} + } try { return ISO8601Utils.parse(s, new ParsePosition(0)); } catch (ParseException e) { @@ -131,9 +155,11 @@ final class DefaultDateTypeAdapter extends TypeAdapter { @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(SIMPLE_NAME); - sb.append('(').append(localFormat.getClass().getSimpleName()).append(')'); - return sb.toString(); + DateFormat defaultFormat = dateFormats.get(0); + if (defaultFormat instanceof SimpleDateFormat) { + return SIMPLE_NAME + '(' + ((SimpleDateFormat) defaultFormat).toPattern() + ')'; + } else { + return SIMPLE_NAME + '(' + defaultFormat.getClass().getSimpleName() + ')'; + } } } diff --git a/gson/src/main/java/com/google/gson/internal/PreJava9DateFormatProvider.java b/gson/src/main/java/com/google/gson/internal/PreJava9DateFormatProvider.java new file mode 100644 index 00000000..beb527c9 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/PreJava9DateFormatProvider.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2017 The Gson authors + * + * 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. + */ +package com.google.gson.internal; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Locale; + +/** + * Provides DateFormats for US locale with patterns which were the default ones before Java 9. + */ +public class PreJava9DateFormatProvider { + + /** + * Returns the same DateFormat as {@code DateFormat.getDateInstance(style, Locale.US)} in Java 8 or below. + */ + public static DateFormat getUSDateFormat(int style) { + return new SimpleDateFormat(getDateFormatPattern(style), Locale.US); + } + + /** + * Returns the same DateFormat as {@code DateFormat.getDateTimeInstance(dateStyle, timeStyle, Locale.US)} + * in Java 8 or below. + */ + public static DateFormat getUSDateTimeFormat(int dateStyle, int timeStyle) { + String pattern = getDatePartOfDateTimePattern(dateStyle) + " " + getTimePartOfDateTimePattern(timeStyle); + return new SimpleDateFormat(pattern, Locale.US); + } + + private static String getDateFormatPattern(int style) { + switch (style) { + case DateFormat.SHORT: + return "M/d/yy"; + case DateFormat.MEDIUM: + return "MMM d, y"; + case DateFormat.LONG: + return "MMMM d, y"; + case DateFormat.FULL: + return "EEEE, MMMM d, y"; + default: + throw new IllegalArgumentException("Unknown DateFormat style: " + style); + } + } + + private static String getDatePartOfDateTimePattern(int dateStyle) { + switch (dateStyle) { + case DateFormat.SHORT: + return "M/d/yy"; + case DateFormat.MEDIUM: + return "MMM d, yyyy"; + case DateFormat.LONG: + return "MMMM d, yyyy"; + case DateFormat.FULL: + return "EEEE, MMMM d, yyyy"; + default: + throw new IllegalArgumentException("Unknown DateFormat style: " + dateStyle); + } + } + + private static String getTimePartOfDateTimePattern(int timeStyle) { + switch (timeStyle) { + case DateFormat.SHORT: + return "h:mm a"; + case DateFormat.MEDIUM: + return "h:mm:ss a"; + case DateFormat.FULL: + case DateFormat.LONG: + return "h:mm:ss a z"; + default: + throw new IllegalArgumentException("Unknown DateFormat style: " + timeStyle); + } + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/DateTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/DateTypeAdapter.java index 561af198..c3a3de1b 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/DateTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/DateTypeAdapter.java @@ -20,16 +20,21 @@ import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.PreJava9DateFormatProvider; import com.google.gson.internal.bind.util.ISO8601Utils; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; +import com.google.gson.util.VersionUtils; + import java.io.IOException; import java.text.DateFormat; import java.text.ParseException; import java.text.ParsePosition; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Locale; /** @@ -46,10 +51,21 @@ public final class DateTypeAdapter extends TypeAdapter { } }; - private final DateFormat enUsFormat - = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.US); - private final DateFormat localFormat - = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT); + /** + * List of 1 or more different date formats used for de-serialization attempts. + * The first of them (default US format) is used for serialization as well. + */ + private final List dateFormats = new ArrayList(); + + public DateTypeAdapter() { + dateFormats.add(DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.US)); + if (!Locale.getDefault().equals(Locale.US)) { + dateFormats.add(DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT)); + } + if (VersionUtils.isJava9OrLater()) { + dateFormats.add(PreJava9DateFormatProvider.getUSDateTimeFormat(DateFormat.DEFAULT, DateFormat.DEFAULT)); + } + } @Override public Date read(JsonReader in) throws IOException { if (in.peek() == JsonToken.NULL) { @@ -60,13 +76,10 @@ public final class DateTypeAdapter extends TypeAdapter { } private synchronized Date deserializeToDate(String json) { - try { - return localFormat.parse(json); - } catch (ParseException ignored) { - } - try { - return enUsFormat.parse(json); - } catch (ParseException ignored) { + for (DateFormat dateFormat : dateFormats) { + try { + return dateFormat.parse(json); + } catch (ParseException ignored) {} } try { return ISO8601Utils.parse(json, new ParsePosition(0)); @@ -80,7 +93,7 @@ public final class DateTypeAdapter extends TypeAdapter { out.nullValue(); return; } - String dateFormatAsString = enUsFormat.format(value); + String dateFormatAsString = dateFormats.get(0).format(value); out.value(dateFormatAsString); } diff --git a/gson/src/main/java/com/google/gson/util/VersionUtils.java b/gson/src/main/java/com/google/gson/util/VersionUtils.java new file mode 100644 index 00000000..d81e43c0 --- /dev/null +++ b/gson/src/main/java/com/google/gson/util/VersionUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 The Gson authors + * + * 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. + */ + +package com.google.gson.util; + +/** + * Utility to check the major Java version of the current JVM. + */ +public class VersionUtils { + + private static final int majorJavaVersion = determineMajorJavaVersion(); + + private static int determineMajorJavaVersion() { + String[] parts = System.getProperty("java.version").split("[._]"); + int firstVer = Integer.parseInt(parts[0]); + if (firstVer == 1 && parts.length > 1) { + return Integer.parseInt(parts[1]); + } else { + return firstVer; + } + } + + /** + * @return the major Java version, i.e. '8' for Java 1.8, '9' for Java 9 etc. + */ + public static int getMajorJavaVersion() { + return majorJavaVersion; + } + + /** + * @return {@code true} if the application is running on Java 9 or later; and {@code false} otherwise. + */ + public static boolean isJava9OrLater() { + return majorJavaVersion >= 9; + } +} diff --git a/gson/src/test/java/com/google/gson/DefaultDateTypeAdapterTest.java b/gson/src/test/java/com/google/gson/DefaultDateTypeAdapterTest.java index 3c787a66..a074bea0 100644 --- a/gson/src/test/java/com/google/gson/DefaultDateTypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/DefaultDateTypeAdapterTest.java @@ -22,6 +22,8 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; + +import com.google.gson.util.VersionUtils; import junit.framework.TestCase; /** @@ -45,17 +47,21 @@ public class DefaultDateTypeAdapterTest extends TestCase { Locale defaultLocale = Locale.getDefault(); Locale.setDefault(locale); try { - assertFormatted("Jan 1, 1970 12:00:00 AM", new DefaultDateTypeAdapter(Date.class)); + String afterYearSep = VersionUtils.isJava9OrLater() ? ", " : " "; + String afterYearLongSep = VersionUtils.isJava9OrLater() ? " at " : " "; + String utcFull = VersionUtils.isJava9OrLater() ? "Coordinated Universal Time" : "UTC"; + assertFormatted(String.format("Jan 1, 1970%s12:00:00 AM", afterYearSep), + new DefaultDateTypeAdapter(Date.class)); assertFormatted("1/1/70", new DefaultDateTypeAdapter(Date.class, DateFormat.SHORT)); assertFormatted("Jan 1, 1970", new DefaultDateTypeAdapter(Date.class, DateFormat.MEDIUM)); assertFormatted("January 1, 1970", new DefaultDateTypeAdapter(Date.class, DateFormat.LONG)); - assertFormatted("1/1/70 12:00 AM", + assertFormatted(String.format("1/1/70%s12:00 AM", afterYearSep), new DefaultDateTypeAdapter(DateFormat.SHORT, DateFormat.SHORT)); - assertFormatted("Jan 1, 1970 12:00:00 AM", + assertFormatted(String.format("Jan 1, 1970%s12:00:00 AM", afterYearSep), new DefaultDateTypeAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM)); - assertFormatted("January 1, 1970 12:00:00 AM UTC", + assertFormatted(String.format("January 1, 1970%s12:00:00 AM UTC", afterYearLongSep), new DefaultDateTypeAdapter(DateFormat.LONG, DateFormat.LONG)); - assertFormatted("Thursday, January 1, 1970 12:00:00 AM UTC", + assertFormatted(String.format("Thursday, January 1, 1970%s12:00:00 AM %s", afterYearLongSep, utcFull), new DefaultDateTypeAdapter(DateFormat.FULL, DateFormat.FULL)); } finally { TimeZone.setDefault(defaultTimeZone); @@ -69,17 +75,21 @@ public class DefaultDateTypeAdapterTest extends TestCase { Locale defaultLocale = Locale.getDefault(); Locale.setDefault(Locale.FRANCE); try { - assertParsed("1 janv. 1970 00:00:00", new DefaultDateTypeAdapter(Date.class)); + String afterYearSep = VersionUtils.isJava9OrLater() ? " à " : " "; + assertParsed(String.format("1 janv. 1970%s00:00:00", afterYearSep), + new DefaultDateTypeAdapter(Date.class)); assertParsed("01/01/70", new DefaultDateTypeAdapter(Date.class, DateFormat.SHORT)); assertParsed("1 janv. 1970", new DefaultDateTypeAdapter(Date.class, DateFormat.MEDIUM)); assertParsed("1 janvier 1970", new DefaultDateTypeAdapter(Date.class, DateFormat.LONG)); assertParsed("01/01/70 00:00", new DefaultDateTypeAdapter(DateFormat.SHORT, DateFormat.SHORT)); - assertParsed("1 janv. 1970 00:00:00", + assertParsed(String.format("1 janv. 1970%s00:00:00", afterYearSep), new DefaultDateTypeAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM)); - assertParsed("1 janvier 1970 00:00:00 UTC", + assertParsed(String.format("1 janvier 1970%s00:00:00 UTC", afterYearSep), new DefaultDateTypeAdapter(DateFormat.LONG, DateFormat.LONG)); - assertParsed("jeudi 1 janvier 1970 00 h 00 UTC", + assertParsed(VersionUtils.isJava9OrLater() ? + "jeudi 1 janvier 1970 à 00:00:00 Coordinated Universal Time" : + "jeudi 1 janvier 1970 00 h 00 UTC", new DefaultDateTypeAdapter(DateFormat.FULL, DateFormat.FULL)); } finally { TimeZone.setDefault(defaultTimeZone); @@ -117,7 +127,9 @@ public class DefaultDateTypeAdapterTest extends TestCase { Locale defaultLocale = Locale.getDefault(); Locale.setDefault(Locale.US); try { - assertFormatted("Dec 31, 1969 4:00:00 PM", new DefaultDateTypeAdapter(Date.class)); + String afterYearSep = VersionUtils.isJava9OrLater() ? ", " : " "; + assertFormatted(String.format("Dec 31, 1969%s4:00:00 PM", afterYearSep), + new DefaultDateTypeAdapter(Date.class)); assertParsed("Dec 31, 1969 4:00:00 PM", new DefaultDateTypeAdapter(Date.class)); } finally { TimeZone.setDefault(defaultTimeZone); diff --git a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java index 19866716..635c2088 100644 --- a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java +++ b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java @@ -55,6 +55,8 @@ import java.util.Set; import java.util.TimeZone; import java.util.TreeSet; import java.util.UUID; + +import com.google.gson.util.VersionUtils; import junit.framework.TestCase; /** @@ -328,7 +330,11 @@ public class DefaultTypeAdaptersTest extends TestCase { public void testDefaultDateSerialization() { Date now = new Date(1315806903103L); String json = gson.toJson(now); - assertEquals("\"Sep 11, 2011 10:55:03 PM\"", json); + if (VersionUtils.isJava9OrLater()) { + assertEquals("\"Sep 11, 2011, 10:55:03 PM\"", json); + } else { + assertEquals("\"Sep 11, 2011 10:55:03 PM\"", json); + } } public void testDefaultDateDeserialization() { @@ -369,7 +375,11 @@ public class DefaultTypeAdaptersTest extends TestCase { public void testDefaultJavaSqlTimestampSerialization() { Timestamp now = new java.sql.Timestamp(1259875082000L); String json = gson.toJson(now); - assertEquals("\"Dec 3, 2009 1:18:02 PM\"", json); + if (VersionUtils.isJava9OrLater()) { + assertEquals("\"Dec 3, 2009, 1:18:02 PM\"", json); + } else { + assertEquals("\"Dec 3, 2009 1:18:02 PM\"", json); + } } public void testDefaultJavaSqlTimestampDeserialization() { @@ -395,7 +405,11 @@ public class DefaultTypeAdaptersTest extends TestCase { Gson gson = new GsonBuilder().create(); Date now = new Date(1315806903103L); String json = gson.toJson(now); - assertEquals("\"Sep 11, 2011 10:55:03 PM\"", json); + if (VersionUtils.isJava9OrLater()) { + assertEquals("\"Sep 11, 2011, 10:55:03 PM\"", json); + } else { + assertEquals("\"Sep 11, 2011 10:55:03 PM\"", json); + } } public void testDefaultDateDeserializationUsingBuilder() throws Exception { diff --git a/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java index de1219a6..cf82457a 100644 --- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java +++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java @@ -43,6 +43,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; + +import com.google.gson.util.VersionUtils; import junit.framework.TestCase; /** @@ -482,7 +484,11 @@ public class ObjectTest extends TestCase { public void testDateAsMapObjectField() { HasObjectMap a = new HasObjectMap(); a.map.put("date", new Date(0)); - assertEquals("{\"map\":{\"date\":\"Dec 31, 1969 4:00:00 PM\"}}", gson.toJson(a)); + if (VersionUtils.isJava9OrLater()) { + assertEquals("{\"map\":{\"date\":\"Dec 31, 1969, 4:00:00 PM\"}}", gson.toJson(a)); + } else { + assertEquals("{\"map\":{\"date\":\"Dec 31, 1969 4:00:00 PM\"}}", gson.toJson(a)); + } } public class HasObjectMap {