Implement DefaultDateTypeAdapter in a type safer way

This commit is contained in:
Marcono1234 2020-02-12 19:25:56 +01:00
parent 1a9469a8c5
commit 541252a9fb
3 changed files with 94 additions and 78 deletions

View File

@ -41,11 +41,52 @@ import com.google.gson.stream.JsonWriter;
* @author Inderjeet Singh
* @author Joel Leitch
*/
final class DefaultDateTypeAdapter extends TypeAdapter<Date> {
final class DefaultDateTypeAdapter<T extends Date> extends TypeAdapter<T> {
private static final String SIMPLE_NAME = "DefaultDateTypeAdapter";
private final Class<? extends Date> dateType;
static abstract class DateType<T extends Date> {
private DateType() {
}
public static final DateType<Date> DATE = new DateType<Date>() {
@Override
protected Date deserialize(Date date) {
return date;
}
};
public static final DateType<java.sql.Date> SQL_DATE = new DateType<java.sql.Date>() {
@Override
protected java.sql.Date deserialize(Date date) {
return new java.sql.Date(date.getTime());
}
};
public static final DateType<Timestamp> SQL_TIMESTAMP = new DateType<Timestamp>() {
@Override
protected Timestamp deserialize(Date date) {
return new Timestamp(date.getTime());
}
};
protected abstract T deserialize(Date date);
public DefaultDateTypeAdapter<T> createAdapter(String datePattern) {
return new DefaultDateTypeAdapter<T>(this, datePattern);
}
public DefaultDateTypeAdapter<T> createAdapter(int style) {
return new DefaultDateTypeAdapter<T>(this, style);
}
public DefaultDateTypeAdapter<T> createAdapter(int dateStyle, int timeStyle) {
return new DefaultDateTypeAdapter<T>(this, dateStyle, timeStyle);
}
public DefaultDateTypeAdapter<T> createDefaultsAdapter() {
return new DefaultDateTypeAdapter<T>(this, DateFormat.DEFAULT, DateFormat.DEFAULT);
}
}
private final DateType<T> dateType;
/**
* List of 1 or more different date formats used for de-serialization attempts.
@ -53,18 +94,7 @@ final class DefaultDateTypeAdapter extends TypeAdapter<Date> {
*/
private final List<DateFormat> dateFormats = new ArrayList<DateFormat>();
DefaultDateTypeAdapter(Class<? extends Date> dateType) {
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 (JavaVersion.isJava9OrLater()) {
dateFormats.add(PreJava9DateFormatProvider.getUSDateTimeFormat(DateFormat.DEFAULT, DateFormat.DEFAULT));
}
}
DefaultDateTypeAdapter(Class<? extends Date> dateType, String datePattern) {
private DefaultDateTypeAdapter(DateType<T> dateType, String datePattern) {
this.dateType = verifyDateType(dateType);
dateFormats.add(new SimpleDateFormat(datePattern, Locale.US));
if (!Locale.getDefault().equals(Locale.US)) {
@ -72,7 +102,7 @@ final class DefaultDateTypeAdapter extends TypeAdapter<Date> {
}
}
DefaultDateTypeAdapter(Class<? extends Date> dateType, int style) {
private DefaultDateTypeAdapter(DateType<T> dateType, int style) {
this.dateType = verifyDateType(dateType);
dateFormats.add(DateFormat.getDateInstance(style, Locale.US));
if (!Locale.getDefault().equals(Locale.US)) {
@ -83,11 +113,7 @@ final class DefaultDateTypeAdapter extends TypeAdapter<Date> {
}
}
public DefaultDateTypeAdapter(int dateStyle, int timeStyle) {
this(Date.class, dateStyle, timeStyle);
}
public DefaultDateTypeAdapter(Class<? extends Date> dateType, int dateStyle, int timeStyle) {
private DefaultDateTypeAdapter(DateType<T> dateType, int dateStyle, int timeStyle) {
this.dateType = verifyDateType(dateType);
dateFormats.add(DateFormat.getDateTimeInstance(dateStyle, timeStyle, Locale.US));
if (!Locale.getDefault().equals(Locale.US)) {
@ -98,9 +124,9 @@ final class DefaultDateTypeAdapter extends TypeAdapter<Date> {
}
}
private static Class<? extends Date> verifyDateType(Class<? extends Date> 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);
private static <T extends Date> DateType<T> verifyDateType(DateType<T> dateType) {
if (dateType == null) {
throw new NullPointerException("dateType == null");
}
return dateType;
}
@ -120,22 +146,13 @@ final class DefaultDateTypeAdapter extends TypeAdapter<Date> {
}
@Override
public Date read(JsonReader in) throws IOException {
public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
Date date = deserializeToDate(in.nextString());
if (dateType == Date.class) {
return date;
} else if (dateType == Timestamp.class) {
return new Timestamp(date.getTime());
} else if (dateType == java.sql.Date.class) {
return new java.sql.Date(date.getTime());
} else {
// This must never happen: dateType is guarded in the primary constructor
throw new AssertionError();
}
return dateType.deserialize(date);
}
private Date deserializeToDate(String s) {

View File

@ -602,20 +602,19 @@ public final class GsonBuilder {
this.factories, this.hierarchyFactories, factories);
}
@SuppressWarnings("unchecked")
private void addTypeAdaptersForDate(String datePattern, int dateStyle, int timeStyle,
List<TypeAdapterFactory> factories) {
DefaultDateTypeAdapter dateTypeAdapter;
DefaultDateTypeAdapter<Date> dateTypeAdapter;
TypeAdapter<Timestamp> timestampTypeAdapter;
TypeAdapter<java.sql.Date> javaSqlDateTypeAdapter;
if (datePattern != null && !"".equals(datePattern.trim())) {
dateTypeAdapter = new DefaultDateTypeAdapter(Date.class, datePattern);
timestampTypeAdapter = (TypeAdapter) new DefaultDateTypeAdapter(Timestamp.class, datePattern);
javaSqlDateTypeAdapter = (TypeAdapter) new DefaultDateTypeAdapter(java.sql.Date.class, datePattern);
dateTypeAdapter = DefaultDateTypeAdapter.DateType.DATE.createAdapter(datePattern);
timestampTypeAdapter = DefaultDateTypeAdapter.DateType.SQL_TIMESTAMP.createAdapter(datePattern);
javaSqlDateTypeAdapter = DefaultDateTypeAdapter.DateType.SQL_DATE.createAdapter(datePattern);
} else if (dateStyle != DateFormat.DEFAULT && timeStyle != DateFormat.DEFAULT) {
dateTypeAdapter = new DefaultDateTypeAdapter(Date.class, dateStyle, timeStyle);
timestampTypeAdapter = (TypeAdapter) new DefaultDateTypeAdapter(Timestamp.class, dateStyle, timeStyle);
javaSqlDateTypeAdapter = (TypeAdapter) new DefaultDateTypeAdapter(java.sql.Date.class, dateStyle, timeStyle);
dateTypeAdapter = DefaultDateTypeAdapter.DateType.DATE.createAdapter(dateStyle, timeStyle);
timestampTypeAdapter = DefaultDateTypeAdapter.DateType.SQL_TIMESTAMP.createAdapter(dateStyle, timeStyle);
javaSqlDateTypeAdapter = DefaultDateTypeAdapter.DateType.SQL_DATE.createAdapter(dateStyle, timeStyle);
} else {
return;
}

View File

@ -23,6 +23,7 @@ import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import com.google.gson.DefaultDateTypeAdapter.DateType;
import com.google.gson.internal.JavaVersion;
import junit.framework.TestCase;
@ -52,18 +53,18 @@ public class DefaultDateTypeAdapterTest extends TestCase {
String afterYearLongSep = JavaVersion.isJava9OrLater() ? " at " : " ";
String utcFull = JavaVersion.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));
DateType.DATE.createDefaultsAdapter());
assertFormatted("1/1/70", DateType.DATE.createAdapter(DateFormat.SHORT));
assertFormatted("Jan 1, 1970", DateType.DATE.createAdapter(DateFormat.MEDIUM));
assertFormatted("January 1, 1970", DateType.DATE.createAdapter(DateFormat.LONG));
assertFormatted(String.format("1/1/70%s12:00 AM", afterYearSep),
new DefaultDateTypeAdapter(DateFormat.SHORT, DateFormat.SHORT));
DateType.DATE.createAdapter(DateFormat.SHORT, DateFormat.SHORT));
assertFormatted(String.format("Jan 1, 1970%s12:00:00 AM", afterYearSep),
new DefaultDateTypeAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM));
DateType.DATE.createAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM));
assertFormatted(String.format("January 1, 1970%s12:00:00 AM UTC", afterYearLongSep),
new DefaultDateTypeAdapter(DateFormat.LONG, DateFormat.LONG));
DateType.DATE.createAdapter(DateFormat.LONG, DateFormat.LONG));
assertFormatted(String.format("Thursday, January 1, 1970%s12:00:00 AM %s", afterYearLongSep, utcFull),
new DefaultDateTypeAdapter(DateFormat.FULL, DateFormat.FULL));
DateType.DATE.createAdapter(DateFormat.FULL, DateFormat.FULL));
} finally {
TimeZone.setDefault(defaultTimeZone);
Locale.setDefault(defaultLocale);
@ -78,21 +79,21 @@ public class DefaultDateTypeAdapterTest extends TestCase {
try {
String afterYearSep = JavaVersion.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));
DateType.DATE.createDefaultsAdapter());
assertParsed("01/01/70", DateType.DATE.createAdapter(DateFormat.SHORT));
assertParsed("1 janv. 1970", DateType.DATE.createAdapter(DateFormat.MEDIUM));
assertParsed("1 janvier 1970", DateType.DATE.createAdapter(DateFormat.LONG));
assertParsed("01/01/70 00:00",
new DefaultDateTypeAdapter(DateFormat.SHORT, DateFormat.SHORT));
DateType.DATE.createAdapter(DateFormat.SHORT, DateFormat.SHORT));
assertParsed(String.format("1 janv. 1970%s00:00:00", afterYearSep),
new DefaultDateTypeAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM));
DateType.DATE.createAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM));
assertParsed(String.format("1 janvier 1970%s00:00:00 UTC", afterYearSep),
new DefaultDateTypeAdapter(DateFormat.LONG, DateFormat.LONG));
DateType.DATE.createAdapter(DateFormat.LONG, DateFormat.LONG));
assertParsed(JavaVersion.isJava9OrLater() ? (JavaVersion.getMajorJavaVersion() <11 ?
"jeudi 1 janvier 1970 à 00:00:00 Coordinated Universal Time" :
"jeudi 1 janvier 1970 à 00:00:00 Temps universel coordonné") :
"jeudi 1 janvier 1970 00 h 00 UTC",
new DefaultDateTypeAdapter(DateFormat.FULL, DateFormat.FULL));
DateType.DATE.createAdapter(DateFormat.FULL, DateFormat.FULL));
} finally {
TimeZone.setDefault(defaultTimeZone);
Locale.setDefault(defaultLocale);
@ -105,18 +106,18 @@ public class DefaultDateTypeAdapterTest extends TestCase {
Locale defaultLocale = Locale.getDefault();
Locale.setDefault(Locale.US);
try {
assertParsed("Jan 1, 1970 0:00:00 AM", new DefaultDateTypeAdapter(Date.class));
assertParsed("1/1/70", new DefaultDateTypeAdapter(Date.class, DateFormat.SHORT));
assertParsed("Jan 1, 1970", new DefaultDateTypeAdapter(Date.class, DateFormat.MEDIUM));
assertParsed("January 1, 1970", new DefaultDateTypeAdapter(Date.class, DateFormat.LONG));
assertParsed("Jan 1, 1970 0:00:00 AM", DateType.DATE.createDefaultsAdapter());
assertParsed("1/1/70", DateType.DATE.createAdapter(DateFormat.SHORT));
assertParsed("Jan 1, 1970", DateType.DATE.createAdapter(DateFormat.MEDIUM));
assertParsed("January 1, 1970", DateType.DATE.createAdapter(DateFormat.LONG));
assertParsed("1/1/70 0:00 AM",
new DefaultDateTypeAdapter(DateFormat.SHORT, DateFormat.SHORT));
DateType.DATE.createAdapter(DateFormat.SHORT, DateFormat.SHORT));
assertParsed("Jan 1, 1970 0:00:00 AM",
new DefaultDateTypeAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM));
DateType.DATE.createAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM));
assertParsed("January 1, 1970 0:00:00 AM UTC",
new DefaultDateTypeAdapter(DateFormat.LONG, DateFormat.LONG));
DateType.DATE.createAdapter(DateFormat.LONG, DateFormat.LONG));
assertParsed("Thursday, January 1, 1970 0:00:00 AM UTC",
new DefaultDateTypeAdapter(DateFormat.FULL, DateFormat.FULL));
DateType.DATE.createAdapter(DateFormat.FULL, DateFormat.FULL));
} finally {
TimeZone.setDefault(defaultTimeZone);
Locale.setDefault(defaultLocale);
@ -131,8 +132,8 @@ public class DefaultDateTypeAdapterTest extends TestCase {
try {
String afterYearSep = JavaVersion.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));
DateType.DATE.createDefaultsAdapter());
assertParsed("Dec 31, 1969 4:00:00 PM", DateType.DATE.createDefaultsAdapter());
} finally {
TimeZone.setDefault(defaultTimeZone);
Locale.setDefault(defaultLocale);
@ -140,7 +141,7 @@ public class DefaultDateTypeAdapterTest extends TestCase {
}
public void testDateDeserializationISO8601() throws Exception {
DefaultDateTypeAdapter adapter = new DefaultDateTypeAdapter(Date.class);
DefaultDateTypeAdapter<Date> adapter = DateType.DATE.createDefaultsAdapter();
assertParsed("1970-01-01T00:00:00.000Z", adapter);
assertParsed("1970-01-01T00:00Z", adapter);
assertParsed("1970-01-01T00:00:00+00:00", adapter);
@ -150,7 +151,7 @@ public class DefaultDateTypeAdapterTest extends TestCase {
public void testDateSerialization() throws Exception {
int dateStyle = DateFormat.LONG;
DefaultDateTypeAdapter dateTypeAdapter = new DefaultDateTypeAdapter(Date.class, dateStyle);
DefaultDateTypeAdapter<Date> dateTypeAdapter = DateType.DATE.createAdapter(dateStyle);
DateFormat formatter = DateFormat.getDateInstance(dateStyle, Locale.US);
Date currentDate = new Date();
@ -160,7 +161,7 @@ public class DefaultDateTypeAdapterTest extends TestCase {
public void testDatePattern() throws Exception {
String pattern = "yyyy-MM-dd";
DefaultDateTypeAdapter dateTypeAdapter = new DefaultDateTypeAdapter(Date.class, pattern);
DefaultDateTypeAdapter<Date> dateTypeAdapter = DateType.DATE.createAdapter(pattern);
DateFormat formatter = new SimpleDateFormat(pattern);
Date currentDate = new Date();
@ -168,33 +169,32 @@ public class DefaultDateTypeAdapterTest extends TestCase {
assertEquals(toLiteral(formatter.format(currentDate)), dateString);
}
@SuppressWarnings("unused")
public void testInvalidDatePattern() throws Exception {
try {
new DefaultDateTypeAdapter(Date.class, "I am a bad Date pattern....");
DateType.DATE.createAdapter("I am a bad Date pattern....");
fail("Invalid date pattern should fail.");
} catch (IllegalArgumentException expected) { }
}
public void testNullValue() throws Exception {
DefaultDateTypeAdapter adapter = new DefaultDateTypeAdapter(Date.class);
DefaultDateTypeAdapter<Date> adapter = DateType.DATE.createDefaultsAdapter();
assertNull(adapter.fromJson("null"));
assertEquals("null", adapter.toJson(null));
}
public void testUnexpectedToken() throws Exception {
try {
DefaultDateTypeAdapter adapter = new DefaultDateTypeAdapter(Date.class);
DefaultDateTypeAdapter<Date> adapter = DateType.DATE.createDefaultsAdapter();
adapter.fromJson("{}");
fail("Unexpected token should fail.");
} catch (IllegalStateException expected) { }
}
private void assertFormatted(String formatted, DefaultDateTypeAdapter adapter) {
private void assertFormatted(String formatted, DefaultDateTypeAdapter<Date> adapter) {
assertEquals(toLiteral(formatted), adapter.toJson(new Date(0)));
}
private void assertParsed(String date, DefaultDateTypeAdapter adapter) throws IOException {
private void assertParsed(String date, DefaultDateTypeAdapter<Date> adapter) throws IOException {
assertEquals(date, new Date(0), adapter.fromJson(toLiteral(date)));
assertEquals("ISO 8601", new Date(0), adapter.fromJson(toLiteral("1970-01-01T00:00:00Z")));
}