blob: 114feea544c6ff11cfefac09ae8f5d2a5a6fb866 [file] [log] [blame]
// 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.
/**
* Utilities for encoding and decoding JSON (JavaScript Object Notation) data.
*/
@deprecated
library dart.json;
import "dart:convert";
import "dart:_collection-dev" show deprecated;
export "dart:convert" show JsonUnsupportedObjectError, JsonCyclicError;
// JSON parsing and serialization.
/**
* *DEPRECATED* Use `dart:convert JSON.decode` instead.
*
* 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.
*
* The optional [reviver] function, if provided, is called once for each
* object or list property parsed. The arguments are the property name
* ([String]) or list index ([int]), and the value is the parsed value.
* The return value of the reviver will be used as the value of that property
* instead the parsed value.
*
* Throws [FormatException] if the input is not valid JSON text.
*/
@deprecated
parse(String json, [reviver(var key, var value)]) {
if (reviver != null) {
var original = reviver;
reviver = (key, value) => original(key == null ? "" : key, value);
}
return JSON.decode(json, reviver: reviver);
}
/**
* *DEPRECATED* Use `dart:convert JSON.encode` instead.
*
* Serializes [object] into a JSON string.
*
* Directly serializable values are [num], [String], [bool], and [Null], as well
* as some [List] and [Map] values.
* 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 value, 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.
*
* If a [List] or [Map] contains a reference to itself, directly or through
* other lists or maps, it cannot be serialized and a [JsonCyclicError] is
* thrown.
*
* Json 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.
*/
@deprecated
String stringify(Object object) {
return _JsonStringifier.stringify(object);
}
/**
* *DEPRECATED* Use `package:json/json.dart` or `dart:convert` instead.
*
* Serializes [object] into [output] stream.
*
* Performs the same operations as [stringify] but outputs the resulting
* string to an existing [StringSink] 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.
*/
@deprecated
void printOn(Object object, StringSink output) {
return _JsonStringifier.printOn(object, output);
}
//// Implementation ///////////////////////////////////////////////////////////
// Simple API for JSON parsing.
/// *DEPRECATED* Use `package:json/json.dart` instead.
@deprecated
abstract class JsonListener {
void handleString(String value) {}
void handleNumber(num value) {}
void handleBool(bool value) {}
void handleNull() {}
void beginObject() {}
void propertyName() {}
void propertyValue() {}
void endObject() {}
void beginArray() {}
void arrayElement() {}
void endArray() {}
/** Called on failure to parse [source]. */
void fail(String source, int position, String message) {}
}
/**
* *DEPRECATED* Use `package:json/json.dart` instead.
*
* A [JsonListener] that builds data objects from the parser events.
*
* This is a simple stack-based object builder. It keeps the most recently
* seen value in a variable, and uses it depending on the following event.
*/
@deprecated
class BuildJsonListener extends JsonListener {
/**
* Stack used to handle nested containers.
*
* The current container is pushed on the stack when a new one is
* started. If the container is a [Map], there is also a current [key]
* which is also stored on the stack.
*/
List stack = [];
/** The current [Map] or [List] being built. */
var currentContainer;
/** The most recently read property key. */
String key;
/** The most recently read value. */
var value;
/** Pushes the currently active container (and key, if a [Map]). */
void pushContainer() {
if (currentContainer is Map) stack.add(key);
stack.add(currentContainer);
}
/** Pops the top container from the [stack], including a key if applicable. */
void popContainer() {
value = currentContainer;
currentContainer = stack.removeLast();
if (currentContainer is Map) key = stack.removeLast();
}
void handleString(String value) { this.value = value; }
void handleNumber(num value) { this.value = value; }
void handleBool(bool value) { this.value = value; }
void handleNull() { this.value = null; }
void beginObject() {
pushContainer();
currentContainer = {};
}
void propertyName() {
key = value;
value = null;
}
void propertyValue() {
Map map = currentContainer;
map[key] = value;
key = value = null;
}
void endObject() {
popContainer();
}
void beginArray() {
pushContainer();
currentContainer = [];
}
void arrayElement() {
List list = currentContainer;
currentContainer.add(value);
value = null;
}
void endArray() {
popContainer();
}
/** Read out the final result of parsing a JSON string. */
get result {
assert(currentContainer == null);
return value;
}
}
typedef _Reviver(var key, var value);
/// *DEPRECATED* Use `package:json/json.dart` instead.
@deprecated
class ReviverJsonListener extends BuildJsonListener {
final _Reviver reviver;
ReviverJsonListener(reviver(key, value)) : this.reviver = reviver;
void arrayElement() {
List list = currentContainer;
value = reviver(list.length, value);
super.arrayElement();
}
void propertyValue() {
value = reviver(key, value);
super.propertyValue();
}
get result {
return reviver("", value);
}
}
/// *DEPRECATED* Use `package:json/json.dart` instead.
@deprecated
class JsonParser {
// A simple non-recursive state-based parser for JSON.
//
// Literal values accepted in states ARRAY_EMPTY, ARRAY_COMMA, OBJECT_COLON
// and strings also in OBJECT_EMPTY, OBJECT_COMMA.
// VALUE STRING : , } ] Transitions to
// EMPTY X X -> END
// ARRAY_EMPTY X X @ -> ARRAY_VALUE / pop
// ARRAY_VALUE @ @ -> ARRAY_COMMA / pop
// ARRAY_COMMA X X -> ARRAY_VALUE
// OBJECT_EMPTY X @ -> OBJECT_KEY / pop
// OBJECT_KEY @ -> OBJECT_COLON
// OBJECT_COLON X X -> OBJECT_VALUE
// OBJECT_VALUE @ @ -> OBJECT_COMMA / pop
// OBJECT_COMMA X -> OBJECT_KEY
// END
// Starting a new array or object will push the current state. The "pop"
// above means restoring this state and then marking it as an ended value.
// X means generic handling, @ means special handling for just that
// state - that is, values are handled generically, only punctuation
// cares about the current state.
// Values for states are chosen so bits 0 and 1 tell whether
// a string/value is allowed, and setting bits 0 through 2 after a value
// gets to the next state (not empty, doesn't allow a value).
// State building-block constants.
static const int INSIDE_ARRAY = 1;
static const int INSIDE_OBJECT = 2;
static const int AFTER_COLON = 3; // Always inside object.
static const int ALLOW_STRING_MASK = 8; // Allowed if zero.
static const int ALLOW_VALUE_MASK = 4; // Allowed if zero.
static const int ALLOW_VALUE = 0;
static const int STRING_ONLY = 4;
static const int NO_VALUES = 12;
// Objects and arrays are "empty" until their first property/element.
static const int EMPTY = 0;
static const int NON_EMPTY = 16;
static const int EMPTY_MASK = 16; // Empty if zero.
static const int VALUE_READ_BITS = NO_VALUES | NON_EMPTY;
// Actual states.
static const int STATE_INITIAL = EMPTY | ALLOW_VALUE;
static const int STATE_END = NON_EMPTY | NO_VALUES;
static const int STATE_ARRAY_EMPTY = INSIDE_ARRAY | EMPTY | ALLOW_VALUE;
static const int STATE_ARRAY_VALUE = INSIDE_ARRAY | NON_EMPTY | NO_VALUES;
static const int STATE_ARRAY_COMMA = INSIDE_ARRAY | NON_EMPTY | ALLOW_VALUE;
static const int STATE_OBJECT_EMPTY = INSIDE_OBJECT | EMPTY | STRING_ONLY;
static const int STATE_OBJECT_KEY = INSIDE_OBJECT | NON_EMPTY | NO_VALUES;
static const int STATE_OBJECT_COLON = AFTER_COLON | NON_EMPTY | ALLOW_VALUE;
static const int STATE_OBJECT_VALUE = AFTER_COLON | NON_EMPTY | NO_VALUES;
static const int STATE_OBJECT_COMMA = INSIDE_OBJECT | NON_EMPTY | STRING_ONLY;
// Character code constants.
static const int BACKSPACE = 0x08;
static const int TAB = 0x09;
static const int NEWLINE = 0x0a;
static const int CARRIAGE_RETURN = 0x0d;
static const int FORM_FEED = 0x0c;
static const int SPACE = 0x20;
static const int QUOTE = 0x22;
static const int PLUS = 0x2b;
static const int COMMA = 0x2c;
static const int MINUS = 0x2d;
static const int DECIMALPOINT = 0x2e;
static const int SLASH = 0x2f;
static const int CHAR_0 = 0x30;
static const int CHAR_9 = 0x39;
static const int COLON = 0x3a;
static const int CHAR_E = 0x45;
static const int LBRACKET = 0x5b;
static const int BACKSLASH = 0x5c;
static const int RBRACKET = 0x5d;
static const int CHAR_a = 0x61;
static const int CHAR_b = 0x62;
static const int CHAR_e = 0x65;
static const int CHAR_f = 0x66;
static const int CHAR_l = 0x6c;
static const int CHAR_n = 0x6e;
static const int CHAR_r = 0x72;
static const int CHAR_s = 0x73;
static const int CHAR_t = 0x74;
static const int CHAR_u = 0x75;
static const int LBRACE = 0x7b;
static const int RBRACE = 0x7d;
final String source;
final JsonListener listener;
JsonParser(this.source, this.listener);
/** Parses [source], or throws if it fails. */
void parse() {
final List<int> states = <int>[];
int state = STATE_INITIAL;
int position = 0;
int length = source.length;
while (position < length) {
int char = source.codeUnitAt(position);
switch (char) {
case SPACE:
case CARRIAGE_RETURN:
case NEWLINE:
case TAB:
position++;
break;
case QUOTE:
if ((state & ALLOW_STRING_MASK) != 0) fail(position);
position = parseString(position + 1);
state |= VALUE_READ_BITS;
break;
case LBRACKET:
if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
listener.beginArray();
states.add(state);
state = STATE_ARRAY_EMPTY;
position++;
break;
case LBRACE:
if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
listener.beginObject();
states.add(state);
state = STATE_OBJECT_EMPTY;
position++;
break;
case CHAR_n:
if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
position = parseNull(position);
state |= VALUE_READ_BITS;
break;
case CHAR_f:
if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
position = parseFalse(position);
state |= VALUE_READ_BITS;
break;
case CHAR_t:
if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
position = parseTrue(position);
state |= VALUE_READ_BITS;
break;
case COLON:
if (state != STATE_OBJECT_KEY) fail(position);
listener.propertyName();
state = STATE_OBJECT_COLON;
position++;
break;
case COMMA:
if (state == STATE_OBJECT_VALUE) {
listener.propertyValue();
state = STATE_OBJECT_COMMA;
position++;
} else if (state == STATE_ARRAY_VALUE) {
listener.arrayElement();
state = STATE_ARRAY_COMMA;
position++;
} else {
fail(position);
}
break;
case RBRACKET:
if (state == STATE_ARRAY_EMPTY) {
listener.endArray();
} else if (state == STATE_ARRAY_VALUE) {
listener.arrayElement();
listener.endArray();
} else {
fail(position);
}
state = states.removeLast() | VALUE_READ_BITS;
position++;
break;
case RBRACE:
if (state == STATE_OBJECT_EMPTY) {
listener.endObject();
} else if (state == STATE_OBJECT_VALUE) {
listener.propertyValue();
listener.endObject();
} else {
fail(position);
}
state = states.removeLast() | VALUE_READ_BITS;
position++;
break;
default:
if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
position = parseNumber(char, position);
state |= VALUE_READ_BITS;
break;
}
}
if (state != STATE_END) fail(position);
}
/**
* Parses a "true" literal starting at [position].
*
* [:source[position]:] must be "t".
*/
int parseTrue(int position) {
assert(source.codeUnitAt(position) == CHAR_t);
if (source.length < position + 4) fail(position, "Unexpected identifier");
if (source.codeUnitAt(position + 1) != CHAR_r ||
source.codeUnitAt(position + 2) != CHAR_u ||
source.codeUnitAt(position + 3) != CHAR_e) {
fail(position);
}
listener.handleBool(true);
return position + 4;
}
/**
* Parses a "false" literal starting at [position].
*
* [:source[position]:] must be "f".
*/
int parseFalse(int position) {
assert(source.codeUnitAt(position) == CHAR_f);
if (source.length < position + 5) fail(position, "Unexpected identifier");
if (source.codeUnitAt(position + 1) != CHAR_a ||
source.codeUnitAt(position + 2) != CHAR_l ||
source.codeUnitAt(position + 3) != CHAR_s ||
source.codeUnitAt(position + 4) != CHAR_e) {
fail(position);
}
listener.handleBool(false);
return position + 5;
}
/** Parses a "null" literal starting at [position].
*
* [:source[position]:] must be "n".
*/
int parseNull(int position) {
assert(source.codeUnitAt(position) == CHAR_n);
if (source.length < position + 4) fail(position, "Unexpected identifier");
if (source.codeUnitAt(position + 1) != CHAR_u ||
source.codeUnitAt(position + 2) != CHAR_l ||
source.codeUnitAt(position + 3) != CHAR_l) {
fail(position);
}
listener.handleNull();
return position + 4;
}
int parseString(int position) {
// Format: '"'([^\x00-\x1f\\\"]|'\\'[bfnrt/\\"])*'"'
// Initial position is right after first '"'.
int start = position;
int char;
do {
if (position == source.length) {
fail(start - 1, "Unterminated string");
}
char = source.codeUnitAt(position);
if (char == QUOTE) {
String result = source.substring(start, position);
listener.handleString(result);
return position + 1;
}
if (char < SPACE) {
fail(position, "Control character in string");
}
position++;
} while (char != BACKSLASH);
// Backslash escape detected. Collect character codes for rest of string.
int firstEscape = position - 1;
List<int> chars = <int>[];
while (true) {
if (position == source.length) {
fail(start - 1, "Unterminated string");
}
char = source.codeUnitAt(position);
switch (char) {
case CHAR_b: char = BACKSPACE; break;
case CHAR_f: char = FORM_FEED; break;
case CHAR_n: char = NEWLINE; break;
case CHAR_r: char = CARRIAGE_RETURN; break;
case CHAR_t: char = TAB; break;
case SLASH:
case BACKSLASH:
case QUOTE:
break;
case CHAR_u:
int hexStart = position - 1;
int value = 0;
for (int i = 0; i < 4; i++) {
position++;
if (position == source.length) {
fail(start - 1, "Unterminated string");
}
char = source.codeUnitAt(position);
char -= 0x30;
if (char < 0) fail(hexStart, "Invalid unicode escape");
if (char < 10) {
value = value * 16 + char;
} else {
char = (char | 0x20) - 0x31;
if (char < 0 || char > 5) {
fail(hexStart, "Invalid unicode escape");
}
value = value * 16 + char + 10;
}
}
char = value;
break;
default:
if (char < SPACE) fail(position, "Control character in string");
fail(position, "Unrecognized string escape");
}
do {
chars.add(char);
position++;
if (position == source.length) fail(start - 1, "Unterminated string");
char = source.codeUnitAt(position);
if (char == QUOTE) {
String result = new String.fromCharCodes(chars);
if (start < firstEscape) {
result = "${source.substring(start, firstEscape)}$result";
}
listener.handleString(result);
return position + 1;
}
if (char < SPACE) {
fail(position, "Control character in string");
}
} while (char != BACKSLASH);
position++;
}
}
int _handleLiteral(start, position, isDouble) {
String literal = source.substring(start, position);
// This correctly creates -0 for doubles.
num value = (isDouble ? double.parse(literal) : int.parse(literal));
listener.handleNumber(value);
return position;
}
int parseNumber(int char, int position) {
// Format:
// '-'?('0'|[1-9][0-9]*)('.'[0-9]+)?([eE][+-]?[0-9]+)?
int start = position;
int length = source.length;
bool isDouble = false;
if (char == MINUS) {
position++;
if (position == length) fail(position, "Missing expected digit");
char = source.codeUnitAt(position);
}
if (char < CHAR_0 || char > CHAR_9) {
fail(position, "Missing expected digit");
}
if (char == CHAR_0) {
position++;
if (position == length) return _handleLiteral(start, position, false);
char = source.codeUnitAt(position);
if (CHAR_0 <= char && char <= CHAR_9) {
fail(position);
}
} else {
do {
position++;
if (position == length) return _handleLiteral(start, position, false);
char = source.codeUnitAt(position);
} while (CHAR_0 <= char && char <= CHAR_9);
}
if (char == DECIMALPOINT) {
isDouble = true;
position++;
if (position == length) fail(position, "Missing expected digit");
char = source.codeUnitAt(position);
if (char < CHAR_0 || char > CHAR_9) fail(position);
do {
position++;
if (position == length) return _handleLiteral(start, position, true);
char = source.codeUnitAt(position);
} while (CHAR_0 <= char && char <= CHAR_9);
}
if (char == CHAR_e || char == CHAR_E) {
isDouble = true;
position++;
if (position == length) fail(position, "Missing expected digit");
char = source.codeUnitAt(position);
if (char == PLUS || char == MINUS) {
position++;
if (position == length) fail(position, "Missing expected digit");
char = source.codeUnitAt(position);
}
if (char < CHAR_0 || char > CHAR_9) {
fail(position, "Missing expected digit");
}
do {
position++;
if (position == length) return _handleLiteral(start, position, true);
char = source.codeUnitAt(position);
} while (CHAR_0 <= char && char <= CHAR_9);
}
return _handleLiteral(start, position, isDouble);
}
void fail(int position, [String message]) {
if (message == null) message = "Unexpected character";
listener.fail(source, position, message);
// If the listener didn't throw, do it here.
String slice;
int sliceEnd = position + 20;
if (sliceEnd > source.length) {
slice = "'${source.substring(position)}'";
} else {
slice = "'${source.substring(position, sliceEnd)}...'";
}
throw new FormatException("Unexpected character at $position: $slice");
}
}
class _JsonStringifier {
StringSink sink;
List<Object> seen; // TODO: that should be identity set.
_JsonStringifier(this.sink) : 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, StringSink 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(StringSink 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.codeUnitAt(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.NEWLINE:
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.write(needsEscape ? new String.fromCharCodes(charCodes) : s);
}
void checkCycle(final object) {
// TODO: use Iterables.
for (int i = 0; i < seen.length; i++) {
if (identical(seen[i], object)) {
throw new JsonCyclicError(object);
}
}
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(object, cause: 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.
sink.write(numberToString(object));
return true;
} else if (identical(object, true)) {
sink.write('true');
return true;
} else if (identical(object, false)) {
sink.write('false');
return true;
} else if (object == null) {
sink.write('null');
return true;
} else if (object is String) {
sink.write('"');
escape(sink, object);
sink.write('"');
return true;
} else if (object is List) {
checkCycle(object);
List a = object;
sink.write('[');
if (a.length > 0) {
stringifyValue(a[0]);
// TODO: switch to Iterables.
for (int i = 1; i < a.length; i++) {
sink.write(',');
stringifyValue(a[i]);
}
}
sink.write(']');
seen.removeLast();
return true;
} else if (object is Map) {
checkCycle(object);
Map<String, Object> m = object;
sink.write('{');
bool first = true;
m.forEach((String key, Object value) {
if (!first) {
sink.write(',"');
} else {
sink.write('"');
}
escape(sink, key);
sink.write('":');
stringifyValue(value);
first = false;
});
sink.write('}');
seen.removeLast();
return true;
} else {
return false;
}
}
}