OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012, 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 /// Generic utility functions. Stuff that should possibly be in core. | |
6 library utils; | |
7 | |
8 import 'dart:async'; | |
9 import 'dart:crypto'; | |
10 import 'dart:io'; | |
11 import 'dart:isolate'; | |
12 import 'dart:uri'; | |
13 | |
14 /// A pair of values. | |
15 class Pair<E, F> { | |
16 E first; | |
17 F last; | |
18 | |
19 Pair(this.first, this.last); | |
20 | |
21 String toString() => '($first, $last)'; | |
22 | |
23 bool operator==(other) { | |
24 if (other is! Pair) return false; | |
25 return other.first == first && other.last == last; | |
26 } | |
27 | |
28 int get hashCode => first.hashCode ^ last.hashCode; | |
29 } | |
30 | |
31 /// A completer that waits until all added [Future]s complete. | |
32 // TODO(rnystrom): Copied from web_components. Remove from here when it gets | |
33 // added to dart:core. (See #6626.) | |
34 class FutureGroup<T> { | |
35 int _pending = 0; | |
36 Completer<List<T>> _completer = new Completer<List<T>>(); | |
37 final List<Future<T>> futures = <Future<T>>[]; | |
38 bool completed = false; | |
39 | |
40 final List<T> _values = <T>[]; | |
41 | |
42 /// Wait for [task] to complete. | |
43 Future<T> add(Future<T> task) { | |
44 if (completed) { | |
45 throw new StateError("The FutureGroup has already completed."); | |
46 } | |
47 | |
48 _pending++; | |
49 futures.add(task.then((value) { | |
50 if (completed) return; | |
51 | |
52 _pending--; | |
53 _values.add(value); | |
54 | |
55 if (_pending <= 0) { | |
56 completed = true; | |
57 _completer.complete(_values); | |
58 } | |
59 }).catchError((e) { | |
60 if (completed) return; | |
61 | |
62 completed = true; | |
63 _completer.completeError(e); | |
64 })); | |
65 | |
66 return task; | |
67 } | |
68 | |
69 Future<List> get future => _completer.future; | |
70 } | |
71 | |
72 // TODO(rnystrom): Move into String? | |
73 /// Pads [source] to [length] by adding spaces at the end. | |
74 String padRight(String source, int length) { | |
75 final result = new StringBuffer(); | |
76 result.write(source); | |
77 | |
78 while (result.length < length) { | |
79 result.write(' '); | |
80 } | |
81 | |
82 return result.toString(); | |
83 } | |
84 | |
85 /// Flattens nested lists inside an iterable into a single list containing only | |
86 /// non-list elements. | |
87 List flatten(Iterable nested) { | |
88 var result = []; | |
89 helper(list) { | |
90 for (var element in list) { | |
91 if (element is List) { | |
92 helper(element); | |
93 } else { | |
94 result.add(element); | |
95 } | |
96 } | |
97 } | |
98 helper(nested); | |
99 return result; | |
100 } | |
101 | |
102 /// Asserts that [iter] contains only one element, and returns it. | |
103 only(Iterable iter) { | |
104 var iterator = iter.iterator; | |
105 var currentIsValid = iterator.moveNext(); | |
106 assert(currentIsValid); | |
107 var obj = iterator.current; | |
108 assert(!iterator.moveNext()); | |
109 return obj; | |
110 } | |
111 | |
112 /// Returns a set containing all elements in [minuend] that are not in | |
113 /// [subtrahend]. | |
114 Set setMinus(Iterable minuend, Iterable subtrahend) { | |
115 var minuendSet = new Set.from(minuend); | |
116 minuendSet.removeAll(subtrahend); | |
117 return minuendSet; | |
118 } | |
119 | |
120 /// Replace each instance of [matcher] in [source] with the return value of | |
121 /// [fn]. | |
122 String replace(String source, Pattern matcher, String fn(Match)) { | |
123 var buffer = new StringBuffer(); | |
124 var start = 0; | |
125 for (var match in matcher.allMatches(source)) { | |
126 buffer.write(source.substring(start, match.start)); | |
127 start = match.end; | |
128 buffer.write(fn(match)); | |
129 } | |
130 buffer.write(source.substring(start)); | |
131 return buffer.toString(); | |
132 } | |
133 | |
134 /// Returns whether or not [str] ends with [matcher]. | |
135 bool endsWithPattern(String str, Pattern matcher) { | |
136 for (var match in matcher.allMatches(str)) { | |
137 if (match.end == str.length) return true; | |
138 } | |
139 return false; | |
140 } | |
141 | |
142 /// Returns the hex-encoded sha1 hash of [source]. | |
143 String sha1(String source) { | |
144 var sha = new SHA1(); | |
145 sha.add(source.codeUnits); | |
146 return CryptoUtils.bytesToHex(sha.close()); | |
147 } | |
148 | |
149 /// Returns a [Future] that completes in [milliseconds]. | |
150 Future sleep(int milliseconds) { | |
151 var completer = new Completer(); | |
152 new Timer(new Duration(milliseconds: milliseconds), completer.complete); | |
153 return completer.future; | |
154 } | |
155 | |
156 /// Configures [future] so that its result (success or exception) is passed on | |
157 /// to [completer]. | |
158 void chainToCompleter(Future future, Completer completer) { | |
159 future.then((value) => completer.complete(value), | |
160 onError: (e) => completer.completeError(e)); | |
161 } | |
162 | |
163 // TODO(nweiz): remove this when issue 7964 is fixed. | |
164 /// Returns a [Future] that will complete to the first element of [stream]. | |
165 /// Unlike [Stream.first], this is safe to use with single-subscription streams. | |
166 Future streamFirst(Stream stream) { | |
167 var completer = new Completer(); | |
168 var subscription; | |
169 subscription = stream.listen((value) { | |
170 subscription.cancel(); | |
171 completer.complete(value); | |
172 }, onError: (e) { | |
173 completer.completeError(e); | |
174 }, onDone: () { | |
175 completer.completeError(new StateError("No elements")); | |
176 }, cancelOnError: true); | |
177 return completer.future; | |
178 } | |
179 | |
180 /// Returns a wrapped version of [stream] along with a [StreamSubscription] that | |
181 /// can be used to control the wrapped stream. | |
182 Pair<Stream, StreamSubscription> streamWithSubscription(Stream stream) { | |
183 var controller = new StreamController(); | |
184 var controllerStream = stream.isBroadcast ? | |
185 controller.stream.asBroadcastStream() : | |
186 controller.stream; | |
187 var subscription = stream.listen(controller.add, | |
188 onError: controller.addError, | |
189 onDone: controller.close); | |
190 return new Pair<Stream, StreamSubscription>(controllerStream, subscription); | |
191 } | |
192 | |
193 // TODO(nweiz): remove this when issue 7787 is fixed. | |
194 /// Creates two single-subscription [Stream]s that each emit all values and | |
195 /// errors from [stream]. This is useful if [stream] is single-subscription but | |
196 /// multiple subscribers are necessary. | |
197 Pair<Stream, Stream> tee(Stream stream) { | |
198 var controller1 = new StreamController(); | |
199 var controller2 = new StreamController(); | |
200 stream.listen((value) { | |
201 controller1.add(value); | |
202 controller2.add(value); | |
203 }, onError: (error) { | |
204 controller1.addError(error); | |
205 controller2.addError(error); | |
206 }, onDone: () { | |
207 controller1.close(); | |
208 controller2.close(); | |
209 }); | |
210 return new Pair<Stream, Stream>(controller1.stream, controller2.stream); | |
211 } | |
212 | |
213 /// A regular expression matching a trailing CR character. | |
214 final _trailingCR = new RegExp(r"\r$"); | |
215 | |
216 // TODO(nweiz): Use `text.split(new RegExp("\r\n?|\n\r?"))` when issue 9360 is | |
217 // fixed. | |
218 /// Splits [text] on its line breaks in a Windows-line-break-friendly way. | |
219 List<String> splitLines(String text) => | |
220 text.split("\n").map((line) => line.replaceFirst(_trailingCR, "")).toList(); | |
221 | |
222 /// Converts a stream of arbitrarily chunked strings into a line-by-line stream. | |
223 /// The lines don't include line termination characters. A single trailing | |
224 /// newline is ignored. | |
225 Stream<String> streamToLines(Stream<String> stream) { | |
226 var buffer = new StringBuffer(); | |
227 return stream.transform(new StreamTransformer( | |
228 handleData: (chunk, sink) { | |
229 var lines = splitLines(chunk); | |
230 var leftover = lines.removeLast(); | |
231 for (var line in lines) { | |
232 if (!buffer.isEmpty) { | |
233 buffer.write(line); | |
234 line = buffer.toString(); | |
235 buffer = new StringBuffer(); | |
236 } | |
237 | |
238 sink.add(line); | |
239 } | |
240 buffer.write(leftover); | |
241 }, | |
242 handleDone: (sink) { | |
243 if (!buffer.isEmpty) sink.add(buffer.toString()); | |
244 sink.close(); | |
245 })); | |
246 } | |
247 | |
248 /// Like [Iterable.where], but allows [test] to return [Future]s and uses the | |
249 /// results of those [Future]s as the test. | |
250 Future<Iterable> futureWhere(Iterable iter, test(value)) { | |
251 return Future.wait(iter.map((e) { | |
252 var result = test(e); | |
253 if (result is! Future) result = new Future.value(result); | |
254 return result.then((result) => new Pair(e, result)); | |
255 })) | |
256 .then((pairs) => pairs.where((pair) => pair.last)) | |
257 .then((pairs) => pairs.map((pair) => pair.first)); | |
258 } | |
259 | |
260 // TODO(nweiz): unify the following functions with the utility functions in | |
261 // pkg/http. | |
262 | |
263 /// Like [String.split], but only splits on the first occurrence of the pattern. | |
264 /// This will always return an array of two elements or fewer. | |
265 List<String> split1(String toSplit, String pattern) { | |
266 if (toSplit.isEmpty) return <String>[]; | |
267 | |
268 var index = toSplit.indexOf(pattern); | |
269 if (index == -1) return [toSplit]; | |
270 return [toSplit.substring(0, index), | |
271 toSplit.substring(index + pattern.length)]; | |
272 } | |
273 | |
274 /// Adds additional query parameters to [url], overwriting the original | |
275 /// parameters if a name conflict occurs. | |
276 Uri addQueryParameters(Uri url, Map<String, String> parameters) { | |
277 var queryMap = queryToMap(url.query); | |
278 mapAddAll(queryMap, parameters); | |
279 return url.resolve("?${mapToQuery(queryMap)}"); | |
280 } | |
281 | |
282 /// Convert a URL query string (or `application/x-www-form-urlencoded` body) | |
283 /// into a [Map] from parameter names to values. | |
284 Map<String, String> queryToMap(String queryList) { | |
285 var map = {}; | |
286 for (var pair in queryList.split("&")) { | |
287 var split = split1(pair, "="); | |
288 if (split.isEmpty) continue; | |
289 var key = urlDecode(split[0]); | |
290 var value = split.length > 1 ? urlDecode(split[1]) : ""; | |
291 map[key] = value; | |
292 } | |
293 return map; | |
294 } | |
295 | |
296 /// Convert a [Map] from parameter names to values to a URL query string. | |
297 String mapToQuery(Map<String, String> map) { | |
298 var pairs = <List<String>>[]; | |
299 map.forEach((key, value) { | |
300 key = encodeUriComponent(key); | |
301 value = (value == null || value.isEmpty) ? null : encodeUriComponent(value); | |
302 pairs.add([key, value]); | |
303 }); | |
304 return pairs.map((pair) { | |
305 if (pair[1] == null) return pair[0]; | |
306 return "${pair[0]}=${pair[1]}"; | |
307 }).join("&"); | |
308 } | |
309 | |
310 // TODO(nweiz): remove this when issue 9068 has been fixed. | |
311 /// Whether [uri1] and [uri2] are equal. This consider HTTP URIs to default to | |
312 /// port 80, and HTTPs URIs to default to port 443. | |
313 bool urisEqual(Uri uri1, Uri uri2) => | |
314 canonicalizeUri(uri1) == canonicalizeUri(uri2); | |
315 | |
316 /// Return [uri] with redundant port information removed. | |
317 Uri canonicalizeUri(Uri uri) { | |
318 if (uri == null) return null; | |
319 | |
320 var sansPort = new Uri.fromComponents( | |
321 scheme: uri.scheme, userInfo: uri.userInfo, domain: uri.domain, | |
322 path: uri.path, query: uri.query, fragment: uri.fragment); | |
323 if (uri.scheme == 'http' && uri.port == 80) return sansPort; | |
324 if (uri.scheme == 'https' && uri.port == 443) return sansPort; | |
325 return uri; | |
326 } | |
327 | |
328 /// Add all key/value pairs from [source] to [destination], overwriting any | |
329 /// pre-existing values. | |
330 void mapAddAll(Map destination, Map source) => | |
331 source.forEach((key, value) => destination[key] = value); | |
332 | |
333 /// Decodes a URL-encoded string. Unlike [decodeUriComponent], this includes | |
334 /// replacing `+` with ` `. | |
335 String urlDecode(String encoded) => | |
336 decodeUriComponent(encoded.replaceAll("+", " ")); | |
337 | |
338 /// Takes a simple data structure (composed of [Map]s, [Iterable]s, scalar | |
339 /// objects, and [Future]s) and recursively resolves all the [Future]s contained | |
340 /// within. Completes with the fully resolved structure. | |
341 Future awaitObject(object) { | |
342 // Unroll nested futures. | |
343 if (object is Future) return object.then(awaitObject); | |
344 if (object is Iterable) { | |
345 return Future.wait(object.map(awaitObject).toList()); | |
346 } | |
347 if (object is! Map) return new Future.value(object); | |
348 | |
349 var pairs = <Future<Pair>>[]; | |
350 object.forEach((key, value) { | |
351 pairs.add(awaitObject(value) | |
352 .then((resolved) => new Pair(key, resolved))); | |
353 }); | |
354 return Future.wait(pairs).then((resolvedPairs) { | |
355 var map = {}; | |
356 for (var pair in resolvedPairs) { | |
357 map[pair.first] = pair.last; | |
358 } | |
359 return map; | |
360 }); | |
361 } | |
362 | |
363 /// An exception class for exceptions that are intended to be seen by the user. | |
364 /// These exceptions won't have any debugging information printed when they're | |
365 /// thrown. | |
366 class ApplicationException implements Exception { | |
367 final String message; | |
368 | |
369 ApplicationException(this.message); | |
370 } | |
371 | |
372 /// Throw a [ApplicationException] with [message]. | |
373 void fail(String message) { | |
374 throw new ApplicationException(message); | |
375 } | |
376 | |
377 /// Returns whether [error] is a user-facing error object. This includes both | |
378 /// [ApplicationException] and any dart:io errors. | |
379 bool isUserFacingException(error) { | |
380 return error is ApplicationException || | |
381 // TODO(nweiz): clean up this branch when issue 9955 is fixed. | |
382 error is DirectoryIOException || | |
383 error is FileIOException || | |
384 error is HttpException || | |
385 error is HttpParserException || | |
386 error is LinkIOException || | |
387 error is MimeParserException || | |
388 error is OSError || | |
389 error is ProcessException || | |
390 error is SocketIOException || | |
391 error is WebSocketException; | |
392 } | |
OLD | NEW |