1 /* 2 * Copyright (C) 2011 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.gson.internal.bind; 18 19 import com.google.gson.JsonArray; 20 import com.google.gson.JsonElement; 21 import com.google.gson.JsonNull; 22 import com.google.gson.JsonObject; 23 import com.google.gson.JsonPrimitive; 24 import com.google.gson.stream.JsonReader; 25 import com.google.gson.stream.JsonToken; 26 import com.google.gson.stream.MalformedJsonException; 27 import java.io.IOException; 28 import java.io.Reader; 29 import java.util.Arrays; 30 import java.util.Iterator; 31 import java.util.Map; 32 33 /** 34 * This reader walks the elements of a JsonElement as if it was coming from a 35 * character stream. 36 * 37 * @author Jesse Wilson 38 */ 39 public final class JsonTreeReader extends JsonReader { 40 private static final Reader UNREADABLE_READER = new Reader() { 41 @Override public int read(char[] buffer, int offset, int count) { 42 throw new AssertionError(); 43 } 44 @Override public void close() { 45 throw new AssertionError(); 46 } 47 }; 48 private static final Object SENTINEL_CLOSED = new Object(); 49 50 /* 51 * The nesting stack. Using a manual array rather than an ArrayList saves 20%. 52 */ 53 private Object[] stack = new Object[32]; 54 private int stackSize = 0; 55 56 /* 57 * The path members. It corresponds directly to stack: At indices where the 58 * stack contains an object (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT), 59 * pathNames contains the name at this scope. Where it contains an array 60 * (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index in 61 * that array. Otherwise the value is undefined, and we take advantage of that 62 * by incrementing pathIndices when doing so isn't useful. 63 */ 64 private String[] pathNames = new String[32]; 65 private int[] pathIndices = new int[32]; 66 JsonTreeReader(JsonElement element)67 public JsonTreeReader(JsonElement element) { 68 super(UNREADABLE_READER); 69 push(element); 70 } 71 beginArray()72 @Override public void beginArray() throws IOException { 73 expect(JsonToken.BEGIN_ARRAY); 74 JsonArray array = (JsonArray) peekStack(); 75 push(array.iterator()); 76 pathIndices[stackSize - 1] = 0; 77 } 78 endArray()79 @Override public void endArray() throws IOException { 80 expect(JsonToken.END_ARRAY); 81 popStack(); // empty iterator 82 popStack(); // array 83 if (stackSize > 0) { 84 pathIndices[stackSize - 1]++; 85 } 86 } 87 beginObject()88 @Override public void beginObject() throws IOException { 89 expect(JsonToken.BEGIN_OBJECT); 90 JsonObject object = (JsonObject) peekStack(); 91 push(object.entrySet().iterator()); 92 } 93 endObject()94 @Override public void endObject() throws IOException { 95 expect(JsonToken.END_OBJECT); 96 pathNames[stackSize - 1] = null; // Free the last path name so that it can be garbage collected 97 popStack(); // empty iterator 98 popStack(); // object 99 if (stackSize > 0) { 100 pathIndices[stackSize - 1]++; 101 } 102 } 103 hasNext()104 @Override public boolean hasNext() throws IOException { 105 JsonToken token = peek(); 106 return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY && token != JsonToken.END_DOCUMENT; 107 } 108 peek()109 @Override public JsonToken peek() throws IOException { 110 if (stackSize == 0) { 111 return JsonToken.END_DOCUMENT; 112 } 113 114 Object o = peekStack(); 115 if (o instanceof Iterator) { 116 boolean isObject = stack[stackSize - 2] instanceof JsonObject; 117 Iterator<?> iterator = (Iterator<?>) o; 118 if (iterator.hasNext()) { 119 if (isObject) { 120 return JsonToken.NAME; 121 } else { 122 push(iterator.next()); 123 return peek(); 124 } 125 } else { 126 return isObject ? JsonToken.END_OBJECT : JsonToken.END_ARRAY; 127 } 128 } else if (o instanceof JsonObject) { 129 return JsonToken.BEGIN_OBJECT; 130 } else if (o instanceof JsonArray) { 131 return JsonToken.BEGIN_ARRAY; 132 } else if (o instanceof JsonPrimitive) { 133 JsonPrimitive primitive = (JsonPrimitive) o; 134 if (primitive.isString()) { 135 return JsonToken.STRING; 136 } else if (primitive.isBoolean()) { 137 return JsonToken.BOOLEAN; 138 } else if (primitive.isNumber()) { 139 return JsonToken.NUMBER; 140 } else { 141 throw new AssertionError(); 142 } 143 } else if (o instanceof JsonNull) { 144 return JsonToken.NULL; 145 } else if (o == SENTINEL_CLOSED) { 146 throw new IllegalStateException("JsonReader is closed"); 147 } else { 148 throw new MalformedJsonException("Custom JsonElement subclass " + o.getClass().getName() + " is not supported"); 149 } 150 } 151 peekStack()152 private Object peekStack() { 153 return stack[stackSize - 1]; 154 } 155 popStack()156 private Object popStack() { 157 Object result = stack[--stackSize]; 158 stack[stackSize] = null; 159 return result; 160 } 161 expect(JsonToken expected)162 private void expect(JsonToken expected) throws IOException { 163 if (peek() != expected) { 164 throw new IllegalStateException( 165 "Expected " + expected + " but was " + peek() + locationString()); 166 } 167 } 168 nextName(boolean skipName)169 private String nextName(boolean skipName) throws IOException { 170 expect(JsonToken.NAME); 171 Iterator<?> i = (Iterator<?>) peekStack(); 172 Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next(); 173 String result = (String) entry.getKey(); 174 pathNames[stackSize - 1] = skipName ? "<skipped>" : result; 175 push(entry.getValue()); 176 return result; 177 } 178 nextName()179 @Override public String nextName() throws IOException { 180 return nextName(false); 181 } 182 nextString()183 @Override public String nextString() throws IOException { 184 JsonToken token = peek(); 185 if (token != JsonToken.STRING && token != JsonToken.NUMBER) { 186 throw new IllegalStateException( 187 "Expected " + JsonToken.STRING + " but was " + token + locationString()); 188 } 189 String result = ((JsonPrimitive) popStack()).getAsString(); 190 if (stackSize > 0) { 191 pathIndices[stackSize - 1]++; 192 } 193 return result; 194 } 195 nextBoolean()196 @Override public boolean nextBoolean() throws IOException { 197 expect(JsonToken.BOOLEAN); 198 boolean result = ((JsonPrimitive) popStack()).getAsBoolean(); 199 if (stackSize > 0) { 200 pathIndices[stackSize - 1]++; 201 } 202 return result; 203 } 204 nextNull()205 @Override public void nextNull() throws IOException { 206 expect(JsonToken.NULL); 207 popStack(); 208 if (stackSize > 0) { 209 pathIndices[stackSize - 1]++; 210 } 211 } 212 nextDouble()213 @Override public double nextDouble() throws IOException { 214 JsonToken token = peek(); 215 if (token != JsonToken.NUMBER && token != JsonToken.STRING) { 216 throw new IllegalStateException( 217 "Expected " + JsonToken.NUMBER + " but was " + token + locationString()); 218 } 219 double result = ((JsonPrimitive) peekStack()).getAsDouble(); 220 if (!isLenient() && (Double.isNaN(result) || Double.isInfinite(result))) { 221 throw new MalformedJsonException("JSON forbids NaN and infinities: " + result); 222 } 223 popStack(); 224 if (stackSize > 0) { 225 pathIndices[stackSize - 1]++; 226 } 227 return result; 228 } 229 nextLong()230 @Override public long nextLong() throws IOException { 231 JsonToken token = peek(); 232 if (token != JsonToken.NUMBER && token != JsonToken.STRING) { 233 throw new IllegalStateException( 234 "Expected " + JsonToken.NUMBER + " but was " + token + locationString()); 235 } 236 long result = ((JsonPrimitive) peekStack()).getAsLong(); 237 popStack(); 238 if (stackSize > 0) { 239 pathIndices[stackSize - 1]++; 240 } 241 return result; 242 } 243 nextInt()244 @Override public int nextInt() throws IOException { 245 JsonToken token = peek(); 246 if (token != JsonToken.NUMBER && token != JsonToken.STRING) { 247 throw new IllegalStateException( 248 "Expected " + JsonToken.NUMBER + " but was " + token + locationString()); 249 } 250 int result = ((JsonPrimitive) peekStack()).getAsInt(); 251 popStack(); 252 if (stackSize > 0) { 253 pathIndices[stackSize - 1]++; 254 } 255 return result; 256 } 257 nextJsonElement()258 JsonElement nextJsonElement() throws IOException { 259 final JsonToken peeked = peek(); 260 if (peeked == JsonToken.NAME 261 || peeked == JsonToken.END_ARRAY 262 || peeked == JsonToken.END_OBJECT 263 || peeked == JsonToken.END_DOCUMENT) { 264 throw new IllegalStateException("Unexpected " + peeked + " when reading a JsonElement."); 265 } 266 final JsonElement element = (JsonElement) peekStack(); 267 skipValue(); 268 return element; 269 } 270 close()271 @Override public void close() throws IOException { 272 stack = new Object[] { SENTINEL_CLOSED }; 273 stackSize = 1; 274 } 275 skipValue()276 @Override public void skipValue() throws IOException { 277 JsonToken peeked = peek(); 278 switch (peeked) { 279 case NAME: 280 @SuppressWarnings("unused") 281 String unused = nextName(true); 282 break; 283 case END_ARRAY: 284 endArray(); 285 break; 286 case END_OBJECT: 287 endObject(); 288 break; 289 case END_DOCUMENT: 290 // Do nothing 291 break; 292 default: 293 popStack(); 294 if (stackSize > 0) { 295 pathIndices[stackSize - 1]++; 296 } 297 break; 298 } 299 } 300 toString()301 @Override public String toString() { 302 return getClass().getSimpleName() + locationString(); 303 } 304 promoteNameToValue()305 public void promoteNameToValue() throws IOException { 306 expect(JsonToken.NAME); 307 Iterator<?> i = (Iterator<?>) peekStack(); 308 Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next(); 309 push(entry.getValue()); 310 push(new JsonPrimitive((String) entry.getKey())); 311 } 312 push(Object newTop)313 private void push(Object newTop) { 314 if (stackSize == stack.length) { 315 int newLength = stackSize * 2; 316 stack = Arrays.copyOf(stack, newLength); 317 pathIndices = Arrays.copyOf(pathIndices, newLength); 318 pathNames = Arrays.copyOf(pathNames, newLength); 319 } 320 stack[stackSize++] = newTop; 321 } 322 getPath(boolean usePreviousPath)323 private String getPath(boolean usePreviousPath) { 324 StringBuilder result = new StringBuilder().append('$'); 325 for (int i = 0; i < stackSize; i++) { 326 if (stack[i] instanceof JsonArray) { 327 if (++i < stackSize && stack[i] instanceof Iterator) { 328 int pathIndex = pathIndices[i]; 329 // If index is last path element it points to next array element; have to decrement 330 // `- 1` covers case where iterator for next element is on stack 331 // `- 2` covers case where peek() already pushed next element onto stack 332 if (usePreviousPath && pathIndex > 0 && (i == stackSize - 1 || i == stackSize - 2)) { 333 pathIndex--; 334 } 335 result.append('[').append(pathIndex).append(']'); 336 } 337 } else if (stack[i] instanceof JsonObject) { 338 if (++i < stackSize && stack[i] instanceof Iterator) { 339 result.append('.'); 340 if (pathNames[i] != null) { 341 result.append(pathNames[i]); 342 } 343 } 344 } 345 } 346 return result.toString(); 347 } 348 getPreviousPath()349 @Override public String getPreviousPath() { 350 return getPath(true); 351 } 352 getPath()353 @Override public String getPath() { 354 return getPath(false); 355 } 356 locationString()357 private String locationString() { 358 return " at path " + getPath(); 359 } 360 } 361