OLD | NEW |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
| 5 #library('uri'); |
| 6 |
5 /** | 7 /** |
6 * A parsed URI, inspired by: | 8 * A parsed URI, inspired by: |
7 * http://closure-library.googlecode.com/svn/docs/class_goog_Uri.html | 9 * http://closure-library.googlecode.com/svn/docs/class_goog_Uri.html |
8 */ | 10 */ |
9 class Uri { | 11 class Uri { |
10 /** | 12 final String scheme; |
11 * Parses a URL query string into a map. Because you can have multiple values | 13 final String userInfo; |
12 * for the same parameter name, each parameter name maps to a list of | 14 final String domain; |
13 * values. For example, '?a=b&c=d&a=e' would be parsed as | 15 final int port; |
14 * [{'a':['b','e'],'c':['d']}]. | 16 final String path; |
15 */ | 17 final String query; |
16 // TODO(jmesserly): consolidate with new Uri.fromString(...) | 18 final String fragment; |
17 static Map<String, List<String>> parseQuery(String queryString) { | |
18 final queryParams = new Map<String, List<String>>(); | |
19 if (queryString.startsWith('?')) { | |
20 final params = queryString.substring(1, queryString.length).split('&'); | |
21 for (final param in params) { | |
22 List<String> parts = param.split('='); | |
23 if (parts.length == 2) { | |
24 // TODO(hiltonc) the name and value should be URL decoded. | |
25 String name = parts[0]; | |
26 String value = parts[1]; | |
27 | 19 |
28 // Create a list of values for this name if not yet done. | 20 Uri.fromString(String uri) : this._fromMatch(_splitRe.firstMatch(uri)); |
29 List values = queryParams[name]; | |
30 if (values === null) { | |
31 values = new List(); | |
32 queryParams[name] = values; | |
33 } | |
34 | 21 |
35 values.add(value); | 22 Uri._fromMatch(Match m) : this(_emptyIfNull(m[_COMPONENT_SCHEME]), |
36 } | 23 _emptyIfNull(m[_COMPONENT_USER_INFO]), |
37 } | 24 _emptyIfNull(m[_COMPONENT_DOMAIN]), |
38 } | 25 _parseIntOrZero(m[_COMPONENT_PORT]), |
39 return queryParams; | 26 _emptyIfNull(m[_COMPONENT_PATH]), |
40 } | 27 _emptyIfNull(m[_COMPONENT_QUERY_DATA]), |
| 28 _emptyIfNull(m[_COMPONENT_FRAGMENT])); |
41 | 29 |
42 /** | 30 const Uri([String this.scheme = "", String this.userInfo ="", |
43 * Percent-encodes a string for use as a query parameter in a URI. | 31 String this.domain = "", int this.port = 0, |
44 */ | 32 String this.path = "", String this.query = "", |
45 // TODO(rnystrom): Get rid of this when the real encodeURIComponent() | 33 String this.fragment = ""]); |
46 // function is available within Dart. | |
47 static String encodeComponent(String component) { | |
48 if (component == null) return component; | |
49 | 34 |
50 // TODO(terry): Added b/5096547 to track replace should by default behave | 35 static String _emptyIfNull(String val) => val != null ? val : ''; |
51 // like replaceAll to avoid a problematic usage pattern. | |
52 return component.replaceAll(':', '%3A') | |
53 .replaceAll('/', '%2F') | |
54 .replaceAll('?', '%3F') | |
55 .replaceAll('=', '%3D') | |
56 .replaceAll('&', '%26') | |
57 .replaceAll(' ', '%20'); | |
58 } | |
59 | |
60 /** | |
61 * Decodes a string used a query parameter by replacing percent-encoded | |
62 * sequences with their original characters. | |
63 */ | |
64 // TODO(jmesserly): replace this with a better implementation | |
65 static String decodeComponent(String component) { | |
66 if (component == null) return component; | |
67 | |
68 return component.replaceAll('%3A', ':') | |
69 .replaceAll('%2F', '/') | |
70 .replaceAll('%3F', '?') | |
71 .replaceAll('%3D', '=') | |
72 .replaceAll('%26', '&') | |
73 .replaceAll('%20', ' '); | |
74 } | |
75 | |
76 String scheme; | |
77 String userInfo; | |
78 String domain; | |
79 int port; | |
80 String path; | |
81 String query; | |
82 String fragment; | |
83 | |
84 Uri.fromString(String uri) { | |
85 final m = _splitRe.firstMatch(uri); | |
86 | |
87 scheme = _decodeOrEmpty(m[_COMPONENT_SCHEME]); | |
88 userInfo = _decodeOrEmpty(m[_COMPONENT_USER_INFO]); | |
89 domain = _decodeOrEmpty(m[_COMPONENT_DOMAIN]); | |
90 port = _parseIntOrZero(m[_COMPONENT_PORT]); | |
91 path = _decodeOrEmpty(m[_COMPONENT_PATH]); | |
92 query = _decodeOrEmpty(m[_COMPONENT_QUERY_DATA]); | |
93 fragment = _decodeOrEmpty(m[_COMPONENT_FRAGMENT]); | |
94 } | |
95 | |
96 static String _decodeOrEmpty(String val) { | |
97 // TODO(jmesserly): use Uri.decodeComponent when available | |
98 //return val ? Uri.decodeComponent(val) : ''; | |
99 return val != null ? val : ''; | |
100 } | |
101 | 36 |
102 static int _parseIntOrZero(String val) { | 37 static int _parseIntOrZero(String val) { |
103 if (val !== null && val != '') { | 38 if (val !== null && val != '') { |
104 return Math.parseInt(val); | 39 return Math.parseInt(val); |
105 } else { | 40 } else { |
106 return 0; | 41 return 0; |
107 } | 42 } |
108 } | 43 } |
109 | 44 |
110 // NOTE: This code was ported from: closure-library/closure/goog/uri/utils.js | 45 // NOTE: This code was ported from: closure-library/closure/goog/uri/utils.js |
111 static RegExp _splitReLazy; | 46 static final RegExp _splitRe = const RegExp( |
112 | 47 '^' + |
113 static RegExp get _splitRe() { | 48 '(?:' + |
114 if (_splitReLazy == null) { | 49 '([^:/?#.]+)' + // scheme - ignore special characters |
115 _splitReLazy = new RegExp( | 50 // used by other URL parts such as :, |
116 '^' + | 51 // ?, /, #, and . |
117 '(?:' + | 52 ':)?' + |
118 '([^:/?#.]+)' + // scheme - ignore special characters | 53 '(?://' + |
119 // used by other URL parts such as :, | 54 '(?:([^/?#]*)@)?' + // userInfo |
120 // ?, /, #, and . | 55 '([\\w\\d\\-\\u0100-\\uffff.%]*)' + |
121 ':)?' + | 56 // domain - restrict to letters, |
122 '(?://' + | 57 // digits, dashes, dots, percent |
123 '(?:([^/?#]*)@)?' + // userInfo | 58 // escapes, and unicode characters. |
124 '([\\w\\d\\-\\u0100-\\uffff.%]*)' + | 59 '(?::([0-9]+))?' + // port |
125 // domain - restrict to letters, | 60 ')?' + |
126 // digits, dashes, dots, percent | 61 '([^?#]+)?' + // path |
127 // escapes, and unicode characters. | 62 '(?:\\?([^#]*))?' + // query |
128 '(?::([0-9]+))?' + // port | 63 '(?:#(.*))?' + // fragment |
129 ')?' + | 64 '\$'); |
130 '([^?#]+)?' + // path | |
131 '(?:\\?([^#]*))?' + // query | |
132 '(?:#(.*))?' + // fragment | |
133 '\$'); | |
134 } | |
135 return _splitReLazy; | |
136 } | |
137 | 65 |
138 static final _COMPONENT_SCHEME = 1; | 66 static final _COMPONENT_SCHEME = 1; |
139 static final _COMPONENT_USER_INFO = 2; | 67 static final _COMPONENT_USER_INFO = 2; |
140 static final _COMPONENT_DOMAIN = 3; | 68 static final _COMPONENT_DOMAIN = 3; |
141 static final _COMPONENT_PORT = 4; | 69 static final _COMPONENT_PORT = 4; |
142 static final _COMPONENT_PATH = 5; | 70 static final _COMPONENT_PATH = 5; |
143 static final _COMPONENT_QUERY_DATA = 6; | 71 static final _COMPONENT_QUERY_DATA = 6; |
144 static final _COMPONENT_FRAGMENT = 7; | 72 static final _COMPONENT_FRAGMENT = 7; |
| 73 |
| 74 /** |
| 75 * Determines whether a URI is absolute. |
| 76 * |
| 77 * See: http://tools.ietf.org/html/rfc3986#section-4.3 |
| 78 */ |
| 79 bool isAbsolute() { |
| 80 if ("" == scheme) return false; |
| 81 if ("" != fragment) return false; |
| 82 return true; |
| 83 |
| 84 /* absolute-URI = scheme ":" hier-part [ "?" query ] |
| 85 * hier-part = "//" authority path-abempty |
| 86 * / path-absolute |
| 87 * / path-rootless |
| 88 * / path-empty |
| 89 * |
| 90 * path = path-abempty ; begins with "/" or is empty |
| 91 * / path-absolute ; begins with "/" but not "//" |
| 92 * / path-noscheme ; begins with a non-colon segment |
| 93 * / path-rootless ; begins with a segment |
| 94 * / path-empty ; zero characters |
| 95 * |
| 96 * path-abempty = *( "/" segment ) |
| 97 * path-absolute = "/" [ segment-nz *( "/" segment ) ] |
| 98 * path-noscheme = segment-nz-nc *( "/" segment ) |
| 99 * path-rootless = segment-nz *( "/" segment ) |
| 100 * path-empty = 0<pchar> |
| 101 * segment = *pchar |
| 102 * segment-nz = 1*pchar |
| 103 * segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) |
| 104 * ; non-zero-length segment without any colon ":" |
| 105 * |
| 106 * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
| 107 */ |
| 108 } |
| 109 |
| 110 Uri resolve(String uri) { |
| 111 return resolveUri(new Uri.fromString(uri)); |
| 112 } |
| 113 |
| 114 Uri resolveUri(Uri reference) { |
| 115 // From RFC 3986. |
| 116 String targetScheme; |
| 117 String targetUserInfo; |
| 118 String targetDomain; |
| 119 int targetPort; |
| 120 String targetPath; |
| 121 String targetQuery; |
| 122 if (reference.scheme != "") { |
| 123 targetScheme = reference.scheme; |
| 124 targetUserInfo = reference.userInfo; |
| 125 targetDomain = reference.domain; |
| 126 targetPort = reference.port; |
| 127 targetPath = removeDotSegments(reference.path); |
| 128 targetQuery = reference.query; |
| 129 } else { |
| 130 if (reference.hasAuthority()) { |
| 131 targetUserInfo = reference.userInfo; |
| 132 targetDomain = reference.domain; |
| 133 targetPort = reference.port; |
| 134 targetPath = removeDotSegments(reference.path); |
| 135 targetQuery = reference.query; |
| 136 } else { |
| 137 if (reference.path == "") { |
| 138 targetPath = this.path; |
| 139 if (reference.query != "") { |
| 140 targetQuery = reference.query; |
| 141 } else { |
| 142 targetQuery = this.query; |
| 143 } |
| 144 } else { |
| 145 if (reference.path.startsWith("/")) { |
| 146 targetPath = removeDotSegments(reference.path); |
| 147 } else { |
| 148 targetPath = removeDotSegments(merge(this.path, reference.path)); |
| 149 } |
| 150 targetQuery = reference.query; |
| 151 } |
| 152 targetUserInfo = this.userInfo; |
| 153 targetDomain = this.domain; |
| 154 targetPort = this.port; |
| 155 } |
| 156 targetScheme = this.scheme; |
| 157 } |
| 158 return new Uri(targetScheme, targetUserInfo, targetDomain, targetPort, |
| 159 targetPath, targetQuery, reference.fragment); |
| 160 } |
| 161 |
| 162 bool hasAuthority() { |
| 163 return (userInfo != "") || (domain != "") || (port != 0); |
| 164 } |
| 165 |
| 166 String toString() { |
| 167 StringBuffer sb = new StringBuffer(); |
| 168 _addIfNonEmpty(sb, scheme, scheme, ':'); |
| 169 if (hasAuthority() || (scheme == "file")) { |
| 170 sb.add("//"); |
| 171 _addIfNonEmpty(sb, userInfo, userInfo, "@"); |
| 172 sb.add(domain); |
| 173 if (port != 0) { |
| 174 sb.add(":"); |
| 175 sb.add(port.toString()); |
| 176 } |
| 177 } |
| 178 sb.add(path); |
| 179 _addIfNonEmpty(sb, query, "?", query); |
| 180 _addIfNonEmpty(sb, fragment, "#", fragment); |
| 181 return sb.toString(); |
| 182 } |
| 183 |
| 184 static void _addIfNonEmpty(StringBuffer sb, String test, |
| 185 String first, String second) { |
| 186 if ("" != test) { |
| 187 sb.add(first); |
| 188 sb.add(second); |
| 189 } |
| 190 } |
145 } | 191 } |
| 192 |
| 193 String merge(String base, String reference) { |
| 194 if (base == "") return "/$reference"; |
| 195 return base.substring(0, base.lastIndexOf("/") + 1) + "$reference"; |
| 196 } |
| 197 |
| 198 String removeDotSegments(String path) { |
| 199 List<String> output = []; |
| 200 bool appendSlash = false; |
| 201 for (String segment in path.split("/")) { |
| 202 appendSlash = false; |
| 203 if (segment == "..") { |
| 204 if (!output.isEmpty() && |
| 205 ((output.length != 1) || (output[0] != ""))) output.removeLast(); |
| 206 appendSlash = true; |
| 207 } else if ("." == segment) { |
| 208 appendSlash = true; |
| 209 } else { |
| 210 output.add(segment); |
| 211 } |
| 212 } |
| 213 if (appendSlash) output.add(""); |
| 214 return Strings.join(output, "/"); |
| 215 } |
OLD | NEW |