Fixing an issue with output parameters that are of type IntPtr
[castle.git] / MonoRail / Castle.MonoRail.Framework / Routing / PatternRoute.cs
blobf30f52a9e5bf79993515cc975a94148c6629fed3
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>();
36 private readonly Dictionary<string, string> defaults =
37 new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
39 /// <summary>
40 /// Initializes a new instance of the <see cref="PatternRoute"/> class.
41 /// </summary>
42 /// <param name="pattern">The pattern.</param>
43 public PatternRoute(string pattern)
45 this.pattern = pattern;
46 CreatePatternNodes();
49 /// <summary>
50 /// Initializes a new instance of the <see cref="PatternRoute"/> class.
51 /// </summary>
52 /// <param name="name">The route name.</param>
53 /// <param name="pattern">The pattern.</param>
54 public PatternRoute(string name, string pattern) : this(pattern)
56 this.name = name;
59 /// <summary>
60 /// Gets the name of the route.
61 /// </summary>
62 /// <value>The name of the route.</value>
63 public string RouteName
65 get { return name; }
68 /// <summary>
69 /// Pendent
70 /// </summary>
71 /// <param name="hostname">The hostname.</param>
72 /// <param name="virtualPath">The virtual path.</param>
73 /// <param name="parameters">The parameters.</param>
74 /// <param name="points">The points.</param>
75 /// <returns></returns>
76 public string CreateUrl(string hostname, string virtualPath, IDictionary parameters, out int points)
78 points = 0;
79 StringBuilder text = new StringBuilder(virtualPath);
80 IList<string> checkedParameters = new List<string>();
82 foreach(DefaultNode node in nodes)
84 AppendSlashOrDot(text, node);
86 if (node.name == null)
88 text.Append(node.start);
90 else
92 checkedParameters.Add(node.name);
94 object value = parameters[node.name];
95 string valAsString = value != null ? value.ToString() : null;
97 if (string.IsNullOrEmpty(valAsString))
99 if (!node.optional)
101 return null;
103 else
105 break;
108 else
110 if (node.hasRestriction && !node.Accepts(value.ToString()))
112 return null;
115 points += 1;
117 if (node.optional &&
118 StringComparer.InvariantCultureIgnoreCase.Compare(node.DefaultVal, value.ToString()) == 0)
120 break; // end as there can't be more required nodes after an optional one
123 text.Append(value.ToString());
128 // Validate that default parameters match parameters passed into to create url.
129 foreach(KeyValuePair<string, string> defaultParameter in defaults)
131 // Skip parameters we already checked.
132 if (checkedParameters.Contains(defaultParameter.Key))
134 continue;
137 object value = parameters[defaultParameter.Key];
138 string valAsString = value != null ? value.ToString() : null;
139 if (!string.IsNullOrEmpty(valAsString) &&
140 !defaultParameter.Value.Equals(valAsString, StringComparison.OrdinalIgnoreCase))
142 return null;
146 if (text.Length == 0 || text[text.Length - 1] == '/' || text[text.Length - 1] == '.')
148 text.Length = text.Length - 1;
151 return text.ToString();
154 /// <summary>
155 /// Determines if the specified URL matches the
156 /// routing rule.
157 /// </summary>
158 /// <param name="url">The URL.</param>
159 /// <param name="context">The context</param>
160 /// <param name="match">The match.</param>
161 /// <returns></returns>
162 public int Matches(string url, IRouteContext context, RouteMatch match)
164 string[] parts = url.Split(new char[] {'/', '.'}, StringSplitOptions.RemoveEmptyEntries);
165 int index = 0;
166 int points = 0;
168 foreach(DefaultNode node in nodes)
170 string part = index < parts.Length ? parts[index] : null;
172 if (!node.Matches(part, match, ref points))
174 return 0;
177 index++;
180 foreach(KeyValuePair<string, string> pair in defaults)
182 if (!match.Parameters.ContainsKey(pair.Key))
184 match.Parameters.Add(pair.Key, pair.Value);
188 return points;
191 private void CreatePatternNodes()
193 string[] parts = pattern.Split(new char[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
195 foreach(string part in parts)
197 string[] subparts = part.Split(new char[] {'.'}, 2, StringSplitOptions.RemoveEmptyEntries);
199 if (subparts.Length == 2)
201 bool afterDot = false;
203 foreach(string subpart in subparts)
205 if (subpart.Contains("["))
207 nodes.Add(CreateNamedOptionalNode(subpart, afterDot));
209 else
211 nodes.Add(CreateRequiredNode(subpart, afterDot));
214 afterDot = true;
217 else
219 if (part.Contains("["))
221 nodes.Add(CreateNamedOptionalNode(part, false));
223 else
225 nodes.Add(CreateRequiredNode(part, false));
231 /// <summary>
232 /// Adds a default entry.
233 /// </summary>
234 /// <param name="key">The key.</param>
235 /// <param name="value">The value.</param>
236 public void AddDefault(string key, string value)
238 defaults[key] = value;
241 private DefaultNode CreateNamedOptionalNode(string part, bool afterDot)
243 return new DefaultNode(part, true, afterDot);
246 private DefaultNode CreateRequiredNode(string part, bool afterDot)
248 return new DefaultNode(part, false, afterDot);
251 private static void AppendSlashOrDot(StringBuilder text, DefaultNode node)
253 if (text.Length == 0 || text[text.Length - 1] != '/')
255 if (node.afterDot)
257 text.Append('.');
259 else
261 text.Append('/');
266 #region DefaultNode
268 [DebuggerDisplay("Node {name} Opt: {optional} default: {defaultVal} Regular exp: {exp}")]
269 private class DefaultNode
271 public readonly string name, start, end;
272 public readonly bool optional;
273 public readonly bool afterDot;
274 public bool hasRestriction, isStaticNode;
275 private string defaultVal;
276 private string[] acceptedTokens;
277 private Regex exp;
278 private string acceptedRegex;
280 public DefaultNode(string part, bool optional, bool afterDot)
282 this.optional = optional;
283 this.afterDot = afterDot;
284 int indexStart = part.IndexOfAny(new char[] {'<', '['});
285 int indexEndStart = -1;
287 if (indexStart != -1)
289 indexEndStart = part.IndexOfAny(new char[] {'>', ']'}, indexStart);
290 name = part.Substring(indexStart + 1, indexEndStart - indexStart - 1);
293 if (indexStart != -1)
295 start = part.Substring(0, indexStart);
297 else
299 start = part;
302 end = "";
304 if (indexEndStart != -1)
306 end = part.Substring(indexEndStart + 1);
309 ReBuildRegularExpression();
312 private void ReBuildRegularExpression()
314 RegexOptions options = RegexOptions.Compiled | RegexOptions.Singleline;
316 if (name != null)
318 isStaticNode = false;
319 exp = new Regex("^" + CharClass(start) + "(" + GetExpression() + ")" + CharClass(end) + "$", options);
321 else
323 isStaticNode = true;
324 exp = new Regex("^(" + CharClass(start) + ")$");
328 private string GetExpression()
330 if (!string.IsNullOrEmpty(acceptedRegex))
332 return acceptedRegex;
334 else if (acceptedTokens != null && acceptedTokens.Length != 0)
336 StringBuilder text = new StringBuilder();
338 foreach(string token in acceptedTokens)
340 if (text.Length != 0)
342 text.Append("|");
344 text.Append("(");
345 text.Append(CharClass(token));
346 text.Append(")");
349 return text.ToString();
351 else
353 return "[a-zA-Z,_,0-9,-]+";
357 public bool Matches(string part, RouteMatch match, ref int points)
359 if (part == null)
361 if (optional)
363 if (name != null)
365 match.AddNamed(name, defaultVal);
368 return true;
370 else
372 return false;
376 Match regExpMatch = exp.Match(part);
378 if (regExpMatch.Success)
380 if (name != null)
382 match.AddNamed(name, part);
385 points += isStaticNode ? 4 : 2;
387 return true;
390 return false;
393 public void AcceptsAnyOf(string[] names)
395 hasRestriction = true;
396 acceptedTokens = names;
397 ReBuildRegularExpression();
400 public string DefaultVal
402 get { return defaultVal; }
403 set { defaultVal = value; }
406 public bool AcceptsIntOnly
408 set { AcceptsRegex("[0-9]+"); }
411 public bool AcceptsGuidsOnly
415 AcceptsRegex("[A-Fa-f0-9]{32}|" +
416 "({|\\()?[A-Fa-f0-9]{8}-([A-Fa-f0-9]{4}-){3}[A-Fa-f0-9]{12}(}|\\))?|" +
417 "({)?[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}(}})");
421 public void AcceptsRegex(string regex)
423 hasRestriction = true;
424 acceptedRegex = regex;
425 ReBuildRegularExpression();
428 public bool Accepts(string val)
430 Match regExpMatch = exp.Match(val);
432 return (regExpMatch.Success);
436 #endregion
438 /// <summary>
439 /// Configures the default for the named pattern part.
440 /// </summary>
441 /// <param name="namedPatternPart">The named pattern part.</param>
442 /// <returns></returns>
443 public DefaultConfigurer DefaultFor(string namedPatternPart)
445 return new DefaultConfigurer(this, namedPatternPart);
448 /// <summary>
449 /// Configures the default for the named pattern part.
450 /// </summary>
451 /// <returns></returns>
452 public DefaultConfigurer DefaultForController()
454 return new DefaultConfigurer(this, "controller");
457 /// <summary>
458 /// Configures the default for the named pattern part.
459 /// </summary>
460 /// <returns></returns>
461 public DefaultConfigurer DefaultForAction()
463 return new DefaultConfigurer(this, "action");
466 /// <summary>
467 /// Configures the default for the named pattern part.
468 /// </summary>
469 /// <returns></returns>
470 public DefaultConfigurer DefaultForArea()
472 return new DefaultConfigurer(this, "area");
475 /// <summary>
476 /// Configures restrictions for the named pattern part.
477 /// </summary>
478 /// <param name="namedPatternPart">The named pattern part.</param>
479 /// <returns></returns>
480 public RestrictionConfigurer Restrict(string namedPatternPart)
482 return new RestrictionConfigurer(this, namedPatternPart);
485 /// <summary>
486 /// Pendent
487 /// </summary>
488 public class RestrictionConfigurer
490 private readonly PatternRoute route;
491 private readonly DefaultNode targetNode;
493 /// <summary>
494 /// Initializes a new instance of the <see cref="RestrictionConfigurer"/> class.
495 /// </summary>
496 /// <param name="route">The route.</param>
497 /// <param name="namedPatternPart">The named pattern part.</param>
498 public RestrictionConfigurer(PatternRoute route, string namedPatternPart)
500 this.route = route;
501 targetNode = route.GetNamedNode(namedPatternPart, true);
504 /// <summary>
505 /// Restricts this named pattern part to only accept one of the
506 /// strings passed in.
507 /// </summary>
508 /// <param name="validNames">The valid names.</param>
509 /// <returns></returns>
510 public PatternRoute AnyOf(params string[] validNames)
512 targetNode.AcceptsAnyOf(validNames);
513 return route;
516 /// <summary>
517 /// Restricts this named pattern part to only accept content
518 /// that does not match the string specified.
519 /// </summary>
520 /// <param name="name">The name that cannot be matched.</param>
521 /// <returns></returns>
522 public PatternRoute AnythingBut(string name)
524 // \w+(?<!view|index)\b
525 // targetNode.DoesNotAccept(name);
526 // return route;
527 throw new NotImplementedException();
530 /// <summary>
531 /// Restricts this named pattern part to only accept integers.
532 /// </summary>
533 /// <value>The valid integer.</value>
534 public PatternRoute ValidInteger
538 targetNode.AcceptsIntOnly = true;
539 return route;
543 /// <summary>
544 /// Restricts this named pattern part to only accept guids.
545 /// </summary>
546 public PatternRoute ValidGuid
550 targetNode.AcceptsGuidsOnly = true;
551 return route;
555 /// <summary>
556 /// Restricts this named pattern part to only accept strings
557 /// matching the regular expression passed in.
558 /// </summary>
559 /// <param name="regex"></param>
560 /// <returns></returns>
561 public PatternRoute ValidRegex(string regex)
563 targetNode.AcceptsRegex(regex);
564 return route;
568 /// <summary>
569 /// Pendent
570 /// </summary>
571 public class DefaultConfigurer
573 private readonly PatternRoute route;
574 private readonly string namedPatternPart;
575 private readonly DefaultNode targetNode;
577 /// <summary>
578 /// Initializes a new instance of the <see cref="DefaultConfigurer"/> class.
579 /// </summary>
580 /// <param name="patternRoute">The pattern route.</param>
581 /// <param name="namedPatternPart">The named pattern part.</param>
582 public DefaultConfigurer(PatternRoute patternRoute, string namedPatternPart)
584 route = patternRoute;
585 this.namedPatternPart = namedPatternPart;
586 targetNode = route.GetNamedNode(namedPatternPart, false);
589 /// <summary>
590 /// Sets the default value for this named pattern part.
591 /// </summary>
592 /// <returns></returns>
593 public PatternRoute Is<T>() where T : class, IController
595 ControllerDescriptor desc = ControllerInspectionUtil.Inspect(typeof(T));
596 if (targetNode != null)
598 targetNode.DefaultVal = desc.Name;
600 route.AddDefault(namedPatternPart, desc.Name);
601 return route;
604 /// <summary>
605 /// Sets the default value for this named pattern part.
606 /// </summary>
607 /// <param name="value">The value.</param>
608 /// <returns></returns>
609 public PatternRoute Is(string value)
611 if (targetNode != null)
613 targetNode.DefaultVal = value;
615 route.AddDefault(namedPatternPart, value);
616 return route;
619 /// <summary>
620 /// Sets the default value as empty for this named pattern part.
621 /// </summary>
622 /// <value>The is empty.</value>
623 public PatternRoute IsEmpty
625 get { return Is(string.Empty); }
629 // See http://weblogs.asp.net/justin_rogers/archive/2004/03/20/93379.aspx
630 private static string CharClass(string content)
632 if (content == String.Empty)
634 return string.Empty;
637 StringBuilder builder = new StringBuilder();
639 foreach(char c in content)
641 if (char.IsLetter(c))
643 builder.AppendFormat("[{0}{1}]", char.ToLower(c), char.ToUpper(c));
645 else
647 builder.Append(c);
651 return builder.ToString();
654 /// <summary>
655 /// Gets the named node.
656 /// </summary>
657 /// <param name="part">The part.</param>
658 /// <param name="mustFind">if set to <c>true</c> [must find].</param>
659 /// <returns></returns>
660 private DefaultNode GetNamedNode(string part, bool mustFind)
662 DefaultNode found = nodes.Find(delegate(DefaultNode node) { return node.name == part; });
664 if (found == null && mustFind)
666 throw new ArgumentException("Could not find pattern node for name " + part);
669 return found;