| 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 |