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;
027
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
053   *                        v
054   * [a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t]        < input
055   *                       [l|m|n|o|p|q|r|s|t|?|?]    < buffer
056   *                          ^               ^
057   *                       |  index           fill
058   */
059
060  /**
061   * Creates a new JsonParser with the given handler. The parser will report all parser events to
062   * this handler.
063   *
064   * @param handler
065   *          the handler to process parser events
066   */
067  @SuppressWarnings("unchecked")
068  public JsonParser(JsonHandler<?, ?> handler) {
069    if (handler == null) {
070      throw new NullPointerException("handler is null");
071    }
072    this.handler = (JsonHandler<Object, Object>)handler;
073    handler.parser = this;
074  }
075
076  /**
077   * Parses the given input string. The input must contain a valid JSON value, optionally padded
078   * with whitespace.
079   *
080   * @param string
081   *          the input string, must be valid JSON
082   * @throws ParseException
083   *           if the input is not valid JSON
084   */
085  public void parse(String string) {
086    if (string == null) {
087      throw new NullPointerException("string is null");
088    }
089    int bufferSize = Math.max(MIN_BUFFER_SIZE, Math.min(DEFAULT_BUFFER_SIZE, string.length()));
090    try {
091      parse(new StringReader(string), bufferSize);
092    } catch (IOException exception) {
093      // StringReader does not throw IOException
094      throw new RuntimeException(exception);
095    }
096  }
097
098  /**
099   * Reads the entire input from the given reader and parses it as JSON. The input must contain a
100   * valid JSON value, optionally padded with whitespace.
101   * <p>
102   * Characters are read in chunks into a default-sized input buffer. Hence, wrapping a reader in an
103   * additional <code>BufferedReader</code> likely won't improve reading performance.
104   * </p>
105   *
106   * @param reader
107   *          the reader to read the input from
108   * @throws IOException
109   *           if an I/O error occurs in the reader
110   * @throws ParseException
111   *           if the input is not valid JSON
112   */
113  public void parse(Reader reader) throws IOException {
114    parse(reader, DEFAULT_BUFFER_SIZE);
115  }
116
117  /**
118   * Reads the entire input from the given reader and parses it as JSON. The input must contain a
119   * valid JSON value, optionally padded with whitespace.
120   * <p>
121   * Characters are read in chunks into an input buffer of the given size. Hence, wrapping a reader
122   * in an additional <code>BufferedReader</code> likely won't improve reading performance.
123   * </p>
124   *
125   * @param reader
126   *          the reader to read the input from
127   * @param buffersize
128   *          the size of the input buffer in chars
129   * @throws IOException
130   *           if an I/O error occurs in the reader
131   * @throws ParseException
132   *           if the input is not valid JSON
133   */
134  public void parse(Reader reader, int buffersize) throws IOException {
135    if (reader == null) {
136      throw new NullPointerException("reader is null");
137    }
138    if (buffersize <= 0) {
139      throw new IllegalArgumentException("buffersize is zero or negative");
140    }
141    this.reader = reader;
142    buffer = new char[buffersize];
143    bufferOffset = 0;
144    index = 0;
145    fill = 0;
146    line = 1;
147    lineOffset = 0;
148    current = 0;
149    captureStart = -1;
150    read();
151    skipWhiteSpace();
152    readValue();
153    skipWhiteSpace();
154    if (!isEndOfText()) {
155      throw error("Unexpected character");
156    }
157  }
158
159  private void readValue() throws IOException {
160    switch (current) {
161      case 'n':
162        readNull();
163        break;
164      case 't':
165        readTrue();
166        break;
167      case 'f':
168        readFalse();
169        break;
170      case '"':
171        readString();
172        break;
173      case '[':
174        readArray();
175        break;
176      case '{':
177        readObject();
178        break;
179      case '-':
180      case '0':
181      case '1':
182      case '2':
183      case '3':
184      case '4':
185      case '5':
186      case '6':
187      case '7':
188      case '8':
189      case '9':
190        readNumber();
191        break;
192      default:
193        throw expected("value");
194    }
195  }
196
197  private void readArray() throws IOException {
198    Object array = handler.startArray();
199    read();
200    if (++nestingLevel > MAX_NESTING_LEVEL) {
201      throw error("Nesting too deep");
202    }
203    skipWhiteSpace();
204    if (readChar(']')) {
205      nestingLevel--;
206      handler.endArray(array);
207      return;
208    }
209    do {
210      skipWhiteSpace();
211      handler.startArrayValue(array);
212      readValue();
213      handler.endArrayValue(array);
214      skipWhiteSpace();
215    } while (readChar(','));
216    if (!readChar(']')) {
217      throw expected("',' or ']'");
218    }
219    nestingLevel--;
220    handler.endArray(array);
221  }
222
223  private void readObject() throws IOException {
224    Object object = handler.startObject();
225    read();
226    if (++nestingLevel > MAX_NESTING_LEVEL) {
227      throw error("Nesting too deep");
228    }
229    skipWhiteSpace();
230    if (readChar('}')) {
231      nestingLevel--;
232      handler.endObject(object);
233      return;
234    }
235    do {
236      skipWhiteSpace();
237      handler.startObjectName(object);
238      String name = readName();
239      handler.endObjectName(object, name);
240      skipWhiteSpace();
241      if (!readChar(':')) {
242        throw expected("':'");
243      }
244      skipWhiteSpace();
245      handler.startObjectValue(object, name);
246      readValue();
247      handler.endObjectValue(object, name);
248      skipWhiteSpace();
249    } while (readChar(','));
250    if (!readChar('}')) {
251      throw expected("',' or '}'");
252    }
253    nestingLevel--;
254    handler.endObject(object);
255  }
256
257  private String readName() throws IOException {
258    if (current != '"') {
259      throw expected("name");
260    }
261    return readStringInternal();
262  }
263
264  private void readNull() throws IOException {
265    handler.startNull();
266    read();
267    readRequiredChar('u');
268    readRequiredChar('l');
269    readRequiredChar('l');
270    handler.endNull();
271  }
272
273  private void readTrue() throws IOException {
274    handler.startBoolean();
275    read();
276    readRequiredChar('r');
277    readRequiredChar('u');
278    readRequiredChar('e');
279    handler.endBoolean(true);
280  }
281
282  private void readFalse() throws IOException {
283    handler.startBoolean();
284    read();
285    readRequiredChar('a');
286    readRequiredChar('l');
287    readRequiredChar('s');
288    readRequiredChar('e');
289    handler.endBoolean(false);
290  }
291
292  private void readRequiredChar(char ch) throws IOException {
293    if (!readChar(ch)) {
294      throw expected("'" + ch + "'");
295    }
296  }
297
298  private void readString() throws IOException {
299    handler.startString();
300    handler.endString(readStringInternal());
301  }
302
303  private String readStringInternal() throws IOException {
304    read();
305    startCapture();
306    while (current != '"') {
307      if (current == '\\') {
308        pauseCapture();
309        readEscape();
310        startCapture();
311      } else if (current < 0x20) {
312        throw expected("valid string character");
313      } else {
314        read();
315      }
316    }
317    String string = endCapture();
318    read();
319    return string;
320  }
321
322  private void readEscape() throws IOException {
323    read();
324    switch (current) {
325    case '"':
326    case '/':
327    case '\\':
328      captureBuffer.append((char) current);
329      break;
330    case 'b':
331      captureBuffer.append('\b');
332      break;
333    case 'f':
334      captureBuffer.append('\f');
335      break;
336    case 'n':
337      captureBuffer.append('\n');
338      break;
339    case 'r':
340      captureBuffer.append('\r');
341      break;
342    case 't':
343      captureBuffer.append('\t');
344      break;
345    case 'u':
346      char[] hexChars = new char[4];
347      for (int i = 0; i < 4; i++) {
348        read();
349        if (!isHexDigit()) {
350          throw expected("hexadecimal digit");
351        }
352        hexChars[i] = (char) current;
353      }
354      captureBuffer.append((char) Integer.parseInt(new String(hexChars), 16));
355      break;
356    default:
357      throw expected("valid escape sequence");
358    }
359    read();
360  }
361
362  private void readNumber() throws IOException {
363    handler.startNumber();
364    startCapture();
365    readChar('-');
366    int firstDigit = current;
367    if (!readDigit()) {
368      throw expected("digit");
369    }
370    if (firstDigit != '0') {
371      while (readDigit()) {
372        // nothing to do here
373      }
374    }
375    readFraction();
376    readExponent();
377    handler.endNumber(endCapture());
378  }
379
380  private boolean readFraction() throws IOException {
381    if (!readChar('.')) {
382      return false;
383    }
384    if (!readDigit()) {
385      throw expected("digit");
386    }
387    while (readDigit()) {
388      // nothing to do here
389    }
390    return true;
391  }
392
393  private boolean readExponent() throws IOException {
394    if (!readChar('e') && !readChar('E')) {
395      return false;
396    }
397    if (!readChar('+')) {
398      readChar('-');
399    }
400    if (!readDigit()) {
401      throw expected("digit");
402    }
403    while (readDigit()) {
404      // nothing to do here
405    }
406    return true;
407  }
408
409  private boolean readChar(char ch) throws IOException {
410    if (current != ch) {
411      return false;
412    }
413    read();
414    return true;
415  }
416
417  private boolean readDigit() throws IOException {
418    if (!isDigit()) {
419      return false;
420    }
421    read();
422    return true;
423  }
424
425  private void skipWhiteSpace() throws IOException {
426    while (isWhiteSpace()) {
427      read();
428    }
429  }
430
431  private void read() throws IOException {
432    if (index == fill) {
433      if (captureStart != -1) {
434        captureBuffer.append(buffer, captureStart, fill - captureStart);
435        captureStart = 0;
436      }
437      bufferOffset += fill;
438      fill = reader.read(buffer, 0, buffer.length);
439      index = 0;
440      if (fill == -1) {
441        current = -1;
442        index++;
443        return;
444      }
445    }
446    if (current == '\n') {
447      line++;
448      lineOffset = bufferOffset + index;
449    }
450    current = buffer[index++];
451  }
452
453  private void startCapture() {
454    if (captureBuffer == null) {
455      captureBuffer = new StringBuilder();
456    }
457    captureStart = index - 1;
458  }
459
460  private void pauseCapture() {
461    int end = current == -1 ? index : index - 1;
462    captureBuffer.append(buffer, captureStart, end - captureStart);
463    captureStart = -1;
464  }
465
466  private String endCapture() {
467    int start = captureStart;
468    int end = index - 1;
469    captureStart = -1;
470    if (captureBuffer.length() > 0) {
471      captureBuffer.append(buffer, start, end - start);
472      String captured = captureBuffer.toString();
473      captureBuffer.setLength(0);
474      return captured;
475    }
476    return new String(buffer, start, end - start);
477  }
478
479  Location getLocation() {
480    int offset = bufferOffset + index - 1;
481    int column = offset - lineOffset + 1;
482    return new Location(offset, line, column);
483  }
484
485  private ParseException expected(String expected) {
486    if (isEndOfText()) {
487      return error("Unexpected end of input");
488    }
489    return error("Expected " + expected);
490  }
491
492  private ParseException error(String message) {
493    return new ParseException(message, getLocation());
494  }
495
496  private boolean isWhiteSpace() {
497    return current == ' ' || current == '\t' || current == '\n' || current == '\r';
498  }
499
500  private boolean isDigit() {
501    return current >= '0' && current <= '9';
502  }
503
504  private boolean isHexDigit() {
505    return current >= '0' && current <= '9' || current >= 'a' && current <= 'f' || current >= 'A' && current <= 'F';
506  }
507
508  private boolean isEndOfText() {
509    return current == -1;
510  }
511
512}