diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 196dc90fc3..e7fef48cec 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -26,7 +26,9 @@ import com.google.gson.internal.bind.ArrayTypeAdapter; import com.google.gson.internal.bind.CollectionTypeAdapterFactory; import com.google.gson.internal.bind.DefaultDateTypeAdapter; +import com.google.gson.internal.bind.EnumTypeAdapter; import com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory; +import com.google.gson.internal.bind.JsonElementTypeAdapter; import com.google.gson.internal.bind.JsonTreeReader; import com.google.gson.internal.bind.JsonTreeWriter; import com.google.gson.internal.bind.MapTypeAdapterFactory; @@ -323,7 +325,7 @@ public Gson() { List factories = new ArrayList<>(); // built-in type adapters that cannot be overridden - factories.add(TypeAdapters.JSON_ELEMENT_FACTORY); + factories.add(JsonElementTypeAdapter.FACTORY); factories.add(ObjectTypeAdapter.getFactory(objectToNumberStrategy)); // the excluder must precede all adapters that handle user-defined types @@ -386,7 +388,7 @@ public Gson() { factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization)); this.jsonAdapterFactory = new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor); factories.add(jsonAdapterFactory); - factories.add(TypeAdapters.ENUM_FACTORY); + factories.add(EnumTypeAdapter.FACTORY); factories.add( new ReflectiveTypeAdapterFactory( constructorConstructor, diff --git a/gson/src/main/java/com/google/gson/internal/Streams.java b/gson/src/main/java/com/google/gson/internal/Streams.java index 46df853f5a..8ef82860de 100644 --- a/gson/src/main/java/com/google/gson/internal/Streams.java +++ b/gson/src/main/java/com/google/gson/internal/Streams.java @@ -21,7 +21,7 @@ import com.google.gson.JsonNull; import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; -import com.google.gson.internal.bind.TypeAdapters; +import com.google.gson.internal.bind.JsonElementTypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; @@ -43,7 +43,7 @@ public static JsonElement parse(JsonReader reader) throws JsonParseException { try { JsonToken unused = reader.peek(); isEmpty = false; - return TypeAdapters.JSON_ELEMENT.read(reader); + return JsonElementTypeAdapter.ADAPTER.read(reader); } catch (EOFException e) { /* * For compatibility with JSON 1.5 and earlier, we return a JsonNull for @@ -65,7 +65,7 @@ public static JsonElement parse(JsonReader reader) throws JsonParseException { /** Writes the JSON element to the writer, recursively. */ public static void write(JsonElement element, JsonWriter writer) throws IOException { - TypeAdapters.JSON_ELEMENT.write(writer, element); + JsonElementTypeAdapter.ADAPTER.write(writer, element); } public static Writer writerForAppendable(Appendable appendable) { diff --git a/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java index d4224d52cd..a18cbf9262 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java @@ -30,7 +30,7 @@ import java.lang.reflect.Type; import java.util.ArrayList; -/** Adapt an array of objects. */ +/** Adapter for arrays. */ public final class ArrayTypeAdapter extends TypeAdapter { public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() { diff --git a/gson/src/main/java/com/google/gson/internal/bind/EnumTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/EnumTypeAdapter.java new file mode 100644 index 0000000000..d1ac077f4b --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/EnumTypeAdapter.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2024 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.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.SerializedName; +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.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** Adapter for enum classes (but not for the base class {@code java.lang.Enum}). */ +public class EnumTypeAdapter> extends TypeAdapter { + public static final TypeAdapterFactory FACTORY = + new TypeAdapterFactory() { + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + Class rawType = typeToken.getRawType(); + if (!Enum.class.isAssignableFrom(rawType) || rawType == Enum.class) { + return null; + } + if (!rawType.isEnum()) { + rawType = rawType.getSuperclass(); // handle anonymous subclasses + } + @SuppressWarnings({"rawtypes", "unchecked"}) + TypeAdapter adapter = (TypeAdapter) new EnumTypeAdapter(rawType); + return adapter; + } + }; + + private final Map nameToConstant = new HashMap<>(); + private final Map stringToConstant = new HashMap<>(); + private final Map constantToName = new HashMap<>(); + + private EnumTypeAdapter(final Class classOfT) { + try { + // Uses reflection to find enum constants to work around name mismatches for obfuscated + // classes + Field[] fields = classOfT.getDeclaredFields(); + ArrayList constantFieldsList = new ArrayList<>(fields.length); + for (Field f : fields) { + if (f.isEnumConstant()) { + constantFieldsList.add(f); + } + } + + Field[] constantFields = constantFieldsList.toArray(new Field[0]); + AccessibleObject.setAccessible(constantFields, true); + + for (Field constantField : constantFields) { + @SuppressWarnings("unchecked") + T constant = (T) constantField.get(null); + String name = constant.name(); + String toStringVal = constant.toString(); + + SerializedName annotation = constantField.getAnnotation(SerializedName.class); + if (annotation != null) { + name = annotation.value(); + for (String alternate : annotation.alternate()) { + nameToConstant.put(alternate, constant); + } + } + nameToConstant.put(name, constant); + stringToConstant.put(toStringVal, constant); + constantToName.put(constant, name); + } + } catch (IllegalAccessException e) { + // IllegalAccessException should be impossible due to the `setAccessible` call above; + // and even that should probably not fail since enum constants are implicitly public + throw new AssertionError(e); + } + } + + @Override + public T read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + String key = in.nextString(); + T constant = nameToConstant.get(key); + // Note: If none of the approaches find the constant, this returns null + return (constant == null) ? stringToConstant.get(key) : constant; + } + + @Override + public void write(JsonWriter out, T value) throws IOException { + out.value(value == null ? null : constantToName.get(value)); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonElementTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/JsonElementTypeAdapter.java new file mode 100644 index 0000000000..8212c507c7 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonElementTypeAdapter.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2024 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.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.LazilyParsedNumber; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; + +/** Adapter for {@link JsonElement} and subclasses. */ +public class JsonElementTypeAdapter extends TypeAdapter { + public static final JsonElementTypeAdapter ADAPTER = new JsonElementTypeAdapter(); + + public static final TypeAdapterFactory FACTORY = + TypeAdapters.newTypeHierarchyFactory(JsonElement.class, ADAPTER); + + private JsonElementTypeAdapter() {} + + /** + * Tries to begin reading a JSON array or JSON object, returning {@code null} if the next element + * is neither of those. + */ + private JsonElement tryBeginNesting(JsonReader in, JsonToken peeked) throws IOException { + switch (peeked) { + case BEGIN_ARRAY: + in.beginArray(); + return new JsonArray(); + case BEGIN_OBJECT: + in.beginObject(); + return new JsonObject(); + default: + return null; + } + } + + /** Reads a {@link JsonElement} which cannot have any nested elements */ + private JsonElement readTerminal(JsonReader in, JsonToken peeked) throws IOException { + switch (peeked) { + case STRING: + return new JsonPrimitive(in.nextString()); + case NUMBER: + String number = in.nextString(); + return new JsonPrimitive(new LazilyParsedNumber(number)); + case BOOLEAN: + return new JsonPrimitive(in.nextBoolean()); + case NULL: + in.nextNull(); + return JsonNull.INSTANCE; + default: + // When read(JsonReader) is called with JsonReader in invalid state + throw new IllegalStateException("Unexpected token: " + peeked); + } + } + + @Override + public JsonElement read(JsonReader in) throws IOException { + // Optimization if value already exists as JsonElement + if (in instanceof JsonTreeReader) { + return ((JsonTreeReader) in).nextJsonElement(); + } + + // Either JsonArray or JsonObject + JsonElement current; + JsonToken peeked = in.peek(); + + current = tryBeginNesting(in, peeked); + if (current == null) { + return readTerminal(in, peeked); + } + + Deque stack = new ArrayDeque<>(); + + while (true) { + while (in.hasNext()) { + String name = null; + // Name is only used for JSON object members + if (current instanceof JsonObject) { + name = in.nextName(); + } + + peeked = in.peek(); + JsonElement value = tryBeginNesting(in, peeked); + boolean isNesting = value != null; + + if (value == null) { + value = readTerminal(in, peeked); + } + + if (current instanceof JsonArray) { + ((JsonArray) current).add(value); + } else { + ((JsonObject) current).add(name, value); + } + + if (isNesting) { + stack.addLast(current); + current = value; + } + } + + // End current element + if (current instanceof JsonArray) { + in.endArray(); + } else { + in.endObject(); + } + + if (stack.isEmpty()) { + return current; + } else { + // Continue with enclosing element + current = stack.removeLast(); + } + } + } + + @Override + public void write(JsonWriter out, JsonElement value) throws IOException { + if (value == null || value.isJsonNull()) { + out.nullValue(); + } else if (value.isJsonPrimitive()) { + JsonPrimitive primitive = value.getAsJsonPrimitive(); + if (primitive.isNumber()) { + out.value(primitive.getAsNumber()); + } else if (primitive.isBoolean()) { + out.value(primitive.getAsBoolean()); + } else { + out.value(primitive.getAsString()); + } + + } else if (value.isJsonArray()) { + out.beginArray(); + for (JsonElement e : value.getAsJsonArray()) { + write(out, e); + } + out.endArray(); + + } else if (value.isJsonObject()) { + out.beginObject(); + for (Map.Entry e : value.getAsJsonObject().entrySet()) { + out.name(e.getKey()); + write(out, e.getValue()); + } + out.endObject(); + + } else { + throw new IllegalArgumentException("Couldn't write " + value.getClass()); + } + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java index 75177a2134..d3de3805c6 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java @@ -17,16 +17,11 @@ package com.google.gson.internal.bind; import com.google.gson.Gson; -import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonIOException; -import com.google.gson.JsonNull; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; -import com.google.gson.annotations.SerializedName; import com.google.gson.internal.LazilyParsedNumber; import com.google.gson.internal.NumberLimits; import com.google.gson.internal.TroubleshootingGuide; @@ -35,32 +30,29 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; -import java.lang.reflect.AccessibleObject; -import java.lang.reflect.Field; import java.math.BigDecimal; import java.math.BigInteger; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.BitSet; import java.util.Calendar; import java.util.Currency; -import java.util.Deque; import java.util.GregorianCalendar; -import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.StringTokenizer; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicIntegerArray; -/** Type adapters for basic types. */ +/** + * Type adapters for basic types. More complex adapters exist as separate classes in the enclosing + * package. + */ public final class TypeAdapters { private TypeAdapters() { throw new UnsupportedOperationException(); @@ -814,218 +806,40 @@ public void write(JsonWriter out, Locale value) throws IOException { public static final TypeAdapterFactory LOCALE_FACTORY = newFactory(Locale.class, LOCALE); - public static final TypeAdapter JSON_ELEMENT = - new TypeAdapter() { - /** - * Tries to begin reading a JSON array or JSON object, returning {@code null} if the next - * element is neither of those. - */ - private JsonElement tryBeginNesting(JsonReader in, JsonToken peeked) throws IOException { - switch (peeked) { - case BEGIN_ARRAY: - in.beginArray(); - return new JsonArray(); - case BEGIN_OBJECT: - in.beginObject(); - return new JsonObject(); - default: - return null; - } - } - - /** Reads a {@link JsonElement} which cannot have any nested elements */ - private JsonElement readTerminal(JsonReader in, JsonToken peeked) throws IOException { - switch (peeked) { - case STRING: - return new JsonPrimitive(in.nextString()); - case NUMBER: - String number = in.nextString(); - return new JsonPrimitive(new LazilyParsedNumber(number)); - case BOOLEAN: - return new JsonPrimitive(in.nextBoolean()); - case NULL: - in.nextNull(); - return JsonNull.INSTANCE; - default: - // When read(JsonReader) is called with JsonReader in invalid state - throw new IllegalStateException("Unexpected token: " + peeked); - } - } - - @Override - public JsonElement read(JsonReader in) throws IOException { - if (in instanceof JsonTreeReader) { - return ((JsonTreeReader) in).nextJsonElement(); - } - - // Either JsonArray or JsonObject - JsonElement current; - JsonToken peeked = in.peek(); - - current = tryBeginNesting(in, peeked); - if (current == null) { - return readTerminal(in, peeked); - } - - Deque stack = new ArrayDeque<>(); - - while (true) { - while (in.hasNext()) { - String name = null; - // Name is only used for JSON object members - if (current instanceof JsonObject) { - name = in.nextName(); - } - - peeked = in.peek(); - JsonElement value = tryBeginNesting(in, peeked); - boolean isNesting = value != null; - - if (value == null) { - value = readTerminal(in, peeked); - } - - if (current instanceof JsonArray) { - ((JsonArray) current).add(value); - } else { - ((JsonObject) current).add(name, value); - } - - if (isNesting) { - stack.addLast(current); - current = value; - } - } - - // End current element - if (current instanceof JsonArray) { - in.endArray(); - } else { - in.endObject(); - } - - if (stack.isEmpty()) { - return current; - } else { - // Continue with enclosing element - current = stack.removeLast(); - } - } - } - - @Override - public void write(JsonWriter out, JsonElement value) throws IOException { - if (value == null || value.isJsonNull()) { - out.nullValue(); - } else if (value.isJsonPrimitive()) { - JsonPrimitive primitive = value.getAsJsonPrimitive(); - if (primitive.isNumber()) { - out.value(primitive.getAsNumber()); - } else if (primitive.isBoolean()) { - out.value(primitive.getAsBoolean()); - } else { - out.value(primitive.getAsString()); - } - - } else if (value.isJsonArray()) { - out.beginArray(); - for (JsonElement e : value.getAsJsonArray()) { - write(out, e); - } - out.endArray(); - - } else if (value.isJsonObject()) { - out.beginObject(); - for (Map.Entry e : value.getAsJsonObject().entrySet()) { - out.name(e.getKey()); - write(out, e.getValue()); - } - out.endObject(); - - } else { - throw new IllegalArgumentException("Couldn't write " + value.getClass()); - } - } - }; - - public static final TypeAdapterFactory JSON_ELEMENT_FACTORY = - newTypeHierarchyFactory(JsonElement.class, JSON_ELEMENT); - - private static final class EnumTypeAdapter> extends TypeAdapter { - private final Map nameToConstant = new HashMap<>(); - private final Map stringToConstant = new HashMap<>(); - private final Map constantToName = new HashMap<>(); - - public EnumTypeAdapter(final Class classOfT) { - try { - // Uses reflection to find enum constants to work around name mismatches for obfuscated - // classes - Field[] fields = classOfT.getDeclaredFields(); - ArrayList constantFieldsList = new ArrayList<>(fields.length); - for (Field f : fields) { - if (f.isEnumConstant()) { - constantFieldsList.add(f); - } - } - - Field[] constantFields = constantFieldsList.toArray(new Field[0]); - AccessibleObject.setAccessible(constantFields, true); - - for (Field constantField : constantFields) { - @SuppressWarnings("unchecked") - T constant = (T) constantField.get(null); - String name = constant.name(); - String toStringVal = constant.toString(); + /* + * The following adapter and factory fields have not been removed yet and are only deprecated + * for now because external projects might be using them, despite being part of Gson's internal + * implementation. + */ - SerializedName annotation = constantField.getAnnotation(SerializedName.class); - if (annotation != null) { - name = annotation.value(); - for (String alternate : annotation.alternate()) { - nameToConstant.put(alternate, constant); - } - } - nameToConstant.put(name, constant); - stringToConstant.put(toStringVal, constant); - constantToName.put(constant, name); - } - } catch (IllegalAccessException e) { - throw new AssertionError(e); - } - } + /** + * @deprecated {@code TypeAdapters} is an internal Gson class. To obtain the adapter for {@link + * JsonElement} and subclasses use instead: + *
{@code
+   * TypeAdapter adapter = gson.getAdapter(JsonElement.class);
+   * }
+ */ + @Deprecated + public static final TypeAdapter JSON_ELEMENT = JsonElementTypeAdapter.ADAPTER; - @Override - public T read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - String key = in.nextString(); - T constant = nameToConstant.get(key); - return (constant == null) ? stringToConstant.get(key) : constant; - } - - @Override - public void write(JsonWriter out, T value) throws IOException { - out.value(value == null ? null : constantToName.get(value)); - } - } + /** + * @deprecated {@code TypeAdapters} is an internal Gson class. To obtain the adapter for {@link + * JsonElement} and subclasses use instead: + *
{@code
+   * TypeAdapter adapter = gson.getAdapter(JsonElement.class);
+   * }
+ */ + @Deprecated + public static final TypeAdapterFactory JSON_ELEMENT_FACTORY = JsonElementTypeAdapter.FACTORY; - public static final TypeAdapterFactory ENUM_FACTORY = - new TypeAdapterFactory() { - @Override - public TypeAdapter create(Gson gson, TypeToken typeToken) { - Class rawType = typeToken.getRawType(); - if (!Enum.class.isAssignableFrom(rawType) || rawType == Enum.class) { - return null; - } - if (!rawType.isEnum()) { - rawType = rawType.getSuperclass(); // handle anonymous subclasses - } - @SuppressWarnings({"rawtypes", "unchecked"}) - TypeAdapter adapter = (TypeAdapter) new EnumTypeAdapter(rawType); - return adapter; - } - }; + /** + * @deprecated {@code TypeAdapters} is an internal Gson class. To obtain the adapter for a + * specific enum class use instead: + *
{@code
+   * TypeAdapter adapter = gson.getAdapter(MyEnum.class);
+   * }
+ */ + @Deprecated public static final TypeAdapterFactory ENUM_FACTORY = EnumTypeAdapter.FACTORY; @SuppressWarnings("TypeParameterNaming") public static TypeAdapterFactory newFactory(