1 // 2 // UriTemplate.cs 3 // 4 // Author: 5 // Atsushi Enomoto <atsushi@ximian.com> 6 // 7 // Copyright (C) 2008 Novell, Inc (http://www.novell.com) 8 // Copyright 2011 Xamarin Inc (http://www.xamarin.com). 9 // 10 // Permission is hereby granted, free of charge, to any person obtaining 11 // a copy of this software and associated documentation files (the 12 // "Software"), to deal in the Software without restriction, including 13 // without limitation the rights to use, copy, modify, merge, publish, 14 // distribute, sublicense, and/or sell copies of the Software, and to 15 // permit persons to whom the Software is furnished to do so, subject to 16 // the following conditions: 17 // 18 // The above copyright notice and this permission notice shall be 19 // included in all copies or substantial portions of the Software. 20 // 21 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 25 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 26 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 27 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 // 29 using System; 30 using System.Collections.Generic; 31 using System.Collections.ObjectModel; 32 using System.Collections.Specialized; 33 using System.Globalization; 34 using System.Text; 35 36 namespace System 37 { 38 public class UriTemplate 39 { 40 static readonly ReadOnlyCollection<string> empty_strings = new ReadOnlyCollection<string> (new string [0]); 41 42 string template; 43 ReadOnlyCollection<string> path, query; 44 string wild_path_name; 45 Dictionary<string,string> query_params = new Dictionary<string,string> (); 46 UriTemplate(string template)47 public UriTemplate (string template) 48 : this (template, false) 49 { 50 } 51 UriTemplate(string template, IDictionary<string,string> additionalDefaults)52 public UriTemplate (string template, IDictionary<string,string> additionalDefaults) 53 : this (template, false, additionalDefaults) 54 { 55 } 56 UriTemplate(string template, bool ignoreTrailingSlash)57 public UriTemplate (string template, bool ignoreTrailingSlash) 58 : this (template, ignoreTrailingSlash, null) 59 { 60 } 61 UriTemplate(string template, bool ignoreTrailingSlash, IDictionary<string,string> additionalDefaults)62 public UriTemplate (string template, bool ignoreTrailingSlash, IDictionary<string,string> additionalDefaults) 63 { 64 if (template == null) 65 throw new ArgumentNullException ("template"); 66 this.template = template; 67 IgnoreTrailingSlash = ignoreTrailingSlash; 68 Defaults = new Dictionary<string,string> (StringComparer.InvariantCultureIgnoreCase); 69 if (additionalDefaults != null) 70 foreach (var pair in additionalDefaults) 71 Defaults.Add (pair.Key, pair.Value); 72 73 string p = template; 74 // Trim scheme, host name and port if exist. 75 if (CultureInfo.InvariantCulture.CompareInfo.IsPrefix (template, "http")) { 76 int idx = template.IndexOf ('/', 8); // after "http://x" or "https://" 77 if (idx > 0) 78 p = template.Substring (idx); 79 } 80 int q = p.IndexOf ('?'); 81 path = ParsePathTemplate (p, 0, q >= 0 ? q : p.Length); 82 if (q >= 0) 83 ParseQueryTemplate (p, q, p.Length); 84 else 85 query = empty_strings; 86 } 87 88 public bool IgnoreTrailingSlash { get; private set; } 89 90 public IDictionary<string,string> Defaults { get; private set; } 91 92 public ReadOnlyCollection<string> PathSegmentVariableNames { 93 get { return path; } 94 } 95 96 public ReadOnlyCollection<string> QueryValueVariableNames { 97 get { return query; } 98 } 99 ToString()100 public override string ToString () 101 { 102 return template; 103 } 104 105 // Bind 106 BindByName(Uri baseAddress, NameValueCollection parameters)107 public Uri BindByName (Uri baseAddress, NameValueCollection parameters) 108 { 109 return BindByName (baseAddress, parameters, false); 110 } 111 BindByName(Uri baseAddress, NameValueCollection parameters, bool omitDefaults)112 public Uri BindByName (Uri baseAddress, NameValueCollection parameters, bool omitDefaults) 113 { 114 return BindByNameCommon (baseAddress, parameters, null, omitDefaults); 115 } 116 BindByName(Uri baseAddress, IDictionary<string,string> parameters)117 public Uri BindByName (Uri baseAddress, IDictionary<string,string> parameters) 118 { 119 return BindByName (baseAddress, parameters, false); 120 } 121 BindByName(Uri baseAddress, IDictionary<string,string> parameters, bool omitDefaults)122 public Uri BindByName (Uri baseAddress, IDictionary<string,string> parameters, bool omitDefaults) 123 { 124 return BindByNameCommon (baseAddress, null, parameters, omitDefaults); 125 } 126 SuffixEndRenderedUri(string s)127 string SuffixEndRenderedUri (string s) 128 { 129 return s.Length > 0 && s [s.Length - 1] == '/' ? s : s + '/'; 130 } 131 TrimStartRenderedUri(StringBuilder sb)132 string TrimStartRenderedUri (StringBuilder sb) 133 { 134 if (sb.Length == 0) 135 return String.Empty; 136 137 if (sb [0] == '/') 138 return sb.ToString (1, sb.Length - 1); 139 140 return sb.ToString (); 141 } 142 BindByNameCommon(Uri baseAddress, NameValueCollection nvc, IDictionary<string,string> dic, bool omitDefaults)143 Uri BindByNameCommon (Uri baseAddress, NameValueCollection nvc, IDictionary<string,string> dic, bool omitDefaults) 144 { 145 CheckBaseAddress (baseAddress); 146 147 // take care of case sensitivity. 148 if (dic != null) 149 dic = new Dictionary<string,string> (dic, StringComparer.OrdinalIgnoreCase); 150 151 int src = 0; 152 StringBuilder sb = new StringBuilder (template.Length); 153 BindByName (ref src, sb, path, nvc, dic, omitDefaults, false); 154 BindByName (ref src, sb, query, nvc, dic, omitDefaults, true); 155 sb.Append (template.Substring (src)); 156 return new Uri (SuffixEndRenderedUri (baseAddress.ToString ()) + TrimStartRenderedUri (sb)); 157 } 158 BindByName(ref int src, StringBuilder sb, ReadOnlyCollection<string> names, NameValueCollection nvc, IDictionary<string,string> dic, bool omitDefaults, bool query)159 void BindByName (ref int src, StringBuilder sb, ReadOnlyCollection<string> names, NameValueCollection nvc, IDictionary<string,string> dic, bool omitDefaults, bool query) 160 { 161 if (query) { 162 int idx = template.IndexOf ('?', src); 163 if (idx > 0) { 164 sb.Append (template.Substring (src, idx - src)); 165 src = idx; 166 // note that it doesn't append '?'. It is added only when there is actual parameter binding. 167 } 168 } 169 170 foreach (string name in names) { 171 int s = template.IndexOf ('{', src); 172 int e = template.IndexOf ('}', s + 1); 173 string value = nvc != null ? nvc [name] : null; 174 175 if (dic != null) 176 dic.TryGetValue (name, out value); 177 178 if (query) { 179 if (value != null || (!omitDefaults && Defaults.TryGetValue (name, out value))) { 180 sb.Append (template.Substring (src, s - src)); 181 sb.Append (value); 182 } 183 } else { 184 if (value == null && (omitDefaults || !Defaults.TryGetValue (name, out value))) 185 throw new ArgumentException (string.Format("The argument name value collection does not contain non-null value for '{0}'", name), "parameters"); 186 187 sb.Append (template.Substring (src, s - src)); 188 sb.Append (value); 189 } 190 src = e + 1; 191 } 192 } 193 BindByPosition(Uri baseAddress, params string [] values)194 public Uri BindByPosition (Uri baseAddress, params string [] values) 195 { 196 CheckBaseAddress (baseAddress); 197 198 if (values.Length != path.Count + query.Count) 199 throw new FormatException (String.Format ("Template '{0}' contains {1} parameters but the argument values to bind are {2}", template, path.Count + query.Count, values.Length)); 200 201 int src = 0, index = 0; 202 StringBuilder sb = new StringBuilder (template.Length); 203 BindByPosition (ref src, sb, path, values, ref index); 204 BindByPosition (ref src, sb, query, values, ref index); 205 sb.Append (template.Substring (src)); 206 return new Uri (SuffixEndRenderedUri (baseAddress.ToString ()) + TrimStartRenderedUri (sb)); 207 } 208 BindByPosition(ref int src, StringBuilder sb, ReadOnlyCollection<string> names, string [] values, ref int index)209 void BindByPosition (ref int src, StringBuilder sb, ReadOnlyCollection<string> names, string [] values, ref int index) 210 { 211 for (int i = 0; i < names.Count; i++) { 212 int s = template.IndexOf ('{', src); 213 int e = template.IndexOf ('}', s + 1); 214 sb.Append (template.Substring (src, s - src)); 215 string value = values [index++]; 216 if (value == null) 217 throw new FormatException (String.Format ("The argument value collection contains null at {0}", index - 1)); 218 sb.Append (value); 219 src = e + 1; 220 } 221 } 222 223 // Compare 224 IsEquivalentTo(UriTemplate other)225 public bool IsEquivalentTo (UriTemplate other) 226 { 227 if (other == null) 228 throw new ArgumentNullException ("other"); 229 return this.template == other.template; 230 } 231 232 // Match 233 234 static readonly char [] slashSep = {'/'}; 235 Match(Uri baseAddress, Uri candidate)236 public UriTemplateMatch Match (Uri baseAddress, Uri candidate) 237 { 238 CheckBaseAddress (baseAddress); 239 if (candidate == null) 240 throw new ArgumentNullException ("candidate"); 241 242 var us = baseAddress.LocalPath; 243 if (us [us.Length - 1] != '/') 244 baseAddress = new Uri ( 245 baseAddress.GetComponents (UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) + '/' + baseAddress.Query, 246 baseAddress.IsAbsoluteUri ? UriKind.Absolute : UriKind.RelativeOrAbsolute 247 ); 248 if (IgnoreTrailingSlash) { 249 us = candidate.LocalPath; 250 if (us.Length > 0 && us [us.Length - 1] != '/') 251 candidate = new Uri ( 252 candidate.GetComponents (UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) + '/' + candidate.Query, 253 candidate.IsAbsoluteUri ? UriKind.Absolute : UriKind.RelativeOrAbsolute 254 ); 255 } 256 257 int i = 0, c = 0; 258 UriTemplateMatch m = new UriTemplateMatch (); 259 m.BaseUri = baseAddress; 260 m.Template = this; 261 m.RequestUri = candidate; 262 var vc = m.BoundVariables; 263 264 string cp = baseAddress.MakeRelativeUri (new Uri ( 265 baseAddress, 266 candidate.GetComponents (UriComponents.PathAndQuery, UriFormat.UriEscaped) 267 )) 268 .ToString (); 269 if (IgnoreTrailingSlash && cp [cp.Length - 1] == '/') 270 cp = cp.Substring (0, cp.Length - 1); 271 272 int tEndCp = cp.IndexOf ('?'); 273 if (tEndCp >= 0) 274 cp = cp.Substring (0, tEndCp); 275 276 if (template.Length > 0 && template [0] == '/') 277 i++; 278 if (cp.Length > 0 && cp [0] == '/') 279 c++; 280 281 foreach (string name in path) { 282 if (name == wild_path_name) { 283 vc [name] = Uri.UnescapeDataString (cp.Substring (c)); // all remaining paths. 284 continue; 285 } 286 int n = StringIndexOf (template, '{' + name + '}', i); 287 if (String.CompareOrdinal (cp, c, template, i, n - i) != 0) 288 return null; // doesn't match before current template part. 289 c += n - i; 290 i = n + 2 + name.Length; 291 int ce = cp.IndexOf ('/', c); 292 if (ce < 0) 293 ce = cp.Length; 294 string value = cp.Substring (c, ce - c); 295 string unescapedVaule = Uri.UnescapeDataString (value); 296 if (value.Length == 0) 297 return null; // empty => mismatch 298 vc [name] = unescapedVaule; 299 m.RelativePathSegments.Add (unescapedVaule); 300 c += value.Length; 301 } 302 int tEnd = template.IndexOf ('?'); 303 int wildIdx = template.IndexOf ('*'); 304 bool wild = wildIdx >= 0; 305 if (tEnd < 0) 306 tEnd = template.Length; 307 if (wild) 308 tEnd = Math.Max (wildIdx - 1, 0); 309 if (!wild && (cp.Length - c) != (tEnd - i) || 310 String.CompareOrdinal (cp, c, template, i, tEnd - i) != 0) 311 return null; // suffix doesn't match 312 if (wild) { 313 c += tEnd - i; 314 foreach (var pe in cp.Substring (c).Split (slashSep, StringSplitOptions.RemoveEmptyEntries)) 315 m.WildcardPathSegments.Add (pe); 316 } 317 if (candidate.Query.Length == 0) 318 return m; 319 320 321 string [] parameters = Uri.UnescapeDataString (candidate.Query.Substring (1)).Split ('&'); // chop first '?' 322 foreach (string parameter in parameters) { 323 string [] pair = parameter.Split ('='); 324 if (pair.Length > 0) { 325 m.QueryParameters.Add (pair [0], pair.Length == 2 ? pair [1] : null); 326 if (!query_params.ContainsKey (pair [0])) 327 continue; 328 if (pair.Length > 1) { 329 string templateName = query_params [pair [0]]; 330 vc.Add (templateName, pair [1]); 331 } 332 } 333 } 334 335 return m; 336 } 337 StringIndexOf(string s, string pattern, int idx)338 int StringIndexOf (string s, string pattern, int idx) 339 { 340 return CultureInfo.InvariantCulture.CompareInfo.IndexOf (s, pattern, idx, CompareOptions.OrdinalIgnoreCase); 341 } 342 343 // Helpers 344 CheckBaseAddress(Uri baseAddress)345 void CheckBaseAddress (Uri baseAddress) 346 { 347 if (baseAddress == null) 348 throw new ArgumentNullException ("baseAddress"); 349 if (!baseAddress.IsAbsoluteUri) 350 throw new ArgumentException ("baseAddress must be an absolute URI."); 351 if (baseAddress.Scheme == Uri.UriSchemeHttp || 352 baseAddress.Scheme == Uri.UriSchemeHttps) 353 return; 354 throw new ArgumentException ("baseAddress scheme must be either http or https."); 355 } 356 ParsePathTemplate(string template, int index, int end)357 ReadOnlyCollection<string> ParsePathTemplate (string template, int index, int end) 358 { 359 int widx = template.IndexOf ('*', index, end); 360 if (widx >= 0) 361 if (widx != end - 1 && template.IndexOf ('}', widx) != end - 1) 362 throw new FormatException (String.Format ("Wildcard in UriTemplate is valid only if it is placed at the last part of the path: '{0}'", template)); 363 List<string> list = null; 364 int prevEnd = -2; 365 for (int i = index; i <= end; ) { 366 i = template.IndexOf ('{', i); 367 if (i < 0 || i > end) 368 break; 369 if (i == prevEnd + 1) 370 throw new ArgumentException (String.Format ("The UriTemplate '{0}' contains adjacent templated segments, which is invalid.", template)); 371 int e = template.IndexOf ('}', i + 1); 372 if (e < 0 || i > end) 373 throw new FormatException (String.Format ("Missing '}' in URI template '{0}'", template)); 374 prevEnd = e; 375 if (list == null) 376 list = new List<string> (); 377 i++; 378 string name = template.Substring (i, e - i); 379 string uname = name.ToUpper (CultureInfo.InvariantCulture); 380 if (uname [0] == '*') 381 uname = wild_path_name = uname.Substring (1); 382 if (list.Contains (uname) || (path != null && path.Contains (uname))) 383 throw new InvalidOperationException (String.Format ("The URI template string contains duplicate template item {{'{0}'}}", name)); 384 list.Add (uname); 385 i = e + 1; 386 } 387 return list != null ? new ReadOnlyCollection<string> (list) : empty_strings; 388 } 389 ParseQueryTemplate(string template, int index, int end)390 void ParseQueryTemplate (string template, int index, int end) 391 { 392 // template starts with '?' 393 string [] parameters = template.Substring (index + 1, end - index - 1).Split ('&'); 394 List<string> list = null; 395 foreach (string parameter in parameters) { 396 string [] pair = parameter.Split ('='); 397 if (pair.Length != 2) 398 throw new FormatException ("Invalid URI query string format"); 399 string pname = pair [0]; 400 string pvalue = pair [1]; 401 if (pvalue.Length >= 2 && pvalue [0] == '{' && pvalue [pvalue.Length - 1] == '}') { 402 string ptemplate = pvalue.Substring (1, pvalue.Length - 2).ToUpper (CultureInfo.InvariantCulture); 403 query_params.Add (pname, ptemplate); 404 if (list == null) 405 list = new List<string> (); 406 if (list.Contains (ptemplate) || (path != null && path.Contains (ptemplate))) 407 throw new InvalidOperationException (String.Format ("The URI template string contains duplicate template item {{'{0}'}}", pvalue)); 408 list.Add (ptemplate); 409 } 410 } 411 query = list != null ? new ReadOnlyCollection<string> (list.ToArray ()) : empty_strings; 412 } 413 } 414 } 415