Skip to content

Commit

Permalink
Merge pull request #871 from google/jw/tree-json-path
Browse files Browse the repository at this point in the history
Implement JSON Path for JsonTreeReader.
  • Loading branch information
swankjesse committed Jun 10, 2016
2 parents 5f63fce + c2fae85 commit c24af30
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 49 deletions.
138 changes: 114 additions & 24 deletions gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java
Expand Up @@ -25,9 +25,7 @@
import com.google.gson.stream.JsonToken;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
Expand All @@ -47,35 +45,57 @@ public final class JsonTreeReader extends JsonReader {
};
private static final Object SENTINEL_CLOSED = new Object();

private final List<Object> stack = new ArrayList<Object>();
/*
* The nesting stack. Using a manual array rather than an ArrayList saves 20%.
*/
private Object[] stack = new Object[32];
private int stackSize = 0;

/*
* The path members. It corresponds directly to stack: At indices where the
* stack contains an object (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT),
* pathNames contains the name at this scope. Where it contains an array
* (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index in
* that array. Otherwise the value is undefined, and we take advantage of that
* by incrementing pathIndices when doing so isn't useful.
*/
private String[] pathNames = new String[32];
private int[] pathIndices = new int[32];

public JsonTreeReader(JsonElement element) {
super(UNREADABLE_READER);
stack.add(element);
push(element);
}

@Override public void beginArray() throws IOException {
expect(JsonToken.BEGIN_ARRAY);
JsonArray array = (JsonArray) peekStack();
stack.add(array.iterator());
push(array.iterator());
pathIndices[stackSize - 1] = 0;
}

@Override public void endArray() throws IOException {
expect(JsonToken.END_ARRAY);
popStack(); // empty iterator
popStack(); // array
if (stackSize > 0) {
pathIndices[stackSize - 1]++;
}
}

@Override public void beginObject() throws IOException {
expect(JsonToken.BEGIN_OBJECT);
JsonObject object = (JsonObject) peekStack();
stack.add(object.entrySet().iterator());
push(object.entrySet().iterator());
}

@Override public void endObject() throws IOException {
expect(JsonToken.END_OBJECT);
popStack(); // empty iterator
popStack(); // object
if (stackSize > 0) {
pathIndices[stackSize - 1]++;
}
}

@Override public boolean hasNext() throws IOException {
Expand All @@ -84,19 +104,19 @@ public JsonTreeReader(JsonElement element) {
}

@Override public JsonToken peek() throws IOException {
if (stack.isEmpty()) {
if (stackSize == 0) {
return JsonToken.END_DOCUMENT;
}

Object o = peekStack();
if (o instanceof Iterator) {
boolean isObject = stack.get(stack.size() - 2) instanceof JsonObject;
boolean isObject = stack[stackSize - 2] instanceof JsonObject;
Iterator<?> iterator = (Iterator<?>) o;
if (iterator.hasNext()) {
if (isObject) {
return JsonToken.NAME;
} else {
stack.add(iterator.next());
push(iterator.next());
return peek();
}
} else {
Expand Down Expand Up @@ -127,89 +147,121 @@ public JsonTreeReader(JsonElement element) {
}

private Object peekStack() {
return stack.get(stack.size() - 1);
return stack[stackSize - 1];
}

private Object popStack() {
return stack.remove(stack.size() - 1);
Object result = stack[--stackSize];
stack[stackSize] = null;
return result;
}

private void expect(JsonToken expected) throws IOException {
if (peek() != expected) {
throw new IllegalStateException("Expected " + expected + " but was " + peek());
throw new IllegalStateException(
"Expected " + expected + " but was " + peek() + locationString());
}
}

@Override public String nextName() throws IOException {
expect(JsonToken.NAME);
Iterator<?> i = (Iterator<?>) peekStack();
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next();
stack.add(entry.getValue());
return (String) entry.getKey();
String result = (String) entry.getKey();
pathNames[stackSize - 1] = result;
push(entry.getValue());
return result;
}

@Override public String nextString() throws IOException {
JsonToken token = peek();
if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
throw new IllegalStateException("Expected " + JsonToken.STRING + " but was " + token);
throw new IllegalStateException(
"Expected " + JsonToken.STRING + " but was " + token + locationString());
}
String result = ((JsonPrimitive) popStack()).getAsString();
if (stackSize > 0) {
pathIndices[stackSize - 1]++;
}
return ((JsonPrimitive) popStack()).getAsString();
return result;
}

@Override public boolean nextBoolean() throws IOException {
expect(JsonToken.BOOLEAN);
return ((JsonPrimitive) popStack()).getAsBoolean();
boolean result = ((JsonPrimitive) popStack()).getAsBoolean();
if (stackSize > 0) {
pathIndices[stackSize - 1]++;
}
return result;
}

@Override public void nextNull() throws IOException {
expect(JsonToken.NULL);
popStack();
if (stackSize > 0) {
pathIndices[stackSize - 1]++;
}
}

@Override public double nextDouble() throws IOException {
JsonToken token = peek();
if (token != JsonToken.NUMBER && token != JsonToken.STRING) {
throw new IllegalStateException("Expected " + JsonToken.NUMBER + " but was " + token);
throw new IllegalStateException(
"Expected " + JsonToken.NUMBER + " but was " + token + locationString());
}
double result = ((JsonPrimitive) peekStack()).getAsDouble();
if (!isLenient() && (Double.isNaN(result) || Double.isInfinite(result))) {
throw new NumberFormatException("JSON forbids NaN and infinities: " + result);
}
popStack();
if (stackSize > 0) {
pathIndices[stackSize - 1]++;
}
return result;
}

@Override public long nextLong() throws IOException {
JsonToken token = peek();
if (token != JsonToken.NUMBER && token != JsonToken.STRING) {
throw new IllegalStateException("Expected " + JsonToken.NUMBER + " but was " + token);
throw new IllegalStateException(
"Expected " + JsonToken.NUMBER + " but was " + token + locationString());
}
long result = ((JsonPrimitive) peekStack()).getAsLong();
popStack();
if (stackSize > 0) {
pathIndices[stackSize - 1]++;
}
return result;
}

@Override public int nextInt() throws IOException {
JsonToken token = peek();
if (token != JsonToken.NUMBER && token != JsonToken.STRING) {
throw new IllegalStateException("Expected " + JsonToken.NUMBER + " but was " + token);
throw new IllegalStateException(
"Expected " + JsonToken.NUMBER + " but was " + token + locationString());
}
int result = ((JsonPrimitive) peekStack()).getAsInt();
popStack();
if (stackSize > 0) {
pathIndices[stackSize - 1]++;
}
return result;
}

@Override public void close() throws IOException {
stack.clear();
stack.add(SENTINEL_CLOSED);
stack = new Object[] { SENTINEL_CLOSED };
stackSize = 1;
}

@Override public void skipValue() throws IOException {
if (peek() == JsonToken.NAME) {
nextName();
pathNames[stackSize - 2] = "null";
} else {
popStack();
pathNames[stackSize - 1] = "null";
}
pathIndices[stackSize - 1]++;
}

@Override public String toString() {
Expand All @@ -220,7 +272,45 @@ public void promoteNameToValue() throws IOException {
expect(JsonToken.NAME);
Iterator<?> i = (Iterator<?>) peekStack();
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next();
stack.add(entry.getValue());
stack.add(new JsonPrimitive((String)entry.getKey()));
push(entry.getValue());
push(new JsonPrimitive((String) entry.getKey()));
}

private void push(Object newTop) {
if (stackSize == stack.length) {
Object[] newStack = new Object[stackSize * 2];
int[] newPathIndices = new int[stackSize * 2];
String[] newPathNames = new String[stackSize * 2];
System.arraycopy(stack, 0, newStack, 0, stackSize);
System.arraycopy(pathIndices, 0, newPathIndices, 0, stackSize);
System.arraycopy(pathNames, 0, newPathNames, 0, stackSize);
stack = newStack;
pathIndices = newPathIndices;
pathNames = newPathNames;
}
stack[stackSize++] = newTop;
}

@Override public String getPath() {
StringBuilder result = new StringBuilder().append('$');
for (int i = 0; i < stackSize; i++) {
if (stack[i] instanceof JsonArray) {
if (stack[++i] instanceof Iterator) {
result.append('[').append(pathIndices[i]).append(']');
}
} else if (stack[i] instanceof JsonObject) {
if (stack[++i] instanceof Iterator) {
result.append('.');
if (pathNames[i] != null) {
result.append(pathNames[i]);
}
}
}
}
return result.toString();
}

private String locationString() {
return " at path " + getPath();
}
}

0 comments on commit c24af30

Please sign in to comment.