2009-05-21 Atsushi Enomoto <atsushi@ximian.com>
[mono.git] / mcs / class / System.Web.Routing / System.Web.Routing / UrlPattern.cs
1 //
2 // Route.cs
3 //
4 // Author:
5 //      Atsushi Enomoto <atsushi@ximian.com>
6 //
7 // Copyright (C) 2008 Novell Inc. http://novell.com
8 //
9
10 //
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:
18 // 
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
21 // 
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.
29 //
30 using System;
31 using System.Collections.Generic;
32 using System.Security.Permissions;
33 using System.Web;
34
35 namespace System.Web.Routing
36 {
37         internal class UrlPattern
38         {
39                 sealed class TrimEntry 
40                 {
41                         public bool Allowed;
42                 }
43                 
44                 int segmentsCount;
45                 string [] segments;
46                 int [] segmentLengths;
47                 bool [] segment_flags;
48                 List <string> tokens;
49
50                 public UrlPattern (string url)
51                 {
52                         Url = url;
53                         Parse ();
54                 }
55
56                 public string Url { get; private set; }
57
58                 void Parse ()
59                 {
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 '?'");
66
67                         tokens = new List<string> ();
68
69                         segments = Url.Split ('/');
70                         segmentsCount = segments.Length;
71                         segment_flags = new bool [segmentsCount];
72                         segmentLengths = new int [segmentsCount];
73                         
74                         for (int i = 0; i < segmentsCount; i++) {
75                                 string s = segments [i];
76                                 int slen = s.Length;
77                                 segmentLengths [i] = slen;
78                                 if (slen == 0 && i < segmentsCount - 1)
79                                         throw new ArgumentException ("Consecutive URL segment separators '/' are not allowed");
80                                 int from = 0;
81                                 while (from < slen) {
82                                         int start = s.IndexOf ('{', from);
83                                         if (start == slen - 1)
84                                                 throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
85                                         if (start < 0) {
86                                                 if (s.IndexOf ('}', from) >= from)
87                                                         throw new ArgumentException ("Unmatched URL parameter closer '}'. A corresponding '{' must precede");
88                                                 from = slen;
89                                                 continue;
90                                         }
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 '}'");
96                                         if (end == start + 1)
97                                                 throw new ArgumentException ("Empty URL parameter name is not allowed");
98                                         if (next == end + 1)
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))
102                                                 tokens.Add (token);
103                                         from = end + 1;
104                                 }
105                         }
106                 }
107
108                 string SegmentToKey (string segment)
109                 {
110                         int start = segment.IndexOf ('{');
111                         int end = segment.IndexOf ('}');
112                         if (start == -1)
113                                 start = 0;
114                         else
115                                 start++;
116                         
117                         if (end == -1)
118                                 end = segment.Length;
119                         return segment.Substring (start, end - start);
120                 }
121
122                 RouteValueDictionary AddDefaults (RouteValueDictionary dict, RouteValueDictionary defaults)
123                 {
124                         if (defaults != null && defaults.Count > 0) {
125                                 string key;
126                                 foreach (var def in defaults) {
127                                         key = def.Key;
128                                         if (dict.ContainsKey (key))
129                                                 continue;
130                                         dict.Add (key, def.Value);
131                                 }
132                         }
133
134                         return dict;
135                 }
136                 
137                 RouteValueDictionary tmp = new RouteValueDictionary ();
138                 public RouteValueDictionary Match (string path, RouteValueDictionary defaults)
139                 {
140                         tmp.Clear ();
141
142                         // quick check
143                         if (Url == path && Url.IndexOf ('{') < 0)
144                                 return AddDefaults (tmp, defaults);
145
146                         string [] argSegs = path.Split ('/');
147                         int argsLen = argSegs.Length;
148                         bool haveDefaults = defaults != null && defaults.Count > 0;
149                         if (!haveDefaults && argsLen != segmentsCount)
150                                 return null;
151
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;
156
157                                         if (String.IsNullOrEmpty (v)) {
158                                                 string key = SegmentToKey (segments [i]);
159                                                 object o;
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
165                                                                      // dictionary.
166
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 + "'.");
173                                         }
174                                         
175                                         int tfrom = 0, vfrom = 0;
176                                         while (tfrom < t.Length) {
177                                                 int start = t.IndexOf ('{', tfrom);
178                                                 if (start < 0) {
179                                                         int tlen = t.Length - tfrom;
180                                                         int vlen = v.Length - vfrom;
181                                                         if (tlen != vlen ||
182                                                             String.Compare (t, tfrom, v, vfrom, tlen, StringComparison.Ordinal) != 0)
183                                                                 return null; // mismatch
184                                                         break;
185                                                 }
186
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
191                                                 vfrom += len;
192
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;
198
199                                                 if (next < 0) {
200                                                         var vs = vnext < 0 ? v.Substring (vfrom) : v.Substring (vfrom, vnext - vfrom);
201                                                         tmp.Add (key, vs);
202                                                         vfrom = vs.Length;
203                                                 } else {
204                                                         if (vnext < 0)
205                                                                 return null; // mismatch
206                                                         tmp.Add (key, v.Substring (vfrom, vnext - vfrom));
207                                                         vfrom = vnext;
208                                                 }
209                                                 tfrom = end + 1;
210                                         }
211                                 } else if (i > argsLen || segments [i] != argSegs [i])
212                                         return null;
213                         }
214
215                         return AddDefaults (tmp, defaults);
216                 }
217
218                 static readonly string [] substsep = {"{{"};
219                 
220                 // it may return null for invalid values.
221                 public bool TrySubstitute (RouteValueDictionary values, RouteValueDictionary defaults, out string value)
222                 {
223                         if (values == null) {
224                                 value = Url;
225                                 return true;
226                         }
227
228                         var trim = new Dictionary <string, TrimEntry> (StringComparer.OrdinalIgnoreCase);
229                         foreach (string token in tokens) {
230                                 bool trimValue;
231                                 if (!values.ContainsKey (token)) {
232                                         value = null;
233                                         return false;
234                                 }
235
236                                 object left, right;
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)
241                                                 trimValue = true;
242                                         else
243                                                 trimValue = false;
244                                 } else
245                                         trimValue = false;
246
247                                 if (!trimValue) {
248                                         foreach (var de in trim)
249                                                 de.Value.Allowed = false;
250                                 }
251                                 
252                                 if (!trim.ContainsKey (token))
253                                         trim.Add (token, new TrimEntry { Allowed = trimValue});
254                         }
255
256                         var replacements = new RouteValueDictionary (values);
257                         if (defaults != null) {
258                                 string key;
259                                 
260                                 foreach (var de in defaults) {
261                                         key = de.Key;
262                                         if (replacements.ContainsKey (key))
263                                                 continue;
264                                         replacements.Add (key, de.Value);
265                                 }
266                         }
267                         
268                         // horrible hack, but should work
269                         string [] arr = Url.Split (substsep, StringSplitOptions.None);
270                         for (int i = 0; i < arr.Length; i++) {
271                                 string s = arr [i];
272                                 foreach (var p in replacements) {
273                                         string pKey = p.Key;
274                                         string pValue;
275                                         TrimEntry trimValue;
276                                         
277                                         if (trim.TryGetValue (pKey, out trimValue)) {
278                                                 if (trimValue.Allowed)
279                                                         pValue = String.Empty;
280                                                 else
281                                                         pValue = p.Value.ToString ();
282                                         } else
283                                                 pValue = p.Value.ToString ();
284
285                                         string rkey = "{" + pKey + "}";
286                                         if (String.IsNullOrEmpty (pValue) && s.EndsWith ("/" + rkey)) {
287                                                 s = s.Substring (0, s.Length - rkey.Length - 1);
288                                         } else
289                                                 s = s.Replace (rkey, pValue);
290                                 }
291                                 
292                                 arr [i] = s;
293                         }
294                         value = String.Join ("{{", arr);
295                         return true;
296                 }
297         }
298 }