Chromium Code Reviews| Index: sdk/lib/core/uri.dart |
| diff --git a/sdk/lib/core/uri.dart b/sdk/lib/core/uri.dart |
| index 73a97d80e1c3288aac66beca9735b5bf3d59c4c8..6abca996f6049b3796e894f9a8fa25fdb6e367ca 100644 |
| --- a/sdk/lib/core/uri.dart |
| +++ b/sdk/lib/core/uri.dart |
| @@ -16,8 +16,14 @@ part of dart.core; |
| * [libtour]: http://www.dartlang.org/docs/dart-up-and-running/contents/ch03.html |
| */ |
| class Uri { |
| + // The host name of the URI. |
| + // Set to `null` if there is no authority in a URI. |
| final String _host; |
| + // The port. Set to null if there is no port. Normalized to null if |
| + // the port is the default port for the scheme. |
| + // Set to the value of the default port if an empty port was supplied. |
| int _port; |
| + // The path. Always non-null. |
| String _path; |
| /** |
| @@ -25,6 +31,8 @@ class Uri { |
| * |
| * Returns the empty string if there is no scheme component. |
| */ |
| + // We represent the missing scheme as an empty string. |
| + // A valid scheme cannot be empty. |
| final String scheme; |
| /** |
| @@ -43,12 +51,22 @@ class Uri { |
| } |
| /** |
| + * The user-info part of the authority. |
| + * |
| + * Does not distinguish between an empty user-info and an absent one. |
| + * The value is always non-null. |
|
Anders Johnsen
2014/06/25 09:18:00
This contradicts with the getter below.
Lasse Reichstein Nielsen
2014/06/25 10:49:18
True, I don't need the getter anyway.
|
| + */ |
| + final String _userInfo; |
| + |
| + /** |
| * Returns the user info part of the authority component. |
| * |
| * Returns the empty string if there is no user info in the |
| * authority component. |
| */ |
| - final String userInfo; |
| + String get userInfo { |
| + return (_userInfo != null) ? _userInfo : ""; |
| + } |
| /** |
| * Returns the host part of the authority component. |
| @@ -60,7 +78,8 @@ class Uri { |
| * removed. |
| */ |
| String get host { |
| - if (_host != null && _host.startsWith('[')) { |
| + if (_host == null) return ""; |
| + if (_host.startsWith('[')) { |
| return _host.substring(1, _host.length - 1); |
| } |
| return _host; |
| @@ -69,16 +88,20 @@ class Uri { |
| /** |
| * Returns the port part of the authority component. |
| * |
| - * Returns 0 if there is no port in the authority component. |
| + * Also returns 0 if there is no port number in the authority component. |
|
Anders Johnsen
2014/06/25 09:18:00
remove 'Also', it only returns once.
Lasse Reichstein Nielsen
2014/06/25 10:49:18
Done.
|
| */ |
| int get port { |
| - if (_port == 0) { |
| - if (scheme == "http") return 80; |
| - if (scheme == "https") return 443; |
| - } |
| + if (_port == null) return _defaultPort(scheme); |
| return _port; |
| } |
| + // The default port for the scheme of this Uri.. |
| + static int _defaultPort(String scheme) { |
| + if (scheme == "http") return 80; |
| + if (scheme == "https") return 443; |
| + return 0; |
| + } |
| + |
| /** |
| * Returns the path component. |
| * |
| @@ -89,13 +112,19 @@ class Uri { |
| */ |
| String get path => _path; |
| + // The query content, or null if there is no query. |
| + final String _query; |
| + |
| /** |
| * Returns the query component. The returned query is encoded. To get |
| * direct access to the decoded query use [queryParameters]. |
| * |
| * Returns the empty string if there is no query component. |
| */ |
| - final String query; |
| + String get query => (_query == null) ? "" : _query; |
| + |
| + // The fragment content, or null if there is no fragment. |
| + final String _fragment; |
| /** |
| * Returns the fragment identifier component. |
| @@ -103,7 +132,7 @@ class Uri { |
| * Returns the empty string if there is no fragment identifier |
| * component. |
| */ |
| - final String fragment; |
| + String get fragment => (_fragment == null) ? "" : _fragment; |
| /** |
| * Cache the computed return value of [pathSegements]. |
| @@ -182,12 +211,12 @@ class Uri { |
| const int EOI = -1; |
| String scheme = ""; |
| - String path; |
| String userinfo = ""; |
| - String host = ""; |
| - int port = 0; |
| - String query = ""; |
| - String fragment = ""; |
| + String host = null; |
| + int port = null; |
| + String path = null; |
| + String query = null; |
| + String fragment = null; |
| int index = 0; |
| int pathStart = 0; |
| @@ -234,13 +263,16 @@ class Uri { |
| hostStart = lastAt + 1; |
| } |
| if (lastColon >= 0) { |
| - int portNumber = 0; |
| - for (int i = lastColon + 1; i < index; i++) { |
| - int digit = uri.codeUnitAt(i); |
| - if (_ZERO > digit || _NINE < digit) { |
| - _fail(uri, i, "Invalid port number"); |
| + int portNumber; |
| + if (lastColon + 1 < index) { |
| + portNumber = 0; |
| + for (int i = lastColon + 1; i < index; i++) { |
| + int digit = uri.codeUnitAt(i); |
| + if (_ZERO > digit || _NINE < digit) { |
| + _fail(uri, i, "Invalid port number"); |
| + } |
| + portNumber = portNumber * 10 + (digit - _ZERO); |
| } |
| - portNumber = portNumber * 10 + (digit - _ZERO); |
| } |
| port = _makePort(portNumber, scheme); |
| hostEnd = lastColon; |
| @@ -340,7 +372,7 @@ class Uri { |
| } |
| assert(state == NOT_IN_PATH); |
| - bool ensureLeadingSlash = (host != "" || scheme == "file"); |
| + bool ensureLeadingSlash = (host != null || scheme == "file"); |
| path = _makePath(uri, pathStart, index, null, ensureLeadingSlash); |
| if (char == _QUESTION) { |
| @@ -397,27 +429,27 @@ class Uri { |
| /// Internal non-verifying constructor. Only call with validated arguments. |
| Uri._internal(this.scheme, |
| - this.userInfo, |
| + this._userInfo, |
| this._host, |
| this._port, |
| this._path, |
| - this.query, |
| - this.fragment); |
| + this._query, |
| + this._fragment); |
| /** |
| * Creates a new URI from its components. |
| * |
| * Each component is set through a named argument. Any number of |
| - * components can be provided. The default value for the components |
| - * not provided is the empry string, except for [port] which has a |
| - * default value of 0. The [path] and [query] components can be set |
| - * using two different named arguments. |
| + * components can be provided. The [path] and [query] components can be set |
| + * using either of two different named arguments. |
| * |
| * The scheme component is set through [scheme]. The scheme is |
| - * normalized to all lowercase letters. |
| + * normalized to all lowercase letters. If the scheme is omitted or empty, |
| + * the URI will not have a scheme part. |
|
Anders Johnsen
2014/06/25 09:18:00
will have an empty scheme part?
Lasse Reichstein Nielsen
2014/06/25 10:49:18
That sounds like an empty string before a colon, w
|
| * |
| * The user info part of the authority component is set through |
| - * [userInfo]. |
| + * [userInfo]. It defaults to the empty string, which will be omitted |
| + * from the string representation of the URI. |
| * |
| * The host part of the authority component is set through |
| * [host]. The host can either be a hostname, an IPv4 address or an |
| @@ -426,14 +458,22 @@ class Uri { |
| * The host is normalized to all lowercase letters. |
| * |
| * The port part of the authority component is set through |
| - * [port]. The port is normalized for scheme http and https where |
| - * port 80 and port 443 respectively is set. |
| + * [port]. |
| + * If [port] is omitted or `null`, it implies the default port for |
| + * the URI's scheme, and is equivalent to passing that port explicitly. |
| + * The recognized schemes, and their default ports, are "http" (80) and |
| + * "https" (443). All other schemes are considered as having zero as the |
| + * default port. |
| + * |
| + * If any of `userInfo`, `host` or `port` are provided, |
| + * the URI will have an autority according to [hasAuthority]. |
| * |
| * The path component is set through either [path] or |
| - * [pathSegments]. When [path] is used, the provided string is |
| - * expected to be fully percent-encoded, and is used in its literal |
| - * form. When [pathSegments] is used, each of the provided segments |
| - * is percent-encoded and joined using the forward slash |
| + * [pathSegments]. When [path] is used, it should be a valid URI path, |
| + * but invalid characters, except the general delimiters ':/@[]?#', |
| + * will be escaped if necessary. |
| + * When [pathSegments] is used, each of the provided segments |
| + * is first percent-encoded and then joined using the forward slash |
| * separator. The percent-encoding of the path segments encodes all |
| * characters except for the unreserved characters and the following |
| * list of characters: `!$&'()*+,;=:@`. If the other components |
| @@ -441,32 +481,43 @@ class Uri { |
| * not already there. |
| * |
| * The query component is set through either [query] or |
| - * [queryParameters]. When [query] is used the provided string is |
| - * expected to be fully percent-encoded and is used in its literal |
| - * form. When [queryParameters] is used the query is built from the |
| + * [queryParameters]. When [query] is used the provided string should |
| + * be a valid URI query, but invalid characters other than general delimiters, |
| + * will be escaped if necessary. |
| + * When [queryParameters] is used the query is built from the |
| * provided map. Each key and value in the map is percent-encoded |
| * and joined using equal and ampersand characters. The |
| * percent-encoding of the keys and values encodes all characters |
| * except for the unreserved characters. |
| + * If both `query` and `queryParameters` are omitted or `null`, the |
| + * URI will have no query part. |
| * |
| * The fragment component is set through [fragment]. |
| + * It should be a valid URI fragment, but invalid characters other than |
| + * general delimiters, will be escaped if necessary. |
| + * If `fragment` is omitted or `null`, the URI will have no fragment part. |
| */ |
| - factory Uri({String scheme, |
| - String userInfo: "", |
| - String host: "", |
| - port: 0, |
| - String path, |
| - Iterable<String> pathSegments, |
| - String query, |
| - Map<String, String> queryParameters, |
| - fragment: ""}) { |
| + factory Uri({String scheme : "", |
| + String userInfo : "", |
| + String host, |
| + port, |
|
Anders Johnsen
2014/06/25 09:18:00
int port
Lasse Reichstein Nielsen
2014/06/25 10:49:18
Done.
|
| + String path, |
| + Iterable<String> pathSegments, |
| + String query, |
| + Map<String, String> queryParameters, |
| + fragment}) { |
| scheme = _makeScheme(scheme, _stringOrNullLength(scheme)); |
| userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); |
| host = _makeHost(host, 0, _stringOrNullLength(host), false); |
| query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); |
| fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); |
| port = _makePort(port, scheme); |
| - bool ensureLeadingSlash = (host != "" || scheme == "file"); |
| + bool isFile = (scheme == "file"); |
| + if (host == null && |
| + (userInfo.isNotEmpty || port != null || isFile)) { |
| + host = ""; |
| + } |
| + bool ensureLeadingSlash = (host != null || isFile); |
| path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
| ensureLeadingSlash); |
| @@ -496,7 +547,9 @@ class Uri { |
| * The `scheme` is always set to `http`. |
| * |
| * The `userInfo`, `host` and `port` components are set from the |
| - * [authority] argument. |
| + * [authority] argument. If `authority` is `null` or empty, |
| + * the created `Uri` will have no authority, and will not be directly usable |
| + * as an HTTP URL, which must have a non-empty host. |
| * |
| * The `path` component is set from the [unencodedPath] |
| * argument. The path passed must not be encoded as this constructor |
| @@ -527,50 +580,51 @@ class Uri { |
| String authority, |
| String unencodedPath, |
| Map<String, String> queryParameters) { |
| - var userInfo = ""; |
| - var host = ""; |
| - var port = 0; |
| - |
| - var hostStart = 0; |
| - // Split off the user info. |
| - bool hasUserInfo = false; |
| - for (int i = 0; i < authority.length; i++) { |
| - if (authority.codeUnitAt(i) == _AT_SIGN) { |
| - hasUserInfo = true; |
| - userInfo = authority.substring(0, i); |
| - hostStart = i + 1; |
| - break; |
| - } |
| - } |
| - var hostEnd = hostStart; |
| - if (hostStart < authority.length && |
| - authority.codeUnitAt(hostStart) == _LEFT_BRACKET) { |
| - // IPv6 host. |
| - for (; hostEnd < authority.length; hostEnd++) { |
| - if (authority.codeUnitAt(hostEnd) == _RIGHT_BRACKET) break; |
| - } |
| - if (hostEnd == authority.length) { |
| - throw new FormatException("Invalid IPv6 host entry."); |
| + var userInfo = null; |
| + var host = null; |
| + var port = null; |
| + |
| + if (authority != null && authority.isNotEmpty) { |
| + var hostStart = 0; |
| + // Split off the user info. |
| + bool hasUserInfo = false; |
| + for (int i = 0; i < authority.length; i++) { |
| + if (authority.codeUnitAt(i) == _AT_SIGN) { |
| + hasUserInfo = true; |
| + userInfo = authority.substring(0, i); |
| + hostStart = i + 1; |
| + break; |
| + } |
| } |
| - parseIPv6Address(authority, hostStart + 1, hostEnd); |
| - hostEnd++; // Skip the closing bracket. |
| - if (hostEnd != authority.length && |
| - authority.codeUnitAt(hostEnd) != _COLON) { |
| - throw new FormatException("Invalid end of authority"); |
| + var hostEnd = hostStart; |
| + if (hostStart < authority.length && |
| + authority.codeUnitAt(hostStart) == _LEFT_BRACKET) { |
| + // IPv6 host. |
| + for (; hostEnd < authority.length; hostEnd++) { |
| + if (authority.codeUnitAt(hostEnd) == _RIGHT_BRACKET) break; |
| + } |
| + if (hostEnd == authority.length) { |
| + throw new FormatException("Invalid IPv6 host entry."); |
| + } |
| + parseIPv6Address(authority, hostStart + 1, hostEnd); |
| + hostEnd++; // Skip the closing bracket. |
| + if (hostEnd != authority.length && |
| + authority.codeUnitAt(hostEnd) != _COLON) { |
| + throw new FormatException("Invalid end of authority"); |
| + } |
| } |
| - } |
| - // Split host and port. |
| - bool hasPort = false; |
| - for (; hostEnd < authority.length; hostEnd++) { |
| - if (authority.codeUnitAt(hostEnd) == _COLON) { |
| - var portString = authority.substring(hostEnd + 1); |
| - // We allow the empty port - falling back to initial value. |
| - if (portString.isNotEmpty) port = int.parse(portString); |
| - break; |
| + // Split host and port. |
| + bool hasPort = false; |
| + for (; hostEnd < authority.length; hostEnd++) { |
| + if (authority.codeUnitAt(hostEnd) == _COLON) { |
| + var portString = authority.substring(hostEnd + 1); |
| + // We allow the empty port - falling back to initial value. |
| + if (portString.isNotEmpty) port = int.parse(portString); |
| + break; |
| + } |
| } |
| + host = authority.substring(hostStart, hostEnd); |
| } |
| - host = authority.substring(hostStart, hostEnd); |
| - |
| return new Uri(scheme: scheme, |
| userInfo: userInfo, |
| host: host, |
| @@ -721,7 +775,7 @@ class Uri { |
| static _makeFileUri(String path) { |
| String sep = "/"; |
| - if (path.length > 0 && path[0] == sep) { |
| + if (path.startsWith(sep)) { |
| // Absolute file:// URI. |
| return new Uri(scheme: "file", pathSegments: path.split(sep)); |
| } else { |
| @@ -830,12 +884,7 @@ class Uri { |
| static int _makePort(int port, String scheme) { |
| // Perform scheme specific normalization. |
| - if (port == 80 && scheme == "http") { |
| - return 0; |
| - } |
| - if (port == 443 && scheme == "https") { |
| - return 0; |
| - } |
| + if (port != null && port == _defaultPort(scheme)) return null; |
| return port; |
| } |
| @@ -852,7 +901,6 @@ class Uri { |
| */ |
| static String _makeHost(String host, int start, int end, bool strictIPv6) { |
| // TODO(lrn): Should we normalize IPv6 addresses according to RFC 5952? |
| - |
| if (host == null) return null; |
| if (start == end) return ""; |
| // Host is an IPv6 address if it starts with '[' or contains a colon. |
| @@ -985,7 +1033,7 @@ class Uri { |
| } |
| static String _makeUserInfo(String userInfo, int start, int end) { |
| - if (userInfo == null) return "null"; |
| + if (userInfo == null) return ""; |
| return _normalize(userInfo, start, end, _userinfoTable); |
| } |
| @@ -1010,7 +1058,7 @@ class Uri { |
| static String _makeQuery(String query, int start, int end, |
| Map<String, String> queryParameters) { |
| - if (query == null && queryParameters == null) return ""; |
| + if (query == null && queryParameters == null) return null; |
| if (query != null && queryParameters != null) { |
| throw new ArgumentError('Both query and queryParameters specified'); |
| } |
| @@ -1033,7 +1081,7 @@ class Uri { |
| } |
| static String _makeFragment(String fragment, int start, int end) { |
| - if (fragment == null) return ""; |
| + if (fragment == null) return null; |
| return _normalize(fragment, start, end, _queryCharTable); |
| } |
| @@ -1278,55 +1326,81 @@ class Uri { |
| int targetPort; |
| String targetPath; |
| String targetQuery; |
| - if (reference.scheme != "") { |
| + if (reference.scheme.isNotEmpty) { |
| targetScheme = reference.scheme; |
| - targetUserInfo = reference.userInfo; |
| - targetHost = reference.host; |
| - targetPort = reference.port; |
| + if (reference.hasAuthority) { |
| + targetUserInfo = reference.userInfo; |
| + targetHost = reference.host; |
| + targetPort = reference.hasPort ? reference.port : null; |
| + } |
| targetPath = _removeDotSegments(reference.path); |
| - targetQuery = reference.query; |
| + if (reference.hasQuery) { |
| + targetQuery = reference.query; |
| + } |
| } else { |
| + targetScheme = this.scheme; |
| if (reference.hasAuthority) { |
| targetUserInfo = reference.userInfo; |
| targetHost = reference.host; |
| - targetPort = reference.port; |
| + targetPort = _makePort(reference.hasPort ? reference.port : null, |
| + targetScheme); |
| targetPath = _removeDotSegments(reference.path); |
| - targetQuery = reference.query; |
| + if (reference.hasQuery) targetQuery = reference.query; |
| } else { |
| if (reference.path == "") { |
| - targetPath = this.path; |
| - if (reference.query != "") { |
| + targetPath = this._path; |
| + if (reference.hasQuery) { |
| targetQuery = reference.query; |
| } else { |
| - targetQuery = this.query; |
| + targetQuery = this._query; |
| } |
| } else { |
| if (reference.path.startsWith("/")) { |
| targetPath = _removeDotSegments(reference.path); |
| } else { |
| - targetPath = _removeDotSegments(_merge(this.path, reference.path)); |
| + targetPath = _removeDotSegments(_merge(this._path, reference.path)); |
| } |
| - targetQuery = reference.query; |
| + if (reference.hasQuery) targetQuery = reference.query; |
| } |
| - targetUserInfo = this.userInfo; |
| - targetHost = this.host; |
| - targetPort = this.port; |
| + targetUserInfo = this._userInfo; |
| + targetHost = this._host; |
| + targetPort = this._port; |
| } |
| - targetScheme = this.scheme; |
| } |
| - return new Uri(scheme: targetScheme, |
| - userInfo: targetUserInfo, |
| - host: targetHost, |
| - port: targetPort, |
| - path: targetPath, |
| - query: targetQuery, |
| - fragment: reference.fragment); |
| + String fragment = reference.hasFragment ? reference.fragment : null; |
| + return new Uri._internal(targetScheme, |
| + targetUserInfo, |
| + targetHost, |
| + targetPort, |
| + targetPath, |
| + targetQuery, |
| + fragment); |
| } |
| /** |
| * Returns whether the URI has an [authority] component. |
| */ |
| - bool get hasAuthority => host != ""; |
| + bool get hasAuthority => _host != null; |
| + |
| + /** |
| + * Returns whether the URI has an explicit port. |
| + * |
| + * If the port number is the default port number |
| + * (zero for unrecognized schemes, with http (80) and https (443) being |
| + * recognized), |
| + * then the port is made implicit and omitted from the URI. |
| + */ |
| + bool get hasPort => _port != null; |
| + |
| + /** |
| + * Returns whether the URI has a query part. |
| + */ |
| + bool get hasQuery => _query != null; |
| + |
| + /** |
| + * Returns whether the URI has a fragment part. |
| + */ |
| + bool get hasFragment => _fragment != null; |
| /** |
| * Returns the origin of the URI in the form scheme://host:port for the |
| @@ -1344,7 +1418,7 @@ class Uri { |
| throw new StateError( |
| "Origin is only applicable schemes http and https: $this"); |
| } |
| - if (_port == 0) return "$scheme://$_host"; |
| + if (_port == null) return "$scheme://$_host"; |
|
Anders Johnsen
2014/06/25 09:18:00
Maybe use the new getters?
Lasse Reichstein Nielsen
2014/06/25 10:49:18
hasPort?
I *try* not to use public overridable m
|
| return "$scheme://$_host:$_port"; |
| } |
| @@ -1471,11 +1545,14 @@ class Uri { |
| } |
| void _writeAuthority(StringSink ss) { |
| - _addIfNonEmpty(ss, userInfo, userInfo, "@"); |
| - ss.write(_host == null ? "null" : _host); |
| - if (_port != 0) { |
| + if (_userInfo.isNotEmpty) { |
| + ss.write(_userInfo); |
| + ss.write("@"); |
| + } |
| + if (_host != null) ss.write(_host); |
| + if (_port != null) { |
| ss.write(":"); |
| - ss.write(_port.toString()); |
| + ss.write(_port); |
| } |
| } |
| @@ -1489,8 +1566,8 @@ class Uri { |
| _writeAuthority(sb); |
| } |
| sb.write(path); |
| - _addIfNonEmpty(sb, query, "?", query); |
| - _addIfNonEmpty(sb, fragment, "#", fragment); |
| + if (_query != null) { sb..write("?")..write(_query); } |
| + if (_fragment != null) { sb..write("#")..write(_fragment); } |
| return sb.toString(); |
| } |