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
;
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 exp
= new Regex("^" + CharClass(start
) + "(" + GetExpression() + ")" + CharClass(end
) + "$", options
);
322 exp
= new Regex("^(" + CharClass(start
) + ")$");
326 private string GetExpression()
328 if (!string.IsNullOrEmpty(acceptedRegex
))
330 return acceptedRegex
;
332 else if (acceptedTokens
!= null && acceptedTokens
.Length
!= 0)
334 StringBuilder text
= new StringBuilder();
336 foreach(string token
in acceptedTokens
)
338 if (text
.Length
!= 0)
343 text
.Append(CharClass(token
));
347 return text
.ToString();
351 return "[a-zA-Z,_,0-9,-]+";
355 public bool Matches(string part
, RouteMatch match
, ref int points
)
363 match
.AddNamed(name
, defaultVal
);
376 Match regExpMatch
= exp
.Match(part
);
378 if (regExpMatch
.Success
)
382 match
.AddNamed(name
, part
);
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 integers.
519 /// <value>The valid integer.</value>
520 public PatternRoute ValidInteger
524 targetNode
.AcceptsIntOnly
= true;
530 /// Restricts this named pattern part to only accept guids.
532 public PatternRoute ValidGuid
536 targetNode
.AcceptsGuidsOnly
= true;
542 /// Restricts this named pattern part to only accept strings
543 /// matching the regular expression passed in.
545 /// <param name="regex"></param>
546 /// <returns></returns>
547 public PatternRoute
ValidRegex(string regex
)
549 targetNode
.AcceptsRegex(regex
);
557 public class DefaultConfigurer
559 private readonly PatternRoute route
;
560 private readonly string namedPatternPart
;
561 private readonly DefaultNode targetNode
;
564 /// Initializes a new instance of the <see cref="DefaultConfigurer"/> class.
566 /// <param name="patternRoute">The pattern route.</param>
567 /// <param name="namedPatternPart">The named pattern part.</param>
568 public DefaultConfigurer(PatternRoute patternRoute
, string namedPatternPart
)
570 route
= patternRoute
;
571 this.namedPatternPart
= namedPatternPart
;
572 targetNode
= route
.GetNamedNode(namedPatternPart
, false);
576 /// Sets the default value for this named pattern part.
578 /// <returns></returns>
579 public PatternRoute Is
<T
>() where T
: class, IController
581 ControllerDescriptor desc
= ControllerInspectionUtil
.Inspect(typeof(T
));
582 if (targetNode
!= null)
584 targetNode
.DefaultVal
= desc
.Name
;
586 route
.AddDefault(namedPatternPart
, desc
.Name
);
591 /// Sets the default value for this named pattern part.
593 /// <param name="value">The value.</param>
594 /// <returns></returns>
595 public PatternRoute
Is(string value)
597 if (targetNode
!= null)
599 targetNode
.DefaultVal
= value;
601 route
.AddDefault(namedPatternPart
, value);
606 /// Sets the default value as empty for this named pattern part.
608 /// <value>The is empty.</value>
609 public PatternRoute IsEmpty
611 get { return Is(string.Empty); }
615 // See http://weblogs.asp.net/justin_rogers/archive/2004/03/20/93379.aspx
616 private static string CharClass(string content
)
618 if (content
== String
.Empty
)
623 StringBuilder builder
= new StringBuilder();
625 foreach(char c
in content
)
627 if (char.IsLetter(c
))
629 builder
.AppendFormat("[{0}{1}]", char.ToLower(c
), char.ToUpper(c
));
637 return builder
.ToString();
641 /// Gets the named node.
643 /// <param name="part">The part.</param>
644 /// <param name="mustFind">if set to <c>true</c> [must find].</param>
645 /// <returns></returns>
646 private DefaultNode
GetNamedNode(string part
, bool mustFind
)
648 DefaultNode found
= nodes
.Find(delegate(DefaultNode node
) { return node.name == part; }
);
650 if (found
== null && mustFind
)
652 throw new ArgumentException("Could not find pattern node for name " + part
);