5 // Atsushi Enomoto <atsushi@ximian.com>
7 // Copyright (C) 2008 Novell, Inc (http://www.novell.com)
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 using System
.Collections
.Generic
;
30 using System
.Collections
.ObjectModel
;
31 using System
.Collections
.Specialized
;
32 using System
.Globalization
;
36 using NameValueCollection
= System
.Object
;
41 public class UriTemplate
43 static readonly ReadOnlyCollection
<string> empty_strings
= new ReadOnlyCollection
<string> (new string [0]);
46 ReadOnlyCollection
<string> path
, query
;
47 Dictionary
<string,string> query_params
= new Dictionary
<string,string> ();
49 public UriTemplate (string template
)
50 : this (template
, false)
54 public UriTemplate (string template
, IDictionary
<string,string> additionalDefaults
)
55 : this (template
, false, additionalDefaults
)
59 public UriTemplate (string template
, bool ignoreTrailingSlash
)
60 : this (template
, ignoreTrailingSlash
, null)
64 public UriTemplate (string template
, bool ignoreTrailingSlash
, IDictionary
<string,string> additionalDefaults
)
67 throw new ArgumentNullException ("template");
68 this.template
= template
;
69 IgnoreTrailingSlash
= ignoreTrailingSlash
;
70 Defaults
= new Dictionary
<string,string> (StringComparer
.InvariantCultureIgnoreCase
);
71 if (additionalDefaults
!= null)
72 foreach (var pair
in additionalDefaults
)
73 Defaults
.Add (pair
.Key
, pair
.Value
);
76 // Trim scheme, host name and port if exist.
77 if (CultureInfo
.InvariantCulture
.CompareInfo
.IsPrefix (template
, "http")) {
78 int idx
= template
.IndexOf ('/', 8); // after "http://x" or "https://"
80 p
= template
.Substring (idx
);
82 int q
= p
.IndexOf ('?');
83 path
= ParsePathTemplate (p
, 0, q
>= 0 ? q
: p
.Length
);
85 ParseQueryTemplate (p
, q
, p
.Length
);
87 query
= empty_strings
;
90 public bool IgnoreTrailingSlash { get; private set; }
92 public IDictionary
<string,string> Defaults { get; private set; }
94 public ReadOnlyCollection
<string> PathSegmentVariableNames
{
98 public ReadOnlyCollection
<string> QueryValueVariableNames
{
102 public override string ToString ()
110 public Uri
BindByName (Uri baseAddress
, NameValueCollection parameters
)
112 return BindByName (baseAddress
, parameters
, false);
115 public Uri
BindByName (Uri baseAddress
, NameValueCollection parameters
, bool omitDefaults
)
117 return BindByNameCommon (baseAddress
, parameters
, null, omitDefaults
);
121 public Uri
BindByName (Uri baseAddress
, IDictionary
<string,string> parameters
)
123 return BindByName (baseAddress
, parameters
, false);
126 public Uri
BindByName (Uri baseAddress
, IDictionary
<string,string> parameters
, bool omitDefaults
)
128 return BindByNameCommon (baseAddress
, null, parameters
, omitDefaults
);
131 Uri
BindByNameCommon (Uri baseAddress
, NameValueCollection nvc
, IDictionary
<string,string> dic
, bool omitDefaults
)
133 CheckBaseAddress (baseAddress
);
135 // take care of case sensitivity.
137 dic
= new Dictionary
<string,string> (dic
, StringComparer
.OrdinalIgnoreCase
);
140 StringBuilder sb
= new StringBuilder (template
.Length
);
141 BindByName (ref src
, sb
, path
, nvc
, dic
, omitDefaults
);
142 BindByName (ref src
, sb
, query
, nvc
, dic
, omitDefaults
);
143 sb
.Append (template
.Substring (src
));
144 return new Uri (baseAddress
.ToString () + sb
.ToString ());
147 void BindByName (ref int src
, StringBuilder sb
, ReadOnlyCollection
<string> names
, NameValueCollection nvc
, IDictionary
<string,string> dic
, bool omitDefaults
)
149 foreach (string name
in names
) {
150 int s
= template
.IndexOf ('{', src
);
151 int e
= template
.IndexOf ('}', s
+ 1);
152 sb
.Append (template
.Substring (src
, s
- src
));
156 string value = nvc
!= null ? nvc
[name
] : null;
159 dic
.TryGetValue (name
, out value);
160 if (value == null && (omitDefaults
|| !Defaults
.TryGetValue (name
, out value)))
161 throw new ArgumentException (String
.Format ("The argument name value collection does not contain non-null value for '{0}'", name
), "parameters");
167 public Uri
BindByPosition (Uri baseAddress
, params string [] values
)
169 CheckBaseAddress (baseAddress
);
171 if (values
.Length
!= path
.Count
+ query
.Count
)
172 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
));
174 int src
= 0, index
= 0;
175 StringBuilder sb
= new StringBuilder (template
.Length
);
176 BindByPosition (ref src
, sb
, path
, values
, ref index
);
177 BindByPosition (ref src
, sb
, query
, values
, ref index
);
178 sb
.Append (template
.Substring (src
));
179 return new Uri (baseAddress
.ToString () + sb
.ToString ());
182 void BindByPosition (ref int src
, StringBuilder sb
, ReadOnlyCollection
<string> names
, string [] values
, ref int index
)
184 for (int i
= 0; i
< names
.Count
; i
++) {
185 int s
= template
.IndexOf ('{', src
);
186 int e
= template
.IndexOf ('}', s
+ 1);
187 sb
.Append (template
.Substring (src
, s
- src
));
188 string value = values
[index
++];
190 throw new FormatException (String
.Format ("The argument value collection contains null at {0}", index
- 1));
198 public bool IsEquivalentTo (UriTemplate other
)
201 throw new ArgumentNullException ("other");
202 return this.template
== other
.template
;
207 static readonly char [] slashSep
= {'/'}
;
209 public UriTemplateMatch
Match (Uri baseAddress
, Uri candidate
)
211 CheckBaseAddress (baseAddress
);
212 if (candidate
== null)
213 throw new ArgumentNullException ("candidate");
215 var us
= baseAddress
.LocalPath
;
216 if (us
[us
.Length
- 1] != '/')
217 baseAddress
= new Uri (baseAddress
.GetComponents (UriComponents
.SchemeAndServer
| UriComponents
.Path
, UriFormat
.Unescaped
) + '/' + baseAddress
.Query
, baseAddress
.IsAbsoluteUri
? UriKind
.Absolute
: UriKind
.RelativeOrAbsolute
);
218 if (IgnoreTrailingSlash
) {
219 us
= candidate
.LocalPath
;
220 if (us
.Length
> 0 && us
[us
.Length
- 1] != '/')
221 candidate
= new Uri(candidate
.GetComponents (UriComponents
.SchemeAndServer
| UriComponents
.Path
, UriFormat
.Unescaped
) + '/' + candidate
.Query
, candidate
.IsAbsoluteUri
? UriKind
.Absolute
: UriKind
.RelativeOrAbsolute
);
224 if (Uri
.Compare (baseAddress
, candidate
, UriComponents
.StrongAuthority
, UriFormat
.SafeUnescaped
, StringComparison
.Ordinal
) != 0)
228 UriTemplateMatch m
= new UriTemplateMatch ();
229 m
.BaseUri
= baseAddress
;
231 m
.RequestUri
= candidate
;
232 var vc
= m
.BoundVariables
;
234 string cp
= Uri
.UnescapeDataString (baseAddress
.MakeRelativeUri (candidate
).ToString ());
235 if (IgnoreTrailingSlash
&& cp
[cp
.Length
- 1] == '/')
236 cp
= cp
.Substring (0, cp
.Length
- 1);
238 int tEndCp
= cp
.IndexOf ('?');
240 cp
= cp
.Substring (0, tEndCp
);
242 if (template
.Length
> 0 && template
[0] == '/')
244 if (cp
.Length
> 0 && cp
[0] == '/')
247 foreach (string name
in path
) {
248 int n
= StringIndexOf (template
, '{' + name + '}', i
);
249 if (String
.CompareOrdinal (cp
, c
, template
, i
, n
- i
) != 0)
250 return null; // doesn't match before current template part.
252 i
= n
+ 2 + name
.Length
;
253 int ce
= cp
.IndexOf ('/', c
);
256 string value = cp
.Substring (c
, ce
- c
);
257 if (value.Length
== 0)
258 return null; // empty => mismatch
260 m
.RelativePathSegments
.Add (value);
263 int tEnd
= template
.IndexOf ('?');
265 tEnd
= template
.Length
;
266 bool wild
= (template
[tEnd
- 1] == '*');
269 if (!wild
&& (cp
.Length
- c
) != (tEnd
- i
) ||
270 String
.CompareOrdinal (cp
, c
, template
, i
, tEnd
- i
) != 0)
271 return null; // suffix doesn't match
274 foreach (var pe
in cp
.Substring (c
).Split (slashSep
, StringSplitOptions
.RemoveEmptyEntries
))
275 m
.WildcardPathSegments
.Add (pe
);
277 if (candidate
.Query
.Length
== 0)
281 string [] parameters
= Uri
.UnescapeDataString (candidate
.Query
.Substring (1)).Split ('&'); // chop first '?'
282 foreach (string parameter
in parameters
) {
283 string [] pair
= parameter
.Split ('=');
284 m
.QueryParameters
.Add (pair
[0], pair
[1]);
285 if (!query_params
.ContainsKey (pair
[0]))
287 string templateName
= query_params
[pair
[0]];
288 vc
.Add (templateName
, pair
[1]);
294 int StringIndexOf (string s
, string pattern
, int idx
)
296 return CultureInfo
.InvariantCulture
.CompareInfo
.IndexOf (s
, pattern
, idx
, CompareOptions
.OrdinalIgnoreCase
);
301 void CheckBaseAddress (Uri baseAddress
)
303 if (baseAddress
== null)
304 throw new ArgumentNullException ("baseAddress");
305 if (!baseAddress
.IsAbsoluteUri
)
306 throw new ArgumentException ("baseAddress must be an absolute URI.");
307 if (baseAddress
.Scheme
== Uri
.UriSchemeHttp
||
308 baseAddress
.Scheme
== Uri
.UriSchemeHttps
)
310 throw new ArgumentException ("baseAddress scheme must be either http or https.");
313 ReadOnlyCollection
<string> ParsePathTemplate (string template
, int index
, int end
)
315 int widx
= template
.IndexOf ('*', index
, end
);
316 if (widx
>= 0 && widx
!= end
- 1)
317 throw new FormatException (String
.Format ("Wildcard in UriTemplate is valid only if it is placed at the last part of the path: '{0}'", template
));
318 List
<string> list
= null;
320 for (int i
= index
; i
<= end
; ) {
321 i
= template
.IndexOf ('{', i
);
322 if (i
< 0 || i
> end
)
324 if (i
== prevEnd
+ 1)
325 throw new ArgumentException (String
.Format ("The UriTemplate '{0}' contains adjacent templated segments, which is invalid.", template
));
326 int e
= template
.IndexOf ('}', i
+ 1);
327 if (e
< 0 || i
> end
)
328 throw new FormatException (String
.Format ("Missing '}' in URI template '{0}'", template
));
331 list
= new List
<string> ();
333 string name
= template
.Substring (i
, e
- i
);
334 string uname
= name
.ToUpper (CultureInfo
.InvariantCulture
);
335 if (list
.Contains (uname
) || (path
!= null && path
.Contains (uname
)))
336 throw new InvalidOperationException (String
.Format ("The URI template string contains duplicate template item {{'{0}'}}", name
));
340 return list
!= null ? new ReadOnlyCollection
<string> (list
) : empty_strings
;
343 void ParseQueryTemplate (string template
, int index
, int end
)
345 // template starts with '?'
346 string [] parameters
= template
.Substring (index
+ 1, end
- index
- 1).Split ('&');
347 List
<string> list
= null;
348 foreach (string parameter
in parameters
) {
349 string [] pair
= parameter
.Split ('=');
350 if (pair
.Length
!= 2)
351 throw new FormatException ("Invalid URI query string format");
352 string pname
= pair
[0];
353 string pvalue
= pair
[1];
354 if (pvalue
.Length
>= 2 && pvalue
[0] == '{' && pvalue [pvalue.Length - 1] == '}') {
355 string ptemplate
= pvalue
.Substring (1, pvalue
.Length
- 2).ToUpper (CultureInfo
.InvariantCulture
);
356 query_params
.Add (pname
, ptemplate
);
358 list
= new List
<string> ();
359 if (list
.Contains (ptemplate
) || (path
!= null && path
.Contains (ptemplate
)))
360 throw new InvalidOperationException (String
.Format ("The URI template string contains duplicate template item {{'{0}'}}", pvalue
));
361 list
.Add (ptemplate
);
364 query
= list
!= null ? new ReadOnlyCollection
<string> (list
.ToArray ()) : empty_strings
;