OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 // Patch file for dart:convert library. | |
6 | |
7 import 'dart:_js_helper' show argumentErrorValue, patch; | |
8 import 'dart:_foreign_helper' show JS; | |
9 import 'dart:_interceptors' show JSExtendableArray; | |
10 import 'dart:_internal' show MappedIterable, ListIterable; | |
11 import 'dart:collection' show Maps, LinkedHashMap; | |
12 | |
13 /** | |
14 * Parses [json] and builds the corresponding parsed JSON value. | |
15 * | |
16 * Parsed JSON values Nare of the types [num], [String], [bool], [Null], | |
17 * [List]s of parsed JSON values or [Map]s from [String] to parsed | |
18 * JSON values. | |
19 * | |
20 * The optional [reviver] function, if provided, is called once for each object | |
21 * or list property parsed. The arguments are the property name ([String]) or | |
22 * list index ([int]), and the value is the parsed value. The return value of | |
23 * the reviver will be used as the value of that property instead of the parsed | |
24 * value. The top level value is passed to the reviver with the empty string as | |
25 * a key. | |
26 * | |
27 * Throws [FormatException] if the input is not valid JSON text. | |
28 */ | |
29 @patch | |
30 _parseJson(String source, reviver(key, value)) { | |
31 if (source is! String) throw argumentErrorValue(source); | |
32 | |
33 var parsed; | |
34 try { | |
35 parsed = JS('=Object|JSExtendableArray|Null|bool|num|String', | |
36 'JSON.parse(#)', | |
37 source); | |
38 } catch (e) { | |
39 throw new FormatException(JS('String', 'String(#)', e)); | |
40 } | |
41 | |
42 if (reviver == null) { | |
43 return _convertJsonToDartLazy(parsed); | |
44 } else { | |
45 return _convertJsonToDart(parsed, reviver); | |
46 } | |
47 } | |
48 | |
49 /** | |
50 * Walks the raw JavaScript value [json], replacing JavaScript Objects with | |
51 * Maps. [json] is expected to be freshly allocated so elements can be replaced | |
52 * in-place. | |
53 */ | |
54 _convertJsonToDart(json, reviver(key, value)) { | |
55 assert(reviver != null); | |
56 walk(e) { | |
57 // JavaScript null, string, number, bool are in the correct representation. | |
58 if (JS('bool', '# == null', e) || JS('bool', 'typeof # != "object"', e)) { | |
59 return e; | |
60 } | |
61 | |
62 // This test is needed to avoid identifing '{"__proto__":[]}' as an Array. | |
63 // TODO(sra): Replace this test with cheaper '#.constructor === Array' when | |
64 // bug 621 below is fixed. | |
65 if (JS('bool', 'Object.getPrototypeOf(#) === Array.prototype', e)) { | |
66 // In-place update of the elements since JS Array is a Dart List. | |
67 for (int i = 0; i < JS('int', '#.length', e); i++) { | |
68 // Use JS indexing to avoid range checks. We know this is the only | |
69 // reference to the list, but the compiler will likely never be able to | |
70 // tell that this instance of the list cannot have its length changed by | |
71 // the reviver even though it later will be passed to the reviver at the | |
72 // outer level. | |
73 var item = JS('', '#[#]', e, i); | |
74 JS('', '#[#]=#', e, i, reviver(i, walk(item))); | |
75 } | |
76 return e; | |
77 } | |
78 | |
79 // Otherwise it is a plain object, so copy to a JSON map, so we process | |
80 // and revive all entries recursively. | |
81 _JsonMap map = new _JsonMap(e); | |
82 var processed = map._processed; | |
83 List<String> keys = map._computeKeys(); | |
84 for (int i = 0; i < keys.length; i++) { | |
85 String key = keys[i]; | |
86 var revived = reviver(key, walk(JS('', '#[#]', e, key))); | |
87 JS('', '#[#]=#', processed, key, revived); | |
88 } | |
89 | |
90 // Update the JSON map structure so future access is cheaper. | |
91 map._original = processed; // Don't keep two objects around. | |
92 return map; | |
93 } | |
94 | |
95 return reviver(null, walk(json)); | |
96 } | |
97 | |
98 _convertJsonToDartLazy(object) { | |
99 // JavaScript null and undefined are represented as null. | |
100 if (object == null) return null; | |
101 | |
102 // JavaScript string, number, bool already has the correct representation. | |
103 if (JS('bool', 'typeof # != "object"', object)) { | |
104 return object; | |
105 } | |
106 | |
107 // This test is needed to avoid identifing '{"__proto__":[]}' as an array. | |
108 // TODO(sra): Replace this test with cheaper '#.constructor === Array' when | |
109 // bug https://code.google.com/p/v8/issues/detail?id=621 is fixed. | |
110 if (JS('bool', 'Object.getPrototypeOf(#) !== Array.prototype', object)) { | |
111 return new _JsonMap(object); | |
112 } | |
113 | |
114 // Update the elements in place since JS arrays are Dart lists. | |
115 for (int i = 0; i < JS('int', '#.length', object); i++) { | |
116 // Use JS indexing to avoid range checks. We know this is the only | |
117 // reference to the list, but the compiler will likely never be able to | |
118 // tell that this instance of the list cannot have its length changed by | |
119 // the reviver even though it later will be passed to the reviver at the | |
120 // outer level. | |
121 var item = JS('', '#[#]', object, i); | |
122 JS('', '#[#]=#', object, i, _convertJsonToDartLazy(item)); | |
123 } | |
124 return object; | |
125 } | |
126 | |
127 class _JsonMap implements LinkedHashMap { | |
128 // The original JavaScript object remains unchanged until | |
129 // the map is eventually upgraded, in which case we null it | |
130 // out to reclaim the memory used by it. | |
131 var _original; | |
132 | |
133 // We keep track of the map entries that we have already | |
134 // processed by adding them to a separate JavaScript object. | |
135 var _processed = _newJavaScriptObject(); | |
136 | |
137 // If the data slot isn't null, it represents either the list | |
138 // of keys (for non-upgraded JSON maps) or the upgraded map. | |
139 var _data = null; | |
140 | |
141 _JsonMap(this._original); | |
142 | |
143 operator[](key) { | |
144 if (_isUpgraded) { | |
145 return _upgradedMap[key]; | |
146 } else if (key is !String) { | |
147 return null; | |
148 } else { | |
149 var result = _getProperty(_processed, key); | |
150 if (_isUnprocessed(result)) result = _process(key); | |
151 return result; | |
152 } | |
153 } | |
154 | |
155 int get length => _isUpgraded | |
156 ? _upgradedMap.length | |
157 : _computeKeys().length; | |
158 | |
159 bool get isEmpty => length == 0; | |
160 bool get isNotEmpty => length > 0; | |
161 | |
162 Iterable get keys { | |
163 if (_isUpgraded) return _upgradedMap.keys; | |
164 return new _JsonMapKeyIterable(this); | |
165 } | |
166 | |
167 Iterable get values { | |
168 if (_isUpgraded) return _upgradedMap.values; | |
169 return new MappedIterable(_computeKeys(), (each) => this[each]); | |
170 } | |
171 | |
172 operator[]=(key, value) { | |
173 if (_isUpgraded) { | |
174 _upgradedMap[key] = value; | |
175 } else if (containsKey(key)) { | |
176 var processed = _processed; | |
177 _setProperty(processed, key, value); | |
178 var original = _original; | |
179 if (!identical(original, processed)) { | |
180 _setProperty(original, key, null); // Reclaim memory. | |
181 } | |
182 } else { | |
183 _upgrade()[key] = value; | |
184 } | |
185 } | |
186 | |
187 void addAll(Map other) { | |
188 other.forEach((key, value) { | |
189 this[key] = value; | |
190 }); | |
191 } | |
192 | |
193 bool containsValue(value) { | |
194 if (_isUpgraded) return _upgradedMap.containsValue(value); | |
195 List<String> keys = _computeKeys(); | |
196 for (int i = 0; i < keys.length; i++) { | |
197 String key = keys[i]; | |
198 if (this[key] == value) return true; | |
199 } | |
200 return false; | |
201 } | |
202 | |
203 bool containsKey(key) { | |
204 if (_isUpgraded) return _upgradedMap.containsKey(key); | |
205 if (key is !String) return false; | |
206 return _hasProperty(_original, key); | |
207 } | |
208 | |
209 putIfAbsent(key, ifAbsent()) { | |
210 if (containsKey(key)) return this[key]; | |
211 var value = ifAbsent(); | |
212 this[key] = value; | |
213 return value; | |
214 } | |
215 | |
216 remove(Object key) { | |
217 if (!_isUpgraded && !containsKey(key)) return null; | |
218 return _upgrade().remove(key); | |
219 } | |
220 | |
221 void clear() { | |
222 if (_isUpgraded) { | |
223 _upgradedMap.clear(); | |
224 } else { | |
225 if (_data != null) { | |
226 // Clear the list of keys to make sure we force | |
227 // a concurrent modification error if anyone is | |
228 // currently iterating over it. | |
229 _data.clear(); | |
230 } | |
231 _original = _processed = null; | |
232 _data = {}; | |
233 } | |
234 } | |
235 | |
236 void forEach(void f(key, value)) { | |
237 if (_isUpgraded) return _upgradedMap.forEach(f); | |
238 List<String> keys = _computeKeys(); | |
239 for (int i = 0; i < keys.length; i++) { | |
240 String key = keys[i]; | |
241 | |
242 // Compute the value under the assumption that the property | |
243 // is present but potentially not processed. | |
244 var value = _getProperty(_processed, key); | |
245 if (_isUnprocessed(value)) { | |
246 value = _convertJsonToDartLazy(_getProperty(_original, key)); | |
247 _setProperty(_processed, key, value); | |
248 } | |
249 | |
250 // Do the callback. | |
251 f(key, value); | |
252 | |
253 // Check if invoking the callback function changed | |
254 // the key set. If so, throw an exception. | |
255 if (!identical(keys, _data)) { | |
256 throw new ConcurrentModificationError(this); | |
257 } | |
258 } | |
259 } | |
260 | |
261 String toString() => Maps.mapToString(this); | |
262 | |
263 | |
264 // ------------------------------------------ | |
265 // Private helper methods. | |
266 // ------------------------------------------ | |
267 | |
268 bool get _isUpgraded => _processed == null; | |
269 | |
270 Map get _upgradedMap { | |
271 assert(_isUpgraded); | |
272 // 'cast' the union type to LinkedHashMap. It would be even better if we | |
273 // could 'cast' to the implementation type, since LinkedHashMap includes | |
274 // _JsonMap. | |
275 return JS('LinkedHashMap', '#', _data); | |
276 } | |
277 | |
278 List<String> _computeKeys() { | |
279 assert(!_isUpgraded); | |
280 List keys = _data; | |
281 if (keys == null) { | |
282 keys = _data = _getPropertyNames(_original); | |
283 } | |
284 return JS('JSExtendableArray', '#', keys); | |
285 } | |
286 | |
287 Map _upgrade() { | |
288 if (_isUpgraded) return _upgradedMap; | |
289 | |
290 // Copy all the (key, value) pairs to a freshly allocated | |
291 // linked hash map thus preserving the ordering. | |
292 Map result = {}; | |
293 List<String> keys = _computeKeys(); | |
294 for (int i = 0; i < keys.length; i++) { | |
295 String key = keys[i]; | |
296 result[key] = this[key]; | |
297 } | |
298 | |
299 // We only upgrade when we need to extend the map, so we can | |
300 // safely force a concurrent modification error in case | |
301 // someone is iterating over the map here. | |
302 if (keys.isEmpty) { | |
303 keys.add(null); | |
304 } else { | |
305 keys.clear(); | |
306 } | |
307 | |
308 // Clear out the associated JavaScript objects and mark the | |
309 // map as having been upgraded. | |
310 _original = _processed = null; | |
311 _data = result; | |
312 assert(_isUpgraded); | |
313 return result; | |
314 } | |
315 | |
316 _process(String key) { | |
317 if (!_hasProperty(_original, key)) return null; | |
318 var result = _convertJsonToDartLazy(_getProperty(_original, key)); | |
319 return _setProperty(_processed, key, result); | |
320 } | |
321 | |
322 | |
323 // ------------------------------------------ | |
324 // Private JavaScript helper methods. | |
325 // ------------------------------------------ | |
326 | |
327 static bool _hasProperty(object, String key) | |
328 => JS('bool', 'Object.prototype.hasOwnProperty.call(#,#)', object, key); | |
329 static _getProperty(object, String key) | |
330 => JS('', '#[#]', object, key); | |
331 static _setProperty(object, String key, value) | |
332 => JS('', '#[#]=#', object, key, value); | |
333 static List _getPropertyNames(object) | |
334 => JS('JSExtendableArray', 'Object.keys(#)', object); | |
335 static bool _isUnprocessed(object) | |
336 => JS('bool', 'typeof(#)=="undefined"', object); | |
337 static _newJavaScriptObject() | |
338 => JS('=Object', 'Object.create(null)'); | |
339 } | |
340 | |
341 class _JsonMapKeyIterable extends ListIterable { | |
342 final _JsonMap _parent; | |
343 | |
344 _JsonMapKeyIterable(this._parent); | |
345 | |
346 int get length => _parent.length; | |
347 | |
348 String elementAt(int index) { | |
349 return _parent._isUpgraded ? _parent.keys.elementAt(index) | |
350 : _parent._computeKeys()[index]; | |
351 } | |
352 | |
353 /// Although [ListIterable] defines its own iterator, we return the iterator | |
354 /// of the underlying list [_keys] in order to propagate | |
355 /// [ConcurrentModificationError]s. | |
356 Iterator get iterator { | |
357 return _parent._isUpgraded ? _parent.keys.iterator | |
358 : _parent._computeKeys().iterator; | |
359 } | |
360 | |
361 /// Delegate to [parent.containsKey] to ensure the performance expected | |
362 /// from [Map.keys.containsKey]. | |
363 bool contains(Object key) => _parent.containsKey(key); | |
364 } | |
365 | |
366 @patch class JsonDecoder { | |
367 @patch | |
368 StringConversionSink startChunkedConversion(Sink<Object> sink) { | |
369 return new _JsonDecoderSink(_reviver, sink); | |
370 } | |
371 } | |
372 | |
373 /** | |
374 * Implements the chunked conversion from a JSON string to its corresponding | |
375 * object. | |
376 * | |
377 * The sink only creates one object, but its input can be chunked. | |
378 */ | |
379 // TODO(floitsch): don't accumulate everything before starting to decode. | |
380 class _JsonDecoderSink extends _StringSinkConversionSink { | |
381 final _Reviver _reviver; | |
382 final Sink<Object> _sink; | |
383 | |
384 _JsonDecoderSink(this._reviver, this._sink) | |
385 : super(new StringBuffer()); | |
386 | |
387 void close() { | |
388 super.close(); | |
389 StringBuffer buffer = _stringSink; | |
390 String accumulated = buffer.toString(); | |
391 buffer.clear(); | |
392 Object decoded = _parseJson(accumulated, _reviver); | |
393 _sink.add(decoded); | |
394 _sink.close(); | |
395 } | |
396 } | |
397 | |
398 @patch class Utf8Decoder { | |
399 @patch | |
400 Converter<List<int>,dynamic> fuse(Converter<String, dynamic> next) { | |
401 return super.fuse(next); | |
402 } | |
403 | |
404 // Currently not intercepting UTF8 decoding. | |
405 @patch | |
406 static String _convertIntercepted(bool allowMalformed, List<int> codeUnits, | |
407 int start, int end) { | |
408 return null; // This call was not intercepted. | |
409 } | |
410 } | |
OLD | NEW |