Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move enum and JsonElement adapter classes to separate class files #2727

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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