001/******************************************************************************* 002 * Copyright (c) 2013, 2016 EclipseSource. 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining a copy 005 * of this software and associated documentation files (the "Software"), to deal 006 * in the Software without restriction, including without limitation the rights 007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 008 * copies of the Software, and to permit persons to whom the Software is 009 * furnished to do so, subject to the following conditions: 010 * 011 * The above copyright notice and this permission notice shall be included in all 012 * copies or substantial portions of the Software. 013 * 014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 020 * SOFTWARE. 021 ******************************************************************************/ 022package com.restfb.json; 023 024import java.io.IOException; 025import java.io.Reader; 026import java.io.StringReader; 027import java.util.Objects; 028 029/** 030 * A streaming parser for JSON text. The parser reports all events to a given handler. 031 */ 032public class JsonParser { 033 034 private static final int MAX_NESTING_LEVEL = 1000; 035 private static final int MIN_BUFFER_SIZE = 10; 036 private static final int DEFAULT_BUFFER_SIZE = 1024; 037 038 private final JsonHandler<Object, Object> handler; 039 private Reader reader; 040 private char[] buffer; 041 private int bufferOffset; 042 private int index; 043 private int fill; 044 private int line; 045 private int lineOffset; 046 private int current; 047 private StringBuilder captureBuffer; 048 private int captureStart; 049 private int nestingLevel; 050 051 /* 052 * | bufferOffset v [a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t] < input [l|m|n|o|p|q|r|s|t|?|?] < buffer ^ ^ | index 053 * fill 054 */ 055 056 /** 057 * Creates a new JsonParser with the given handler. The parser will report all parser events to this handler. 058 * 059 * @param handler 060 * the handler to process parser events 061 */ 062 @SuppressWarnings("unchecked") 063 public JsonParser(JsonHandler<?, ?> handler) { 064 Objects.requireNonNull(handler, "handler is null"); 065 this.handler = (JsonHandler<Object, Object>) handler; 066 handler.parser = this; 067 } 068 069 /** 070 * Parses the given input string. The input must contain a valid JSON value, optionally padded with whitespace. 071 * 072 * @param string 073 * the input string, must be valid JSON 074 * @throws ParseException 075 * if the input is not valid JSON 076 */ 077 public void parse(String string) { 078 Objects.requireNonNull(string, "string is null"); 079 int bufferSize = Math.max(MIN_BUFFER_SIZE, Math.min(DEFAULT_BUFFER_SIZE, string.length())); 080 try { 081 parse(new StringReader(string), bufferSize); 082 } catch (IOException exception) { 083 // StringReader does not throw IOException 084 throw new RuntimeException(exception); 085 } 086 } 087 088 /** 089 * Reads the entire input from the given reader and parses it as JSON. The input must contain a valid JSON value, 090 * optionally padded with whitespace. 091 * <p> 092 * Characters are read in chunks into a default-sized input buffer. Hence, wrapping a reader in an additional 093 * <code>BufferedReader</code> likely won't improve reading performance. 094 * </p> 095 * 096 * @param reader 097 * the reader to read the input from 098 * @throws IOException 099 * if an I/O error occurs in the reader 100 * @throws ParseException 101 * if the input is not valid JSON 102 */ 103 public void parse(Reader reader) throws IOException { 104 parse(reader, DEFAULT_BUFFER_SIZE); 105 } 106 107 /** 108 * Reads the entire input from the given reader and parses it as JSON. The input must contain a valid JSON value, 109 * optionally padded with whitespace. 110 * <p> 111 * Characters are read in chunks into an input buffer of the given size. Hence, wrapping a reader in an additional 112 * <code>BufferedReader</code> likely won't improve reading performance. 113 * </p> 114 * 115 * @param reader 116 * the reader to read the input from 117 * @param buffersize 118 * the size of the input buffer in chars 119 * @throws IOException 120 * if an I/O error occurs in the reader 121 * @throws ParseException 122 * if the input is not valid JSON 123 */ 124 public void parse(Reader reader, int buffersize) throws IOException { 125 Objects.requireNonNull(reader, "reader is null"); 126 if (buffersize <= 0) { 127 throw new IllegalArgumentException("buffersize is zero or negative"); 128 } 129 this.reader = reader; 130 buffer = new char[buffersize]; 131 bufferOffset = 0; 132 index = 0; 133 fill = 0; 134 line = 1; 135 lineOffset = 0; 136 current = 0; 137 captureStart = -1; 138 read(); 139 skipWhiteSpace(); 140 readValue(); 141 skipWhiteSpace(); 142 if (!isEndOfText()) { 143 throw error("Unexpected character"); 144 } 145 } 146 147 private void readValue() throws IOException { 148 switch (current) { 149 case 'n': 150 readNull(); 151 break; 152 case 't': 153 readTrue(); 154 break; 155 case 'f': 156 readFalse(); 157 break; 158 case '"': 159 readString(); 160 break; 161 case '[': 162 readArray(); 163 break; 164 case '{': 165 readObject(); 166 break; 167 case '-': 168 case '0': 169 case '1': 170 case '2': 171 case '3': 172 case '4': 173 case '5': 174 case '6': 175 case '7': 176 case '8': 177 case '9': 178 readNumber(); 179 break; 180 default: 181 throw expected("value"); 182 } 183 } 184 185 private void readArray() throws IOException { 186 Object array = handler.startArray(); 187 read(); 188 if (++nestingLevel > MAX_NESTING_LEVEL) { 189 throw error("Nesting too deep"); 190 } 191 skipWhiteSpace(); 192 if (readChar(']')) { 193 nestingLevel--; 194 handler.endArray(array); 195 return; 196 } 197 do { 198 skipWhiteSpace(); 199 handler.startArrayValue(array); 200 readValue(); 201 handler.endArrayValue(array); 202 skipWhiteSpace(); 203 } while (readChar(',')); 204 if (!readChar(']')) { 205 throw expected("',' or ']'"); 206 } 207 nestingLevel--; 208 handler.endArray(array); 209 } 210 211 private void readObject() throws IOException { 212 Object object = handler.startObject(); 213 read(); 214 if (++nestingLevel > MAX_NESTING_LEVEL) { 215 throw error("Nesting too deep"); 216 } 217 skipWhiteSpace(); 218 if (readChar('}')) { 219 nestingLevel--; 220 handler.endObject(object); 221 return; 222 } 223 do { 224 skipWhiteSpace(); 225 handler.startObjectName(object); 226 String name = readName(); 227 handler.endObjectName(object, name); 228 skipWhiteSpace(); 229 if (!readChar(':')) { 230 throw expected("':'"); 231 } 232 skipWhiteSpace(); 233 handler.startObjectValue(object, name); 234 readValue(); 235 handler.endObjectValue(object, name); 236 skipWhiteSpace(); 237 } while (readChar(',')); 238 if (!readChar('}')) { 239 throw expected("',' or '}'"); 240 } 241 nestingLevel--; 242 handler.endObject(object); 243 } 244 245 private String readName() throws IOException { 246 if (current != '"') { 247 throw expected("name"); 248 } 249 return readStringInternal(); 250 } 251 252 private void readNull() throws IOException { 253 handler.startNull(); 254 read(); 255 readRequiredChar('u'); 256 readRequiredChar('l'); 257 readRequiredChar('l'); 258 handler.endNull(); 259 } 260 261 private void readTrue() throws IOException { 262 handler.startBoolean(); 263 read(); 264 readRequiredChar('r'); 265 readRequiredChar('u'); 266 readRequiredChar('e'); 267 handler.endBoolean(true); 268 } 269 270 private void readFalse() throws IOException { 271 handler.startBoolean(); 272 read(); 273 readRequiredChar('a'); 274 readRequiredChar('l'); 275 readRequiredChar('s'); 276 readRequiredChar('e'); 277 handler.endBoolean(false); 278 } 279 280 private void readRequiredChar(char ch) throws IOException { 281 if (!readChar(ch)) { 282 throw expected("'" + ch + "'"); 283 } 284 } 285 286 private void readString() throws IOException { 287 handler.startString(); 288 handler.endString(readStringInternal()); 289 } 290 291 private String readStringInternal() throws IOException { 292 read(); 293 startCapture(); 294 while (current != '"') { 295 if (current == '\\') { 296 pauseCapture(); 297 readEscape(); 298 startCapture(); 299 } else if (current < 0x20) { 300 throw expected("valid string character"); 301 } else { 302 read(); 303 } 304 } 305 String string = endCapture(); 306 read(); 307 return string; 308 } 309 310 private void readEscape() throws IOException { 311 read(); 312 switch (current) { 313 case '"': 314 case '/': 315 case '\\': 316 captureBuffer.append((char) current); 317 break; 318 case 'b': 319 captureBuffer.append('\b'); 320 break; 321 case 'f': 322 captureBuffer.append('\f'); 323 break; 324 case 'n': 325 captureBuffer.append('\n'); 326 break; 327 case 'r': 328 captureBuffer.append('\r'); 329 break; 330 case 't': 331 captureBuffer.append('\t'); 332 break; 333 case 'u': 334 char[] hexChars = new char[4]; 335 for (int i = 0; i < 4; i++) { 336 read(); 337 if (!isHexDigit()) { 338 throw expected("hexadecimal digit"); 339 } 340 hexChars[i] = (char) current; 341 } 342 captureBuffer.append((char) Integer.parseInt(new String(hexChars), 16)); 343 break; 344 default: 345 throw expected("valid escape sequence"); 346 } 347 read(); 348 } 349 350 private void readNumber() throws IOException { 351 handler.startNumber(); 352 startCapture(); 353 readChar('-'); 354 int firstDigit = current; 355 if (!readDigit()) { 356 throw expected("digit"); 357 } 358 if (firstDigit != '0') { 359 while (readDigit()) { 360 // nothing to do here 361 } 362 } 363 readFraction(); 364 readExponent(); 365 handler.endNumber(endCapture()); 366 } 367 368 private boolean readFraction() throws IOException { 369 if (!readChar('.')) { 370 return false; 371 } 372 if (!readDigit()) { 373 throw expected("digit"); 374 } 375 while (readDigit()) { 376 // nothing to do here 377 } 378 return true; 379 } 380 381 private boolean readExponent() throws IOException { 382 if (!readChar('e') && !readChar('E')) { 383 return false; 384 } 385 if (!readChar('+')) { 386 readChar('-'); 387 } 388 if (!readDigit()) { 389 throw expected("digit"); 390 } 391 while (readDigit()) { 392 // nothing to do here 393 } 394 return true; 395 } 396 397 private boolean readChar(char ch) throws IOException { 398 if (current != ch) { 399 return false; 400 } 401 read(); 402 return true; 403 } 404 405 private boolean readDigit() throws IOException { 406 if (!isDigit()) { 407 return false; 408 } 409 read(); 410 return true; 411 } 412 413 private void skipWhiteSpace() throws IOException { 414 while (isWhiteSpace()) { 415 read(); 416 } 417 } 418 419 private void read() throws IOException { 420 if (index == fill) { 421 if (captureStart != -1) { 422 captureBuffer.append(buffer, captureStart, fill - captureStart); 423 captureStart = 0; 424 } 425 bufferOffset += fill; 426 fill = reader.read(buffer, 0, buffer.length); 427 index = 0; 428 if (fill == -1) { 429 current = -1; 430 index++; 431 return; 432 } 433 } 434 if (current == '\n') { 435 line++; 436 lineOffset = bufferOffset + index; 437 } 438 current = buffer[index++]; 439 } 440 441 private void startCapture() { 442 if (captureBuffer == null) { 443 captureBuffer = new StringBuilder(); 444 } 445 captureStart = index - 1; 446 } 447 448 private void pauseCapture() { 449 int end = current == -1 ? index : index - 1; 450 captureBuffer.append(buffer, captureStart, end - captureStart); 451 captureStart = -1; 452 } 453 454 private String endCapture() { 455 int start = captureStart; 456 int end = index - 1; 457 captureStart = -1; 458 if (captureBuffer.length() > 0) { 459 captureBuffer.append(buffer, start, end - start); 460 String captured = captureBuffer.toString(); 461 captureBuffer.setLength(0); 462 return captured; 463 } 464 return new String(buffer, start, end - start); 465 } 466 467 Location getLocation() { 468 int offset = bufferOffset + index - 1; 469 int column = offset - lineOffset + 1; 470 return new Location(offset, line, column); 471 } 472 473 private ParseException expected(String expected) { 474 if (isEndOfText()) { 475 return error("Unexpected end of input"); 476 } 477 return error("Expected " + expected); 478 } 479 480 private ParseException error(String message) { 481 return new ParseException(message, getLocation()); 482 } 483 484 private boolean isWhiteSpace() { 485 return current == ' ' || current == '\t' || current == '\n' || current == '\r'; 486 } 487 488 private boolean isDigit() { 489 return current >= '0' && current <= '9'; 490 } 491 492 private boolean isHexDigit() { 493 return current >= '0' && current <= '9' || current >= 'a' && current <= 'f' || current >= 'A' && current <= 'F'; 494 } 495 496 private boolean isEndOfText() { 497 return current == -1; 498 } 499 500}