More working tests.
[castle.git] / MonoRail / Castle.MonoRail.Framework / Routing / PatternRoute.cs
blob203cfb0f2b6b82b28654c036833f508630c0b3cd
1 // Copyright 2004-2008 Castle Project - http://www.castleproject.org/
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
15 namespace Castle.MonoRail.Framework.Routing
17 using System;
18 using System.Collections;
19 using System.Collections.Generic;
20 using System.Diagnostics;
21 using System.Text;
22 using System.Text.RegularExpressions;
23 using Castle.MonoRail.Framework.Services.Utils;
24 using Descriptors;
26 /// <summary>
27 /// Pendent
28 /// </summary>
29 [DebuggerDisplay("PatternRoute {pattern}")]
30 public class PatternRoute : IRoutingRule
32 private readonly string name;
33 private readonly string pattern;
34 private readonly List<DefaultNode> nodes = new List<DefaultNode>();
35 private readonly Dictionary<string, string> defaults = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
37 /// <summary>
38 /// Initializes a new instance of the <see cref="PatternRoute"/> class.
39 /// </summary>
40 /// <param name="pattern">The pattern.</param>
41 public PatternRoute(string pattern)
43 this.pattern = pattern;
44 CreatePatternNodes();
47 /// <summary>
48 /// Initializes a new instance of the <see cref="PatternRoute"/> class.
49 /// </summary>
50 /// <param name="name">The route name.</param>
51 /// <param name="pattern">The pattern.</param>
52 public PatternRoute(string name, string pattern) : this(pattern)
54 this.name = name;
57 /// <summary>
58 /// Gets the name of the route.
59 /// </summary>
60 /// <value>The name of the route.</value>
61 public string RouteName
63 get { return name; }
66 /// <summary>
67 /// Pendent
68 /// </summary>
69 /// <param name="hostname">The hostname.</param>
70 /// <param name="virtualPath">The virtual path.</param>
71 /// <param name="parameters">The parameters.</param>
72 /// <returns></returns>
73 public string CreateUrl(string hostname, string virtualPath, IDictionary parameters)
75 StringBuilder text = new StringBuilder(virtualPath);
76 IList<string> checkedParameters = new List<string>();
78 bool hasNamed = false;
80 foreach(DefaultNode node in nodes)
82 AppendSlashOrDot(text, node);
84 if (node.name == null)
86 text.Append(node.start);
88 else
90 hasNamed = true;
91 checkedParameters.Add(node.name);
93 object value = parameters[node.name];
94 string valAsString = value != null ? value.ToString() : null;
96 if (string.IsNullOrEmpty(valAsString))
98 if (!node.optional)
100 return null;
102 else
104 break;
107 else
109 if (node.hasRestriction && !node.Accepts(value.ToString()))
111 return null;
114 if (node.optional && StringComparer.InvariantCultureIgnoreCase.Compare(node.DefaultVal, value.ToString()) == 0)
116 break; // end as there can't be more required nodes after an optional one
119 text.Append(value.ToString());
124 // Validate that default parameters match parameters passed into to create url.
125 foreach (KeyValuePair<string, string> defaultParameter in defaults)
127 // Skip parameters we already checked.
128 if(checkedParameters.Contains(defaultParameter.Key))
130 continue;
133 object value = parameters[defaultParameter.Key];
134 string valAsString = value != null ? value.ToString() : null;
135 if(!string.IsNullOrEmpty(valAsString) && !defaultParameter.Value.Equals(valAsString, StringComparison.OrdinalIgnoreCase))
137 return null;
141 if (text.Length == 0 || text[text.Length - 1] == '/' || text[text.Length -1] == '.')
143 text.Length = text.Length - 1;
146 return hasNamed ? text.ToString() : null;
149 /// <summary>
150 /// Determines if the specified URL matches the
151 /// routing rule.
152 /// </summary>
153 /// <param name="url">The URL.</param>
154 /// <param name="context">The context</param>
155 /// <param name="match">The match.</param>
156 /// <returns></returns>
157 public bool Matches(string url, IRouteContext context, RouteMatch match)
159 string[] parts = url.Split(new char[] {'/', '.'}, StringSplitOptions.RemoveEmptyEntries);
160 int index = 0;
162 foreach(DefaultNode node in nodes)
164 string part = index < parts.Length ? parts[index] : null;
166 if (!node.Matches(part, match))
168 return false;
171 index++;
174 foreach(KeyValuePair<string, string> pair in defaults)
176 if (!match.Parameters.ContainsKey(pair.Key))
178 match.Parameters.Add(pair.Key, pair.Value);
182 return true;
185 private void CreatePatternNodes()
187 string[] parts = pattern.Split(new char[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
189 foreach(string part in parts)
191 string[] subparts = part.Split(new char[] { '.' }, 2, StringSplitOptions.RemoveEmptyEntries);
193 if (subparts.Length == 2)
195 bool afterDot = false;
197 foreach(string subpart in subparts)
199 if (subpart.Contains("["))
201 nodes.Add(CreateNamedOptionalNode(subpart, afterDot));
203 else
205 nodes.Add(CreateRequiredNode(subpart, afterDot));
208 afterDot = true;
211 else
213 if (part.Contains("["))
215 nodes.Add(CreateNamedOptionalNode(part, false));
217 else
219 nodes.Add(CreateRequiredNode(part, false));
225 /// <summary>
226 /// Adds a default entry.
227 /// </summary>
228 /// <param name="key">The key.</param>
229 /// <param name="value">The value.</param>
230 public void AddDefault(string key, string value)
232 defaults[key] = value;
235 private DefaultNode CreateNamedOptionalNode(string part, bool afterDot)
237 return new DefaultNode(part, true, afterDot);
240 private DefaultNode CreateRequiredNode(string part, bool afterDot)
242 return new DefaultNode(part, false, afterDot);
245 private static void AppendSlashOrDot(StringBuilder text, DefaultNode node)
247 if (text.Length == 0 || text[text.Length - 1] != '/')
249 if (node.afterDot)
251 text.Append('.');
253 else
255 text.Append('/');
260 #region DefaultNode
262 [DebuggerDisplay("Node {name} Opt: {optional} default: {defaultVal} Regular exp: {exp}")]
263 private class DefaultNode
265 public readonly string name, start, end;
266 public readonly bool optional;
267 public readonly bool afterDot;
268 public bool hasRestriction;
269 private string defaultVal;
270 private string[] acceptedTokens;
271 private Regex exp;
272 private string acceptedRegex;
274 public DefaultNode(string part, bool optional, bool afterDot)
276 this.optional = optional;
277 this.afterDot = afterDot;
278 int indexStart = part.IndexOfAny(new char[] {'<', '['});
279 int indexEndStart = -1;
281 if (indexStart != -1)
283 indexEndStart = part.IndexOfAny(new char[] {'>', ']'}, indexStart);
284 name = part.Substring(indexStart + 1, indexEndStart - indexStart - 1);
287 if (indexStart != -1)
289 start = part.Substring(0, indexStart);
291 else
293 start = part;
296 end = "";
298 if (indexEndStart != -1)
300 end = part.Substring(indexEndStart + 1);
303 ReBuildRegularExpression();
306 private void ReBuildRegularExpression()
308 RegexOptions options = RegexOptions.Compiled | RegexOptions.Singleline;
310 if (name != null)
312 exp = new Regex("^" + CharClass(start) + "(" + GetExpression() + ")" + CharClass(end) + "$", options);
314 else
316 exp = new Regex("^(" + CharClass(start) + ")$");
320 private string GetExpression()
322 if (!string.IsNullOrEmpty(acceptedRegex))
324 return acceptedRegex;
326 else if (acceptedTokens != null && acceptedTokens.Length != 0)
328 StringBuilder text = new StringBuilder();
330 foreach(string token in acceptedTokens)
332 if (text.Length != 0)
334 text.Append("|");
336 text.Append("(");
337 text.Append(CharClass(token));
338 text.Append(")");
341 return text.ToString();
343 else
345 return "[a-zA-Z,_,0-9,-]+";
349 public bool Matches(string part, RouteMatch match)
351 if (part == null)
353 if (optional)
355 if (name != null)
357 match.AddNamed(name, defaultVal);
359 return true;
361 else
363 return false;
367 Match regExpMatch = exp.Match(part);
369 if (regExpMatch.Success)
371 if (name != null)
373 match.AddNamed(name, part);
376 return true;
379 return false;
382 public void AcceptsAnyOf(string[] names)
384 hasRestriction = true;
385 acceptedTokens = names;
386 ReBuildRegularExpression();
389 public string DefaultVal
391 get { return defaultVal; }
392 set { defaultVal = value; }
395 public bool AcceptsIntOnly
399 AcceptsRegex("[0-9]+");
403 public bool AcceptsGuidsOnly
407 AcceptsRegex("[A-Fa-f0-9]{32}|" +
408 "({|\\()?[A-Fa-f0-9]{8}-([A-Fa-f0-9]{4}-){3}[A-Fa-f0-9]{12}(}|\\))?|" +
409 "({)?[0xA-Fa-f0-9]{3,10}(, {0,1}[0xA-Fa-f0-9]{3,6}){2}, {0,1}({)([0xA-Fa-f0-9]{3,4}, {0,1}){7}[0xA-Fa-f0-9]{3,4}(}})");
413 public void AcceptsRegex(string regex)
415 hasRestriction = true;
416 acceptedRegex = regex;
417 ReBuildRegularExpression();
420 public bool Accepts(string val)
422 Match regExpMatch = exp.Match(val);
424 return (regExpMatch.Success);
428 #endregion
430 /// <summary>
431 /// Configures the default for the named pattern part.
432 /// </summary>
433 /// <param name="namedPatternPart">The named pattern part.</param>
434 /// <returns></returns>
435 public DefaultConfigurer DefaultFor(string namedPatternPart)
437 return new DefaultConfigurer(this, namedPatternPart);
440 /// <summary>
441 /// Configures the default for the named pattern part.
442 /// </summary>
443 /// <returns></returns>
444 public DefaultConfigurer DefaultForController()
446 return new DefaultConfigurer(this, "controller");
449 /// <summary>
450 /// Configures the default for the named pattern part.
451 /// </summary>
452 /// <returns></returns>
453 public DefaultConfigurer DefaultForAction()
455 return new DefaultConfigurer(this, "action");
458 /// <summary>
459 /// Configures the default for the named pattern part.
460 /// </summary>
461 /// <returns></returns>
462 public DefaultConfigurer DefaultForArea()
464 return new DefaultConfigurer(this, "area");
467 /// <summary>
468 /// Configures restrictions for the named pattern part.
469 /// </summary>
470 /// <param name="namedPatternPart">The named pattern part.</param>
471 /// <returns></returns>
472 public RestrictionConfigurer Restrict(string namedPatternPart)
474 return new RestrictionConfigurer(this, namedPatternPart);
477 /// <summary>
478 /// Pendent
479 /// </summary>
480 public class RestrictionConfigurer
482 private readonly PatternRoute route;
483 private readonly DefaultNode targetNode;
485 /// <summary>
486 /// Initializes a new instance of the <see cref="RestrictionConfigurer"/> class.
487 /// </summary>
488 /// <param name="route">The route.</param>
489 /// <param name="namedPatternPart">The named pattern part.</param>
490 public RestrictionConfigurer(PatternRoute route, string namedPatternPart)
492 this.route = route;
493 targetNode = route.GetNamedNode(namedPatternPart, true);
496 /// <summary>
497 /// Restricts this named pattern part to only accept one of the
498 /// strings passed in.
499 /// </summary>
500 /// <param name="validNames">The valid names.</param>
501 /// <returns></returns>
502 public PatternRoute AnyOf(params string[] validNames)
504 targetNode.AcceptsAnyOf(validNames);
505 return route;
508 /// <summary>
509 /// Restricts this named pattern part to only accept integers.
510 /// </summary>
511 /// <value>The valid integer.</value>
512 public PatternRoute ValidInteger
516 targetNode.AcceptsIntOnly = true;
517 return route;
521 /// <summary>
522 /// Restricts this named pattern part to only accept guids.
523 /// </summary>
524 public PatternRoute ValidGuid
528 targetNode.AcceptsGuidsOnly = true;
529 return route;
533 /// <summary>
534 /// Restricts this named pattern part to only accept strings
535 /// matching the regular expression passed in.
536 /// </summary>
537 /// <param name="regex"></param>
538 /// <returns></returns>
539 public PatternRoute ValidRegex(string regex)
541 targetNode.AcceptsRegex(regex);
542 return route;
546 /// <summary>
547 /// Pendent
548 /// </summary>
549 public class DefaultConfigurer
551 private readonly PatternRoute route;
552 private readonly string namedPatternPart;
553 private readonly DefaultNode targetNode;
555 /// <summary>
556 /// Initializes a new instance of the <see cref="DefaultConfigurer"/> class.
557 /// </summary>
558 /// <param name="patternRoute">The pattern route.</param>
559 /// <param name="namedPatternPart">The named pattern part.</param>
560 public DefaultConfigurer(PatternRoute patternRoute, string namedPatternPart)
562 route = patternRoute;
563 this.namedPatternPart = namedPatternPart;
564 targetNode = route.GetNamedNode(namedPatternPart, false);
567 /// <summary>
568 /// Sets the default value for this named pattern part.
569 /// </summary>
570 /// <returns></returns>
571 public PatternRoute Is<T>() where T : class, IController
573 ControllerDescriptor desc = ControllerInspectionUtil.Inspect(typeof(T));
574 if (targetNode != null)
576 targetNode.DefaultVal = desc.Name;
578 route.AddDefault(namedPatternPart, desc.Name);
579 return route;
582 /// <summary>
583 /// Sets the default value for this named pattern part.
584 /// </summary>
585 /// <param name="value">The value.</param>
586 /// <returns></returns>
587 public PatternRoute Is(string value)
589 if (targetNode != null)
591 targetNode.DefaultVal = value;
593 route.AddDefault(namedPatternPart, value);
594 return route;
597 /// <summary>
598 /// Sets the default value as empty for this named pattern part.
599 /// </summary>
600 /// <value>The is empty.</value>
601 public PatternRoute IsEmpty
603 get { return Is(string.Empty); }
607 // See http://weblogs.asp.net/justin_rogers/archive/2004/03/20/93379.aspx
608 private static string CharClass(string content)
610 if (content == String.Empty)
612 return string.Empty;
615 StringBuilder builder = new StringBuilder();
617 foreach(char c in content)
619 if (char.IsLetter(c))
621 builder.AppendFormat("[{0}{1}]", char.ToLower(c), char.ToUpper(c));
623 else
625 builder.Append(c);
629 return builder.ToString();
632 /// <summary>
633 /// Gets the named node.
634 /// </summary>
635 /// <param name="part">The part.</param>
636 /// <param name="mustFind">if set to <c>true</c> [must find].</param>
637 /// <returns></returns>
638 private DefaultNode GetNamedNode(string part, bool mustFind)
640 DefaultNode found = nodes.Find(delegate(DefaultNode node) { return node.name == part; });
642 if (found == null && mustFind)
644 throw new ArgumentException("Could not find pattern node for name " + part);
647 return found;