5 // Atsushi Enomoto <atsushi@ximian.com>
6 // Marek Habersack <mhabersack@novell.com>
8 // Copyright (C) 2008-2010 Novell Inc. http://novell.com
12 // Permission is hereby granted, free of charge, to any person obtaining
13 // a copy of this software and associated documentation files (the
14 // "Software"), to deal in the Software without restriction, including
15 // without limitation the rights to use, copy, modify, merge, publish,
16 // distribute, sublicense, and/or sell copies of the Software, and to
17 // permit persons to whom the Software is furnished to do so, subject to
18 // the following conditions:
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
23 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 using System.Collections.Generic;
33 using System.Security.Permissions;
36 using System.Web.Util;
38 namespace System.Web.Routing
40 sealed class PatternParser
44 public bool AllLiteral;
45 public List <PatternToken> Tokens;
48 static readonly char[] placeholderDelimiters = { '{', '}' };
50 PatternSegment[] segments;
51 Dictionary <string, bool> parameterNames;
52 PatternToken[] tokens;
55 bool haveSegmentWithCatchAll;
62 public PatternParser (string pattern)
71 parameterNames = new Dictionary <string, bool> (StringComparer.OrdinalIgnoreCase);
73 if (!String.IsNullOrEmpty (url)) {
74 if (url [0] == '~' || url [0] == '/')
75 throw new ArgumentException ("Url must not start with '~' or '/'");
76 if (url.IndexOf ('?') >= 0)
77 throw new ArgumentException ("Url must not contain '?'");
79 segments = new PatternSegment [0];
80 tokens = new PatternToken [0];
84 string[] parts = url.Split ('/');
85 int partsCount = segmentCount = parts.Length;
86 var allTokens = new List <PatternToken> ();
87 PatternToken tmpToken;
89 segments = new PatternSegment [partsCount];
91 for (int i = 0; i < partsCount; i++) {
92 if (haveSegmentWithCatchAll)
93 throw new ArgumentException ("A catch-all parameter can only appear as the last segment of the route URL");
96 string part = parts [i];
97 int partLength = part.Length;
98 var tokens = new List <PatternToken> ();
100 if (partLength == 0 && i < partsCount - 1)
101 throw new ArgumentException ("Consecutive URL segment separators '/' are not allowed");
103 if (part.IndexOf ("{}") != -1)
104 throw new ArgumentException ("Empty URL parameter name is not allowed");
107 allTokens.Add (null);
109 if (part.IndexOfAny (placeholderDelimiters) == -1) {
110 // no placeholders here, short-circuit it
111 tmpToken = new PatternToken (PatternTokenType.Literal, part);
112 tokens.Add (tmpToken);
113 allTokens.Add (tmpToken);
114 segments [i].AllLiteral = true;
115 segments [i].Tokens = tokens;
121 bool allLiteral = true;
122 while (from < partLength) {
123 start = part.IndexOf ('{', from);
124 if (start >= partLength - 2)
125 throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
128 if (part.IndexOf ('}', from) >= from)
129 throw new ArgumentException ("Unmatched URL parameter closer '}'. A corresponding '{' must precede");
130 tmp = part.Substring (from);
131 tmpToken = new PatternToken (PatternTokenType.Literal, tmp);
132 tokens.Add (tmpToken);
133 allTokens.Add (tmpToken);
138 if (from == 0 && start > 0) {
139 tmpToken = new PatternToken (PatternTokenType.Literal, part.Substring (0, start));
140 tokens.Add (tmpToken);
141 allTokens.Add (tmpToken);
144 int end = part.IndexOf ('}', start + 1);
145 int next = part.IndexOf ('{', start + 1);
147 if (end < 0 || next >= 0 && next < end)
148 throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
150 throw new ArgumentException ("Two consecutive URL parameters are not allowed. Split into a different segment by '/', or a literal string.");
155 string token = part.Substring (start + 1, end - start - 1);
156 PatternTokenType type;
157 if (token [0] == '*') {
159 haveSegmentWithCatchAll = true;
160 type = PatternTokenType.CatchAll;
161 token = token.Substring (1);
163 type = PatternTokenType.Standard;
165 if (!parameterNames.ContainsKey (token))
166 parameterNames.Add (token, true);
168 tmpToken = new PatternToken (type, token);
169 tokens.Add (tmpToken);
170 allTokens.Add (tmpToken);
173 if (end < partLength - 1) {
174 token = part.Substring (end + 1, next - end - 1);
175 tmpToken = new PatternToken (PatternTokenType.Literal, token);
176 tokens.Add (tmpToken);
177 allTokens.Add (tmpToken);
181 if (catchAlls > 1 || (catchAlls == 1 && tokens.Count > 1))
182 throw new ArgumentException ("A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.");
186 segments [i].AllLiteral = allLiteral;
187 segments [i].Tokens = tokens;
190 if (allTokens.Count > 0)
191 this.tokens = allTokens.ToArray ();
195 RouteValueDictionary AddDefaults (RouteValueDictionary dict, RouteValueDictionary defaults)
197 if (defaults != null && defaults.Count > 0) {
199 foreach (var def in defaults) {
201 if (dict.ContainsKey (key))
203 dict.Add (key, def.Value);
210 bool MatchSegment (int segIndex, int argsCount, string[] argSegs, List <PatternToken> tokens, RouteValueDictionary ret)
212 string pathSegment = argSegs [segIndex];
213 int pathSegmentLength = pathSegment != null ? pathSegment.Length : -1;
214 int startIndex = pathSegmentLength - 1;
215 PatternTokenType tokenType;
216 int tokensCount = tokens.Count;
220 for (int tokenIndex = tokensCount - 1; tokenIndex > -1; tokenIndex--) {
221 token = tokens [tokenIndex];
225 tokenType = token.Type;
226 tokenName = token.Name;
228 if (segIndex > segmentCount - 1 || tokenType == PatternTokenType.CatchAll) {
229 var sb = new StringBuilder ();
231 for (int j = segIndex; j < argsCount; j++) {
234 sb.Append (argSegs [j]);
237 ret.Add (tokenName, sb.ToString ());
242 if (token.Type == PatternTokenType.Literal) {
243 int nameLen = tokenName.Length;
244 if (startIndex + 1 < nameLen)
246 scanIndex = startIndex - nameLen + 1;
247 if (String.Compare (pathSegment, scanIndex, tokenName, 0, nameLen, StringComparison.OrdinalIgnoreCase) != 0)
249 startIndex = scanIndex - 1;
254 int nextTokenIndex = tokenIndex - 1;
255 if (nextTokenIndex < 0) {
257 ret.Add (tokenName, pathSegment.Substring (0, startIndex + 1));
264 var nextToken = tokens [nextTokenIndex];
265 string nextTokenName = nextToken.Name;
267 // Skip one char, since there can be no empty segments and if the
268 // current token's value happens to be the same as preceeding
269 // literal text, we'll save some time and complexity.
270 scanIndex = startIndex - 1;
271 int lastIndex = pathSegment.LastIndexOf (nextTokenName, scanIndex, StringComparison.OrdinalIgnoreCase);
275 lastIndex += nextTokenName.Length - 1;
277 string sectionValue = pathSegment.Substring (lastIndex + 1, startIndex - lastIndex);
278 if (String.IsNullOrEmpty (sectionValue))
281 ret.Add (tokenName, sectionValue);
282 startIndex = lastIndex;
288 public RouteValueDictionary Match (string path, RouteValueDictionary defaults)
290 var ret = new RouteValueDictionary ();
295 if (String.IsNullOrEmpty (path)) {
300 if (String.Compare (url, path, StringComparison.Ordinal) == 0 && url.IndexOf ('{') < 0)
301 return AddDefaults (ret, defaults);
303 argSegs = path.Split ('/');
304 argsCount = argSegs.Length;
306 if (String.IsNullOrEmpty (argSegs [argsCount - 1]))
307 argsCount--; // path ends with a trailinig '/'
309 bool haveDefaults = defaults != null && defaults.Count > 0;
311 if (argsCount == 1 && String.IsNullOrEmpty (argSegs [0]))
314 if (!haveDefaults && ((haveSegmentWithCatchAll && argsCount < segmentCount) || (!haveSegmentWithCatchAll && argsCount != segmentCount)))
319 foreach (PatternSegment segment in segments) {
323 if (segment.AllLiteral) {
324 if (String.Compare (argSegs [i], segment.Tokens [0].Name, StringComparison.OrdinalIgnoreCase) != 0)
330 if (!MatchSegment (i, argsCount, argSegs, segment.Tokens, ret))
335 // Check the remaining segments, if any, and see if they are required
337 // If a segment has more than one section (i.e. there's at least one
338 // literal, then it cannot match defaults
340 // All of the remaining segments must have all defaults provided and they
341 // must not be literals or the match will fail.
342 if (i < segmentCount) {
346 for (;i < segmentCount; i++) {
347 var segment = segments [i];
348 if (segment.AllLiteral)
351 var tokens = segment.Tokens;
352 if (tokens.Count != 1)
355 if (!defaults.ContainsKey (tokens [0].Name))
358 } else if (!haveSegmentWithCatchAll && argsCount > segmentCount)
361 return AddDefaults (ret, defaults);
364 public bool BuildUrl (Route route, RequestContext requestContext, RouteValueDictionary userValues, out string value)
367 if (requestContext == null)
370 RouteData routeData = requestContext.RouteData;
371 RouteValueDictionary defaultValues = route != null ? route.Defaults : null;
372 RouteValueDictionary ambientValues = routeData.Values;
374 if (defaultValues != null && defaultValues.Count == 0)
375 defaultValues = null;
376 if (ambientValues != null && ambientValues.Count == 0)
377 ambientValues = null;
378 if (userValues != null && userValues.Count == 0)
381 // Check URL parameters
382 // It is allowed to take ambient values for required parameters if:
384 // - there are no default values provided
385 // - the default values dictionary contains at least one required
388 bool canTakeFromAmbient;
389 if (defaultValues == null)
390 canTakeFromAmbient = true;
392 canTakeFromAmbient = false;
393 foreach (KeyValuePair <string, bool> de in parameterNames) {
394 if (defaultValues.ContainsKey (de.Key)) {
395 canTakeFromAmbient = true;
401 bool allMustBeInUserValues = false;
402 foreach (KeyValuePair <string, bool> de in parameterNames) {
403 string parameterName = de.Key;
404 // Is the parameter required?
405 if (defaultValues == null || !defaultValues.ContainsKey (parameterName)) {
406 // Yes, it is required (no value in defaults)
407 // Has the user provided value for it?
408 if (userValues == null || !userValues.ContainsKey (parameterName)) {
409 if (allMustBeInUserValues)
410 return false; // partial override => no match
412 if (!canTakeFromAmbient || ambientValues == null || !ambientValues.ContainsKey (parameterName))
413 return false; // no value provided => no match
414 } else if (canTakeFromAmbient)
415 allMustBeInUserValues = true;
419 // Check for non-url parameters
420 if (defaultValues != null) {
421 foreach (var de in defaultValues) {
422 string parameterName = de.Key;
424 if (parameterNames.ContainsKey (parameterName))
427 object parameterValue = null;
428 // Has the user specified value for this parameter and, if
429 // yes, is it the same as the one in defaults?
430 if (userValues != null && userValues.TryGetValue (parameterName, out parameterValue)) {
431 object defaultValue = de.Value;
432 if (defaultValue is string && parameterValue is string) {
433 if (String.Compare ((string)defaultValue, (string)parameterValue, StringComparison.Ordinal) != 0)
434 return false; // different value => no match
435 } else if (defaultValue != parameterValue)
436 return false; // different value => no match
441 // Check the constraints
442 RouteValueDictionary constraints = route != null ? route.Constraints : null;
443 if (constraints != null && constraints.Count > 0) {
444 HttpContextBase context = requestContext.HttpContext;
445 bool invalidConstraint;
447 foreach (var de in constraints) {
448 if (!Route.ProcessConstraintInternal (context, route, de.Value, de.Key, userValues, RouteDirection.UrlGeneration, out invalidConstraint) ||
450 return false; // constraint not met => no match
454 // We're a match, generate the URL
455 var ret = new StringBuilder ();
458 // Going in reverse order, so that we can trim without much ado
459 int tokensCount = tokens.Length - 1;
460 for (int i = tokensCount; i >= 0; i--) {
461 PatternToken token = tokens [i];
463 if (i < tokensCount && ret.Length > 0 && ret [0] != '/')
468 if (token.Type == PatternTokenType.Literal) {
469 ret.Insert (0, token.Name);
473 string parameterName = token.Name;
477 if (userValues.GetValue (parameterName, out tokenValue)) {
478 if (!defaultValues.Has (parameterName, tokenValue)) {
480 if (tokenValue != null)
481 ret.Insert (0, tokenValue.ToString ());
485 if (!canTrim && tokenValue != null)
486 ret.Insert (0, tokenValue.ToString ());
490 if (defaultValues.GetValue (parameterName, out tokenValue)) {
491 object ambientTokenValue;
492 if (ambientValues.GetValue (parameterName, out ambientTokenValue))
493 tokenValue = ambientTokenValue;
495 if (!canTrim && tokenValue != null)
496 ret.Insert (0, tokenValue.ToString ());
501 if (ambientValues.GetValue (parameterName, out tokenValue)) {
502 if (tokenValue != null)
503 ret.Insert (0, tokenValue.ToString ());
509 // All the values specified in userValues that aren't part of the original
510 // URL, the constraints or defaults collections are treated as overflow
511 // values - they are appended as query parameters to the URL
512 if (userValues != null) {
514 foreach (var de in userValues) {
515 string parameterName = de.Key;
518 if (parameterNames.ContainsKey (parameterName) || defaultValues.Has (parameterName) || constraints.Has (parameterName))
522 object parameterValue = de.Value;
523 if (parameterValue == null)
526 var parameterValueAsString = parameterValue as string;
527 if (parameterValueAsString != null && parameterValueAsString.Length == 0)
537 ret.Append (Uri.EscapeDataString (parameterName));
539 if (parameterValue != null)
540 ret.Append (Uri.EscapeDataString (de.Value.ToString ()));
544 value = ret.ToString ();