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
>();
35 private readonly Dictionary
<string, string> defaults
= new Dictionary
<string, string>(StringComparer
.InvariantCultureIgnoreCase
);
38 /// Initializes a new instance of the <see cref="PatternRoute"/> class.
40 /// <param name="pattern">The pattern.</param>
41 public PatternRoute(string pattern
)
43 this.pattern
= pattern
;
48 /// Initializes a new instance of the <see cref="PatternRoute"/> class.
50 /// <param name="name">The route name.</param>
51 /// <param name="pattern">The pattern.</param>
52 public PatternRoute(string name
, string pattern
) : this(pattern
)
58 /// Gets the name of the route.
60 /// <value>The name of the route.</value>
61 public string RouteName
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
);
91 checkedParameters
.Add(node
.name
);
93 object value = parameters
[node
.name
];
94 string valAsString
= value != null ? value.ToString() : null;
96 if (string.IsNullOrEmpty(valAsString
))
109 if (node
.hasRestriction
&& !node
.Accepts(value.ToString()))
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
))
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
))
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;
150 /// Determines if the specified URL matches the
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
);
162 foreach(DefaultNode node
in nodes
)
164 string part
= index
< parts
.Length
? parts
[index
] : null;
166 if (!node
.Matches(part
, match
))
174 foreach(KeyValuePair
<string, string> pair
in defaults
)
176 if (!match
.Parameters
.ContainsKey(pair
.Key
))
178 match
.Parameters
.Add(pair
.Key
, pair
.Value
);
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
));
205 nodes
.Add(CreateRequiredNode(subpart
, afterDot
));
213 if (part
.Contains("["))
215 nodes
.Add(CreateNamedOptionalNode(part
, false));
219 nodes
.Add(CreateRequiredNode(part
, false));
226 /// Adds a default entry.
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] != '/')
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
;
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
);
298 if (indexEndStart
!= -1)
300 end
= part
.Substring(indexEndStart
+ 1);
303 ReBuildRegularExpression();
306 private void ReBuildRegularExpression()
308 RegexOptions options
= RegexOptions
.Compiled
| RegexOptions
.Singleline
;
312 exp
= new Regex("^" + CharClass(start
) + "(" + GetExpression() + ")" + CharClass(end
) + "$", options
);
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)
337 text
.Append(CharClass(token
));
341 return text
.ToString();
345 return "[a-zA-Z,_,0-9,-]+";
349 public bool Matches(string part
, RouteMatch match
)
357 match
.AddNamed(name
, defaultVal
);
367 Match regExpMatch
= exp
.Match(part
);
369 if (regExpMatch
.Success
)
373 match
.AddNamed(name
, part
);
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
);
431 /// Configures the default for the named pattern part.
433 /// <param name="namedPatternPart">The named pattern part.</param>
434 /// <returns></returns>
435 public DefaultConfigurer
DefaultFor(string namedPatternPart
)
437 return new DefaultConfigurer(this, namedPatternPart
);
441 /// Configures the default for the named pattern part.
443 /// <returns></returns>
444 public DefaultConfigurer
DefaultForController()
446 return new DefaultConfigurer(this, "controller");
450 /// Configures the default for the named pattern part.
452 /// <returns></returns>
453 public DefaultConfigurer
DefaultForAction()
455 return new DefaultConfigurer(this, "action");
459 /// Configures the default for the named pattern part.
461 /// <returns></returns>
462 public DefaultConfigurer
DefaultForArea()
464 return new DefaultConfigurer(this, "area");
468 /// Configures restrictions for the named pattern part.
470 /// <param name="namedPatternPart">The named pattern part.</param>
471 /// <returns></returns>
472 public RestrictionConfigurer
Restrict(string namedPatternPart
)
474 return new RestrictionConfigurer(this, namedPatternPart
);
480 public class RestrictionConfigurer
482 private readonly PatternRoute route
;
483 private readonly DefaultNode targetNode
;
486 /// Initializes a new instance of the <see cref="RestrictionConfigurer"/> class.
488 /// <param name="route">The route.</param>
489 /// <param name="namedPatternPart">The named pattern part.</param>
490 public RestrictionConfigurer(PatternRoute route
, string namedPatternPart
)
493 targetNode
= route
.GetNamedNode(namedPatternPart
, true);
497 /// Restricts this named pattern part to only accept one of the
498 /// strings passed in.
500 /// <param name="validNames">The valid names.</param>
501 /// <returns></returns>
502 public PatternRoute
AnyOf(params string[] validNames
)
504 targetNode
.AcceptsAnyOf(validNames
);
509 /// Restricts this named pattern part to only accept integers.
511 /// <value>The valid integer.</value>
512 public PatternRoute ValidInteger
516 targetNode
.AcceptsIntOnly
= true;
522 /// Restricts this named pattern part to only accept guids.
524 public PatternRoute ValidGuid
528 targetNode
.AcceptsGuidsOnly
= true;
534 /// Restricts this named pattern part to only accept strings
535 /// matching the regular expression passed in.
537 /// <param name="regex"></param>
538 /// <returns></returns>
539 public PatternRoute
ValidRegex(string regex
)
541 targetNode
.AcceptsRegex(regex
);
549 public class DefaultConfigurer
551 private readonly PatternRoute route
;
552 private readonly string namedPatternPart
;
553 private readonly DefaultNode targetNode
;
556 /// Initializes a new instance of the <see cref="DefaultConfigurer"/> class.
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);
568 /// Sets the default value for this named pattern part.
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
);
583 /// Sets the default value for this named pattern part.
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);
598 /// Sets the default value as empty for this named pattern part.
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
)
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
));
629 return builder
.ToString();
633 /// Gets the named node.
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
);