From 4199782f24a46fa85896382dc63916a24aded521 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 19 Nov 2023 01:57:33 +0100 Subject: [PATCH] Remove `DateTypeAdapter` to avoid code duplication The same functionality exists in `DefaultDateTypeAdapter` (with some slight overhead), and there is currently code duplication between these classes. --- gson/src/main/java/com/google/gson/Gson.java | 4 +- .../gson/internal/bind/DateTypeAdapter.java | 116 ------------------ .../internal/bind/DefaultDateTypeAdapter.java | 37 +++++- 3 files changed, 37 insertions(+), 120 deletions(-) delete mode 100644 gson/src/main/java/com/google/gson/internal/bind/DateTypeAdapter.java diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 1c6d949ce5..6ba7ec2bba 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -25,7 +25,7 @@ import com.google.gson.internal.Streams; import com.google.gson.internal.bind.ArrayTypeAdapter; import com.google.gson.internal.bind.CollectionTypeAdapterFactory; -import com.google.gson.internal.bind.DateTypeAdapter; +import com.google.gson.internal.bind.DefaultDateTypeAdapter; import com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory; import com.google.gson.internal.bind.JsonTreeReader; import com.google.gson.internal.bind.JsonTreeWriter; @@ -370,7 +370,7 @@ public Gson() { factories.add(TypeAdapters.LOCALE_FACTORY); factories.add(TypeAdapters.INET_ADDRESS_FACTORY); factories.add(TypeAdapters.BIT_SET_FACTORY); - factories.add(DateTypeAdapter.FACTORY); + factories.add(DefaultDateTypeAdapter.DEFAULT_STYLE_FACTORY); factories.add(TypeAdapters.CALENDAR_FACTORY); if (SqlTypesSupport.SUPPORTS_SQL_TYPES) { 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 deleted file mode 100644 index e12a100763..0000000000 --- a/gson/src/main/java/com/google/gson/internal/bind/DateTypeAdapter.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2011 Google Inc. - * - * 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.bind; - -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.JavaVersion; -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 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; - -/** - * Adapter for 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. - */ -public final class DateTypeAdapter extends TypeAdapter { - public static final TypeAdapterFactory FACTORY = - new TypeAdapterFactory() { - @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal - @Override - public TypeAdapter create(Gson gson, TypeToken typeToken) { - return typeToken.getRawType() == Date.class - ? (TypeAdapter) new DateTypeAdapter() - : null; - } - }; - - /** - * 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 (JavaVersion.isJava9OrLater()) { - dateFormats.add( - PreJava9DateFormatProvider.getUSDateTimeFormat(DateFormat.DEFAULT, DateFormat.DEFAULT)); - } - } - - @Override - public Date read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - return deserializeToDate(in); - } - - private Date deserializeToDate(JsonReader in) throws IOException { - String s = in.nextString(); - synchronized (dateFormats) { - for (DateFormat dateFormat : dateFormats) { - try { - return dateFormat.parse(s); - } catch (ParseException ignored) { - // OK: try the next format - } - } - } - try { - return ISO8601Utils.parse(s, new ParsePosition(0)); - } catch (ParseException e) { - throw new JsonSyntaxException( - "Failed parsing '" + s + "' as Date; at path " + in.getPreviousPath(), e); - } - } - - @Override - public void write(JsonWriter out, Date value) throws IOException { - if (value == null) { - out.nullValue(); - return; - } - - DateFormat dateFormat = dateFormats.get(0); - String dateFormatAsString; - synchronized (dateFormats) { - dateFormatAsString = dateFormat.format(value); - } - out.value(dateFormatAsString); - } -} 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 d4dd07e67d..4a371348a0 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 @@ -16,12 +16,14 @@ package com.google.gson.internal.bind; +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.JavaVersion; 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; @@ -35,17 +37,48 @@ import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.TimeZone; /** * This type adapter supports subclasses of date by defining a {@link * DefaultDateTypeAdapter.DateType} and then using its {@code createAdapterFactory} methods. * + *

Important: Instances of this class (or rather the {@link SimpleDateFormat} they use) + * capture the current default {@link Locale} and {@link TimeZone} when they are created. Therefore + * avoid storing factories obtained from {@link DateType} in {@code static} fields, since they only + * create a single adapter instance and its behavior would then depend on when Gson classes are + * loaded first, and which default {@code Locale} and {@code TimeZone} was used at that point. + * * @author Inderjeet Singh * @author Joel Leitch */ public final class DefaultDateTypeAdapter extends TypeAdapter { private static final String SIMPLE_NAME = "DefaultDateTypeAdapter"; + /** Factory for {@link Date} adapters which use {@link DateFormat#DEFAULT} as style. */ + public static final TypeAdapterFactory DEFAULT_STYLE_FACTORY = + // Because SimpleDateFormat captures the default TimeZone when it was created, let the factory + // always create new DefaultDateTypeAdapter instances (which are then cached by the Gson + // instances) instead of having a single static DefaultDateTypeAdapter instance + // Otherwise the behavior would depend on when an application first loads Gson classes and + // which default TimeZone is set at that point, which would be quite brittle + new TypeAdapterFactory() { + @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + return typeToken.getRawType() == Date.class + ? (TypeAdapter) + new DefaultDateTypeAdapter<>( + DateType.DATE, DateFormat.DEFAULT, DateFormat.DEFAULT) + : null; + } + + @Override + public String toString() { + return "DefaultDateTypeAdapter#DEFAULT_STYLE_FACTORY"; + } + }; + public abstract static class DateType { public static final DateType DATE = new DateType(Date.class) { @@ -123,8 +156,6 @@ private DefaultDateTypeAdapter(DateType dateType, int dateStyle, int timeStyl } } - // These methods need to be synchronized since JDK DateFormat classes are not thread-safe - // See issue 162 @Override public void write(JsonWriter out, Date value) throws IOException { if (value == null) { @@ -134,6 +165,7 @@ public void write(JsonWriter out, Date value) throws IOException { DateFormat dateFormat = dateFormats.get(0); String dateFormatAsString; + // Needs to be synchronized since JDK DateFormat classes are not thread-safe synchronized (dateFormats) { dateFormatAsString = dateFormat.format(value); } @@ -152,6 +184,7 @@ public T read(JsonReader in) throws IOException { private Date deserializeToDate(JsonReader in) throws IOException { String s = in.nextString(); + // Needs to be synchronized since JDK DateFormat classes are not thread-safe synchronized (dateFormats) { for (DateFormat dateFormat : dateFormats) { try {