/* -*- Mode: java; tab-width: 4; indent-tabs-mode: 1; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.javascript.json;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptRuntime;
import java.util.ArrayList;
import java.util.List;
/**
* This class converts a stream of JSON tokens into a JSON value.
*
* See ECMA 15.12.
* @author Raphael Speyer
* @author Hannes Wallnoefer
*/
public class JsonParser {
private Context cx;
private Scriptable scope;
private int pos;
private int length;
private String src;
public JsonParser(Context cx, Scriptable scope) {
this.cx = cx;
this.scope = scope;
}
public synchronized Object parseValue(String json) throws ParseException {
if (json == null) {
throw new ParseException("Input string may not be null");
}
pos = 0;
length = json.length();
src = json;
Object value = readValue();
consumeWhitespace();
if (pos < length) {
throw new ParseException("Expected end of stream at char " + pos);
}
return value;
}
private Object readValue() throws ParseException {
consumeWhitespace();
while (pos < length) {
char c = src.charAt(pos++);
switch (c) {
case '{':
return readObject();
case '[':
return readArray();
case 't':
return readTrue();
case 'f':
return readFalse();
case '"':
return readString();
case 'n':
return readNull();
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '0':
case '-':
return readNumber(c);
default:
throw new ParseException("Unexpected token: " + c);
}
}
throw new ParseException("Empty JSON string");
}
private Object readObject() throws ParseException {
consumeWhitespace();
Scriptable object = cx.newObject(scope);
// handle empty object literal case early
if (pos < length && src.charAt(pos) == '}') {
pos += 1;
return object;
}
String id;
Object value;
boolean needsComma = false;
while (pos < length) {
char c = src.charAt(pos++);
switch(c) {
case '}':
if (!needsComma) {
throw new ParseException("Unexpected comma in object literal");
}
return object;
case ',':
if (!needsComma) {
throw new ParseException("Unexpected comma in object literal");
}
needsComma = false;
break;
case '"':
if (needsComma) {
throw new ParseException("Missing comma in object literal");
}
id = readString();
consume(':');
value = readValue();
long index = ScriptRuntime.indexFromString(id);
if (index < 0) {
object.put(id, object, value);
} else {
object.put((int)index, object, value);
}
needsComma = true;
break;
default:
throw new ParseException("Unexpected token in object literal");
}
consumeWhitespace();
}
throw new ParseException("Unterminated object literal");
}
private Object readArray() throws ParseException {
consumeWhitespace();
// handle empty array literal case early
if (pos < length && src.charAt(pos) == ']') {
pos += 1;
return cx.newArray(scope, 0);
}
List<Object> list = new ArrayList<Object>();
boolean needsComma = false;
while (pos < length) {
char c = src.charAt(pos);
switch(c) {
case ']':
if (!needsComma) {
throw new ParseException("Unexpected comma in array literal");
}
pos += 1;
return cx.newArray(scope, list.toArray());
case ',':
if (!needsComma) {
throw new ParseException("Unexpected comma in array literal");
}
needsComma = false;
pos += 1;
break;
default:
if (needsComma) {
throw new ParseException("Missing comma in array literal");
}
list.add(readValue());
needsComma = true;
}
consumeWhitespace();
}
throw new ParseException("Unterminated array literal");
}
private String readString() throws ParseException {
/*
* Optimization: if the source contains no escaped characters, create the
* string directly from the source text.
*/
int stringStart = pos;
while (pos < length) {
char c = src.charAt(pos++);
if (c <= '\u001F') {
throw new ParseException("String contains control character");
} else if (c == '\\') {
break;
} else if (c == '"') {
return src.substring(stringStart, pos - 1);
}
}
/*
* Slow case: string contains escaped characters. Copy a maximal sequence
* of unescaped characters into a temporary buffer, then an escaped
* character, and repeat until the entire string is consumed.
*/
StringBuilder b = new StringBuilder();
while (pos < length) {
assert src.charAt(pos - 1) == '\\';
b.append(src, stringStart, pos - 1);
if (pos >= length) {
throw new ParseException("Unterminated string");
}
char c = src.charAt(pos++);
switch (c) {
case '"':
b.append('"');
break;
case '\\':
b.append('\\');
break;
case '/':
b.append('/');
break;
case 'b':
b.append('\b');
break;
case 'f':
b.append('\f');
break;
case 'n':
b.append('\n');
break;
case 'r':
b.append('\r');
break;
case 't':
b.append('\t');
break;
case 'u':
if (length - pos < 5) {
throw new ParseException("Invalid character code: \\u" + src.substring(pos));
}
int code = fromHex(src.charAt(pos + 0)) << 12
| fromHex(src.charAt(pos + 1)) << 8
| fromHex(src.charAt(pos + 2)) << 4
| fromHex(src.charAt(pos + 3));
if (code < 0) {
throw new ParseException("Invalid character code: " + src.substring(pos, pos + 4));
}
pos += 4;
b.append((char) code);
break;
default:
throw new ParseException("Unexpected character in string: '\\" + c + "'");
}
stringStart = pos;
while (pos < length) {
c = src.charAt(pos++);
if (c <= '\u001F') {
throw new ParseException("String contains control character");
} else if (c == '\\') {
break;
} else if (c == '"') {
b.append(src, stringStart, pos - 1);
return b.toString();
}
}
}
throw new ParseException("Unterminated string literal");
}
private int fromHex(char c) {
return c >= '0' && c <= '9' ? c - '0'
: c >= 'A' && c <= 'F' ? c - 'A' + 10
: c >= 'a' && c <= 'f' ? c - 'a' + 10
: -1;
}
private Number readNumber(char c) throws ParseException {
assert c == '-' || (c >= '0' && c <= '9');
final int numberStart = pos - 1;
if (c == '-') {
c = nextOrNumberError(numberStart);
if (!(c >= '0' && c <= '9')) {
throw numberError(numberStart, pos);
}
}
if (c != '0') {
readDigits();
}
// read optional fraction part
if (pos < length) {
c = src.charAt(pos);
if (c == '.') {
pos += 1;
c = nextOrNumberError(numberStart);
if (!(c >= '0' && c <= '9')) {
throw numberError(numberStart, pos);
}
readDigits();
}
}
// read optional exponent part
if (pos < length) {
c = src.charAt(pos);
if (c == 'e' || c == 'E') {
pos += 1;
c = nextOrNumberError(numberStart);
if (c == '-' || c == '+') {
c = nextOrNumberError(numberStart);
}
if (!(c >= '0' && c <= '9')) {
throw numberError(numberStart, pos);
}
readDigits();
}
}
String num = src.substring(numberStart, pos);
final double dval = Double.parseDouble(num);
final int ival = (int)dval;
if (ival == dval) {
return Integer.valueOf(ival);
} else {
return Double.valueOf(dval);
}
}
private ParseException numberError(int start, int end) {
return new ParseException("Unsupported number format: " + src.substring(start, end));
}
private char nextOrNumberError(int numberStart) throws ParseException {
if (pos >= length) {
throw numberError(numberStart, length);
}
return src.charAt(pos++);
}
private void readDigits() {
for (; pos < length; ++pos) {
char c = src.charAt(pos);
if (!(c >= '0' && c <= '9')) {
break;
}
}
}
private Boolean readTrue() throws ParseException {
if (length - pos < 3
|| src.charAt(pos) != 'r'
|| src.charAt(pos + 1) != 'u'
|| src.charAt(pos + 2) != 'e') {
throw new ParseException("Unexpected token: t");
}
pos += 3;
return Boolean.TRUE;
}
private Boolean readFalse() throws ParseException {
if (length - pos < 4
|| src.charAt(pos) != 'a'
|| src.charAt(pos + 1) != 'l'
|| src.charAt(pos + 2) != 's'
|| src.charAt(pos + 3) != 'e') {
throw new ParseException("Unexpected token: f");
}
pos += 4;
return Boolean.FALSE;
}
private Object readNull() throws ParseException {
if (length - pos < 3
|| src.charAt(pos) != 'u'
|| src.charAt(pos + 1) != 'l'
|| src.charAt(pos + 2) != 'l') {
throw new ParseException("Unexpected token: n");
}
pos += 3;
return null;
}
private void consumeWhitespace() {
while (pos < length) {
char c = src.charAt(pos);
switch (c) {
case ' ':
case '\t':
case '\r':
case '\n':
pos += 1;
break;
default:
return;
}
}
}
private void consume(char token) throws ParseException {
consumeWhitespace();
if (pos >= length) {
throw new ParseException("Expected " + token + " but reached end of stream");
}
char c = src.charAt(pos++);
if (c == token) {
return;
} else {
throw new ParseException("Expected " + token + " found " + c);
}
}
public static class ParseException extends Exception {
static final long serialVersionUID = 4804542791749920772L;
ParseException(String message) {
super(message);
}
ParseException(Exception cause) {
super(cause);
}
}
}