Merge pull request #600 from tr8dr/master
[mono.git] / mcs / class / System.Web.Routing / System.Web.Routing / PatternParser.cs
1 //
2 // PatternParser.cs
3 //
4 // Author:
5 //      Atsushi Enomoto <atsushi@ximian.com>
6 //      Marek Habersack <mhabersack@novell.com>
7 //
8 // Copyright (C) 2008-2010 Novell Inc. http://novell.com
9 //
10
11 //
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:
19 // 
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
22 // 
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.
30 //
31 using System;
32 using System.Collections.Generic;
33 using System.Security.Permissions;
34 using System.Text;
35 using System.Text.RegularExpressions;
36 using System.Web;
37 using System.Web.Util;
38 using System.Diagnostics;
39 using System.Globalization;
40
41 namespace System.Web.Routing
42 {
43         sealed class PatternParser
44         {
45                 struct PatternSegment
46                 {
47                         public bool AllLiteral;
48                         public List <PatternToken> Tokens;
49                 }
50                 
51                 static readonly char[] placeholderDelimiters = { '{', '}' };
52                 
53                 PatternSegment[] segments;
54                 Dictionary <string, bool> parameterNames;
55                 PatternToken[] tokens;
56                 
57                 int segmentCount;
58                 bool haveSegmentWithCatchAll;
59                 
60                 public string Url {
61                         get;
62                         private set;
63                 }
64                 
65                 public PatternParser (string pattern)
66                 {
67                         this.Url = pattern;
68                         Parse ();
69                 }
70
71                 void Parse ()
72                 {
73                         string url = Url;
74                         parameterNames = new Dictionary <string, bool> (StringComparer.OrdinalIgnoreCase);
75                         
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 '?'");
81                         } else {
82                                 segments = new PatternSegment [0];
83                                 tokens = new PatternToken [0];
84                                 return;
85                         }
86                         
87                         string[] parts = url.Split ('/');
88                         int partsCount = segmentCount = parts.Length;
89                         var allTokens = new List <PatternToken> ();
90                         PatternToken tmpToken;
91                         
92                         segments = new PatternSegment [partsCount];
93                         
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");
97                                 
98                                 int catchAlls = 0;
99                                 string part = parts [i];
100                                 int partLength = part.Length;
101                                 var tokens = new List <PatternToken> ();
102
103                                 if (partLength == 0 && i < partsCount - 1)
104                                         throw new ArgumentException ("Consecutive URL segment separators '/' are not allowed");
105
106                                 if (part.IndexOf ("{}") != -1)
107                                         throw new ArgumentException ("Empty URL parameter name is not allowed");
108
109                                 if (i > 0)
110                                         allTokens.Add (null);
111                                 
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;
119                                         continue;
120                                 }
121
122                                 string tmp;
123                                 int from = 0, start;
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 '}'");
129
130                                         if (start < 0) {
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);
137                                                 from += tmp.Length;
138                                                 break;
139                                         }
140
141                                         if (from == 0 && start > 0) {
142                                                 tmpToken = new PatternToken (PatternTokenType.Literal, part.Substring (0, start));
143                                                 tokens.Add (tmpToken);
144                                                 allTokens.Add (tmpToken);
145                                         }
146                                         
147                                         int end = part.IndexOf ('}', start + 1);
148                                         int next = part.IndexOf ('{', start + 1);
149                                         
150                                         if (end < 0 || next >= 0 && next < end)
151                                                 throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
152                                         if (next == end + 1)
153                                                 throw new ArgumentException ("Two consecutive URL parameters are not allowed. Split into a different segment by '/', or a literal string.");
154
155                                         if (next == -1)
156                                                 next = partLength;
157                                         
158                                         string token = part.Substring (start + 1, end - start - 1);
159                                         PatternTokenType type;
160                                         if (token [0] == '*') {
161                                                 catchAlls++;
162                                                 haveSegmentWithCatchAll = true;
163                                                 type = PatternTokenType.CatchAll;
164                                                 token = token.Substring (1);
165                                         } else
166                                                 type = PatternTokenType.Standard;
167
168                                         if (!parameterNames.ContainsKey (token))
169                                                 parameterNames.Add (token, true);
170
171                                         tmpToken = new PatternToken (type, token);
172                                         tokens.Add (tmpToken);
173                                         allTokens.Add (tmpToken);
174                                         allLiteral = false;
175                                         
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);
181                                                 end += token.Length;
182                                         }
183
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.");
186                                         from = end + 1;
187                                 }
188                                 
189                                 segments [i].AllLiteral = allLiteral;
190                                 segments [i].Tokens = tokens;
191                         }
192
193                         if (allTokens.Count > 0)
194                                 this.tokens = allTokens.ToArray ();
195                         allTokens = null;
196                 }
197
198                 RouteValueDictionary AddDefaults (RouteValueDictionary dict, RouteValueDictionary defaults)
199                 {
200                         if (defaults != null && defaults.Count > 0) {
201                                 string key;
202                                 foreach (var def in defaults) {
203                                         key = def.Key;
204                                         if (dict.ContainsKey (key))
205                                                 continue;
206                                         dict.Add (key, def.Value);
207                                 }
208                         }
209
210                         return dict;
211                 }
212
213                 static bool ParametersAreEqual (object a, object b)
214                 {
215                         if (a is string && b is string) {
216                                 return String.Equals (a as string, b as string, StringComparison.OrdinalIgnoreCase);
217                         } else {
218                                 // Parameter may be a boxed value type, need to use .Equals() for comparison
219                                 return object.Equals (a, b);
220                         }
221                 }
222
223                 static bool ParameterIsNonEmpty (object param)
224                 {
225                         if (param is string)
226                                 return !string.IsNullOrEmpty (param as string);
227
228                         return param != null;
229                 }
230
231                 bool IsParameterRequired (string parameterName, RouteValueDictionary defaultValues, out object defaultValue)
232                 {
233                         foreach (var token in tokens) {
234                                 if (token == null)
235                                         continue;
236
237                                 if (string.Equals (token.Name, parameterName, StringComparison.OrdinalIgnoreCase)) {
238                                         if (token.Type == PatternTokenType.CatchAll) {
239                                                 defaultValue = null;
240                                                 return false;
241                                         }
242                                 }
243                         }
244
245                         if (defaultValues == null)
246                                 throw new ArgumentNullException ("defaultValues is null?!");
247
248                         return !defaultValues.TryGetValue (parameterName, out defaultValue);
249                 }
250
251                 static string EscapeReservedCharacters (Match m)
252                 {
253                         if (m == null)
254                                 throw new ArgumentNullException("m");
255
256                         return Uri.HexEscape (m.Value[0]);
257                 }
258
259                 static string UriEncode (string str)
260                 {
261                         if (string.IsNullOrEmpty (str))
262                                 return str;
263
264                         string escape = Uri.EscapeUriString (str);
265                         return Regex.Replace (escape, "([#?])", new MatchEvaluator (EscapeReservedCharacters));
266                 }
267
268                 bool MatchSegment (int segIndex, int argsCount, string[] argSegs, List <PatternToken> tokens, RouteValueDictionary ret)
269                 {
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;
275                         PatternToken token;
276                         string tokenName;
277
278                         for (int tokenIndex = tokensCount - 1; tokenIndex > -1; tokenIndex--) {
279                                 token = tokens [tokenIndex];
280                                 if (startIndex < 0)
281                                         return false;
282
283                                 tokenType = token.Type;
284                                 tokenName = token.Name;
285
286                                 if (segIndex > segmentCount - 1 || tokenType == PatternTokenType.CatchAll) {
287                                         var sb = new StringBuilder ();
288
289                                         for (int j = segIndex; j < argsCount; j++) {
290                                                 if (j > segIndex)
291                                                         sb.Append ('/');
292                                                 sb.Append (argSegs [j]);
293                                         }
294                                                 
295                                         ret.Add (tokenName, sb.ToString ());
296                                         break;
297                                 }
298
299                                 int scanIndex;
300                                 if (token.Type == PatternTokenType.Literal) {
301                                         int nameLen = tokenName.Length;
302                                         if (startIndex + 1 < nameLen)
303                                                 return false;
304                                         scanIndex = startIndex - nameLen + 1;
305                                         if (String.Compare (pathSegment, scanIndex, tokenName, 0, nameLen, StringComparison.OrdinalIgnoreCase) != 0)
306                                                 return false;
307                                         startIndex = scanIndex - 1;
308                                         continue;
309                                 }
310
311                                 // Standard token
312                                 int nextTokenIndex = tokenIndex - 1;
313                                 if (nextTokenIndex < 0) {
314                                         // First token
315                                         ret.Add (tokenName, pathSegment.Substring (0, startIndex + 1));
316                                         continue;
317                                 }
318
319                                 if (startIndex == 0)
320                                         return false;
321                                 
322                                 var nextToken = tokens [nextTokenIndex];
323                                 string nextTokenName = nextToken.Name;
324
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);
330                                 if (lastIndex == -1)
331                                         return false;
332
333                                 lastIndex += nextTokenName.Length - 1;
334                                                 
335                                 string sectionValue = pathSegment.Substring (lastIndex + 1, startIndex - lastIndex);
336                                 if (String.IsNullOrEmpty (sectionValue))
337                                         return false;
338
339                                 ret.Add (tokenName, sectionValue);
340                                 startIndex = lastIndex;
341                         }
342                         
343                         return true;
344                 }
345
346                 public RouteValueDictionary Match (string path, RouteValueDictionary defaults)
347                 {
348                         var ret = new RouteValueDictionary ();
349                         string url = Url;
350                         string [] argSegs;
351                         int argsCount;
352                         
353                         if (String.IsNullOrEmpty (path)) {
354                                 argSegs = null;
355                                 argsCount = 0;
356                         } else {
357                                 // quick check
358                                 if (String.Compare (url, path, StringComparison.Ordinal) == 0 && url.IndexOf ('{') < 0)
359                                         return AddDefaults (ret, defaults);
360
361                                 argSegs = path.Split ('/');
362                                 argsCount = argSegs.Length;
363
364                                 if (String.IsNullOrEmpty (argSegs [argsCount - 1]))
365                                         argsCount--; // path ends with a trailinig '/'
366                         }
367                         bool haveDefaults = defaults != null && defaults.Count > 0;
368
369                         if (argsCount == 1 && String.IsNullOrEmpty (argSegs [0]))
370                                 argsCount = 0;
371                         
372                         if (!haveDefaults && ((haveSegmentWithCatchAll && argsCount < segmentCount) || (!haveSegmentWithCatchAll && argsCount != segmentCount)))
373                                 return null;
374
375                         int i = 0;
376
377                         foreach (PatternSegment segment in segments) {
378                                 if (i >= argsCount)
379                                         break;
380                                 
381                                 if (segment.AllLiteral) {
382                                         if (String.Compare (argSegs [i], segment.Tokens [0].Name, StringComparison.OrdinalIgnoreCase) != 0)
383                                                 return null;
384                                         i++;
385                                         continue;
386                                 }
387
388                                 if (!MatchSegment (i, argsCount, argSegs, segment.Tokens, ret))
389                                         return null;
390                                 i++;
391                         }
392
393                         // Check the remaining segments, if any, and see if they are required
394                         //
395                         // If a segment has more than one section (i.e. there's at least one
396                         // literal, then it cannot match defaults
397                         //
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) {
401                                 if (!haveDefaults)
402                                         return null;
403                                 
404                                 for (;i < segmentCount; i++) {
405                                         var segment = segments [i];
406                                         if (segment.AllLiteral)
407                                                 return null;
408                                         
409                                         var tokens = segment.Tokens;
410                                         if (tokens.Count != 1)
411                                                 return null;
412
413                                         // if token is catch-all, we're done.
414                                         if (tokens [0].Type == PatternTokenType.CatchAll)
415                                                 break;
416
417                                         if (!defaults.ContainsKey (tokens [0].Name))
418                                                 return null;
419                                 }
420                         } else if (!haveSegmentWithCatchAll && argsCount > segmentCount)
421                                 return null;
422                         
423                         return AddDefaults (ret, defaults);
424                 }
425                 
426                 public string BuildUrl (Route route, RequestContext requestContext, RouteValueDictionary userValues, RouteValueDictionary constraints, out RouteValueDictionary usedValues)
427                 {
428                         usedValues = null;
429
430                         if (requestContext == null)
431                                 return null;
432
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 ();
437
438                         // The set of values we should be using when generating the URL in this route
439                         var acceptedValues = new RouteValueDictionary ();
440
441                         // Keep track of which new values have been used
442                         HashSet<string> unusedNewValues = new HashSet<string> (values.Keys, StringComparer.OrdinalIgnoreCase);
443
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)
447
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;
455
456                                 object newParameterValue;
457                                 bool hasNewParameterValue = values.TryGetValue (parameterName, out newParameterValue);
458                                 if (hasNewParameterValue) {
459                                         unusedNewValues.Remove(parameterName);
460                                 }
461
462                                 object currentParameterValue;
463                                 bool hasCurrentParameterValue = currentValues.TryGetValue (parameterName, out currentParameterValue);
464
465                                 if (hasNewParameterValue && hasCurrentParameterValue) {
466                                         if (!ParametersAreEqual (currentParameterValue, newParameterValue)) {
467                                                 // Stop copying current values when we find one that doesn't match
468                                                 break;
469                                         }
470                                 }
471
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);
476                                         }
477                                 }
478                                 else {
479                                         if (hasCurrentParameterValue) {
480                                                 acceptedValues.Add (parameterName, currentParameterValue);
481                                         }
482                                 }
483                         }
484
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);
489                                 }
490                         }
491
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);
496                                 }
497                         }
498
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) {
501                                 object defaultValue;
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);
507                                 }
508                         }
509
510                         // All required parameters in this URL must have values from somewhere (i.e. the accepted values)
511                         foreach (var item in parameterNames) {
512                                 object defaultValue;
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.
517                                         return null;
518                                 }
519                         }
520
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);
525                         }
526
527                         foreach (var defaultValue in otherDefaultValues) {
528                                 object value;
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.
534                                                 return null;
535                                         }
536                                 }
537                         }
538                         #endregion
539
540                         #region Step 2: If the route is a match generate the appropriate URL
541
542                         var uri = new StringBuilder ();
543                         var pendingParts = new StringBuilder ();
544                         var pendingPartsAreAllSafe = false;
545                         bool blockAllUriAppends = false;
546                         var allSegments = new List<PatternSegment?> ();
547
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);
553                         }
554
555                         // Finally loop thru al segment-templates building the actual uri.
556                         foreach (var item in allSegments) {
557                                 var segment = item.GetValueOrDefault ();
558
559                                 // If segment is a separator..
560                                 if (item == null) {
561                                         if (pendingPartsAreAllSafe) {
562                                                 // Accept
563                                                 if (pendingParts.Length > 0) {
564                                                         if (blockAllUriAppends)
565                                                                 return null;
566
567                                                         // Append any pending literals to the URL
568                                                         uri.Append (pendingParts.ToString ());
569                                                         pendingParts.Length = 0;
570                                                 }
571                                         }
572                                         pendingPartsAreAllSafe = false;
573
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)
579                                                         return null;
580
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;
584                                         } else {
585                                                 pendingParts.Append ("/");
586                                         }
587 #if false
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);
593 #endif
594                                 } else {
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;
602
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);
608                                                 } else {
609                                                         if (token.Type == PatternTokenType.Standard) {
610                                                                 if (pendingPartsAreAllSafe) {
611                                                                         // Accept
612                                                                         if (pendingParts.Length > 0) {
613                                                                                 if (blockAllUriAppends)
614                                                                                         return null;
615
616                                                                                 // Append any pending literals to the URL
617                                                                                 uri.Append (pendingParts.ToString ());
618                                                                                 pendingParts.Length = 0;
619
620                                                                                 addedAnySubsegments = true;
621                                                                         }
622                                                                 }
623                                                                 pendingPartsAreAllSafe = false;
624
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);
630
631                                                                 object defaultParameterValue;
632                                                                 defaultValues.TryGetValue (token.Name, out defaultParameterValue);
633
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));
638                                                                 } else {
639                                                                         if (blockAllUriAppends)
640                                                                                 return null;
641
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;
647                                                                         }
648                                                                         uri.Append (Convert.ToString (acceptedParameterValue, CultureInfo.InvariantCulture));
649
650                                                                         addedAnySubsegments = true;
651                                                                 }
652                                                         } else {
653                                                                 Debug.Fail ("Invalid path subsegment type");
654                                                         }
655                                                 }
656                                         }
657
658                                         if (addedAnySubsegments) {
659                                                 // See comment above about why we add the pending parts
660                                                 if (pendingParts.Length > 0) {
661                                                         if (blockAllUriAppends)
662                                                                 return null;
663
664                                                         // Append any pending literals to the URL
665                                                         uri.Append (pendingParts.ToString ());
666                                                         pendingParts.Length = 0;
667                                                 }
668                                         }
669                                 }
670                         }
671
672                         if (pendingPartsAreAllSafe) {
673                                 // Accept
674                                 if (pendingParts.Length > 0) {
675                                         if (blockAllUriAppends)
676                                                 return null;
677
678                                         // Append any pending literals to the URI
679                                         uri.Append (pendingParts.ToString ());
680                                 }
681                         }
682
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);
690                                 }
691                         }
692
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 ()));
696                         uri = encodedUri;
697
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) {
703                                         object value;
704                                         if (acceptedValues.TryGetValue (unusedNewValue, out value)) {
705                                                 uri.Append (firstParam ? '?' : '&');
706                                                 firstParam = false;
707                                                 uri.Append (Uri.EscapeDataString (unusedNewValue));
708                                                 uri.Append ('=');
709                                                 uri.Append (Uri.EscapeDataString (Convert.ToString (value, CultureInfo.InvariantCulture)));
710                                         }
711                                 }
712                         }
713
714                         #endregion
715
716                         usedValues = acceptedValues;
717                         return uri.ToString();
718                 }
719         }
720 }
721