5 // Atsushi Enomoto <atsushi@ximian.com>
7 // Copyright (C) 2008 Novell Inc. http://novell.com
11 // Permission is hereby granted, free of charge, to any person obtaining
12 // a copy of this software and associated documentation files (the
13 // "Software"), to deal in the Software without restriction, including
14 // without limitation the rights to use, copy, modify, merge, publish,
15 // distribute, sublicense, and/or sell copies of the Software, and to
16 // permit persons to whom the Software is furnished to do so, subject to
17 // the following conditions:
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
22 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
26 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
27 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31 using System.Collections.Generic;
32 using System.Security.Permissions;
35 namespace System.Web.Routing
37 internal class UrlPattern
39 sealed class TrimEntry
46 int [] segmentLengths;
47 bool [] segment_flags;
50 public UrlPattern (string url)
56 public string Url { get; private set; }
60 if (String.IsNullOrEmpty (Url))
61 throw new SystemException ("INTERNAL ERROR: it should not try to parse null or empty string");
62 if (Url [0] == '~' || Url [0] == '/')
63 throw new ArgumentException ("Url must not start with '~' or '/'");
64 if (Url.IndexOf ('?') >= 0)
65 throw new ArgumentException ("Url must not contain '?'");
67 tokens = new List<string> ();
69 segments = Url.Split ('/');
70 segmentsCount = segments.Length;
71 segment_flags = new bool [segmentsCount];
72 segmentLengths = new int [segmentsCount];
74 for (int i = 0; i < segmentsCount; i++) {
75 string s = segments [i];
77 segmentLengths [i] = slen;
78 if (slen == 0 && i < segmentsCount - 1)
79 throw new ArgumentException ("Consecutive URL segment separators '/' are not allowed");
82 int start = s.IndexOf ('{', from);
83 if (start == slen - 1)
84 throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
86 if (s.IndexOf ('}', from) >= from)
87 throw new ArgumentException ("Unmatched URL parameter closer '}'. A corresponding '{' must precede");
91 segment_flags [i] = true;
92 int end = s.IndexOf ('}', start + 1);
93 int next = s.IndexOf ('{', start + 1);
94 if (end < 0 || next >= 0 && next < end)
95 throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
97 throw new ArgumentException ("Empty URL parameter name is not allowed");
99 throw new ArgumentException ("Two consecutive URL parameters are not allowed. Split into a different segment by '/', or a literal string.");
100 string token = s.Substring (start + 1, end - start - 1);
101 if (!tokens.Contains (token))
108 string SegmentToKey (string segment)
110 int start = segment.IndexOf ('{');
111 int end = segment.IndexOf ('}');
118 end = segment.Length;
119 return segment.Substring (start, end - start);
122 RouteValueDictionary AddDefaults (RouteValueDictionary dict, RouteValueDictionary defaults)
124 if (defaults != null && defaults.Count > 0) {
126 foreach (var def in defaults) {
128 if (dict.ContainsKey (key))
130 dict.Add (key, def.Value);
137 RouteValueDictionary tmp = new RouteValueDictionary ();
138 public RouteValueDictionary Match (string path, RouteValueDictionary defaults)
143 if (Url == path && Url.IndexOf ('{') < 0)
144 return AddDefaults (tmp, defaults);
146 string [] argSegs = path.Split ('/');
147 int argsLen = argSegs.Length;
148 bool haveDefaults = defaults != null && defaults.Count > 0;
149 if (!haveDefaults && argsLen != segmentsCount)
152 for (int i = 0; i < segmentsCount; i++) {
153 if (segment_flags [i]) {
154 string t = segments [i];
155 string v = i < argsLen ? argSegs [i] : null;
157 if (String.IsNullOrEmpty (v)) {
158 string key = SegmentToKey (segments [i]);
160 if (haveDefaults && !defaults.TryGetValue (key, out o))
161 return null; // ends with '/' while more
162 // tokens are expected and
163 // there are is no default
164 // value in the defaults
167 v = (o != null) ? o.ToString () : null;
168 if (v == null && o != null) {
169 Type type = (o != null) ? o.GetType () : null;
170 throw new InvalidOperationException ("The RouteData must contain an item named '" + key + "' of type " + type + "'.");
171 } else if (v == null)
172 throw new InvalidOperationException ("The RouteData must contain an item named '" + key + "'.");
175 int tfrom = 0, vfrom = 0;
176 while (tfrom < t.Length) {
177 int start = t.IndexOf ('{', tfrom);
179 int tlen = t.Length - tfrom;
180 int vlen = v.Length - vfrom;
182 String.Compare (t, tfrom, v, vfrom, tlen, StringComparison.Ordinal) != 0)
183 return null; // mismatch
187 // if there is a string literal before next template item, check it in the value string.
188 int len = start - tfrom;
189 if (len > 0 && String.CompareOrdinal (t, tfrom, v, vfrom, len) != 0)
190 return null; // mismatch
193 int end = t.IndexOf ('}', start + 1);
194 int next = t.IndexOf ('{', end + 1);
195 string key = t.Substring (start + 1, end - start - 1);
196 string nextToken = next < 0 ? t.Substring (end + 1) : t.Substring (end + 1, next - end - 1);
197 int vnext = nextToken.Length > 0 ? v.IndexOf (nextToken, vfrom + 1, StringComparison.Ordinal) : -1;
200 var vs = vnext < 0 ? v.Substring (vfrom) : v.Substring (vfrom, vnext - vfrom);
205 return null; // mismatch
206 tmp.Add (key, v.Substring (vfrom, vnext - vfrom));
211 } else if (i > argsLen || segments [i] != argSegs [i])
215 return AddDefaults (tmp, defaults);
218 static readonly string [] substsep = {"{{"};
220 // it may return null for invalid values.
221 public bool TrySubstitute (RouteValueDictionary values, RouteValueDictionary defaults, out string value)
223 if (values == null) {
228 var trim = new Dictionary <string, TrimEntry> (StringComparer.OrdinalIgnoreCase);
229 foreach (string token in tokens) {
231 if (!values.ContainsKey (token)) {
237 if (values.TryGetValue (token, out left) && defaults != null && defaults.TryGetValue (token, out right)) {
238 if (left is string && right is string)
239 trimValue = String.Compare ((string)left, (string)right, StringComparison.OrdinalIgnoreCase) == 0;
240 else if (left == right)
248 foreach (var de in trim)
249 de.Value.Allowed = false;
252 if (!trim.ContainsKey (token))
253 trim.Add (token, new TrimEntry { Allowed = trimValue});
256 var replacements = new RouteValueDictionary (values);
257 if (defaults != null) {
260 foreach (var de in defaults) {
262 if (replacements.ContainsKey (key))
264 replacements.Add (key, de.Value);
268 // horrible hack, but should work
269 string [] arr = Url.Split (substsep, StringSplitOptions.None);
270 for (int i = 0; i < arr.Length; i++) {
272 foreach (var p in replacements) {
277 if (trim.TryGetValue (pKey, out trimValue)) {
278 if (trimValue.Allowed)
279 pValue = String.Empty;
281 pValue = p.Value.ToString ();
283 pValue = p.Value.ToString ();
285 string rkey = "{" + pKey + "}";
286 if (String.IsNullOrEmpty (pValue) && s.EndsWith ("/" + rkey)) {
287 s = s.Substring (0, s.Length - rkey.Length - 1);
289 s = s.Replace (rkey, pValue);
294 value = String.Join ("{{", arr);