| // Copyright (c) 2013, 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. |
| |
| // Patch file for dart:convert library. |
| |
| import 'dart:_js_helper' show argumentErrorValue, patch; |
| import 'dart:_foreign_helper' show JS; |
| import 'dart:_interceptors' show JSExtendableArray; |
| import 'dart:_internal' show MappedIterable, ListIterable; |
| import 'dart:collection' show Maps, LinkedHashMap; |
| |
| /** |
| * Parses [json] and builds the corresponding parsed JSON value. |
| * |
| * Parsed JSON values Nare 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 of the parsed |
| * value. The top level value is passed to the reviver with the empty string as |
| * a key. |
| * |
| * Throws [FormatException] if the input is not valid JSON text. |
| */ |
| @patch |
| _parseJson(String source, reviver(key, value)) { |
| if (source is! String) throw argumentErrorValue(source); |
| |
| var parsed; |
| try { |
| parsed = JS('=Object|JSExtendableArray|Null|bool|num|String', |
| 'JSON.parse(#)', |
| source); |
| } catch (e) { |
| throw new FormatException(JS('String', 'String(#)', e)); |
| } |
| |
| if (reviver == null) { |
| return _convertJsonToDartLazy(parsed); |
| } else { |
| return _convertJsonToDart(parsed, reviver); |
| } |
| } |
| |
| /** |
| * Walks the raw JavaScript value [json], replacing JavaScript Objects with |
| * Maps. [json] is expected to be freshly allocated so elements can be replaced |
| * in-place. |
| */ |
| _convertJsonToDart(json, reviver(key, value)) { |
| assert(reviver != null); |
| walk(e) { |
| // JavaScript null, string, number, bool are in the correct representation. |
| if (JS('bool', '# == null', e) || JS('bool', 'typeof # != "object"', e)) { |
| return e; |
| } |
| |
| // This test is needed to avoid identifing '{"__proto__":[]}' as an Array. |
| // TODO(sra): Replace this test with cheaper '#.constructor === Array' when |
| // bug 621 below is fixed. |
| if (JS('bool', 'Object.getPrototypeOf(#) === Array.prototype', e)) { |
| // In-place update of the elements since JS Array is a Dart List. |
| for (int i = 0; i < JS('int', '#.length', e); i++) { |
| // Use JS indexing to avoid range checks. We know this is the only |
| // reference to the list, but the compiler will likely never be able to |
| // tell that this instance of the list cannot have its length changed by |
| // the reviver even though it later will be passed to the reviver at the |
| // outer level. |
| var item = JS('', '#[#]', e, i); |
| JS('', '#[#]=#', e, i, reviver(i, walk(item))); |
| } |
| return e; |
| } |
| |
| // Otherwise it is a plain object, so copy to a JSON map, so we process |
| // and revive all entries recursively. |
| _JsonMap map = new _JsonMap(e); |
| var processed = map._processed; |
| List<String> keys = map._computeKeys(); |
| for (int i = 0; i < keys.length; i++) { |
| String key = keys[i]; |
| var revived = reviver(key, walk(JS('', '#[#]', e, key))); |
| JS('', '#[#]=#', processed, key, revived); |
| } |
| |
| // Update the JSON map structure so future access is cheaper. |
| map._original = processed; // Don't keep two objects around. |
| return map; |
| } |
| |
| return reviver(null, walk(json)); |
| } |
| |
| _convertJsonToDartLazy(object) { |
| // JavaScript null and undefined are represented as null. |
| if (object == null) return null; |
| |
| // JavaScript string, number, bool already has the correct representation. |
| if (JS('bool', 'typeof # != "object"', object)) { |
| return object; |
| } |
| |
| // This test is needed to avoid identifing '{"__proto__":[]}' as an array. |
| // TODO(sra): Replace this test with cheaper '#.constructor === Array' when |
| // bug https://code.google.com/p/v8/issues/detail?id=621 is fixed. |
| if (JS('bool', 'Object.getPrototypeOf(#) !== Array.prototype', object)) { |
| return new _JsonMap(object); |
| } |
| |
| // Update the elements in place since JS arrays are Dart lists. |
| for (int i = 0; i < JS('int', '#.length', object); i++) { |
| // Use JS indexing to avoid range checks. We know this is the only |
| // reference to the list, but the compiler will likely never be able to |
| // tell that this instance of the list cannot have its length changed by |
| // the reviver even though it later will be passed to the reviver at the |
| // outer level. |
| var item = JS('', '#[#]', object, i); |
| JS('', '#[#]=#', object, i, _convertJsonToDartLazy(item)); |
| } |
| return object; |
| } |
| |
| class _JsonMap implements LinkedHashMap { |
| // The original JavaScript object remains unchanged until |
| // the map is eventually upgraded, in which case we null it |
| // out to reclaim the memory used by it. |
| var _original; |
| |
| // We keep track of the map entries that we have already |
| // processed by adding them to a separate JavaScript object. |
| var _processed = _newJavaScriptObject(); |
| |
| // If the data slot isn't null, it represents either the list |
| // of keys (for non-upgraded JSON maps) or the upgraded map. |
| var _data = null; |
| |
| _JsonMap(this._original); |
| |
| operator[](key) { |
| if (_isUpgraded) { |
| return _upgradedMap[key]; |
| } else if (key is !String) { |
| return null; |
| } else { |
| var result = _getProperty(_processed, key); |
| if (_isUnprocessed(result)) result = _process(key); |
| return result; |
| } |
| } |
| |
| int get length => _isUpgraded |
| ? _upgradedMap.length |
| : _computeKeys().length; |
| |
| bool get isEmpty => length == 0; |
| bool get isNotEmpty => length > 0; |
| |
| Iterable get keys { |
| if (_isUpgraded) return _upgradedMap.keys; |
| return new _JsonMapKeyIterable(this); |
| } |
| |
| Iterable get values { |
| if (_isUpgraded) return _upgradedMap.values; |
| return new MappedIterable(_computeKeys(), (each) => this[each]); |
| } |
| |
| operator[]=(key, value) { |
| if (_isUpgraded) { |
| _upgradedMap[key] = value; |
| } else if (containsKey(key)) { |
| var processed = _processed; |
| _setProperty(processed, key, value); |
| var original = _original; |
| if (!identical(original, processed)) { |
| _setProperty(original, key, null); // Reclaim memory. |
| } |
| } else { |
| _upgrade()[key] = value; |
| } |
| } |
| |
| void addAll(Map other) { |
| other.forEach((key, value) { |
| this[key] = value; |
| }); |
| } |
| |
| bool containsValue(value) { |
| if (_isUpgraded) return _upgradedMap.containsValue(value); |
| List<String> keys = _computeKeys(); |
| for (int i = 0; i < keys.length; i++) { |
| String key = keys[i]; |
| if (this[key] == value) return true; |
| } |
| return false; |
| } |
| |
| bool containsKey(key) { |
| if (_isUpgraded) return _upgradedMap.containsKey(key); |
| if (key is !String) return false; |
| return _hasProperty(_original, key); |
| } |
| |
| putIfAbsent(key, ifAbsent()) { |
| if (containsKey(key)) return this[key]; |
| var value = ifAbsent(); |
| this[key] = value; |
| return value; |
| } |
| |
| remove(Object key) { |
| if (!_isUpgraded && !containsKey(key)) return null; |
| return _upgrade().remove(key); |
| } |
| |
| void clear() { |
| if (_isUpgraded) { |
| _upgradedMap.clear(); |
| } else { |
| if (_data != null) { |
| // Clear the list of keys to make sure we force |
| // a concurrent modification error if anyone is |
| // currently iterating over it. |
| _data.clear(); |
| } |
| _original = _processed = null; |
| _data = {}; |
| } |
| } |
| |
| void forEach(void f(key, value)) { |
| if (_isUpgraded) return _upgradedMap.forEach(f); |
| List<String> keys = _computeKeys(); |
| for (int i = 0; i < keys.length; i++) { |
| String key = keys[i]; |
| |
| // Compute the value under the assumption that the property |
| // is present but potentially not processed. |
| var value = _getProperty(_processed, key); |
| if (_isUnprocessed(value)) { |
| value = _convertJsonToDartLazy(_getProperty(_original, key)); |
| _setProperty(_processed, key, value); |
| } |
| |
| // Do the callback. |
| f(key, value); |
| |
| // Check if invoking the callback function changed |
| // the key set. If so, throw an exception. |
| if (!identical(keys, _data)) { |
| throw new ConcurrentModificationError(this); |
| } |
| } |
| } |
| |
| String toString() => Maps.mapToString(this); |
| |
| |
| // ------------------------------------------ |
| // Private helper methods. |
| // ------------------------------------------ |
| |
| bool get _isUpgraded => _processed == null; |
| |
| Map get _upgradedMap { |
| assert(_isUpgraded); |
| // 'cast' the union type to LinkedHashMap. It would be even better if we |
| // could 'cast' to the implementation type, since LinkedHashMap includes |
| // _JsonMap. |
| return JS('LinkedHashMap', '#', _data); |
| } |
| |
| List<String> _computeKeys() { |
| assert(!_isUpgraded); |
| List keys = _data; |
| if (keys == null) { |
| keys = _data = _getPropertyNames(_original); |
| } |
| return JS('JSExtendableArray', '#', keys); |
| } |
| |
| Map _upgrade() { |
| if (_isUpgraded) return _upgradedMap; |
| |
| // Copy all the (key, value) pairs to a freshly allocated |
| // linked hash map thus preserving the ordering. |
| Map result = {}; |
| List<String> keys = _computeKeys(); |
| for (int i = 0; i < keys.length; i++) { |
| String key = keys[i]; |
| result[key] = this[key]; |
| } |
| |
| // We only upgrade when we need to extend the map, so we can |
| // safely force a concurrent modification error in case |
| // someone is iterating over the map here. |
| if (keys.isEmpty) { |
| keys.add(null); |
| } else { |
| keys.clear(); |
| } |
| |
| // Clear out the associated JavaScript objects and mark the |
| // map as having been upgraded. |
| _original = _processed = null; |
| _data = result; |
| assert(_isUpgraded); |
| return result; |
| } |
| |
| _process(String key) { |
| if (!_hasProperty(_original, key)) return null; |
| var result = _convertJsonToDartLazy(_getProperty(_original, key)); |
| return _setProperty(_processed, key, result); |
| } |
| |
| |
| // ------------------------------------------ |
| // Private JavaScript helper methods. |
| // ------------------------------------------ |
| |
| static bool _hasProperty(object, String key) |
| => JS('bool', 'Object.prototype.hasOwnProperty.call(#,#)', object, key); |
| static _getProperty(object, String key) |
| => JS('', '#[#]', object, key); |
| static _setProperty(object, String key, value) |
| => JS('', '#[#]=#', object, key, value); |
| static List _getPropertyNames(object) |
| => JS('JSExtendableArray', 'Object.keys(#)', object); |
| static bool _isUnprocessed(object) |
| => JS('bool', 'typeof(#)=="undefined"', object); |
| static _newJavaScriptObject() |
| => JS('=Object', 'Object.create(null)'); |
| } |
| |
| class _JsonMapKeyIterable extends ListIterable { |
| final _JsonMap _parent; |
| |
| _JsonMapKeyIterable(this._parent); |
| |
| int get length => _parent.length; |
| |
| String elementAt(int index) { |
| return _parent._isUpgraded ? _parent.keys.elementAt(index) |
| : _parent._computeKeys()[index]; |
| } |
| |
| /// Although [ListIterable] defines its own iterator, we return the iterator |
| /// of the underlying list [_keys] in order to propagate |
| /// [ConcurrentModificationError]s. |
| Iterator get iterator { |
| return _parent._isUpgraded ? _parent.keys.iterator |
| : _parent._computeKeys().iterator; |
| } |
| |
| /// Delegate to [parent.containsKey] to ensure the performance expected |
| /// from [Map.keys.containsKey]. |
| bool contains(Object key) => _parent.containsKey(key); |
| } |
| |
| @patch class JsonDecoder { |
| @patch |
| StringConversionSink startChunkedConversion(Sink<Object> sink) { |
| return new _JsonDecoderSink(_reviver, sink); |
| } |
| } |
| |
| /** |
| * Implements the chunked conversion from a JSON string to its corresponding |
| * object. |
| * |
| * The sink only creates one object, but its input can be chunked. |
| */ |
| // TODO(floitsch): don't accumulate everything before starting to decode. |
| class _JsonDecoderSink extends _StringSinkConversionSink { |
| final _Reviver _reviver; |
| final Sink<Object> _sink; |
| |
| _JsonDecoderSink(this._reviver, this._sink) |
| : super(new StringBuffer()); |
| |
| void close() { |
| super.close(); |
| StringBuffer buffer = _stringSink; |
| String accumulated = buffer.toString(); |
| buffer.clear(); |
| Object decoded = _parseJson(accumulated, _reviver); |
| _sink.add(decoded); |
| _sink.close(); |
| } |
| } |
| |
| @patch class Utf8Decoder { |
| @patch |
| Converter<List<int>,dynamic> fuse(Converter<String, dynamic> next) { |
| return super.fuse(next); |
| } |
| |
| // Currently not intercepting UTF8 decoding. |
| @patch |
| static String _convertIntercepted(bool allowMalformed, List<int> codeUnits, |
| int start, int end) { |
| return null; // This call was not intercepted. |
| } |
| } |