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

Make Object and JsonElement deserialization iterative #1912

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -17,18 +17,19 @@
package com.google.gson.internal.bind;

import com.google.gson.Gson;
import com.google.gson.ToNumberStrategy;
import com.google.gson.ToNumberPolicy;
import com.google.gson.ToNumberStrategy;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.LinkedTreeMap;
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.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -70,42 +71,98 @@ public static TypeAdapterFactory getFactory(ToNumberStrategy toNumberStrategy) {
}
}

/**
* Tries to begin reading a JSON array or JSON object, returning {@code null} if
* the next element is neither of those.
*/
private Object tryBeginNesting(JsonReader in, JsonToken peeked) throws IOException {
switch (peeked) {
case BEGIN_ARRAY:
in.beginArray();
return new ArrayList<>();
case BEGIN_OBJECT:
in.beginObject();
return new LinkedTreeMap<>();
default:
return null;
}
}

/** Reads an {@code Object} which cannot have any nested elements */
private Object readTerminal(JsonReader in, JsonToken peeked) throws IOException {
switch (peeked) {
case STRING:
return in.nextString();
case NUMBER:
return toNumberStrategy.readNumber(in);
case BOOLEAN:
return in.nextBoolean();
case NULL:
in.nextNull();
return null;
default:
// When read(JsonReader) is called with JsonReader in invalid state
throw new IllegalStateException("Unexpected token: " + peeked);
}
}

@Override public Object read(JsonReader in) throws IOException {
JsonToken token = in.peek();
switch (token) {
case BEGIN_ARRAY:
List<Object> list = new ArrayList<>();
in.beginArray();
while (in.hasNext()) {
list.add(read(in));
}
in.endArray();
return list;
// Either List or Map
Object current;
JsonToken peeked = in.peek();

current = tryBeginNesting(in, peeked);
if (current == null) {
return readTerminal(in, peeked);
}

Deque<Object> stack = new ArrayDeque<>();

case BEGIN_OBJECT:
Map<String, Object> map = new LinkedTreeMap<>();
in.beginObject();
while (true) {
while (in.hasNext()) {
map.put(in.nextName(), read(in));
}
in.endObject();
return map;
String name = null;
// Name is only used for JSON object members
if (current instanceof Map) {
name = in.nextName();
}

case STRING:
return in.nextString();
peeked = in.peek();
Object value = tryBeginNesting(in, peeked);
boolean isNesting = value != null;

case NUMBER:
return toNumberStrategy.readNumber(in);
if (value == null) {
value = readTerminal(in, peeked);
}

case BOOLEAN:
return in.nextBoolean();
if (current instanceof List) {
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) current;
list.add(value);
} else {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) current;
map.put(name, value);
}

if (isNesting) {
stack.addLast(current);
current = value;
}
}

case NULL:
in.nextNull();
return null;
// End current element
if (current instanceof List) {
in.endArray();
} else {
in.endObject();
}

default:
throw new IllegalStateException();
if (stack.isEmpty()) {
return current;
} else {
// Continue with enclosing element
current = stack.removeLast();
}
}
}

Expand Down
152 changes: 104 additions & 48 deletions gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java
Expand Up @@ -16,6 +16,22 @@

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.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;
Expand All @@ -27,10 +43,12 @@
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
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;
Expand All @@ -42,23 +60,6 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;

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.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

/**
* Type adapters for basic types.
*/
Expand Down Expand Up @@ -695,44 +696,99 @@ public void write(JsonWriter out, Locale value) throws IOException {
public static final TypeAdapterFactory LOCALE_FACTORY = newFactory(Locale.class, LOCALE);

public static final TypeAdapter<JsonElement> JSON_ELEMENT = new TypeAdapter<JsonElement>() {
/**
* 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();
}

switch (in.peek()) {
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;
case BEGIN_ARRAY:
JsonArray array = new JsonArray();
in.beginArray();
// 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()) {
array.add(read(in));
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;
}
}
in.endArray();
return array;
case BEGIN_OBJECT:
JsonObject object = new JsonObject();
in.beginObject();
while (in.hasNext()) {
object.add(in.nextName(), read(in));

// 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();
}
in.endObject();
return object;
case END_DOCUMENT:
case NAME:
case END_OBJECT:
case END_ARRAY:
default:
throw new IllegalArgumentException();
}
}

Expand Down Expand Up @@ -803,7 +859,7 @@ public EnumTypeAdapter(final Class<T> classOfT) {
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();
Expand Down
@@ -0,0 +1,41 @@
package com.google.gson;

import static org.junit.Assert.assertEquals;

import java.io.IOException;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class JsonParserParameterizedTest {
@Parameters
public static Iterable<String> data() {
return Arrays.asList(
"[]",
"{}",
"null",
"1.0",
"true",
"\"string\"",
"[true,1.0,null,{},2.0,{\"a\":[false]},[3.0,\"test\"],4.0]",
"{\"\":1.0,\"a\":true,\"b\":null,\"c\":[],\"d\":{\"a1\":2.0,\"b2\":[true,{\"a3\":3.0}]},\"e\":[{\"f\":4.0},\"test\"]}"
);
}

private final TypeAdapter<JsonElement> adapter = new Gson().getAdapter(JsonElement.class);
@Parameter
public String json;

@Test
public void testParse() throws IOException {
JsonElement deserialized = JsonParser.parseString(json);
String actualSerialized = adapter.toJson(deserialized);

// Serialized JsonElement should be the same as original JSON
assertEquals(json, actualSerialized);
}
}