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}