Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions gson/src/main/java/com/google/gson/Gson.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -323,7 +325,7 @@ public Gson() {
List<TypeAdapterFactory> 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
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions gson/src/main/java/com/google/gson/internal/Streams.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<E> extends TypeAdapter<Object> {
public static final TypeAdapterFactory FACTORY =
new TypeAdapterFactory() {
Expand Down
112 changes: 112 additions & 0 deletions gson/src/main/java/com/google/gson/internal/bind/EnumTypeAdapter.java
Original file line number Diff line number Diff line change
@@ -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<T extends Enum<T>> extends TypeAdapter<T> {
public static final TypeAdapterFactory FACTORY =
new TypeAdapterFactory() {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
Class<? super T> 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<T> adapter = (TypeAdapter<T>) new EnumTypeAdapter(rawType);
return adapter;
}
};

private final Map<String, T> nameToConstant = new HashMap<>();
private final Map<String, T> stringToConstant = new HashMap<>();
private final Map<T, String> constantToName = new HashMap<>();

private EnumTypeAdapter(final Class<T> classOfT) {
try {
// Uses reflection to find enum constants to work around name mismatches for obfuscated
// classes
Field[] fields = classOfT.getDeclaredFields();
ArrayList<Field> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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<JsonElement> {
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<JsonElement> 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<String, JsonElement> e : value.getAsJsonObject().entrySet()) {
out.name(e.getKey());
write(out, e.getValue());
}
out.endObject();

} else {
throw new IllegalArgumentException("Couldn't write " + value.getClass());
}
}
}
Loading