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;
35 using System.Text.RegularExpressions;
37 using System.Web.Util;
38 using System.Diagnostics;
39 using System.Globalization;
41 namespace System.Web.Routing
43 sealed class PatternParser
47 public bool AllLiteral;
48 public List <PatternToken> Tokens;
51 static readonly char[] placeholderDelimiters = { '{', '}' };
53 PatternSegment[] segments;
54 Dictionary <string, bool> parameterNames;
55 PatternToken[] tokens;
58 bool haveSegmentWithCatchAll;
65 public PatternParser (string pattern)
74 parameterNames = new Dictionary <string, bool> (StringComparer.OrdinalIgnoreCase);
76 if (!String.IsNullOrEmpty (url)) {
77 if (url [0] == '~' || url [0] == '/')
78 throw new ArgumentException ("Url must not start with '~' or '/'");
79 if (url.IndexOf ('?') >= 0)
80 throw new ArgumentException ("Url must not contain '?'");
82 segments = new PatternSegment [0];
83 tokens = new PatternToken [0];
87 string[] parts = url.Split ('/');
88 int partsCount = segmentCount = parts.Length;
89 var allTokens = new List <PatternToken> ();
90 PatternToken tmpToken;
92 segments = new PatternSegment [partsCount];
94 for (int i = 0; i < partsCount; i++) {
95 if (haveSegmentWithCatchAll)
96 throw new ArgumentException ("A catch-all parameter can only appear as the last segment of the route URL");
99 string part = parts [i];
100 int partLength = part.Length;
101 var tokens = new List <PatternToken> ();
103 if (partLength == 0 && i < partsCount - 1)
104 throw new ArgumentException ("Consecutive URL segment separators '/' are not allowed");
106 if (part.IndexOf ("{}") != -1)
107 throw new ArgumentException ("Empty URL parameter name is not allowed");
110 allTokens.Add (null);
112 if (part.IndexOfAny (placeholderDelimiters) == -1) {
113 // no placeholders here, short-circuit it
114 tmpToken = new PatternToken (PatternTokenType.Literal, part);
115 tokens.Add (tmpToken);
116 allTokens.Add (tmpToken);
117 segments [i].AllLiteral = true;
118 segments [i].Tokens = tokens;
124 bool allLiteral = true;
125 while (from < partLength) {
126 start = part.IndexOf ('{', from);
127 if (start >= partLength - 2)
128 throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
131 if (part.IndexOf ('}', from) >= from)
132 throw new ArgumentException ("Unmatched URL parameter closer '}'. A corresponding '{' must precede");
133 tmp = part.Substring (from);
134 tmpToken = new PatternToken (PatternTokenType.Literal, tmp);
135 tokens.Add (tmpToken);
136 allTokens.Add (tmpToken);
141 if (from == 0 && start > 0) {
142 tmpToken = new PatternToken (PatternTokenType.Literal, part.Substring (0, start));
143 tokens.Add (tmpToken);
144 allTokens.Add (tmpToken);
147 int end = part.IndexOf ('}', start + 1);
148 int next = part.IndexOf ('{', start + 1);
150 if (end < 0 || next >= 0 && next < end)
151 throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
153 throw new ArgumentException ("Two consecutive URL parameters are not allowed. Split into a different segment by '/', or a literal string.");
158 string token = part.Substring (start + 1, end - start - 1);
159 PatternTokenType type;
160 if (token [0] == '*') {
162 haveSegmentWithCatchAll = true;
163 type = PatternTokenType.CatchAll;
164 token = token.Substring (1);
166 type = PatternTokenType.Standard;
168 if (!parameterNames.ContainsKey (token))
169 parameterNames.Add (token, true);
171 tmpToken = new PatternToken (type, token);
172 tokens.Add (tmpToken);
173 allTokens.Add (tmpToken);
176 if (end < partLength - 1) {
177 token = part.Substring (end + 1, next - end - 1);
178 tmpToken = new PatternToken (PatternTokenType.Literal, token);
179 tokens.Add (tmpToken);
180 allTokens.Add (tmpToken);
184 if (catchAlls > 1 || (catchAlls == 1 && tokens.Count > 1))
185 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.");
189 segments [i].AllLiteral = allLiteral;
190 segments [i].Tokens = tokens;
193 if (allTokens.Count > 0)
194 this.tokens = allTokens.ToArray ();
198 RouteValueDictionary AddDefaults (RouteValueDictionary dict, RouteValueDictionary defaults)
200 if (defaults != null && defaults.Count > 0) {
202 foreach (var def in defaults) {
204 if (dict.ContainsKey (key))
206 dict.Add (key, def.Value);
213 static bool ParametersAreEqual (object a, object b)
215 if (a is string && b is string) {
216 return String.Equals (a as string, b as string, StringComparison.OrdinalIgnoreCase);
218 // Parameter may be a boxed value type, need to use .Equals() for comparison
219 return object.Equals (a, b);
223 static bool ParameterIsNonEmpty (object param)
226 return !string.IsNullOrEmpty (param as string);
228 return param != null;
231 bool IsParameterRequired (string parameterName, RouteValueDictionary defaultValues, out object defaultValue)
233 foreach (var token in tokens) {
237 if (string.Equals (token.Name, parameterName, StringComparison.OrdinalIgnoreCase)) {
238 if (token.Type == PatternTokenType.CatchAll) {
245 if (defaultValues == null)
246 throw new ArgumentNullException ("defaultValues is null?!");
248 return !defaultValues.TryGetValue (parameterName, out defaultValue);
251 static string EscapeReservedCharacters (Match m)
254 throw new ArgumentNullException("m");
256 return Uri.HexEscape (m.Value[0]);
259 static string UriEncode (string str)
261 if (string.IsNullOrEmpty (str))
264 string escape = Uri.EscapeUriString (str);
265 return Regex.Replace (escape, "([#?])", new MatchEvaluator (EscapeReservedCharacters));
268 bool MatchSegment (int segIndex, int argsCount, string[] argSegs, List <PatternToken> tokens, RouteValueDictionary ret)
270 string pathSegment = argSegs [segIndex];
271 int pathSegmentLength = pathSegment != null ? pathSegment.Length : -1;
272 int startIndex = pathSegmentLength - 1;
273 PatternTokenType tokenType;
274 int tokensCount = tokens.Count;
278 for (int tokenIndex = tokensCount - 1; tokenIndex > -1; tokenIndex--) {
279 token = tokens [tokenIndex];
283 tokenType = token.Type;
284 tokenName = token.Name;
286 if (segIndex > segmentCount - 1 || tokenType == PatternTokenType.CatchAll) {
287 var sb = new StringBuilder ();
289 for (int j = segIndex; j < argsCount; j++) {
292 sb.Append (argSegs [j]);
295 ret.Add (tokenName, sb.ToString ());
300 if (token.Type == PatternTokenType.Literal) {
301 int nameLen = tokenName.Length;
302 if (startIndex + 1 < nameLen)
304 scanIndex = startIndex - nameLen + 1;
305 if (String.Compare (pathSegment, scanIndex, tokenName, 0, nameLen, StringComparison.OrdinalIgnoreCase) != 0)
307 startIndex = scanIndex - 1;
312 int nextTokenIndex = tokenIndex - 1;
313 if (nextTokenIndex < 0) {
315 ret.Add (tokenName, pathSegment.Substring (0, startIndex + 1));
322 var nextToken = tokens [nextTokenIndex];
323 string nextTokenName = nextToken.Name;
325 // Skip one char, since there can be no empty segments and if the
326 // current token's value happens to be the same as preceeding
327 // literal text, we'll save some time and complexity.
328 scanIndex = startIndex - 1;
329 int lastIndex = pathSegment.LastIndexOf (nextTokenName, scanIndex, StringComparison.OrdinalIgnoreCase);
333 lastIndex += nextTokenName.Length - 1;
335 string sectionValue = pathSegment.Substring (lastIndex + 1, startIndex - lastIndex);
336 if (String.IsNullOrEmpty (sectionValue))
339 ret.Add (tokenName, sectionValue);
340 startIndex = lastIndex;
346 public RouteValueDictionary Match (string path, RouteValueDictionary defaults)
348 var ret = new RouteValueDictionary ();
353 if (String.IsNullOrEmpty (path)) {
358 if (String.Compare (url, path, StringComparison.Ordinal) == 0 && url.IndexOf ('{') < 0)
359 return AddDefaults (ret, defaults);
361 argSegs = path.Split ('/');
362 argsCount = argSegs.Length;
364 if (String.IsNullOrEmpty (argSegs [argsCount - 1]))
365 argsCount--; // path ends with a trailinig '/'
367 bool haveDefaults = defaults != null && defaults.Count > 0;
369 if (argsCount == 1 && String.IsNullOrEmpty (argSegs [0]))
372 if (!haveDefaults && ((haveSegmentWithCatchAll && argsCount < segmentCount) || (!haveSegmentWithCatchAll && argsCount != segmentCount)))
377 foreach (PatternSegment segment in segments) {
381 if (segment.AllLiteral) {
382 if (String.Compare (argSegs [i], segment.Tokens [0].Name, StringComparison.OrdinalIgnoreCase) != 0)
388 if (!MatchSegment (i, argsCount, argSegs, segment.Tokens, ret))
393 // Check the remaining segments, if any, and see if they are required
395 // If a segment has more than one section (i.e. there's at least one
396 // literal, then it cannot match defaults
398 // All of the remaining segments must have all defaults provided and they
399 // must not be literals or the match will fail.
400 if (i < segmentCount) {
404 for (;i < segmentCount; i++) {
405 var segment = segments [i];
406 if (segment.AllLiteral)
409 var tokens = segment.Tokens;
410 if (tokens.Count != 1)
413 // if token is catch-all, we're done.
414 if (tokens [0].Type == PatternTokenType.CatchAll)
417 if (!defaults.ContainsKey (tokens [0].Name))
420 } else if (!haveSegmentWithCatchAll && argsCount > segmentCount)
423 return AddDefaults (ret, defaults);
426 public string BuildUrl (Route route, RequestContext requestContext, RouteValueDictionary userValues, RouteValueDictionary constraints, out RouteValueDictionary usedValues)
430 if (requestContext == null)
433 RouteData routeData = requestContext.RouteData;
434 var currentValues = routeData.Values ?? new RouteValueDictionary ();
435 var values = userValues ?? new RouteValueDictionary ();
436 var defaultValues = (route != null ? route.Defaults : null) ?? new RouteValueDictionary ();
438 // The set of values we should be using when generating the URL in this route
439 var acceptedValues = new RouteValueDictionary ();
441 // Keep track of which new values have been used
442 HashSet<string> unusedNewValues = new HashSet<string> (values.Keys, StringComparer.OrdinalIgnoreCase);
444 // This route building logic is based on System.Web.Http's Routing code (which is Apache Licensed by MS)
445 // and which can be found at mono's external/aspnetwebstack/src/System.Web.Http/Routing/HttpParsedRoute.cs
446 // Hopefully this will ensure a much higher compatiblity with MS.NET's System.Web.Routing logic. (pruiz)
448 #region Step 1: Get the list of values we're going to use to match and generate this URL
449 // Find out which entries in the URL are valid for the URL we want to generate.
450 // If the URL had ordered parameters a="1", b="2", c="3" and the new values
451 // specified that b="9", then we need to invalidate everything after it. The new
452 // values should then be a="1", b="9", c=<no value>.
453 foreach (var item in parameterNames) {
454 var parameterName = item.Key;
456 object newParameterValue;
457 bool hasNewParameterValue = values.TryGetValue (parameterName, out newParameterValue);
458 if (hasNewParameterValue) {
459 unusedNewValues.Remove(parameterName);
462 object currentParameterValue;
463 bool hasCurrentParameterValue = currentValues.TryGetValue (parameterName, out currentParameterValue);
465 if (hasNewParameterValue && hasCurrentParameterValue) {
466 if (!ParametersAreEqual (currentParameterValue, newParameterValue)) {
467 // Stop copying current values when we find one that doesn't match
472 // If the parameter is a match, add it to the list of values we will use for URL generation
473 if (hasNewParameterValue) {
474 if (ParameterIsNonEmpty (newParameterValue)) {
475 acceptedValues.Add (parameterName, newParameterValue);
479 if (hasCurrentParameterValue) {
480 acceptedValues.Add (parameterName, currentParameterValue);
485 // Add all remaining new values to the list of values we will use for URL generation
486 foreach (var newValue in values) {
487 if (ParameterIsNonEmpty (newValue.Value) && !acceptedValues.ContainsKey (newValue.Key)) {
488 acceptedValues.Add (newValue.Key, newValue.Value);
492 // Add all current values that aren't in the URL at all
493 foreach (var currentValue in currentValues) {
494 if (!acceptedValues.ContainsKey (currentValue.Key) && !parameterNames.ContainsKey (currentValue.Key)) {
495 acceptedValues.Add (currentValue.Key, currentValue.Value);
499 // Add all remaining default values from the route to the list of values we will use for URL generation
500 foreach (var item in parameterNames) {
502 if (!acceptedValues.ContainsKey (item.Key) && !IsParameterRequired (item.Key, defaultValues, out defaultValue)) {
503 // Add the default value only if there isn't already a new value for it and
504 // only if it actually has a default value, which we determine based on whether
505 // the parameter value is required.
506 acceptedValues.Add (item.Key, defaultValue);
510 // All required parameters in this URL must have values from somewhere (i.e. the accepted values)
511 foreach (var item in parameterNames) {
513 if (IsParameterRequired (item.Key, defaultValues, out defaultValue) && !acceptedValues.ContainsKey (item.Key)) {
514 // If the route parameter value is required that means there's
515 // no default value, so if there wasn't a new value for it
516 // either, this route won't match.
521 // All other default values must match if they are explicitly defined in the new values
522 var otherDefaultValues = new RouteValueDictionary (defaultValues);
523 foreach (var item in parameterNames) {
524 otherDefaultValues.Remove (item.Key);
527 foreach (var defaultValue in otherDefaultValues) {
529 if (values.TryGetValue (defaultValue.Key, out value)) {
530 unusedNewValues.Remove (defaultValue.Key);
531 if (!ParametersAreEqual (value, defaultValue.Value)) {
532 // If there is a non-parameterized value in the route and there is a
533 // new value for it and it doesn't match, this route won't match.
540 #region Step 2: If the route is a match generate the appropriate URL
542 var uri = new StringBuilder ();
543 var pendingParts = new StringBuilder ();
544 var pendingPartsAreAllSafe = false;
545 bool blockAllUriAppends = false;
546 var allSegments = new List<PatternSegment?> ();
548 // Build a list of segments plus separators we can use as template.
549 foreach (var segment in segments) {
550 if (allSegments.Count > 0)
551 allSegments.Add (null); // separator exposed as null.
552 allSegments.Add (segment);
555 // Finally loop thru al segment-templates building the actual uri.
556 foreach (var item in allSegments) {
557 var segment = item.GetValueOrDefault ();
559 // If segment is a separator..
561 if (pendingPartsAreAllSafe) {
563 if (pendingParts.Length > 0) {
564 if (blockAllUriAppends)
567 // Append any pending literals to the URL
568 uri.Append (pendingParts.ToString ());
569 pendingParts.Length = 0;
572 pendingPartsAreAllSafe = false;
574 // Guard against appending multiple separators for empty segments
575 if (pendingParts.Length > 0 && pendingParts[pendingParts.Length - 1] == '/') {
576 // Dev10 676725: Route should not be matched if that causes mismatched tokens
577 // Dev11 86819: We will allow empty matches if all subsequent segments are null
578 if (blockAllUriAppends)
581 // Append any pending literals to the URI (without the trailing slash) and prevent any future appends
582 uri.Append(pendingParts.ToString (0, pendingParts.Length - 1));
583 pendingParts.Length = 0;
585 pendingParts.Append ("/");
588 } else if (segment.AllLiteral) {
589 // Spezial (optimized) case: all elements of segment are literals.
590 pendingPartsAreAllSafe = true;
591 foreach (var tk in segment.Tokens)
592 pendingParts.Append (tk.Name);
595 // Segments are treated as all-or-none. We should never output a partial segment.
596 // If we add any subsegment of this segment to the generated URL, we have to add
597 // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we
598 // used a value for {p1}, we have to output the entire segment up to the next "/".
599 // Otherwise we could end up with the partial segment "v1" instead of the entire
600 // segment "v1-v2.xml".
601 bool addedAnySubsegments = false;
603 foreach (var token in segment.Tokens) {
604 if (token.Type == PatternTokenType.Literal) {
605 // If it's a literal we hold on to it until we are sure we need to add it
606 pendingPartsAreAllSafe = true;
607 pendingParts.Append (token.Name);
609 if (token.Type == PatternTokenType.Standard) {
610 if (pendingPartsAreAllSafe) {
612 if (pendingParts.Length > 0) {
613 if (blockAllUriAppends)
616 // Append any pending literals to the URL
617 uri.Append (pendingParts.ToString ());
618 pendingParts.Length = 0;
620 addedAnySubsegments = true;
623 pendingPartsAreAllSafe = false;
625 // If it's a parameter, get its value
626 object acceptedParameterValue;
627 bool hasAcceptedParameterValue = acceptedValues.TryGetValue (token.Name, out acceptedParameterValue);
628 if (hasAcceptedParameterValue)
629 unusedNewValues.Remove (token.Name);
631 object defaultParameterValue;
632 defaultValues.TryGetValue (token.Name, out defaultParameterValue);
634 if (ParametersAreEqual (acceptedParameterValue, defaultParameterValue)) {
635 // If the accepted value is the same as the default value, mark it as pending since
636 // we won't necessarily add it to the URL we generate.
637 pendingParts.Append (Convert.ToString (acceptedParameterValue, CultureInfo.InvariantCulture));
639 if (blockAllUriAppends)
642 // Add the new part to the URL as well as any pending parts
643 if (pendingParts.Length > 0) {
644 // Append any pending literals to the URL
645 uri.Append (pendingParts.ToString ());
646 pendingParts.Length = 0;
648 uri.Append (Convert.ToString (acceptedParameterValue, CultureInfo.InvariantCulture));
650 addedAnySubsegments = true;
653 Debug.Fail ("Invalid path subsegment type");
658 if (addedAnySubsegments) {
659 // See comment above about why we add the pending parts
660 if (pendingParts.Length > 0) {
661 if (blockAllUriAppends)
664 // Append any pending literals to the URL
665 uri.Append (pendingParts.ToString ());
666 pendingParts.Length = 0;
672 if (pendingPartsAreAllSafe) {
674 if (pendingParts.Length > 0) {
675 if (blockAllUriAppends)
678 // Append any pending literals to the URI
679 uri.Append (pendingParts.ToString ());
683 // Process constraints keys
684 if (constraints != null) {
685 // If there are any constraints, mark all the keys as being used so that we don't
686 // generate query string items for custom constraints that don't appear as parameters
687 // in the URI format.
688 foreach (var constraintsItem in constraints) {
689 unusedNewValues.Remove (constraintsItem.Key);
693 // Encode the URI before we append the query string, otherwise we would double encode the query string
694 var encodedUri = new StringBuilder ();
695 encodedUri.Append (UriEncode (uri.ToString ()));
698 // Add remaining new values as query string parameters to the URI
699 if (unusedNewValues.Count > 0) {
700 // Generate the query string
701 bool firstParam = true;
702 foreach (string unusedNewValue in unusedNewValues) {
704 if (acceptedValues.TryGetValue (unusedNewValue, out value)) {
705 uri.Append (firstParam ? '?' : '&');
707 uri.Append (Uri.EscapeDataString (unusedNewValue));
709 uri.Append (Uri.EscapeDataString (Convert.ToString (value, CultureInfo.InvariantCulture)));
716 usedValues = acceptedValues;
717 return uri.ToString();