Skip to content

Commit c2fae85

Browse files
committed
Implement JSON Path for JsonTreeReader.
1 parent ecaa571 commit c2fae85

File tree

3 files changed

+178
-49
lines changed

3 files changed

+178
-49
lines changed

gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java

Lines changed: 114 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@
2525
import com.google.gson.stream.JsonToken;
2626
import java.io.IOException;
2727
import java.io.Reader;
28-
import java.util.ArrayList;
2928
import java.util.Iterator;
30-
import java.util.List;
3129
import java.util.Map;
3230

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

50-
private final List<Object> stack = new ArrayList<Object>();
48+
/*
49+
* The nesting stack. Using a manual array rather than an ArrayList saves 20%.
50+
*/
51+
private Object[] stack = new Object[32];
52+
private int stackSize = 0;
53+
54+
/*
55+
* The path members. It corresponds directly to stack: At indices where the
56+
* stack contains an object (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT),
57+
* pathNames contains the name at this scope. Where it contains an array
58+
* (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index in
59+
* that array. Otherwise the value is undefined, and we take advantage of that
60+
* by incrementing pathIndices when doing so isn't useful.
61+
*/
62+
private String[] pathNames = new String[32];
63+
private int[] pathIndices = new int[32];
5164

5265
public JsonTreeReader(JsonElement element) {
5366
super(UNREADABLE_READER);
54-
stack.add(element);
67+
push(element);
5568
}
5669

5770
@Override public void beginArray() throws IOException {
5871
expect(JsonToken.BEGIN_ARRAY);
5972
JsonArray array = (JsonArray) peekStack();
60-
stack.add(array.iterator());
73+
push(array.iterator());
74+
pathIndices[stackSize - 1] = 0;
6175
}
6276

6377
@Override public void endArray() throws IOException {
6478
expect(JsonToken.END_ARRAY);
6579
popStack(); // empty iterator
6680
popStack(); // array
81+
if (stackSize > 0) {
82+
pathIndices[stackSize - 1]++;
83+
}
6784
}
6885

6986
@Override public void beginObject() throws IOException {
7087
expect(JsonToken.BEGIN_OBJECT);
7188
JsonObject object = (JsonObject) peekStack();
72-
stack.add(object.entrySet().iterator());
89+
push(object.entrySet().iterator());
7390
}
7491

7592
@Override public void endObject() throws IOException {
7693
expect(JsonToken.END_OBJECT);
7794
popStack(); // empty iterator
7895
popStack(); // object
96+
if (stackSize > 0) {
97+
pathIndices[stackSize - 1]++;
98+
}
7999
}
80100

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

86106
@Override public JsonToken peek() throws IOException {
87-
if (stack.isEmpty()) {
107+
if (stackSize == 0) {
88108
return JsonToken.END_DOCUMENT;
89109
}
90110

91111
Object o = peekStack();
92112
if (o instanceof Iterator) {
93-
boolean isObject = stack.get(stack.size() - 2) instanceof JsonObject;
113+
boolean isObject = stack[stackSize - 2] instanceof JsonObject;
94114
Iterator<?> iterator = (Iterator<?>) o;
95115
if (iterator.hasNext()) {
96116
if (isObject) {
97117
return JsonToken.NAME;
98118
} else {
99-
stack.add(iterator.next());
119+
push(iterator.next());
100120
return peek();
101121
}
102122
} else {
@@ -127,89 +147,121 @@ public JsonTreeReader(JsonElement element) {
127147
}
128148

129149
private Object peekStack() {
130-
return stack.get(stack.size() - 1);
150+
return stack[stackSize - 1];
131151
}
132152

133153
private Object popStack() {
134-
return stack.remove(stack.size() - 1);
154+
Object result = stack[--stackSize];
155+
stack[stackSize] = null;
156+
return result;
135157
}
136158

137159
private void expect(JsonToken expected) throws IOException {
138160
if (peek() != expected) {
139-
throw new IllegalStateException("Expected " + expected + " but was " + peek());
161+
throw new IllegalStateException(
162+
"Expected " + expected + " but was " + peek() + locationString());
140163
}
141164
}
142165

143166
@Override public String nextName() throws IOException {
144167
expect(JsonToken.NAME);
145168
Iterator<?> i = (Iterator<?>) peekStack();
146169
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next();
147-
stack.add(entry.getValue());
148-
return (String) entry.getKey();
170+
String result = (String) entry.getKey();
171+
pathNames[stackSize - 1] = result;
172+
push(entry.getValue());
173+
return result;
149174
}
150175

151176
@Override public String nextString() throws IOException {
152177
JsonToken token = peek();
153178
if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
154-
throw new IllegalStateException("Expected " + JsonToken.STRING + " but was " + token);
179+
throw new IllegalStateException(
180+
"Expected " + JsonToken.STRING + " but was " + token + locationString());
181+
}
182+
String result = ((JsonPrimitive) popStack()).getAsString();
183+
if (stackSize > 0) {
184+
pathIndices[stackSize - 1]++;
155185
}
156-
return ((JsonPrimitive) popStack()).getAsString();
186+
return result;
157187
}
158188

159189
@Override public boolean nextBoolean() throws IOException {
160190
expect(JsonToken.BOOLEAN);
161-
return ((JsonPrimitive) popStack()).getAsBoolean();
191+
boolean result = ((JsonPrimitive) popStack()).getAsBoolean();
192+
if (stackSize > 0) {
193+
pathIndices[stackSize - 1]++;
194+
}
195+
return result;
162196
}
163197

164198
@Override public void nextNull() throws IOException {
165199
expect(JsonToken.NULL);
166200
popStack();
201+
if (stackSize > 0) {
202+
pathIndices[stackSize - 1]++;
203+
}
167204
}
168205

169206
@Override public double nextDouble() throws IOException {
170207
JsonToken token = peek();
171208
if (token != JsonToken.NUMBER && token != JsonToken.STRING) {
172-
throw new IllegalStateException("Expected " + JsonToken.NUMBER + " but was " + token);
209+
throw new IllegalStateException(
210+
"Expected " + JsonToken.NUMBER + " but was " + token + locationString());
173211
}
174212
double result = ((JsonPrimitive) peekStack()).getAsDouble();
175213
if (!isLenient() && (Double.isNaN(result) || Double.isInfinite(result))) {
176214
throw new NumberFormatException("JSON forbids NaN and infinities: " + result);
177215
}
178216
popStack();
217+
if (stackSize > 0) {
218+
pathIndices[stackSize - 1]++;
219+
}
179220
return result;
180221
}
181222

182223
@Override public long nextLong() throws IOException {
183224
JsonToken token = peek();
184225
if (token != JsonToken.NUMBER && token != JsonToken.STRING) {
185-
throw new IllegalStateException("Expected " + JsonToken.NUMBER + " but was " + token);
226+
throw new IllegalStateException(
227+
"Expected " + JsonToken.NUMBER + " but was " + token + locationString());
186228
}
187229
long result = ((JsonPrimitive) peekStack()).getAsLong();
188230
popStack();
231+
if (stackSize > 0) {
232+
pathIndices[stackSize - 1]++;
233+
}
189234
return result;
190235
}
191236

192237
@Override public int nextInt() throws IOException {
193238
JsonToken token = peek();
194239
if (token != JsonToken.NUMBER && token != JsonToken.STRING) {
195-
throw new IllegalStateException("Expected " + JsonToken.NUMBER + " but was " + token);
240+
throw new IllegalStateException(
241+
"Expected " + JsonToken.NUMBER + " but was " + token + locationString());
196242
}
197243
int result = ((JsonPrimitive) peekStack()).getAsInt();
198244
popStack();
245+
if (stackSize > 0) {
246+
pathIndices[stackSize - 1]++;
247+
}
199248
return result;
200249
}
201250

202251
@Override public void close() throws IOException {
203-
stack.clear();
204-
stack.add(SENTINEL_CLOSED);
252+
stack = new Object[] { SENTINEL_CLOSED };
253+
stackSize = 1;
205254
}
206255

207256
@Override public void skipValue() throws IOException {
208257
if (peek() == JsonToken.NAME) {
209258
nextName();
259+
pathNames[stackSize - 2] = "null";
210260
} else {
211261
popStack();
262+
pathNames[stackSize - 1] = "null";
212263
}
264+
pathIndices[stackSize - 1]++;
213265
}
214266

215267
@Override public String toString() {
@@ -220,7 +272,45 @@ public void promoteNameToValue() throws IOException {
220272
expect(JsonToken.NAME);
221273
Iterator<?> i = (Iterator<?>) peekStack();
222274
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next();
223-
stack.add(entry.getValue());
224-
stack.add(new JsonPrimitive((String)entry.getKey()));
275+
push(entry.getValue());
276+
push(new JsonPrimitive((String) entry.getKey()));
277+
}
278+
279+
private void push(Object newTop) {
280+
if (stackSize == stack.length) {
281+
Object[] newStack = new Object[stackSize * 2];
282+
int[] newPathIndices = new int[stackSize * 2];
283+
String[] newPathNames = new String[stackSize * 2];
284+
System.arraycopy(stack, 0, newStack, 0, stackSize);
285+
System.arraycopy(pathIndices, 0, newPathIndices, 0, stackSize);
286+
System.arraycopy(pathNames, 0, newPathNames, 0, stackSize);
287+
stack = newStack;
288+
pathIndices = newPathIndices;
289+
pathNames = newPathNames;
290+
}
291+
stack[stackSize++] = newTop;
292+
}
293+
294+
@Override public String getPath() {
295+
StringBuilder result = new StringBuilder().append('$');
296+
for (int i = 0; i < stackSize; i++) {
297+
if (stack[i] instanceof JsonArray) {
298+
if (stack[++i] instanceof Iterator) {
299+
result.append('[').append(pathIndices[i]).append(']');
300+
}
301+
} else if (stack[i] instanceof JsonObject) {
302+
if (stack[++i] instanceof Iterator) {
303+
result.append('.');
304+
if (pathNames[i] != null) {
305+
result.append(pathNames[i]);
306+
}
307+
}
308+
}
309+
}
310+
return result.toString();
311+
}
312+
313+
private String locationString() {
314+
return " at path " + getPath();
225315
}
226316
}

0 commit comments

Comments
 (0)