diff --git a/lib/src/body.dart b/lib/src/body.dart new file mode 100644 index 0000000000..e3589d0326 --- /dev/null +++ b/lib/src/body.dart @@ -0,0 +1,96 @@ +// Copyright (c) 2017, 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. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:async/async.dart'; +import 'package:collection/collection.dart'; + +/// The body of a request or response. +/// +/// This tracks whether the body has been read. It's separate from [Message] +/// because the message may be changed with [Message.change], but each instance +/// should share a notion of whether the body was read. +class Body { + /// The contents of the message body. + /// + /// This will be `null` after [read] is called. + Stream> _stream; + + /// The encoding used to encode the stream returned by [read], or `null` if no + /// encoding was used. + final Encoding encoding; + + /// The length of the stream returned by [read], or `null` if that can't be + /// determined efficiently. + final int contentLength; + + Body._(this._stream, this.encoding, this.contentLength); + + /// Converts [body] to a byte stream and wraps it in a [Body]. + /// + /// [body] may be either a [Body], a [String], a [List], a + /// [Stream>], or `null`. If it's a [String], [encoding] will be + /// used to convert it to a [Stream>]. + factory Body(body, [Encoding encoding]) { + if (body is Body) return body; + + Stream> stream; + int contentLength; + if (body == null) { + contentLength = 0; + stream = new Stream.fromIterable([]); + } else if (body is String) { + if (encoding == null) { + var encoded = UTF8.encode(body); + // If the text is plain ASCII, don't modify the encoding. This means + // that an encoding of "text/plain" will stay put. + if (!_isPlainAscii(encoded, body.length)) encoding = UTF8; + contentLength = encoded.length; + stream = new Stream.fromIterable([encoded]); + } else { + var encoded = encoding.encode(body); + contentLength = encoded.length; + stream = new Stream.fromIterable([encoded]); + } + } else if (body is List) { + contentLength = body.length; + stream = new Stream.fromIterable([DelegatingList.typed(body)]); + } else if (body is Stream) { + stream = DelegatingStream.typed(body); + } else { + throw new ArgumentError('Response body "$body" must be a String or a ' + 'Stream.'); + } + + return new Body._(stream, encoding, contentLength); + } + + /// Returns whether [bytes] is plain ASCII. + /// + /// [codeUnits] is the number of code units in the original string. + static bool _isPlainAscii(List bytes, int codeUnits) { + // Most non-ASCII code units will produce multiple bytes and make the text + // longer. + if (bytes.length != codeUnits) return false; + + // Non-ASCII code units between U+0080 and U+009F produce 8-bit characters + // with the high bit set. + return bytes.every((byte) => byte & 0x80 == 0); + } + + /// Returns a [Stream] representing the body. + /// + /// Can only be called once. + Stream> read() { + if (_stream == null) { + throw new StateError("The 'read' method can only be called once on a " + "http.Request/http.Response object."); + } + var stream = _stream; + _stream = null; + return stream; + } +} diff --git a/lib/src/http_unmodifiable_map.dart b/lib/src/http_unmodifiable_map.dart new file mode 100644 index 0000000000..5769dcc6c8 --- /dev/null +++ b/lib/src/http_unmodifiable_map.dart @@ -0,0 +1,58 @@ +// Copyright (c) 2017, 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. + +import 'dart:collection'; + +import 'package:collection/collection.dart'; +import 'package:http_parser/http_parser.dart'; + +/// A simple wrapper over [UnmodifiableMapView] which avoids re-wrapping itself. +class HttpUnmodifiableMap extends UnmodifiableMapView { + /// `true` if the key values are already lowercase. + final bool _ignoreKeyCase; + + /// If [source] is a [HttpUnmodifiableMap] with matching [ignoreKeyCase], + /// then [source] is returned. + /// + /// If [source] is `null` it is treated like an empty map. + /// + /// If [ignoreKeyCase] is `true`, the keys will have case-insensitive access. + /// + /// [source] is copied to a new [Map] to ensure changes to the parameter value + /// after constructions are not reflected. + factory HttpUnmodifiableMap(Map source, + {bool ignoreKeyCase: false}) { + if (source is HttpUnmodifiableMap && + // !ignoreKeyCase: no transformation of the input is required + // source._ignoreKeyCase: the input cannot be transformed any more + (!ignoreKeyCase || source._ignoreKeyCase)) { + return source; + } + + if (source == null || source.isEmpty) { + return const _EmptyHttpUnmodifiableMap(); + } + + if (ignoreKeyCase) { + source = new CaseInsensitiveMap.from(source); + } else { + source = new Map.from(source); + } + + return new HttpUnmodifiableMap._(source, ignoreKeyCase); + } + + /// Returns an empty [HttpUnmodifiableMap]. + const factory HttpUnmodifiableMap.empty() = _EmptyHttpUnmodifiableMap; + + HttpUnmodifiableMap._(Map source, this._ignoreKeyCase) + : super(source); +} + +/// A const implementation of an empty [HttpUnmodifiableMap]. +class _EmptyHttpUnmodifiableMap extends MapView + implements HttpUnmodifiableMap { + bool get _ignoreKeyCase => true; + const _EmptyHttpUnmodifiableMap() : super(const {}); +}