PatternParser.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. //
  2. // PatternParser.cs
  3. //
  4. // Author:
  5. // Atsushi Enomoto <[email protected]>
  6. // Marek Habersack <[email protected]>
  7. //
  8. // Copyright (C) 2008-2010 Novell Inc. http://novell.com
  9. //
  10. //
  11. // Permission is hereby granted, free of charge, to any person obtaining
  12. // a copy of this software and associated documentation files (the
  13. // "Software"), to deal in the Software without restriction, including
  14. // without limitation the rights to use, copy, modify, merge, publish,
  15. // distribute, sublicense, and/or sell copies of the Software, and to
  16. // permit persons to whom the Software is furnished to do so, subject to
  17. // the following conditions:
  18. //
  19. // The above copyright notice and this permission notice shall be
  20. // included in all copies or substantial portions of the Software.
  21. //
  22. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  23. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  24. // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  25. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  26. // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  27. // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  28. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  29. //
  30. using System;
  31. using System.Collections.Generic;
  32. using System.Security.Permissions;
  33. using System.Text;
  34. using System.Text.RegularExpressions;
  35. using System.Web;
  36. using System.Web.Util;
  37. using System.Diagnostics;
  38. using System.Globalization;
  39. namespace System.Web.Routing
  40. {
  41. sealed class PatternParser
  42. {
  43. struct PatternSegment
  44. {
  45. public bool AllLiteral;
  46. public List <PatternToken> Tokens;
  47. }
  48. static readonly char[] placeholderDelimiters = { '{', '}' };
  49. PatternSegment[] segments;
  50. Dictionary <string, bool> parameterNames;
  51. PatternToken[] tokens;
  52. int segmentCount;
  53. bool haveSegmentWithCatchAll;
  54. public string Url {
  55. get;
  56. private set;
  57. }
  58. public PatternParser (string pattern)
  59. {
  60. this.Url = pattern;
  61. Parse ();
  62. }
  63. void Parse ()
  64. {
  65. string url = Url;
  66. parameterNames = new Dictionary <string, bool> (StringComparer.OrdinalIgnoreCase);
  67. if (!String.IsNullOrEmpty (url)) {
  68. if (url [0] == '~' || url [0] == '/')
  69. throw new ArgumentException ("Url must not start with '~' or '/'");
  70. if (url.IndexOf ('?') >= 0)
  71. throw new ArgumentException ("Url must not contain '?'");
  72. } else {
  73. segments = new PatternSegment [0];
  74. tokens = new PatternToken [0];
  75. return;
  76. }
  77. string[] parts = url.Split ('/');
  78. int partsCount = segmentCount = parts.Length;
  79. var allTokens = new List <PatternToken> ();
  80. PatternToken tmpToken;
  81. segments = new PatternSegment [partsCount];
  82. for (int i = 0; i < partsCount; i++) {
  83. if (haveSegmentWithCatchAll)
  84. throw new ArgumentException ("A catch-all parameter can only appear as the last segment of the route URL");
  85. int catchAlls = 0;
  86. string part = parts [i];
  87. int partLength = part.Length;
  88. var tokens = new List <PatternToken> ();
  89. if (partLength == 0 && i < partsCount - 1)
  90. throw new ArgumentException ("Consecutive URL segment separators '/' are not allowed");
  91. if (part.IndexOf ("{}") != -1)
  92. throw new ArgumentException ("Empty URL parameter name is not allowed");
  93. if (i > 0)
  94. allTokens.Add (null);
  95. if (part.IndexOfAny (placeholderDelimiters) == -1) {
  96. // no placeholders here, short-circuit it
  97. tmpToken = new PatternToken (PatternTokenType.Literal, part);
  98. tokens.Add (tmpToken);
  99. allTokens.Add (tmpToken);
  100. segments [i].AllLiteral = true;
  101. segments [i].Tokens = tokens;
  102. continue;
  103. }
  104. string tmp;
  105. int from = 0, start;
  106. bool allLiteral = true;
  107. while (from < partLength) {
  108. start = part.IndexOf ('{', from);
  109. if (start >= partLength - 2)
  110. throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
  111. if (start < 0) {
  112. if (part.IndexOf ('}', from) >= from)
  113. throw new ArgumentException ("Unmatched URL parameter closer '}'. A corresponding '{' must precede");
  114. tmp = part.Substring (from);
  115. tmpToken = new PatternToken (PatternTokenType.Literal, tmp);
  116. tokens.Add (tmpToken);
  117. allTokens.Add (tmpToken);
  118. from += tmp.Length;
  119. break;
  120. }
  121. if (from == 0 && start > 0) {
  122. tmpToken = new PatternToken (PatternTokenType.Literal, part.Substring (0, start));
  123. tokens.Add (tmpToken);
  124. allTokens.Add (tmpToken);
  125. }
  126. int end = part.IndexOf ('}', start + 1);
  127. int next = part.IndexOf ('{', start + 1);
  128. if (end < 0 || next >= 0 && next < end)
  129. throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
  130. if (next == end + 1)
  131. throw new ArgumentException ("Two consecutive URL parameters are not allowed. Split into a different segment by '/', or a literal string.");
  132. if (next == -1)
  133. next = partLength;
  134. string token = part.Substring (start + 1, end - start - 1);
  135. PatternTokenType type;
  136. if (token [0] == '*') {
  137. catchAlls++;
  138. haveSegmentWithCatchAll = true;
  139. type = PatternTokenType.CatchAll;
  140. token = token.Substring (1);
  141. } else
  142. type = PatternTokenType.Standard;
  143. if (!parameterNames.ContainsKey (token))
  144. parameterNames.Add (token, true);
  145. tmpToken = new PatternToken (type, token);
  146. tokens.Add (tmpToken);
  147. allTokens.Add (tmpToken);
  148. allLiteral = false;
  149. if (end < partLength - 1) {
  150. token = part.Substring (end + 1, next - end - 1);
  151. tmpToken = new PatternToken (PatternTokenType.Literal, token);
  152. tokens.Add (tmpToken);
  153. allTokens.Add (tmpToken);
  154. end += token.Length;
  155. }
  156. if (catchAlls > 1 || (catchAlls == 1 && tokens.Count > 1))
  157. 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.");
  158. from = end + 1;
  159. }
  160. segments [i].AllLiteral = allLiteral;
  161. segments [i].Tokens = tokens;
  162. }
  163. if (allTokens.Count > 0)
  164. this.tokens = allTokens.ToArray ();
  165. allTokens = null;
  166. }
  167. RouteValueDictionary AddDefaults (RouteValueDictionary dict, RouteValueDictionary defaults)
  168. {
  169. if (defaults != null && defaults.Count > 0) {
  170. string key;
  171. foreach (var def in defaults) {
  172. key = def.Key;
  173. if (dict.ContainsKey (key))
  174. continue;
  175. dict.Add (key, def.Value);
  176. }
  177. }
  178. return dict;
  179. }
  180. static bool ParametersAreEqual (object a, object b)
  181. {
  182. if (a is string && b is string) {
  183. return String.Equals (a as string, b as string, StringComparison.OrdinalIgnoreCase);
  184. } else {
  185. // Parameter may be a boxed value type, need to use .Equals() for comparison
  186. return object.Equals (a, b);
  187. }
  188. }
  189. static bool ParameterIsNonEmpty (object param)
  190. {
  191. if (param is string)
  192. return !string.IsNullOrEmpty (param as string);
  193. return param != null;
  194. }
  195. bool IsParameterRequired (string parameterName, RouteValueDictionary defaultValues, out object defaultValue)
  196. {
  197. foreach (var token in tokens) {
  198. if (token == null)
  199. continue;
  200. if (string.Equals (token.Name, parameterName, StringComparison.OrdinalIgnoreCase)) {
  201. if (token.Type == PatternTokenType.CatchAll) {
  202. defaultValue = null;
  203. return false;
  204. }
  205. }
  206. }
  207. if (defaultValues == null)
  208. throw new ArgumentNullException ("defaultValues is null?!");
  209. return !defaultValues.TryGetValue (parameterName, out defaultValue);
  210. }
  211. static string EscapeReservedCharacters (Match m)
  212. {
  213. if (m == null)
  214. throw new ArgumentNullException("m");
  215. return Uri.HexEscape (m.Value[0]);
  216. }
  217. static string UriEncode (string str)
  218. {
  219. if (string.IsNullOrEmpty (str))
  220. return str;
  221. string escape = Uri.EscapeUriString (str);
  222. return Regex.Replace (escape, "([#?])", new MatchEvaluator (EscapeReservedCharacters));
  223. }
  224. bool MatchSegment (int segIndex, int argsCount, string[] argSegs, List <PatternToken> tokens, RouteValueDictionary ret)
  225. {
  226. string pathSegment = argSegs [segIndex];
  227. int pathSegmentLength = pathSegment != null ? pathSegment.Length : -1;
  228. int startIndex = pathSegmentLength - 1;
  229. PatternTokenType tokenType;
  230. int tokensCount = tokens.Count;
  231. PatternToken token;
  232. string tokenName;
  233. for (int tokenIndex = tokensCount - 1; tokenIndex > -1; tokenIndex--) {
  234. token = tokens [tokenIndex];
  235. if (startIndex < 0)
  236. return false;
  237. tokenType = token.Type;
  238. tokenName = token.Name;
  239. if (segIndex > segmentCount - 1 || tokenType == PatternTokenType.CatchAll) {
  240. var sb = new StringBuilder ();
  241. for (int j = segIndex; j < argsCount; j++) {
  242. if (j > segIndex)
  243. sb.Append ('/');
  244. sb.Append (argSegs [j]);
  245. }
  246. ret.Add (tokenName, sb.ToString ());
  247. break;
  248. }
  249. int scanIndex;
  250. if (token.Type == PatternTokenType.Literal) {
  251. int nameLen = tokenName.Length;
  252. if (startIndex + 1 < nameLen)
  253. return false;
  254. scanIndex = startIndex - nameLen + 1;
  255. if (String.Compare (pathSegment, scanIndex, tokenName, 0, nameLen, StringComparison.OrdinalIgnoreCase) != 0)
  256. return false;
  257. startIndex = scanIndex - 1;
  258. continue;
  259. }
  260. // Standard token
  261. int nextTokenIndex = tokenIndex - 1;
  262. if (nextTokenIndex < 0) {
  263. // First token
  264. ret.Add (tokenName, pathSegment.Substring (0, startIndex + 1));
  265. continue;
  266. }
  267. if (startIndex == 0)
  268. return false;
  269. var nextToken = tokens [nextTokenIndex];
  270. string nextTokenName = nextToken.Name;
  271. // Skip one char, since there can be no empty segments and if the
  272. // current token's value happens to be the same as preceeding
  273. // literal text, we'll save some time and complexity.
  274. scanIndex = startIndex - 1;
  275. int lastIndex = pathSegment.LastIndexOf (nextTokenName, scanIndex, StringComparison.OrdinalIgnoreCase);
  276. if (lastIndex == -1)
  277. return false;
  278. lastIndex += nextTokenName.Length - 1;
  279. string sectionValue = pathSegment.Substring (lastIndex + 1, startIndex - lastIndex);
  280. if (String.IsNullOrEmpty (sectionValue))
  281. return false;
  282. ret.Add (tokenName, sectionValue);
  283. startIndex = lastIndex;
  284. }
  285. return true;
  286. }
  287. public RouteValueDictionary Match (string path, RouteValueDictionary defaults)
  288. {
  289. var ret = new RouteValueDictionary ();
  290. string url = Url;
  291. string [] argSegs;
  292. int argsCount;
  293. if (String.IsNullOrEmpty (path)) {
  294. argSegs = null;
  295. argsCount = 0;
  296. } else {
  297. // quick check
  298. if (String.Compare (url, path, StringComparison.Ordinal) == 0 && url.IndexOf ('{') < 0)
  299. return AddDefaults (ret, defaults);
  300. argSegs = path.Split ('/');
  301. argsCount = argSegs.Length;
  302. if (String.IsNullOrEmpty (argSegs [argsCount - 1]))
  303. argsCount--; // path ends with a trailinig '/'
  304. }
  305. bool haveDefaults = defaults != null && defaults.Count > 0;
  306. if (argsCount == 1 && String.IsNullOrEmpty (argSegs [0]))
  307. argsCount = 0;
  308. if (!haveDefaults && ((haveSegmentWithCatchAll && argsCount < segmentCount) || (!haveSegmentWithCatchAll && argsCount != segmentCount)))
  309. return null;
  310. int i = 0;
  311. foreach (PatternSegment segment in segments) {
  312. if (i >= argsCount)
  313. break;
  314. if (segment.AllLiteral) {
  315. if (String.Compare (argSegs [i], segment.Tokens [0].Name, StringComparison.OrdinalIgnoreCase) != 0)
  316. return null;
  317. i++;
  318. continue;
  319. }
  320. if (!MatchSegment (i, argsCount, argSegs, segment.Tokens, ret))
  321. return null;
  322. i++;
  323. }
  324. // Check the remaining segments, if any, and see if they are required
  325. //
  326. // If a segment has more than one section (i.e. there's at least one
  327. // literal, then it cannot match defaults
  328. //
  329. // All of the remaining segments must have all defaults provided and they
  330. // must not be literals or the match will fail.
  331. if (i < segmentCount) {
  332. if (!haveDefaults)
  333. return null;
  334. for (;i < segmentCount; i++) {
  335. var segment = segments [i];
  336. if (segment.AllLiteral)
  337. return null;
  338. var tokens = segment.Tokens;
  339. if (tokens.Count != 1)
  340. return null;
  341. // if token is catch-all, we're done.
  342. if (tokens [0].Type == PatternTokenType.CatchAll)
  343. break;
  344. if (!defaults.ContainsKey (tokens [0].Name))
  345. return null;
  346. }
  347. } else if (!haveSegmentWithCatchAll && argsCount > segmentCount)
  348. return null;
  349. return AddDefaults (ret, defaults);
  350. }
  351. public string BuildUrl (Route route, RequestContext requestContext, RouteValueDictionary userValues, RouteValueDictionary constraints, out RouteValueDictionary usedValues)
  352. {
  353. usedValues = null;
  354. if (requestContext == null)
  355. return null;
  356. RouteData routeData = requestContext.RouteData;
  357. var currentValues = routeData.Values ?? new RouteValueDictionary ();
  358. var values = userValues ?? new RouteValueDictionary ();
  359. var defaultValues = (route != null ? route.Defaults : null) ?? new RouteValueDictionary ();
  360. // The set of values we should be using when generating the URL in this route
  361. var acceptedValues = new RouteValueDictionary ();
  362. // Keep track of which new values have been used
  363. HashSet<string> unusedNewValues = new HashSet<string> (values.Keys, StringComparer.OrdinalIgnoreCase);
  364. // This route building logic is based on System.Web.Http's Routing code (which is Apache Licensed by MS)
  365. // and which can be found at mono's external/aspnetwebstack/src/System.Web.Http/Routing/HttpParsedRoute.cs
  366. // Hopefully this will ensure a much higher compatiblity with MS.NET's System.Web.Routing logic. (pruiz)
  367. #region Step 1: Get the list of values we're going to use to match and generate this URL
  368. // Find out which entries in the URL are valid for the URL we want to generate.
  369. // If the URL had ordered parameters a="1", b="2", c="3" and the new values
  370. // specified that b="9", then we need to invalidate everything after it. The new
  371. // values should then be a="1", b="9", c=<no value>.
  372. foreach (var item in parameterNames) {
  373. var parameterName = item.Key;
  374. object newParameterValue;
  375. bool hasNewParameterValue = values.TryGetValue (parameterName, out newParameterValue);
  376. if (hasNewParameterValue) {
  377. unusedNewValues.Remove(parameterName);
  378. }
  379. object currentParameterValue;
  380. bool hasCurrentParameterValue = currentValues.TryGetValue (parameterName, out currentParameterValue);
  381. if (hasNewParameterValue && hasCurrentParameterValue) {
  382. if (!ParametersAreEqual (currentParameterValue, newParameterValue)) {
  383. // Stop copying current values when we find one that doesn't match
  384. break;
  385. }
  386. }
  387. // If the parameter is a match, add it to the list of values we will use for URL generation
  388. if (hasNewParameterValue) {
  389. if (ParameterIsNonEmpty (newParameterValue)) {
  390. acceptedValues.Add (parameterName, newParameterValue);
  391. }
  392. }
  393. else {
  394. if (hasCurrentParameterValue) {
  395. acceptedValues.Add (parameterName, currentParameterValue);
  396. }
  397. }
  398. }
  399. // Add all remaining new values to the list of values we will use for URL generation
  400. foreach (var newValue in values) {
  401. if (ParameterIsNonEmpty (newValue.Value) && !acceptedValues.ContainsKey (newValue.Key)) {
  402. acceptedValues.Add (newValue.Key, newValue.Value);
  403. }
  404. }
  405. // Add all current values that aren't in the URL at all
  406. foreach (var currentValue in currentValues) {
  407. if (!acceptedValues.ContainsKey (currentValue.Key) && !parameterNames.ContainsKey (currentValue.Key)) {
  408. acceptedValues.Add (currentValue.Key, currentValue.Value);
  409. }
  410. }
  411. // Add all remaining default values from the route to the list of values we will use for URL generation
  412. foreach (var item in parameterNames) {
  413. object defaultValue;
  414. if (!acceptedValues.ContainsKey (item.Key) && !IsParameterRequired (item.Key, defaultValues, out defaultValue)) {
  415. // Add the default value only if there isn't already a new value for it and
  416. // only if it actually has a default value, which we determine based on whether
  417. // the parameter value is required.
  418. acceptedValues.Add (item.Key, defaultValue);
  419. }
  420. }
  421. // All required parameters in this URL must have values from somewhere (i.e. the accepted values)
  422. foreach (var item in parameterNames) {
  423. object defaultValue;
  424. if (IsParameterRequired (item.Key, defaultValues, out defaultValue) && !acceptedValues.ContainsKey (item.Key)) {
  425. // If the route parameter value is required that means there's
  426. // no default value, so if there wasn't a new value for it
  427. // either, this route won't match.
  428. return null;
  429. }
  430. }
  431. // All other default values must match if they are explicitly defined in the new values
  432. var otherDefaultValues = new RouteValueDictionary (defaultValues);
  433. foreach (var item in parameterNames) {
  434. otherDefaultValues.Remove (item.Key);
  435. }
  436. foreach (var defaultValue in otherDefaultValues) {
  437. object value;
  438. if (values.TryGetValue (defaultValue.Key, out value)) {
  439. unusedNewValues.Remove (defaultValue.Key);
  440. if (!ParametersAreEqual (value, defaultValue.Value)) {
  441. // If there is a non-parameterized value in the route and there is a
  442. // new value for it and it doesn't match, this route won't match.
  443. return null;
  444. }
  445. }
  446. }
  447. #endregion
  448. #region Step 2: If the route is a match generate the appropriate URL
  449. var uri = new StringBuilder ();
  450. var pendingParts = new StringBuilder ();
  451. var pendingPartsAreAllSafe = false;
  452. bool blockAllUriAppends = false;
  453. var allSegments = new List<PatternSegment?> ();
  454. // Build a list of segments plus separators we can use as template.
  455. foreach (var segment in segments) {
  456. if (allSegments.Count > 0)
  457. allSegments.Add (null); // separator exposed as null.
  458. allSegments.Add (segment);
  459. }
  460. // Finally loop thru al segment-templates building the actual uri.
  461. foreach (var item in allSegments) {
  462. var segment = item.GetValueOrDefault ();
  463. // If segment is a separator..
  464. if (item == null) {
  465. if (pendingPartsAreAllSafe) {
  466. // Accept
  467. if (pendingParts.Length > 0) {
  468. if (blockAllUriAppends)
  469. return null;
  470. // Append any pending literals to the URL
  471. uri.Append (pendingParts.ToString ());
  472. pendingParts.Length = 0;
  473. }
  474. }
  475. pendingPartsAreAllSafe = false;
  476. // Guard against appending multiple separators for empty segments
  477. if (pendingParts.Length > 0 && pendingParts[pendingParts.Length - 1] == '/') {
  478. // Dev10 676725: Route should not be matched if that causes mismatched tokens
  479. // Dev11 86819: We will allow empty matches if all subsequent segments are null
  480. if (blockAllUriAppends)
  481. return null;
  482. // Append any pending literals to the URI (without the trailing slash) and prevent any future appends
  483. uri.Append(pendingParts.ToString (0, pendingParts.Length - 1));
  484. pendingParts.Length = 0;
  485. } else {
  486. pendingParts.Append ("/");
  487. }
  488. #if false
  489. } else if (segment.AllLiteral) {
  490. // Spezial (optimized) case: all elements of segment are literals.
  491. pendingPartsAreAllSafe = true;
  492. foreach (var tk in segment.Tokens)
  493. pendingParts.Append (tk.Name);
  494. #endif
  495. } else {
  496. // Segments are treated as all-or-none. We should never output a partial segment.
  497. // If we add any subsegment of this segment to the generated URL, we have to add
  498. // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we
  499. // used a value for {p1}, we have to output the entire segment up to the next "/".
  500. // Otherwise we could end up with the partial segment "v1" instead of the entire
  501. // segment "v1-v2.xml".
  502. bool addedAnySubsegments = false;
  503. foreach (var token in segment.Tokens) {
  504. if (token.Type == PatternTokenType.Literal) {
  505. // If it's a literal we hold on to it until we are sure we need to add it
  506. pendingPartsAreAllSafe = true;
  507. pendingParts.Append (token.Name);
  508. } else {
  509. if (token.Type == PatternTokenType.Standard || token.Type == PatternTokenType.CatchAll) {
  510. if (pendingPartsAreAllSafe) {
  511. // Accept
  512. if (pendingParts.Length > 0) {
  513. if (blockAllUriAppends)
  514. return null;
  515. // Append any pending literals to the URL
  516. uri.Append (pendingParts.ToString ());
  517. pendingParts.Length = 0;
  518. addedAnySubsegments = true;
  519. }
  520. }
  521. pendingPartsAreAllSafe = false;
  522. // If it's a parameter, get its value
  523. object acceptedParameterValue;
  524. bool hasAcceptedParameterValue = acceptedValues.TryGetValue (token.Name, out acceptedParameterValue);
  525. if (hasAcceptedParameterValue)
  526. unusedNewValues.Remove (token.Name);
  527. object defaultParameterValue;
  528. defaultValues.TryGetValue (token.Name, out defaultParameterValue);
  529. if (ParametersAreEqual (acceptedParameterValue, defaultParameterValue)) {
  530. // If the accepted value is the same as the default value, mark it as pending since
  531. // we won't necessarily add it to the URL we generate.
  532. pendingParts.Append (Convert.ToString (acceptedParameterValue, CultureInfo.InvariantCulture));
  533. } else {
  534. if (blockAllUriAppends)
  535. return null;
  536. // Add the new part to the URL as well as any pending parts
  537. if (pendingParts.Length > 0) {
  538. // Append any pending literals to the URL
  539. uri.Append (pendingParts.ToString ());
  540. pendingParts.Length = 0;
  541. }
  542. uri.Append (Convert.ToString (acceptedParameterValue, CultureInfo.InvariantCulture));
  543. addedAnySubsegments = true;
  544. }
  545. } else {
  546. Debug.Fail ("Invalid path subsegment type");
  547. }
  548. }
  549. }
  550. if (addedAnySubsegments) {
  551. // See comment above about why we add the pending parts
  552. if (pendingParts.Length > 0) {
  553. if (blockAllUriAppends)
  554. return null;
  555. // Append any pending literals to the URL
  556. uri.Append (pendingParts.ToString ());
  557. pendingParts.Length = 0;
  558. }
  559. }
  560. }
  561. }
  562. if (pendingPartsAreAllSafe) {
  563. // Accept
  564. if (pendingParts.Length > 0) {
  565. if (blockAllUriAppends)
  566. return null;
  567. // Append any pending literals to the URI
  568. uri.Append (pendingParts.ToString ());
  569. }
  570. }
  571. // Process constraints keys
  572. if (constraints != null) {
  573. // If there are any constraints, mark all the keys as being used so that we don't
  574. // generate query string items for custom constraints that don't appear as parameters
  575. // in the URI format.
  576. foreach (var constraintsItem in constraints) {
  577. unusedNewValues.Remove (constraintsItem.Key);
  578. }
  579. }
  580. // Encode the URI before we append the query string, otherwise we would double encode the query string
  581. var encodedUri = new StringBuilder ();
  582. encodedUri.Append (UriEncode (uri.ToString ()));
  583. uri = encodedUri;
  584. // Add remaining new values as query string parameters to the URI
  585. if (unusedNewValues.Count > 0) {
  586. // Generate the query string
  587. bool firstParam = true;
  588. foreach (string unusedNewValue in unusedNewValues) {
  589. object value;
  590. if (acceptedValues.TryGetValue (unusedNewValue, out value)) {
  591. uri.Append (firstParam ? '?' : '&');
  592. firstParam = false;
  593. uri.Append (Uri.EscapeDataString (unusedNewValue));
  594. uri.Append ('=');
  595. uri.Append (Uri.EscapeDataString (Convert.ToString (value, CultureInfo.InvariantCulture)));
  596. }
  597. }
  598. }
  599. #endregion
  600. usedValues = acceptedValues;
  601. return uri.ToString();
  602. }
  603. }
  604. }