5 // Atsushi Enomoto <atsushi@ximian.com>
6 // Marek Habersack <mhabersack@novell.com>
8 // Copyright (C) 2008-2009 Novell Inc. http://novell.com
12 // Permission is hereby granted, free of charge, to any person obtaining
13 // a copy of this software and associated documentation files (the
14 // "Software"), to deal in the Software without restriction, including
15 // without limitation the rights to use, copy, modify, merge, publish,
16 // distribute, sublicense, and/or sell copies of the Software, and to
17 // permit persons to whom the Software is furnished to do so, subject to
18 // the following conditions:
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
23 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 using System.Collections.Generic;
33 using System.Security.Permissions;
36 using System.Web.Util;
38 namespace System.Web.Routing
40 sealed class PatternParser
44 public bool AllLiteral;
45 public List <PatternToken> Tokens;
48 static readonly char[] placeholderDelimiters = { '{', '}' };
50 PatternSegment[] segments;
51 Dictionary <string, bool> parameterNames;
52 PatternToken[] tokens;
55 bool haveSegmentWithCatchAll;
62 public PatternParser (string pattern)
72 if (String.IsNullOrEmpty (url))
73 throw new SystemException ("INTERNAL ERROR: it should not try to parse null or empty string");
74 if (url [0] == '~' || url [0] == '/')
75 throw new ArgumentException ("Url must not start with '~' or '/'");
76 if (url.IndexOf ('?') >= 0)
77 throw new ArgumentException ("Url must not contain '?'");
79 string[] parts = url.Split ('/');
80 int partsCount = segmentCount = parts.Length;
81 var allTokens = new List <PatternToken> ();
82 PatternToken tmpToken;
84 segments = new PatternSegment [partsCount];
85 parameterNames = new Dictionary <string, bool> ();
87 for (int i = 0; i < partsCount; i++) {
88 if (haveSegmentWithCatchAll)
89 throw new ArgumentException ("A catch-all parameter can only appear as the last segment of the route URL");
92 string part = parts [i];
93 int partLength = part.Length;
94 var tokens = new List <PatternToken> ();
96 if (partLength == 0 && i < partsCount - 1)
97 throw new ArgumentException ("Consecutive URL segment separators '/' are not allowed");
99 if (part.IndexOf ("{}") != -1)
100 throw new ArgumentException ("Empty URL parameter name is not allowed");
103 allTokens.Add (null);
105 if (part.IndexOfAny (placeholderDelimiters) == -1) {
106 // no placeholders here, short-circuit it
107 tmpToken = new PatternToken (PatternTokenType.Literal, part);
108 tokens.Add (tmpToken);
109 allTokens.Add (tmpToken);
110 segments [i].AllLiteral = true;
111 segments [i].Tokens = tokens;
117 bool allLiteral = true;
118 while (from < partLength) {
119 start = part.IndexOf ('{', from);
120 if (start >= partLength - 2)
121 throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
124 if (part.IndexOf ('}', from) >= from)
125 throw new ArgumentException ("Unmatched URL parameter closer '}'. A corresponding '{' must precede");
126 tmp = part.Substring (from);
127 tmpToken = new PatternToken (PatternTokenType.Literal, tmp);
128 tokens.Add (tmpToken);
129 allTokens.Add (tmpToken);
134 if (from == 0 && start > 0) {
135 tmpToken = new PatternToken (PatternTokenType.Literal, part.Substring (0, start));
136 tokens.Add (tmpToken);
137 allTokens.Add (tmpToken);
140 int end = part.IndexOf ('}', start + 1);
141 int next = part.IndexOf ('{', start + 1);
143 if (end < 0 || next >= 0 && next < end)
144 throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
146 throw new ArgumentException ("Two consecutive URL parameters are not allowed. Split into a different segment by '/', or a literal string.");
151 string token = part.Substring (start + 1, end - start - 1);
152 PatternTokenType type;
153 if (token [0] == '*') {
155 haveSegmentWithCatchAll = true;
156 type = PatternTokenType.CatchAll;
157 token = token.Substring (1);
159 type = PatternTokenType.Standard;
161 if (!parameterNames.ContainsKey (token))
162 parameterNames.Add (token, true);
164 tmpToken = new PatternToken (type, token);
165 tokens.Add (tmpToken);
166 allTokens.Add (tmpToken);
169 if (end < partLength - 1) {
170 token = part.Substring (end + 1, next - end - 1);
171 tmpToken = new PatternToken (PatternTokenType.Literal, token);
172 tokens.Add (tmpToken);
173 allTokens.Add (tmpToken);
177 if (catchAlls > 1 || (catchAlls == 1 && tokens.Count > 1))
178 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.");
182 segments [i].AllLiteral = allLiteral;
183 segments [i].Tokens = tokens;
186 if (allTokens.Count > 0)
187 this.tokens = allTokens.ToArray ();
191 RouteValueDictionary AddDefaults (RouteValueDictionary dict, RouteValueDictionary defaults)
193 if (defaults != null && defaults.Count > 0) {
195 foreach (var def in defaults) {
197 if (dict.ContainsKey (key))
199 dict.Add (key, def.Value);
206 public RouteValueDictionary Match (string path, RouteValueDictionary defaults)
208 var ret = new RouteValueDictionary ();
213 if (String.IsNullOrEmpty (path)) {
218 if (String.Compare (url, path, StringComparison.Ordinal) == 0 && url.IndexOf ('{') < 0)
219 return AddDefaults (ret, defaults);
221 argSegs = path.Split ('/');
222 argsCount = argSegs.Length;
225 bool haveDefaults = defaults != null && defaults.Count > 0;
227 if (argsCount == 1 && String.IsNullOrEmpty (argSegs [0]))
230 if (!haveDefaults && ((haveSegmentWithCatchAll && argsCount < segmentCount) || (!haveSegmentWithCatchAll && argsCount != segmentCount)))
235 foreach (PatternSegment segment in segments) {
239 if (segment.AllLiteral) {
240 if (String.Compare (argSegs [i], segment.Tokens [0].Name, StringComparison.OrdinalIgnoreCase) != 0)
246 string pathSegment = argSegs [i];
247 int pathSegmentLength = pathSegment != null ? pathSegment.Length : -1;
249 PatternTokenType tokenType;
250 List <PatternToken> tokens = segment.Tokens;
251 int tokensCount = tokens.Count;
253 // Process the path segments ignoring the defaults
254 for (int tokenIndex = 0; tokenIndex < tokensCount; tokenIndex++) {
255 var token = tokens [tokenIndex];
256 if (pathIndex > pathSegmentLength - 1)
259 tokenType = token.Type;
260 var tokenName = token.Name;
263 if (i > segmentCount - 1 || tokenType == PatternTokenType.CatchAll) {
264 if (tokenType != PatternTokenType.CatchAll)
267 StringBuilder sb = new StringBuilder ();
268 for (int j = i; j < argsCount; j++) {
271 sb.Append (argSegs [j]);
274 ret.Add (tokenName, sb.ToString ());
279 if (token.Type == PatternTokenType.Literal) {
280 int nameLen = tokenName.Length;
281 if (pathSegmentLength < nameLen || String.Compare (pathSegment, pathIndex, tokenName, 0, nameLen, StringComparison.OrdinalIgnoreCase) != 0)
283 pathIndex += nameLen;
287 int nextTokenIndex = tokenIndex + 1;
288 if (nextTokenIndex >= tokensCount) {
290 ret.Add (tokenName, pathSegment.Substring (pathIndex));
294 // Next token is a literal - greedy matching. It seems .NET
295 // uses a simple and naive algorithm here which finds the
296 // last ocurrence of the next section literal and assigns
297 // everything before that to this token. See the
298 // GetRouteData28 test in RouteTest.cs
299 var nextToken = tokens [nextTokenIndex];
300 string nextTokenName = nextToken.Name;
301 int lastIndex = pathSegment.LastIndexOf (nextTokenName, pathSegmentLength - 1, pathSegmentLength - pathIndex, StringComparison.OrdinalIgnoreCase);
305 int copyLength = lastIndex - pathIndex;
306 string sectionValue = pathSegment.Substring (pathIndex, copyLength);
307 if (String.IsNullOrEmpty (sectionValue))
310 ret.Add (tokenName, sectionValue);
311 pathIndex += copyLength;
316 // Check the remaining segments, if any, and see if they are required
318 // If a segment has more than one section (i.e. there's at least one
319 // literal, then it cannot match defaults
321 // All of the remaining segments must have all defaults provided and they
322 // must not be literals or the match will fail.
323 if (i < segmentCount) {
327 for (;i < segmentCount; i++) {
328 var segment = segments [i];
329 if (segment.AllLiteral)
332 var tokens = segment.Tokens;
333 if (tokens.Count != 1)
336 if (!defaults.ContainsKey (tokens [0].Name))
341 return AddDefaults (ret, defaults);
344 public bool BuildUrl (Route route, RequestContext requestContext, RouteValueDictionary userValues, out string value)
347 if (requestContext == null)
350 RouteData routeData = requestContext.RouteData;
351 RouteValueDictionary defaultValues = route != null ? route.Defaults : null;
352 RouteValueDictionary ambientValues = routeData.Values;
354 if (defaultValues != null && defaultValues.Count == 0)
355 defaultValues = null;
356 if (ambientValues != null && ambientValues.Count == 0)
357 ambientValues = null;
358 if (userValues != null && userValues.Count == 0)
361 // Check URL parameters
362 // It is allowed to take ambient values for required parameters if:
364 // - there are no default values provided
365 // - the default values dictionary contains at least one required
368 bool canTakeFromAmbient;
369 if (defaultValues == null)
370 canTakeFromAmbient = true;
372 canTakeFromAmbient = false;
373 foreach (KeyValuePair <string, bool> de in parameterNames) {
374 if (defaultValues.ContainsKey (de.Key)) {
375 canTakeFromAmbient = true;
381 bool allMustBeInUserValues = false;
382 foreach (KeyValuePair <string, bool> de in parameterNames) {
383 string parameterName = de.Key;
384 // Is the parameter required?
385 if (defaultValues == null || !defaultValues.ContainsKey (parameterName)) {
386 // Yes, it is required (no value in defaults)
387 // Has the user provided value for it?
388 if (userValues == null || !userValues.ContainsKey (parameterName)) {
389 if (allMustBeInUserValues)
390 return false; // partial override => no match
392 if (!canTakeFromAmbient || ambientValues == null || !ambientValues.ContainsKey (parameterName))
393 return false; // no value provided => no match
394 } else if (canTakeFromAmbient)
395 allMustBeInUserValues = true;
399 // Check for non-url parameters
400 if (defaultValues != null) {
401 foreach (var de in defaultValues) {
402 string parameterName = de.Key;
404 if (parameterNames.ContainsKey (parameterName))
407 object parameterValue = null;
408 // Has the user specified value for this parameter and, if
409 // yes, is it the same as the one in defaults?
410 if (userValues != null && userValues.TryGetValue (parameterName, out parameterValue)) {
411 object defaultValue = de.Value;
412 if (defaultValue is string && parameterValue is string) {
413 if (String.Compare ((string)defaultValue, (string)parameterValue, StringComparison.Ordinal) != 0)
414 return false; // different value => no match
415 } else if (defaultValue != parameterValue)
416 return false; // different value => no match
421 // Check the constraints
422 RouteValueDictionary constraints = route != null ? route.Constraints : null;
423 if (constraints != null && constraints.Count > 0) {
424 HttpContextBase context = requestContext.HttpContext;
425 bool invalidConstraint;
427 foreach (var de in constraints) {
428 if (!Route.ProcessConstraintInternal (context, route, de.Value, de.Key, userValues, RouteDirection.UrlGeneration, out invalidConstraint) ||
430 return false; // constraint not met => no match
434 // We're a match, generate the URL
435 var ret = new StringBuilder ();
438 // Going in reverse order, so that we can trim without much ado
439 int tokensCount = tokens.Length - 1;
440 for (int i = tokensCount; i >= 0; i--) {
441 PatternToken token = tokens [i];
443 if (i < tokensCount && ret.Length > 0 && ret [0] != '/')
448 if (token.Type == PatternTokenType.Literal) {
449 ret.Insert (0, token.Name);
453 string parameterName = token.Name;
456 if (userValues.GetValue (parameterName, out tokenValue)) {
457 if (!defaultValues.Has (parameterName, tokenValue)) {
459 if (tokenValue != null)
460 ret.Insert (0, tokenValue.ToString ());
464 if (!canTrim && tokenValue != null)
465 ret.Insert (0, tokenValue.ToString ());
469 if (defaultValues.GetValue (parameterName, out tokenValue)) {
470 object ambientTokenValue;
471 if (ambientValues.GetValue (parameterName, out ambientTokenValue))
472 tokenValue = ambientTokenValue;
474 if (!canTrim && tokenValue != null)
475 ret.Insert (0, tokenValue.ToString ());
480 if (ambientValues.GetValue (parameterName, out tokenValue)) {
481 if (tokenValue != null)
482 ret.Insert (0, tokenValue.ToString ());
487 // All the values specified in userValues that aren't part of the original
488 // URL, the constraints or defaults collections are treated as overflow
489 // values - they are appended as query parameters to the URL
490 if (userValues != null) {
492 foreach (var de in userValues) {
493 string parameterName = de.Key;
495 if (parameterNames.ContainsKey (parameterName) || defaultValues.Has (parameterName) || constraints.Has (parameterName))
504 object parameterValue = de.Value;
505 ret.Append (Uri.EscapeDataString (parameterName));
507 if (parameterValue != null)
508 ret.Append (Uri.EscapeDataString (de.Value.ToString ()));
512 value = ret.ToString ();