/**
* Copyright 2008 - 2015 The Loon Game Engine Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* @project loon
* @author cping
* @email:javachenpeng@yahoo.com
* @version 0.5
*/
package loon.utils.json;
import java.math.BigInteger;
import loon.utils.MathUtils;
final class JsonParser {
private int linePos = 1, rowPos, charOffset, utf8adjust;
private int tokenLinePos, tokenCharPos, tokenCharOffset;
private Object value;
private Token token;
private StringBuilder reusableBuffer = new StringBuilder();
private boolean eof;
private int index;
private String string;
private int bufferLength;
private static final char[] TRUE = { 'r', 'u', 'e' };
private static final char[] FALSE = { 'a', 'l', 's', 'e' };
private static final char[] NULL = { 'u', 'l', 'l' };
private enum Token {
EOF(false), NULL(true), TRUE(true), FALSE(true), STRING(true), NUMBER(
true), COMMA(false), COLON(false), //
OBJECT_START(true), OBJECT_END(false), ARRAY_START(true), ARRAY_END(
false);
public boolean isValue;
Token(boolean isValue) {
this.isValue = isValue;
}
}
public static final class JsonParserContext<T> {
private final Class<T> clazz;
JsonParserContext(Class<T> clazz) {
this.clazz = clazz;
}
public T from(String s) throws JsonParserException {
return new JsonParser(s).parse(clazz);
}
}
JsonParser(String s) throws JsonParserException {
if (s == null) {
throw new JsonParserException(new Exception(), "The json is null !", 0, 0, 0);
}
this.string = s;
this.bufferLength = s.length();
eof = (s.length() == 0);
}
public static JsonParserContext<JsonObject> object() {
return new JsonParserContext<JsonObject>(JsonObject.class);
}
public static JsonParserContext<JsonArray> array() {
return new JsonParserContext<JsonArray>(JsonArray.class);
}
public static JsonParserContext<Object> any() {
return new JsonParserContext<Object>(Object.class);
}
@SuppressWarnings("unchecked")
<T> T parse(Class<T> clazz) throws JsonParserException {
advanceToken();
Object parsed = currentValue();
if (advanceToken() != Token.EOF) {
throw createParseException(null, "Expected end of input, got "
+ token, true);
}
if (clazz != Object.class
&& (parsed == null || clazz != parsed.getClass())) {
throw createParseException(
null,
"JSON did not contain the correct type, expected "
+ clazz.getName() + ".", true);
}
return (T) (parsed);
}
private Object currentValue() throws JsonParserException {
if (token.isValue) {
return value;
}
throw createParseException(null, "Expected JSON value, got " + token,
true);
}
private Token advanceToken() throws JsonParserException {
int c = advanceChar();
while (isWhitespace(c)) {
c = advanceChar();
}
tokenLinePos = linePos;
tokenCharPos = index - rowPos - utf8adjust;
tokenCharOffset = charOffset + index;
switch (c) {
case -1:
return token = Token.EOF;
case '[':
JsonArray list = new JsonArray();
if (advanceToken() != Token.ARRAY_END)
while (true) {
list.add(currentValue());
if (advanceToken() == Token.ARRAY_END)
break;
if (token != Token.COMMA)
throw createParseException(null,
"Expected a comma or end of the array instead of "
+ token, true);
if (advanceToken() == Token.ARRAY_END)
throw createParseException(null,
"Trailing comma found in array", true);
}
value = list;
return token = Token.ARRAY_START;
case ']':
return token = Token.ARRAY_END;
case ',':
return token = Token.COMMA;
case ':':
return token = Token.COLON;
case '{':
JsonObject map = new JsonObject();
if (advanceToken() != Token.OBJECT_END)
while (true) {
if (token != Token.STRING) {
throw createParseException(null,
"Expected STRING, got " + token, true);
}
String key = (String) value;
if (advanceToken() != Token.COLON) {
throw createParseException(null, "Expected COLON, got "
+ token, true);
}
advanceToken();
map.put(key, currentValue());
if (advanceToken() == Token.OBJECT_END) {
break;
}
if (token != Token.COMMA) {
throw createParseException(null,
"Expected a comma or end of the object instead of "
+ token, true);
}
if (advanceToken() == Token.OBJECT_END) {
throw createParseException(null,
"Trailing object found in array", true);
}
}
value = map;
return token = Token.OBJECT_START;
case '}':
return token = Token.OBJECT_END;
case 't':
consumeKeyword((char) c, TRUE);
value = Boolean.TRUE;
return token = Token.TRUE;
case 'f':
consumeKeyword((char) c, FALSE);
value = Boolean.FALSE;
return token = Token.FALSE;
case 'n':
consumeKeyword((char) c, NULL);
value = null;
return token = Token.NULL;
case '\"':
value = consumeTokenString();
return token = Token.STRING;
case '-':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
value = consumeTokenNumber((char) c);
return token = Token.NUMBER;
case '+':
case '.':
throw createParseException(null, "Numbers may not start with '"
+ (char) c + "'", true);
default:
}
if (isAsciiLetter(c)) {
throw createHelpfulException((char) c, null, 0);
}
throw createParseException(null, "Unexpected character: " + (char) c,
true);
}
private void consumeKeyword(char first, char[] expected)
throws JsonParserException {
for (int i = 0; i < expected.length; i++) {
if (advanceChar() != expected[i]) {
throw createHelpfulException(first, expected, i);
}
}
if (isAsciiLetter(peekChar())) {
throw createHelpfulException(first, expected, expected.length);
}
}
private Number consumeTokenNumber(char c) throws JsonParserException {
int start = index - 1;
int end = index;
boolean isDouble = false;
while (isDigitCharacter(peekChar())) {
char next = (char) advanceChar();
isDouble = next == '.' || next == 'e' || next == 'E' || isDouble;
end++;
}
String number = string.substring(start, end);
try {
if (isDouble) {
if (number.charAt(0) == '0') {
if (number.charAt(1) == '.') {
if (number.length() == 2)
throw createParseException(null,
"Malformed number: " + number, true);
} else if (number.charAt(1) != 'e'
&& number.charAt(1) != 'E')
throw createParseException(null, "Malformed number: "
+ number, true);
}
if (number.charAt(0) == '-') {
if (number.charAt(1) == '0') {
if (number.charAt(2) == '.') {
if (number.length() == 3)
throw createParseException(null,
"Malformed number: " + number, true);
} else if (number.charAt(2) != 'e'
&& number.charAt(2) != 'E')
throw createParseException(null,
"Malformed number: " + number, true);
} else if (number.charAt(1) == '.') {
throw createParseException(null, "Malformed number: "
+ number, true);
}
}
return Double.parseDouble(number);
}
if (number.charAt(0) == '0') {
if (number.length() == 1) {
return 0;
}
throw createParseException(null, "Malformed number: " + number,
true);
}
if (number.length() > 1 && number.charAt(0) == '-'
&& number.charAt(1) == '0') {
if (number.length() == 2) {
return -0.0;
}
throw createParseException(null, "Malformed number: " + number,
true);
}
int length = number.charAt(0) == '-' ? number.length() - 1 : number
.length();
if (length < 10) {
return Integer.parseInt(number);
}
if (length < 19) {
return Long.parseLong(number);
}
return new BigInteger(number);
} catch (NumberFormatException e) {
throw createParseException(e, "Malformed number: " + number, true);
}
}
private String consumeTokenString() throws JsonParserException {
reusableBuffer.setLength(0);
while (true) {
char c = stringChar();
switch (c) {
case '\"':
return reusableBuffer.toString();
case '\\':
int escape = advanceChar();
switch (escape) {
case -1:
throw createParseException(null,
"EOF encountered in the middle of a string escape",
false);
case 'b':
reusableBuffer.append('\b');
break;
case 'f':
reusableBuffer.append('\f');
break;
case 'n':
reusableBuffer.append('\n');
break;
case 'r':
reusableBuffer.append('\r');
break;
case 't':
reusableBuffer.append('\t');
break;
case '"':
case '/':
case '\\':
reusableBuffer.append((char) escape);
break;
case 'u':
reusableBuffer.append((char) (stringHexChar() << 12
| stringHexChar() << 8 //
| stringHexChar() << 4 | stringHexChar()));
break;
default:
throw createParseException(null, "Invalid escape: \\"
+ (char) escape, false);
}
break;
default:
reusableBuffer.append(c);
}
}
}
private char stringChar() throws JsonParserException {
int c = advanceChar();
if (c == -1) {
throw createParseException(null,
"String was not terminated before end of input", true);
}
if (c < 32) {
throw createParseException(
null,
"Strings may not contain control characters: 0x"
+ Integer.toString(c, 16), false);
}
return (char) c;
}
private int stringHexChar() throws JsonParserException {
int c = "0123456789abcdef0123456789ABCDEF".indexOf(advanceChar()) % 16;
if (c == -1) {
throw createParseException(null,
"Expected unicode hex escape character", false);
}
return c;
}
private boolean isDigitCharacter(int c) {
return (c >= '0' && c <= '9') || c == 'e' || c == 'E' || c == '.'
|| c == '+' || c == '-';
}
private boolean isWhitespace(int c) {
return c == ' ' || c == '\n' || c == '\r' || c == '\t';
}
private boolean isAsciiLetter(int c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}
private int peekChar() {
return eof ? -1 : string.charAt(index);
}
private int advanceChar() throws JsonParserException {
if (eof) {
return -1;
}
int c = string.charAt(index);
if (c == '\n') {
linePos++;
rowPos = index + 1;
utf8adjust = 0;
}
index++;
if (index >= bufferLength) {
eof = true;
}
return c;
}
private JsonParserException createHelpfulException(char first,
char[] expected, int failurePosition) throws JsonParserException {
StringBuilder errorToken = new StringBuilder(first
+ (expected == null ? "" : new String(expected, 0,
failurePosition)));
while (isAsciiLetter(peekChar()) && errorToken.length() < 15) {
errorToken.append((char) advanceChar());
}
return createParseException(null, "Unexpected token '"
+ errorToken
+ "'"
+ (expected == null ? "" : ". Did you mean '" + first
+ new String(expected) + "'?"), true);
}
private JsonParserException createParseException(Exception e,
String message, boolean tokenPos) {
if (tokenPos)
return new JsonParserException(e, message + " on line "
+ tokenLinePos + ", char " + tokenCharPos, tokenLinePos,
tokenCharPos, tokenCharOffset);
else {
int charPos = MathUtils.max(1, index - rowPos - utf8adjust);
return new JsonParserException(e, message + " on line " + linePos
+ ", char " + charPos, linePos, charPos, index + charOffset);
}
}
}