1
// Copyright 2004-2008 Castle Project - http://www.castleproject.org/
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
7 // http://www.apache.org/licenses/LICENSE-2.0
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
18 using System
.Collections
;
19 using System
.Collections
.Generic
;
20 using System
.Diagnostics
;
22 using System
.Text
.RegularExpressions
;
23 using Castle
.MonoRail
.Framework
.Services
.Utils
;
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
);
40 /// Initializes a new instance of the <see cref="PatternRoute"/> class.
42 /// <param name="pattern">The pattern.</param>
43 public PatternRoute(string pattern
)
45 this.pattern
= pattern
;
50 /// Initializes a new instance of the <see cref="PatternRoute"/> class.
52 /// <param name="name">The route name.</param>
53 /// <param name="pattern">The pattern.</param>
54 public PatternRoute(string name
, string pattern
) : this(pattern
)
60 /// Gets the name of the route.
62 /// <value>The name of the route.</value>
63 public string RouteName
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
)
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
);
92 checkedParameters
.Add(node
.name
);
94 object value = parameters
[node
.name
];
95 string valAsString
= value != null ? value.ToString() : null;
97 if (string.IsNullOrEmpty(valAsString
))
110 if (node
.hasRestriction
&& !node
.Accepts(value.ToString()))
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
))
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
))
146 if (text
.Length
== 0 || text
[text
.Length
- 1] == '/' || text
[text
.Length
- 1] == '.')
148 text
.Length
= text
.Length
- 1;
151 return text
.ToString();
155 /// Determines if the specified URL matches the
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
);
168 foreach(DefaultNode node
in nodes
)
170 string part
= index
< parts
.Length
? parts
[index
] : null;
172 if (!node
.Matches(part
, match
, ref points
))
180 foreach(KeyValuePair
<string, string> pair
in defaults
)
182 if (!match
.Parameters
.ContainsKey(pair
.Key
))
184 match
.Parameters
.Add(pair
.Key
, pair
.Value
);
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
));
211 nodes
.Add(CreateRequiredNode(subpart
, afterDot
));
219 if (part
.Contains("["))
221 nodes
.Add(CreateNamedOptionalNode(part
, false));
225 nodes
.Add(CreateRequiredNode(part
, false));
232 /// Adds a default entry.
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] != '/')
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
;
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
);
304 if (indexEndStart
!= -1)
306 end
= part
.Substring(indexEndStart
+ 1);
309 ReBuildRegularExpression();
312 private void ReBuildRegularExpression()
314 RegexOptions options
= RegexOptions
.Compiled
| RegexOptions
.Singleline
;
318 isStaticNode
= false;
319 exp
= new Regex("^" + CharClass(start
) + "(" + GetExpression() + ")" + CharClass(end
) + "$", options
);
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)
345 text
.Append(CharClass(token
));
349 return text
.ToString();
353 return "[a-zA-Z,_,0-9,-]+";
357 public bool Matches(string part
, RouteMatch match
, ref int points
)
365 match
.AddNamed(name
, defaultVal
);
376 Match regExpMatch
= exp
.Match(part
);
378 if (regExpMatch
.Success
)
382 match
.AddNamed(name
, part
);
385 points
+= isStaticNode
? 4 : 2;
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
);
439 /// Configures the default for the named pattern part.
441 /// <param name="namedPatternPart">The named pattern part.</param>
442 /// <returns></returns>
443 public DefaultConfigurer
DefaultFor(string namedPatternPart
)
445 return new DefaultConfigurer(this, namedPatternPart
);
449 /// Configures the default for the named pattern part.
451 /// <returns></returns>
452 public DefaultConfigurer
DefaultForController()
454 return new DefaultConfigurer(this, "controller");
458 /// Configures the default for the named pattern part.
460 /// <returns></returns>
461 public DefaultConfigurer
DefaultForAction()
463 return new DefaultConfigurer(this, "action");
467 /// Configures the default for the named pattern part.
469 /// <returns></returns>
470 public DefaultConfigurer
DefaultForArea()
472 return new DefaultConfigurer(this, "area");
476 /// Configures restrictions for the named pattern part.
478 /// <param name="namedPatternPart">The named pattern part.</param>
479 /// <returns></returns>
480 public RestrictionConfigurer
Restrict(string namedPatternPart
)
482 return new RestrictionConfigurer(this, namedPatternPart
);
488 public class RestrictionConfigurer
490 private readonly PatternRoute route
;
491 private readonly DefaultNode targetNode
;
494 /// Initializes a new instance of the <see cref="RestrictionConfigurer"/> class.
496 /// <param name="route">The route.</param>
497 /// <param name="namedPatternPart">The named pattern part.</param>
498 public RestrictionConfigurer(PatternRoute route
, string namedPatternPart
)
501 targetNode
= route
.GetNamedNode(namedPatternPart
, true);
505 /// Restricts this named pattern part to only accept one of the
506 /// strings passed in.
508 /// <param name="validNames">The valid names.</param>
509 /// <returns></returns>
510 public PatternRoute
AnyOf(params string[] validNames
)
512 targetNode
.AcceptsAnyOf(validNames
);
517 /// Restricts this named pattern part to only accept content
518 /// that does not match the string specified.
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);
527 throw new NotImplementedException();
531 /// Restricts this named pattern part to only accept integers.
533 /// <value>The valid integer.</value>
534 public PatternRoute ValidInteger
538 targetNode
.AcceptsIntOnly
= true;
544 /// Restricts this named pattern part to only accept guids.
546 public PatternRoute ValidGuid
550 targetNode
.AcceptsGuidsOnly
= true;
556 /// Restricts this named pattern part to only accept strings
557 /// matching the regular expression passed in.
559 /// <param name="regex"></param>
560 /// <returns></returns>
561 public PatternRoute
ValidRegex(string regex
)
563 targetNode
.AcceptsRegex(regex
);
571 public class DefaultConfigurer
573 private readonly PatternRoute route
;
574 private readonly string namedPatternPart
;
575 private readonly DefaultNode targetNode
;
578 /// Initializes a new instance of the <see cref="DefaultConfigurer"/> class.
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);
590 /// Sets the default value for this named pattern part.
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
);
605 /// Sets the default value for this named pattern part.
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);
620 /// Sets the default value as empty for this named pattern part.
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
)
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
));
651 return builder
.ToString();
655 /// Gets the named node.
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
);