26fab5183a
Remove math dependency from core/coreimpl in dart2js. The existing Math.parseInt and Math.parseDouble are kept *for now*, but simply redirect to the static methods. They will be removed in time (when library code is ready for it). Review URL: https://codereview.chromium.org//10913271 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@12417 260f80e4-7a28-3924-810f-c04153c831b5
603 lines
16 KiB
Dart
603 lines
16 KiB
Dart
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
#library("dart:json");
|
|
|
|
#import('dart:math');
|
|
|
|
// JSON parsing and serialization.
|
|
|
|
/**
|
|
* Error thrown by JSON serialization if an object cannot be serialized.
|
|
*
|
|
* The [unsupportedObject] field holds that object that failed to be serialized.
|
|
*
|
|
* If an isn't directly serializable, the serializer calls the 'toJson' method
|
|
* on the object. If that call fails, the error will be stored in the [cause]
|
|
* field. If the call returns an object that isn't directly serializable,
|
|
* the [cause] will be null.
|
|
*/
|
|
class JsonUnsupportedObjectError {
|
|
// TODO: proper base class.
|
|
/** The object that could not be serialized. */
|
|
final unsupportedObject;
|
|
/** The exception thrown by object's [:toJson:] method, if any. */
|
|
final cause;
|
|
JsonUnsupportedObjectError(this.unsupportedObject) : cause = null;
|
|
JsonUnsupportedObjectError.withCause(this.unsupportedObject, this.cause);
|
|
|
|
String toString() {
|
|
if (cause != null) {
|
|
return "Calling toJson method on object failed.";
|
|
} else {
|
|
return "Object toJson method returns non-serializable value.";
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Utility class to parse JSON and serialize objects to JSON.
|
|
*/
|
|
class JSON {
|
|
/**
|
|
* Parses [json] and build the corresponding parsed JSON value.
|
|
*
|
|
* Parsed JSON values are of the types [num], [String], [bool], [Null],
|
|
* [List]s of parsed JSON values or [Map]s from [String] to parsed
|
|
* JSON values.
|
|
*
|
|
* Throws [JSONParseException] if the input is not valid JSON text.
|
|
*/
|
|
static parse(String json) {
|
|
return _JsonParser.parse(json);
|
|
}
|
|
|
|
/**
|
|
* Serializes [object] into a JSON string.
|
|
*
|
|
* Directly serializable types are [num], [String], [bool], [Null], [List]
|
|
* and [Map].
|
|
* For [List], the elements must all be serializable.
|
|
* For [Map], the keys must be [String] and the values must be serializable.
|
|
* If a value is any other type is attempted serialized, a "toJson()" method
|
|
* is invoked on the object and the result, which must be a directly
|
|
* serializable type, is serialized instead of the original value.
|
|
* If the object does not support this method, throws, or returns a
|
|
* value that is not directly serializable, a [JsonUnsupportedObjectError]
|
|
* exception is thrown. If the call throws (including the case where there
|
|
* is no nullary "toJson" method, the error is caught and stored in the
|
|
* [JsonUnsupportedObjectError]'s [:cause:] field.
|
|
*
|
|
* Objects should not change during serialization.
|
|
* If an object is serialized more than once, [stringify] is allowed to cache
|
|
* the JSON text for it. I.e., if an object changes after it is first
|
|
* serialized, the new values may or may not be reflected in the result.
|
|
*/
|
|
static String stringify(Object object) {
|
|
return _JsonStringifier.stringify(object);
|
|
}
|
|
|
|
/**
|
|
* Serializes [object] into [output] stream.
|
|
*
|
|
* Performs the same operations as [stringify] but outputs the resulting
|
|
* string to an existing [StringBuffer] instead of creating a new [String].
|
|
*
|
|
* If serialization fails by throwing, some data might have been added to
|
|
* [output], but it won't contain valid JSON text.
|
|
*/
|
|
static void printOn(Object object, StringBuffer output) {
|
|
return _JsonStringifier.printOn(object, output);
|
|
}
|
|
}
|
|
|
|
//// Implementation ///////////////////////////////////////////////////////////
|
|
|
|
// TODO(ajohnsen): Introduce when we have a common exception interface for json.
|
|
class JSONParseException {
|
|
JSONParseException(int position, String message) :
|
|
position = position,
|
|
message = 'JSONParseException: $message, at offset $position';
|
|
|
|
String toString() => message;
|
|
|
|
final String message;
|
|
final int position;
|
|
}
|
|
|
|
class _JsonParser {
|
|
static const int BACKSPACE = 8;
|
|
static const int TAB = 9;
|
|
static const int NEW_LINE = 10;
|
|
static const int FORM_FEED = 12;
|
|
static const int CARRIAGE_RETURN = 13;
|
|
static const int SPACE = 32;
|
|
static const int QUOTE = 34;
|
|
static const int PLUS = 43;
|
|
static const int COMMA = 44;
|
|
static const int MINUS = 45;
|
|
static const int DOT = 46;
|
|
static const int SLASH = 47;
|
|
static const int CHAR_0 = 48;
|
|
static const int CHAR_1 = 49;
|
|
static const int CHAR_2 = 50;
|
|
static const int CHAR_3 = 51;
|
|
static const int CHAR_4 = 52;
|
|
static const int CHAR_5 = 53;
|
|
static const int CHAR_6 = 54;
|
|
static const int CHAR_7 = 55;
|
|
static const int CHAR_8 = 56;
|
|
static const int CHAR_9 = 57;
|
|
static const int COLON = 58;
|
|
static const int CHAR_CAPITAL_E = 69;
|
|
static const int LBRACKET = 91;
|
|
static const int BACKSLASH = 92;
|
|
static const int RBRACKET = 93;
|
|
static const int CHAR_B = 98;
|
|
static const int CHAR_E = 101;
|
|
static const int CHAR_F = 102;
|
|
static const int CHAR_N = 110;
|
|
static const int CHAR_R = 114;
|
|
static const int CHAR_T = 116;
|
|
static const int CHAR_U = 117;
|
|
static const int LBRACE = 123;
|
|
static const int RBRACE = 125;
|
|
|
|
static const int STRING_LITERAL = QUOTE;
|
|
static const int NUMBER_LITERAL = MINUS;
|
|
static const int NULL_LITERAL = CHAR_N;
|
|
static const int FALSE_LITERAL = CHAR_F;
|
|
static const int TRUE_LITERAL = CHAR_T;
|
|
|
|
static const int WHITESPACE = SPACE;
|
|
|
|
static const int LAST_ASCII = RBRACE;
|
|
|
|
static const String NULL_STRING = "null";
|
|
static const String TRUE_STRING = "true";
|
|
static const String FALSE_STRING = "false";
|
|
|
|
static List<int> tokens;
|
|
|
|
final String json;
|
|
final int length;
|
|
int position = 0;
|
|
|
|
static parse(String json) {
|
|
return new _JsonParser(json).parseToplevel();
|
|
}
|
|
|
|
_JsonParser(String json)
|
|
: json = json,
|
|
length = json.length {
|
|
if (tokens !== null) return;
|
|
|
|
// Use a list as jump-table. It is faster than switch and if.
|
|
tokens = new List<int>(LAST_ASCII + 1);
|
|
tokens[TAB] = WHITESPACE;
|
|
tokens[NEW_LINE] = WHITESPACE;
|
|
tokens[CARRIAGE_RETURN] = WHITESPACE;
|
|
tokens[SPACE] = WHITESPACE;
|
|
tokens[CHAR_0] = NUMBER_LITERAL;
|
|
tokens[CHAR_1] = NUMBER_LITERAL;
|
|
tokens[CHAR_2] = NUMBER_LITERAL;
|
|
tokens[CHAR_3] = NUMBER_LITERAL;
|
|
tokens[CHAR_4] = NUMBER_LITERAL;
|
|
tokens[CHAR_5] = NUMBER_LITERAL;
|
|
tokens[CHAR_6] = NUMBER_LITERAL;
|
|
tokens[CHAR_7] = NUMBER_LITERAL;
|
|
tokens[CHAR_8] = NUMBER_LITERAL;
|
|
tokens[CHAR_9] = NUMBER_LITERAL;
|
|
tokens[MINUS] = NUMBER_LITERAL;
|
|
tokens[LBRACE] = LBRACE;
|
|
tokens[RBRACE] = RBRACE;
|
|
tokens[LBRACKET] = LBRACKET;
|
|
tokens[RBRACKET] = RBRACKET;
|
|
tokens[QUOTE] = STRING_LITERAL;
|
|
tokens[COLON] = COLON;
|
|
tokens[COMMA] = COMMA;
|
|
tokens[CHAR_N] = NULL_LITERAL;
|
|
tokens[CHAR_T] = TRUE_LITERAL;
|
|
tokens[CHAR_F] = FALSE_LITERAL;
|
|
}
|
|
|
|
parseToplevel() {
|
|
final result = parseValue();
|
|
if (token() !== null) {
|
|
error('Junk at the end of JSON input');
|
|
}
|
|
return result;
|
|
}
|
|
|
|
parseValue() {
|
|
final int token = token();
|
|
if (token === null) {
|
|
error('Nothing to parse');
|
|
}
|
|
switch (token) {
|
|
case STRING_LITERAL: return parseString();
|
|
case NUMBER_LITERAL: return parseNumber();
|
|
case NULL_LITERAL: return expectKeyword(NULL_STRING, null);
|
|
case FALSE_LITERAL: return expectKeyword(FALSE_STRING, false);
|
|
case TRUE_LITERAL: return expectKeyword(TRUE_STRING, true);
|
|
case LBRACE: return parseObject();
|
|
case LBRACKET: return parseList();
|
|
|
|
default:
|
|
error('Unexpected token');
|
|
}
|
|
}
|
|
|
|
Object expectKeyword(String word, Object value) {
|
|
for (int i = 0; i < word.length; i++) {
|
|
// Implicit end check in char().
|
|
if (char() != word.charCodeAt(i)) error("Expected keyword '$word'");
|
|
position++;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
parseObject() {
|
|
final object = {};
|
|
|
|
position++; // Eat '{'.
|
|
|
|
if (!isToken(RBRACE)) {
|
|
while (true) {
|
|
final String key = parseString();
|
|
if (!isToken(COLON)) error("Expected ':' when parsing object");
|
|
position++;
|
|
object[key] = parseValue();
|
|
|
|
if (!isToken(COMMA)) break;
|
|
position++; // Skip ','.
|
|
};
|
|
|
|
if (!isToken(RBRACE)) error("Expected '}' at end of object");
|
|
}
|
|
position++;
|
|
|
|
return object;
|
|
}
|
|
|
|
parseList() {
|
|
final list = [];
|
|
|
|
position++; // Eat '['.
|
|
|
|
if (!isToken(RBRACKET)) {
|
|
while (true) {
|
|
list.add(parseValue());
|
|
|
|
if (!isToken(COMMA)) break;
|
|
position++;
|
|
};
|
|
|
|
if (!isToken(RBRACKET)) error("Expected ']' at end of list");
|
|
}
|
|
position++;
|
|
|
|
return list;
|
|
}
|
|
|
|
String parseString() {
|
|
if (!isToken(STRING_LITERAL)) error("Expected string literal");
|
|
|
|
position++; // Eat '"'.
|
|
|
|
List<int> charCodes = new List<int>();
|
|
while (true) {
|
|
int c = char();
|
|
if (c == QUOTE) {
|
|
position++;
|
|
break;
|
|
}
|
|
if (c == BACKSLASH) {
|
|
position++;
|
|
if (position == length) {
|
|
error('\\ at the end of input');
|
|
}
|
|
|
|
switch (char()) {
|
|
case QUOTE:
|
|
c = QUOTE;
|
|
break;
|
|
case BACKSLASH:
|
|
c = BACKSLASH;
|
|
break;
|
|
case SLASH:
|
|
c = SLASH;
|
|
break;
|
|
case CHAR_B:
|
|
c = BACKSPACE;
|
|
break;
|
|
case CHAR_N:
|
|
c = NEW_LINE;
|
|
break;
|
|
case CHAR_R:
|
|
c = CARRIAGE_RETURN;
|
|
break;
|
|
case CHAR_F:
|
|
c = FORM_FEED;
|
|
break;
|
|
case CHAR_T:
|
|
c = TAB;
|
|
break;
|
|
case CHAR_U:
|
|
if (position + 5 > length) {
|
|
error('Invalid unicode esacape sequence');
|
|
}
|
|
final codeString = json.substring(position + 1, position + 5);
|
|
try {
|
|
c = int.parse('0x${codeString}');
|
|
} catch (e) {
|
|
error('Invalid unicode esacape sequence');
|
|
}
|
|
position += 4;
|
|
break;
|
|
default:
|
|
error('Invalid esacape sequence in string literal');
|
|
}
|
|
}
|
|
charCodes.add(c);
|
|
position++;
|
|
}
|
|
|
|
return new String.fromCharCodes(charCodes);
|
|
}
|
|
|
|
num parseNumber() {
|
|
if (!isToken(NUMBER_LITERAL)) error('Expected number literal');
|
|
|
|
final int startPos = position;
|
|
int char = char();
|
|
if (char === MINUS) char = nextChar();
|
|
if (char === CHAR_0) {
|
|
char = nextChar();
|
|
} else if (isDigit(char)) {
|
|
char = nextChar();
|
|
while (isDigit(char)) char = nextChar();
|
|
} else {
|
|
error('Expected digit when parsing number');
|
|
}
|
|
|
|
bool isInt = true;
|
|
if (char === DOT) {
|
|
char = nextChar();
|
|
if (isDigit(char)) {
|
|
char = nextChar();
|
|
isInt = false;
|
|
while (isDigit(char)) char = nextChar();
|
|
} else {
|
|
error('Expected digit following comma');
|
|
}
|
|
}
|
|
|
|
if (char === CHAR_E || char === CHAR_CAPITAL_E) {
|
|
char = nextChar();
|
|
if (char === MINUS || char === PLUS) char = nextChar();
|
|
if (isDigit(char)) {
|
|
char = nextChar();
|
|
isInt = false;
|
|
while (isDigit(char)) char = nextChar();
|
|
} else {
|
|
error('Expected digit following \'e\' or \'E\'');
|
|
}
|
|
}
|
|
|
|
String number = json.substring(startPos, position);
|
|
if (isInt) {
|
|
return int.parse(number);
|
|
} else {
|
|
return double.parse(number);
|
|
}
|
|
}
|
|
|
|
bool isChar(int char) {
|
|
if (position >= length) return false;
|
|
return json.charCodeAt(position) == char;
|
|
}
|
|
|
|
bool isDigit(int char) {
|
|
return char >= CHAR_0 && char <= CHAR_9;
|
|
}
|
|
|
|
bool isToken(int tokenKind) => token() == tokenKind;
|
|
|
|
int char() {
|
|
if (position >= length) {
|
|
error('Unexpected end of JSON stream');
|
|
}
|
|
return json.charCodeAt(position);
|
|
}
|
|
|
|
int nextChar() {
|
|
position++;
|
|
if (position >= length) return 0;
|
|
return json.charCodeAt(position);
|
|
}
|
|
|
|
int token() {
|
|
while (true) {
|
|
if (position >= length) return null;
|
|
int char = json.charCodeAt(position);
|
|
int token = tokens[char];
|
|
if (token === WHITESPACE) {
|
|
position++;
|
|
continue;
|
|
}
|
|
if (token === null) return 0;
|
|
return token;
|
|
}
|
|
}
|
|
|
|
void error(String message) {
|
|
throw message;
|
|
}
|
|
}
|
|
|
|
class _JsonStringifier {
|
|
StringBuffer sb;
|
|
List<Object> seen; // TODO: that should be identity set.
|
|
|
|
_JsonStringifier(this.sb) : seen = [];
|
|
|
|
static String stringify(final object) {
|
|
StringBuffer output = new StringBuffer();
|
|
_JsonStringifier stringifier = new _JsonStringifier(output);
|
|
stringifier.stringifyValue(object);
|
|
return output.toString();
|
|
}
|
|
|
|
static void printOn(final object, StringBuffer output) {
|
|
_JsonStringifier stringifier = new _JsonStringifier(output);
|
|
stringifier.stringifyValue(object);
|
|
}
|
|
|
|
static String numberToString(num x) {
|
|
return x.toString();
|
|
}
|
|
|
|
// ('0' + x) or ('a' + x - 10)
|
|
static int hexDigit(int x) => x < 10 ? 48 + x : 87 + x;
|
|
|
|
static void escape(StringBuffer sb, String s) {
|
|
final int length = s.length;
|
|
bool needsEscape = false;
|
|
final charCodes = new List<int>();
|
|
for (int i = 0; i < length; i++) {
|
|
int charCode = s.charCodeAt(i);
|
|
if (charCode < 32) {
|
|
needsEscape = true;
|
|
charCodes.add(_JsonParser.BACKSLASH);
|
|
switch (charCode) {
|
|
case _JsonParser.BACKSPACE:
|
|
charCodes.add(_JsonParser.CHAR_B);
|
|
break;
|
|
case _JsonParser.TAB:
|
|
charCodes.add(_JsonParser.CHAR_T);
|
|
break;
|
|
case _JsonParser.NEW_LINE:
|
|
charCodes.add(_JsonParser.CHAR_N);
|
|
break;
|
|
case _JsonParser.FORM_FEED:
|
|
charCodes.add(_JsonParser.CHAR_F);
|
|
break;
|
|
case _JsonParser.CARRIAGE_RETURN:
|
|
charCodes.add(_JsonParser.CHAR_R);
|
|
break;
|
|
default:
|
|
charCodes.add(_JsonParser.CHAR_U);
|
|
charCodes.add(hexDigit((charCode >> 12) & 0xf));
|
|
charCodes.add(hexDigit((charCode >> 8) & 0xf));
|
|
charCodes.add(hexDigit((charCode >> 4) & 0xf));
|
|
charCodes.add(hexDigit(charCode & 0xf));
|
|
break;
|
|
}
|
|
} else if (charCode == _JsonParser.QUOTE ||
|
|
charCode == _JsonParser.BACKSLASH) {
|
|
needsEscape = true;
|
|
charCodes.add(_JsonParser.BACKSLASH);
|
|
charCodes.add(charCode);
|
|
} else {
|
|
charCodes.add(charCode);
|
|
}
|
|
}
|
|
sb.add(needsEscape ? new String.fromCharCodes(charCodes) : s);
|
|
}
|
|
|
|
void checkCycle(final object) {
|
|
// TODO: use Iterables.
|
|
for (int i = 0; i < seen.length; i++) {
|
|
if (seen[i] === object) {
|
|
throw 'Cyclic structure';
|
|
}
|
|
}
|
|
seen.add(object);
|
|
}
|
|
|
|
void stringifyValue(final object) {
|
|
// Tries stringifying object directly. If it's not a simple value, List or
|
|
// Map, call toJson() to get a custom representation and try serializing
|
|
// that.
|
|
if (!stringifyJsonValue(object)) {
|
|
checkCycle(object);
|
|
try {
|
|
var customJson = object.toJson();
|
|
if (!stringifyJsonValue(customJson)) {
|
|
throw new JsonUnsupportedObjectError(object);
|
|
}
|
|
seen.removeLast();
|
|
} catch (e) {
|
|
throw new JsonUnsupportedObjectError.withCause(object, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Serializes a [num], [String], [bool], [Null], [List] or [Map] value.
|
|
*
|
|
* Returns true if the value is one of these types, and false if not.
|
|
* If a value is both a [List] and a [Map], it's serialized as a [List].
|
|
*/
|
|
bool stringifyJsonValue(final object) {
|
|
if (object is num) {
|
|
// TODO: use writeOn.
|
|
sb.add(numberToString(object));
|
|
return true;
|
|
} else if (object === true) {
|
|
sb.add('true');
|
|
return true;
|
|
} else if (object === false) {
|
|
sb.add('false');
|
|
return true;
|
|
} else if (object === null) {
|
|
sb.add('null');
|
|
return true;
|
|
} else if (object is String) {
|
|
sb.add('"');
|
|
escape(sb, object);
|
|
sb.add('"');
|
|
return true;
|
|
} else if (object is List) {
|
|
checkCycle(object);
|
|
List a = object;
|
|
sb.add('[');
|
|
if (a.length > 0) {
|
|
stringifyValue(a[0]);
|
|
// TODO: switch to Iterables.
|
|
for (int i = 1; i < a.length; i++) {
|
|
sb.add(',');
|
|
stringifyValue(a[i]);
|
|
}
|
|
}
|
|
sb.add(']');
|
|
seen.removeLast();
|
|
return true;
|
|
} else if (object is Map) {
|
|
checkCycle(object);
|
|
Map<String, Object> m = object;
|
|
sb.add('{');
|
|
bool first = true;
|
|
m.forEach((String key, Object value) {
|
|
if (!first) {
|
|
sb.add(',"');
|
|
} else {
|
|
sb.add('"');
|
|
}
|
|
escape(sb, key);
|
|
sb.add('":');
|
|
stringifyValue(value);
|
|
first = false;
|
|
});
|
|
sb.add('}');
|
|
seen.removeLast();
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|