You've already forked linux-packaging-mono
722 lines
23 KiB
C#
722 lines
23 KiB
C#
![]() |
//
|
||
|
// PatternParser.cs
|
||
|
//
|
||
|
// Author:
|
||
|
// Atsushi Enomoto <atsushi@ximian.com>
|
||
|
// Marek Habersack <mhabersack@novell.com>
|
||
|
//
|
||
|
// Copyright (C) 2008-2010 Novell Inc. http://novell.com
|
||
|
//
|
||
|
|
||
|
//
|
||
|
// Permission is hereby granted, free of charge, to any person obtaining
|
||
|
// a copy of this software and associated documentation files (the
|
||
|
// "Software"), to deal in the Software without restriction, including
|
||
|
// without limitation the rights to use, copy, modify, merge, publish,
|
||
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
||
|
// permit persons to whom the Software is furnished to do so, subject to
|
||
|
// the following conditions:
|
||
|
//
|
||
|
// The above copyright notice and this permission notice shall be
|
||
|
// included in all copies or substantial portions of the Software.
|
||
|
//
|
||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||
|
//
|
||
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Security.Permissions;
|
||
|
using System.Text;
|
||
|
using System.Text.RegularExpressions;
|
||
|
using System.Web;
|
||
|
using System.Web.Util;
|
||
|
using System.Diagnostics;
|
||
|
using System.Globalization;
|
||
|
|
||
|
namespace System.Web.Routing
|
||
|
{
|
||
|
sealed class PatternParser
|
||
|
{
|
||
|
struct PatternSegment
|
||
|
{
|
||
|
public bool AllLiteral;
|
||
|
public List <PatternToken> Tokens;
|
||
|
}
|
||
|
|
||
|
static readonly char[] placeholderDelimiters = { '{', '}' };
|
||
|
|
||
|
PatternSegment[] segments;
|
||
|
Dictionary <string, bool> parameterNames;
|
||
|
PatternToken[] tokens;
|
||
|
|
||
|
int segmentCount;
|
||
|
bool haveSegmentWithCatchAll;
|
||
|
|
||
|
public string Url {
|
||
|
get;
|
||
|
private set;
|
||
|
}
|
||
|
|
||
|
public PatternParser (string pattern)
|
||
|
{
|
||
|
this.Url = pattern;
|
||
|
Parse ();
|
||
|
}
|
||
|
|
||
|
void Parse ()
|
||
|
{
|
||
|
string url = Url;
|
||
|
parameterNames = new Dictionary <string, bool> (StringComparer.OrdinalIgnoreCase);
|
||
|
|
||
|
if (!String.IsNullOrEmpty (url)) {
|
||
|
if (url [0] == '~' || url [0] == '/')
|
||
|
throw new ArgumentException ("Url must not start with '~' or '/'");
|
||
|
if (url.IndexOf ('?') >= 0)
|
||
|
throw new ArgumentException ("Url must not contain '?'");
|
||
|
} else {
|
||
|
segments = new PatternSegment [0];
|
||
|
tokens = new PatternToken [0];
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
string[] parts = url.Split ('/');
|
||
|
int partsCount = segmentCount = parts.Length;
|
||
|
var allTokens = new List <PatternToken> ();
|
||
|
PatternToken tmpToken;
|
||
|
|
||
|
segments = new PatternSegment [partsCount];
|
||
|
|
||
|
for (int i = 0; i < partsCount; i++) {
|
||
|
if (haveSegmentWithCatchAll)
|
||
|
throw new ArgumentException ("A catch-all parameter can only appear as the last segment of the route URL");
|
||
|
|
||
|
int catchAlls = 0;
|
||
|
string part = parts [i];
|
||
|
int partLength = part.Length;
|
||
|
var tokens = new List <PatternToken> ();
|
||
|
|
||
|
if (partLength == 0 && i < partsCount - 1)
|
||
|
throw new ArgumentException ("Consecutive URL segment separators '/' are not allowed");
|
||
|
|
||
|
if (part.IndexOf ("{}") != -1)
|
||
|
throw new ArgumentException ("Empty URL parameter name is not allowed");
|
||
|
|
||
|
if (i > 0)
|
||
|
allTokens.Add (null);
|
||
|
|
||
|
if (part.IndexOfAny (placeholderDelimiters) == -1) {
|
||
|
// no placeholders here, short-circuit it
|
||
|
tmpToken = new PatternToken (PatternTokenType.Literal, part);
|
||
|
tokens.Add (tmpToken);
|
||
|
allTokens.Add (tmpToken);
|
||
|
segments [i].AllLiteral = true;
|
||
|
segments [i].Tokens = tokens;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
string tmp;
|
||
|
int from = 0, start;
|
||
|
bool allLiteral = true;
|
||
|
while (from < partLength) {
|
||
|
start = part.IndexOf ('{', from);
|
||
|
if (start >= partLength - 2)
|
||
|
throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
|
||
|
|
||
|
if (start < 0) {
|
||
|
if (part.IndexOf ('}', from) >= from)
|
||
|
throw new ArgumentException ("Unmatched URL parameter closer '}'. A corresponding '{' must precede");
|
||
|
tmp = part.Substring (from);
|
||
|
tmpToken = new PatternToken (PatternTokenType.Literal, tmp);
|
||
|
tokens.Add (tmpToken);
|
||
|
allTokens.Add (tmpToken);
|
||
|
from += tmp.Length;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (from == 0 && start > 0) {
|
||
|
tmpToken = new PatternToken (PatternTokenType.Literal, part.Substring (0, start));
|
||
|
tokens.Add (tmpToken);
|
||
|
allTokens.Add (tmpToken);
|
||
|
}
|
||
|
|
||
|
int end = part.IndexOf ('}', start + 1);
|
||
|
int next = part.IndexOf ('{', start + 1);
|
||
|
|
||
|
if (end < 0 || next >= 0 && next < end)
|
||
|
throw new ArgumentException ("Unterminated URL parameter. It must contain matching '}'");
|
||
|
if (next == end + 1)
|
||
|
throw new ArgumentException ("Two consecutive URL parameters are not allowed. Split into a different segment by '/', or a literal string.");
|
||
|
|
||
|
if (next == -1)
|
||
|
next = partLength;
|
||
|
|
||
|
string token = part.Substring (start + 1, end - start - 1);
|
||
|
PatternTokenType type;
|
||
|
if (token [0] == '*') {
|
||
|
catchAlls++;
|
||
|
haveSegmentWithCatchAll = true;
|
||
|
type = PatternTokenType.CatchAll;
|
||
|
token = token.Substring (1);
|
||
|
} else
|
||
|
type = PatternTokenType.Standard;
|
||
|
|
||
|
if (!parameterNames.ContainsKey (token))
|
||
|
parameterNames.Add (token, true);
|
||
|
|
||
|
tmpToken = new PatternToken (type, token);
|
||
|
tokens.Add (tmpToken);
|
||
|
allTokens.Add (tmpToken);
|
||
|
allLiteral = false;
|
||
|
|
||
|
if (end < partLength - 1) {
|
||
|
token = part.Substring (end + 1, next - end - 1);
|
||
|
tmpToken = new PatternToken (PatternTokenType.Literal, token);
|
||
|
tokens.Add (tmpToken);
|
||
|
allTokens.Add (tmpToken);
|
||
|
end += token.Length;
|
||
|
}
|
||
|
|
||
|
if (catchAlls > 1 || (catchAlls == 1 && tokens.Count > 1))
|
||
|
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.");
|
||
|
from = end + 1;
|
||
|
}
|
||
|
|
||
|
segments [i].AllLiteral = allLiteral;
|
||
|
segments [i].Tokens = tokens;
|
||
|
}
|
||
|
|
||
|
if (allTokens.Count > 0)
|
||
|
this.tokens = allTokens.ToArray ();
|
||
|
allTokens = null;
|
||
|
}
|
||
|
|
||
|
RouteValueDictionary AddDefaults (RouteValueDictionary dict, RouteValueDictionary defaults)
|
||
|
{
|
||
|
if (defaults != null && defaults.Count > 0) {
|
||
|
string key;
|
||
|
foreach (var def in defaults) {
|
||
|
key = def.Key;
|
||
|
if (dict.ContainsKey (key))
|
||
|
continue;
|
||
|
dict.Add (key, def.Value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return dict;
|
||
|
}
|
||
|
|
||
|
static bool ParametersAreEqual (object a, object b)
|
||
|
{
|
||
|
if (a is string && b is string) {
|
||
|
return String.Equals (a as string, b as string, StringComparison.OrdinalIgnoreCase);
|
||
|
} else {
|
||
|
// Parameter may be a boxed value type, need to use .Equals() for comparison
|
||
|
return object.Equals (a, b);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static bool ParameterIsNonEmpty (object param)
|
||
|
{
|
||
|
if (param is string)
|
||
|
return !string.IsNullOrEmpty (param as string);
|
||
|
|
||
|
return param != null;
|
||
|
}
|
||
|
|
||
|
bool IsParameterRequired (string parameterName, RouteValueDictionary defaultValues, out object defaultValue)
|
||
|
{
|
||
|
foreach (var token in tokens) {
|
||
|
if (token == null)
|
||
|
continue;
|
||
|
|
||
|
if (string.Equals (token.Name, parameterName, StringComparison.OrdinalIgnoreCase)) {
|
||
|
if (token.Type == PatternTokenType.CatchAll) {
|
||
|
defaultValue = null;
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (defaultValues == null)
|
||
|
throw new ArgumentNullException ("defaultValues is null?!");
|
||
|
|
||
|
return !defaultValues.TryGetValue (parameterName, out defaultValue);
|
||
|
}
|
||
|
|
||
|
static string EscapeReservedCharacters (Match m)
|
||
|
{
|
||
|
if (m == null)
|
||
|
throw new ArgumentNullException("m");
|
||
|
|
||
|
return Uri.HexEscape (m.Value[0]);
|
||
|
}
|
||
|
|
||
|
static string UriEncode (string str)
|
||
|
{
|
||
|
if (string.IsNullOrEmpty (str))
|
||
|
return str;
|
||
|
|
||
|
string escape = Uri.EscapeUriString (str);
|
||
|
return Regex.Replace (escape, "([#?])", new MatchEvaluator (EscapeReservedCharacters));
|
||
|
}
|
||
|
|
||
|
bool MatchSegment (int segIndex, int argsCount, string[] argSegs, List <PatternToken> tokens, RouteValueDictionary ret)
|
||
|
{
|
||
|
string pathSegment = argSegs [segIndex];
|
||
|
int pathSegmentLength = pathSegment != null ? pathSegment.Length : -1;
|
||
|
int startIndex = pathSegmentLength - 1;
|
||
|
PatternTokenType tokenType;
|
||
|
int tokensCount = tokens.Count;
|
||
|
PatternToken token;
|
||
|
string tokenName;
|
||
|
|
||
|
for (int tokenIndex = tokensCount - 1; tokenIndex > -1; tokenIndex--) {
|
||
|
token = tokens [tokenIndex];
|
||
|
if (startIndex < 0)
|
||
|
return false;
|
||
|
|
||
|
tokenType = token.Type;
|
||
|
tokenName = token.Name;
|
||
|
|
||
|
if (segIndex > segmentCount - 1 || tokenType == PatternTokenType.CatchAll) {
|
||
|
var sb = new StringBuilder ();
|
||
|
|
||
|
for (int j = segIndex; j < argsCount; j++) {
|
||
|
if (j > segIndex)
|
||
|
sb.Append ('/');
|
||
|
sb.Append (argSegs [j]);
|
||
|
}
|
||
|
|
||
|
ret.Add (tokenName, sb.ToString ());
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
int scanIndex;
|
||
|
if (token.Type == PatternTokenType.Literal) {
|
||
|
int nameLen = tokenName.Length;
|
||
|
if (startIndex + 1 < nameLen)
|
||
|
return false;
|
||
|
scanIndex = startIndex - nameLen + 1;
|
||
|
if (String.Compare (pathSegment, scanIndex, tokenName, 0, nameLen, StringComparison.OrdinalIgnoreCase) != 0)
|
||
|
return false;
|
||
|
startIndex = scanIndex - 1;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Standard token
|
||
|
int nextTokenIndex = tokenIndex - 1;
|
||
|
if (nextTokenIndex < 0) {
|
||
|
// First token
|
||
|
ret.Add (tokenName, pathSegment.Substring (0, startIndex + 1));
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (startIndex == 0)
|
||
|
return false;
|
||
|
|
||
|
var nextToken = tokens [nextTokenIndex];
|
||
|
string nextTokenName = nextToken.Name;
|
||
|
|
||
|
// Skip one char, since there can be no empty segments and if the
|
||
|
// current token's value happens to be the same as preceeding
|
||
|
// literal text, we'll save some time and complexity.
|
||
|
scanIndex = startIndex - 1;
|
||
|
int lastIndex = pathSegment.LastIndexOf (nextTokenName, scanIndex, StringComparison.OrdinalIgnoreCase);
|
||
|
if (lastIndex == -1)
|
||
|
return false;
|
||
|
|
||
|
lastIndex += nextTokenName.Length - 1;
|
||
|
|
||
|
string sectionValue = pathSegment.Substring (lastIndex + 1, startIndex - lastIndex);
|
||
|
if (String.IsNullOrEmpty (sectionValue))
|
||
|
return false;
|
||
|
|
||
|
ret.Add (tokenName, sectionValue);
|
||
|
startIndex = lastIndex;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public RouteValueDictionary Match (string path, RouteValueDictionary defaults)
|
||
|
{
|
||
|
var ret = new RouteValueDictionary ();
|
||
|
string url = Url;
|
||
|
string [] argSegs;
|
||
|
int argsCount;
|
||
|
|
||
|
if (String.IsNullOrEmpty (path)) {
|
||
|
argSegs = null;
|
||
|
argsCount = 0;
|
||
|
} else {
|
||
|
// quick check
|
||
|
if (String.Compare (url, path, StringComparison.Ordinal) == 0 && url.IndexOf ('{') < 0)
|
||
|
return AddDefaults (ret, defaults);
|
||
|
|
||
|
argSegs = path.Split ('/');
|
||
|
argsCount = argSegs.Length;
|
||
|
|
||
|
if (String.IsNullOrEmpty (argSegs [argsCount - 1]))
|
||
|
argsCount--; // path ends with a trailinig '/'
|
||
|
}
|
||
|
bool haveDefaults = defaults != null && defaults.Count > 0;
|
||
|
|
||
|
if (argsCount == 1 && String.IsNullOrEmpty (argSegs [0]))
|
||
|
argsCount = 0;
|
||
|
|
||
|
if (!haveDefaults && ((haveSegmentWithCatchAll && argsCount < segmentCount) || (!haveSegmentWithCatchAll && argsCount != segmentCount)))
|
||
|
return null;
|
||
|
|
||
|
int i = 0;
|
||
|
|
||
|
foreach (PatternSegment segment in segments) {
|
||
|
if (i >= argsCount)
|
||
|
break;
|
||
|
|
||
|
if (segment.AllLiteral) {
|
||
|
if (String.Compare (argSegs [i], segment.Tokens [0].Name, StringComparison.OrdinalIgnoreCase) != 0)
|
||
|
return null;
|
||
|
i++;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (!MatchSegment (i, argsCount, argSegs, segment.Tokens, ret))
|
||
|
return null;
|
||
|
i++;
|
||
|
}
|
||
|
|
||
|
// Check the remaining segments, if any, and see if they are required
|
||
|
//
|
||
|
// If a segment has more than one section (i.e. there's at least one
|
||
|
// literal, then it cannot match defaults
|
||
|
//
|
||
|
// All of the remaining segments must have all defaults provided and they
|
||
|
// must not be literals or the match will fail.
|
||
|
if (i < segmentCount) {
|
||
|
if (!haveDefaults)
|
||
|
return null;
|
||
|
|
||
|
for (;i < segmentCount; i++) {
|
||
|
var segment = segments [i];
|
||
|
if (segment.AllLiteral)
|
||
|
return null;
|
||
|
|
||
|
var tokens = segment.Tokens;
|
||
|
if (tokens.Count != 1)
|
||
|
return null;
|
||
|
|
||
|
// if token is catch-all, we're done.
|
||
|
if (tokens [0].Type == PatternTokenType.CatchAll)
|
||
|
break;
|
||
|
|
||
|
if (!defaults.ContainsKey (tokens [0].Name))
|
||
|
return null;
|
||
|
}
|
||
|
} else if (!haveSegmentWithCatchAll && argsCount > segmentCount)
|
||
|
return null;
|
||
|
|
||
|
return AddDefaults (ret, defaults);
|
||
|
}
|
||
|
|
||
|
public string BuildUrl (Route route, RequestContext requestContext, RouteValueDictionary userValues, RouteValueDictionary constraints, out RouteValueDictionary usedValues)
|
||
|
{
|
||
|
usedValues = null;
|
||
|
|
||
|
if (requestContext == null)
|
||
|
return null;
|
||
|
|
||
|
RouteData routeData = requestContext.RouteData;
|
||
|
var currentValues = routeData.Values ?? new RouteValueDictionary ();
|
||
|
var values = userValues ?? new RouteValueDictionary ();
|
||
|
var defaultValues = (route != null ? route.Defaults : null) ?? new RouteValueDictionary ();
|
||
|
|
||
|
// The set of values we should be using when generating the URL in this route
|
||
|
var acceptedValues = new RouteValueDictionary ();
|
||
|
|
||
|
// Keep track of which new values have been used
|
||
|
HashSet<string> unusedNewValues = new HashSet<string> (values.Keys, StringComparer.OrdinalIgnoreCase);
|
||
|
|
||
|
// This route building logic is based on System.Web.Http's Routing code (which is Apache Licensed by MS)
|
||
|
// and which can be found at mono's external/aspnetwebstack/src/System.Web.Http/Routing/HttpParsedRoute.cs
|
||
|
// Hopefully this will ensure a much higher compatiblity with MS.NET's System.Web.Routing logic. (pruiz)
|
||
|
|
||
|
#region Step 1: Get the list of values we're going to use to match and generate this URL
|
||
|
// Find out which entries in the URL are valid for the URL we want to generate.
|
||
|
// If the URL had ordered parameters a="1", b="2", c="3" and the new values
|
||
|
// specified that b="9", then we need to invalidate everything after it. The new
|
||
|
// values should then be a="1", b="9", c=<no value>.
|
||
|
foreach (var item in parameterNames) {
|
||
|
var parameterName = item.Key;
|
||
|
|
||
|
object newParameterValue;
|
||
|
bool hasNewParameterValue = values.TryGetValue (parameterName, out newParameterValue);
|
||
|
if (hasNewParameterValue) {
|
||
|
unusedNewValues.Remove(parameterName);
|
||
|
}
|
||
|
|
||
|
object currentParameterValue;
|
||
|
bool hasCurrentParameterValue = currentValues.TryGetValue (parameterName, out currentParameterValue);
|
||
|
|
||
|
if (hasNewParameterValue && hasCurrentParameterValue) {
|
||
|
if (!ParametersAreEqual (currentParameterValue, newParameterValue)) {
|
||
|
// Stop copying current values when we find one that doesn't match
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If the parameter is a match, add it to the list of values we will use for URL generation
|
||
|
if (hasNewParameterValue) {
|
||
|
if (ParameterIsNonEmpty (newParameterValue)) {
|
||
|
acceptedValues.Add (parameterName, newParameterValue);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
if (hasCurrentParameterValue) {
|
||
|
acceptedValues.Add (parameterName, currentParameterValue);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add all remaining new values to the list of values we will use for URL generation
|
||
|
foreach (var newValue in values) {
|
||
|
if (ParameterIsNonEmpty (newValue.Value) && !acceptedValues.ContainsKey (newValue.Key)) {
|
||
|
acceptedValues.Add (newValue.Key, newValue.Value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add all current values that aren't in the URL at all
|
||
|
foreach (var currentValue in currentValues) {
|
||
|
if (!acceptedValues.ContainsKey (currentValue.Key) && !parameterNames.ContainsKey (currentValue.Key)) {
|
||
|
acceptedValues.Add (currentValue.Key, currentValue.Value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add all remaining default values from the route to the list of values we will use for URL generation
|
||
|
foreach (var item in parameterNames) {
|
||
|
object defaultValue;
|
||
|
if (!acceptedValues.ContainsKey (item.Key) && !IsParameterRequired (item.Key, defaultValues, out defaultValue)) {
|
||
|
// Add the default value only if there isn't already a new value for it and
|
||
|
// only if it actually has a default value, which we determine based on whether
|
||
|
// the parameter value is required.
|
||
|
acceptedValues.Add (item.Key, defaultValue);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// All required parameters in this URL must have values from somewhere (i.e. the accepted values)
|
||
|
foreach (var item in parameterNames) {
|
||
|
object defaultValue;
|
||
|
if (IsParameterRequired (item.Key, defaultValues, out defaultValue) && !acceptedValues.ContainsKey (item.Key)) {
|
||
|
// If the route parameter value is required that means there's
|
||
|
// no default value, so if there wasn't a new value for it
|
||
|
// either, this route won't match.
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// All other default values must match if they are explicitly defined in the new values
|
||
|
var otherDefaultValues = new RouteValueDictionary (defaultValues);
|
||
|
foreach (var item in parameterNames) {
|
||
|
otherDefaultValues.Remove (item.Key);
|
||
|
}
|
||
|
|
||
|
foreach (var defaultValue in otherDefaultValues) {
|
||
|
object value;
|
||
|
if (values.TryGetValue (defaultValue.Key, out value)) {
|
||
|
unusedNewValues.Remove (defaultValue.Key);
|
||
|
if (!ParametersAreEqual (value, defaultValue.Value)) {
|
||
|
// If there is a non-parameterized value in the route and there is a
|
||
|
// new value for it and it doesn't match, this route won't match.
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region Step 2: If the route is a match generate the appropriate URL
|
||
|
|
||
|
var uri = new StringBuilder ();
|
||
|
var pendingParts = new StringBuilder ();
|
||
|
var pendingPartsAreAllSafe = false;
|
||
|
bool blockAllUriAppends = false;
|
||
|
var allSegments = new List<PatternSegment?> ();
|
||
|
|
||
|
// Build a list of segments plus separators we can use as template.
|
||
|
foreach (var segment in segments) {
|
||
|
if (allSegments.Count > 0)
|
||
|
allSegments.Add (null); // separator exposed as null.
|
||
|
allSegments.Add (segment);
|
||
|
}
|
||
|
|
||
|
// Finally loop thru al segment-templates building the actual uri.
|
||
|
foreach (var item in allSegments) {
|
||
|
var segment = item.GetValueOrDefault ();
|
||
|
|
||
|
// If segment is a separator..
|
||
|
if (item == null) {
|
||
|
if (pendingPartsAreAllSafe) {
|
||
|
// Accept
|
||
|
if (pendingParts.Length > 0) {
|
||
|
if (blockAllUriAppends)
|
||
|
return null;
|
||
|
|
||
|
// Append any pending literals to the URL
|
||
|
uri.Append (pendingParts.ToString ());
|
||
|
pendingParts.Length = 0;
|
||
|
}
|
||
|
}
|
||
|
pendingPartsAreAllSafe = false;
|
||
|
|
||
|
// Guard against appending multiple separators for empty segments
|
||
|
if (pendingParts.Length > 0 && pendingParts[pendingParts.Length - 1] == '/') {
|
||
|
// Dev10 676725: Route should not be matched if that causes mismatched tokens
|
||
|
// Dev11 86819: We will allow empty matches if all subsequent segments are null
|
||
|
if (blockAllUriAppends)
|
||
|
return null;
|
||
|
|
||
|
// Append any pending literals to the URI (without the trailing slash) and prevent any future appends
|
||
|
uri.Append(pendingParts.ToString (0, pendingParts.Length - 1));
|
||
|
pendingParts.Length = 0;
|
||
|
} else {
|
||
|
pendingParts.Append ("/");
|
||
|
}
|
||
|
#if false
|
||
|
} else if (segment.AllLiteral) {
|
||
|
// Spezial (optimized) case: all elements of segment are literals.
|
||
|
pendingPartsAreAllSafe = true;
|
||
|
foreach (var tk in segment.Tokens)
|
||
|
pendingParts.Append (tk.Name);
|
||
|
#endif
|
||
|
} else {
|
||
|
// Segments are treated as all-or-none. We should never output a partial segment.
|
||
|
// If we add any subsegment of this segment to the generated URL, we have to add
|
||
|
// the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we
|
||
|
// used a value for {p1}, we have to output the entire segment up to the next "/".
|
||
|
// Otherwise we could end up with the partial segment "v1" instead of the entire
|
||
|
// segment "v1-v2.xml".
|
||
|
bool addedAnySubsegments = false;
|
||
|
|
||
|
foreach (var token in segment.Tokens) {
|
||
|
if (token.Type == PatternTokenType.Literal) {
|
||
|
// If it's a literal we hold on to it until we are sure we need to add it
|
||
|
pendingPartsAreAllSafe = true;
|
||
|
pendingParts.Append (token.Name);
|
||
|
} else {
|
||
|
if (token.Type == PatternTokenType.Standard || token.Type == PatternTokenType.CatchAll) {
|
||
|
if (pendingPartsAreAllSafe) {
|
||
|
// Accept
|
||
|
if (pendingParts.Length > 0) {
|
||
|
if (blockAllUriAppends)
|
||
|
return null;
|
||
|
|
||
|
// Append any pending literals to the URL
|
||
|
uri.Append (pendingParts.ToString ());
|
||
|
pendingParts.Length = 0;
|
||
|
|
||
|
addedAnySubsegments = true;
|
||
|
}
|
||
|
}
|
||
|
pendingPartsAreAllSafe = false;
|
||
|
|
||
|
// If it's a parameter, get its value
|
||
|
object acceptedParameterValue;
|
||
|
bool hasAcceptedParameterValue = acceptedValues.TryGetValue (token.Name, out acceptedParameterValue);
|
||
|
if (hasAcceptedParameterValue)
|
||
|
unusedNewValues.Remove (token.Name);
|
||
|
|
||
|
object defaultParameterValue;
|
||
|
defaultValues.TryGetValue (token.Name, out defaultParameterValue);
|
||
|
|
||
|
if (ParametersAreEqual (acceptedParameterValue, defaultParameterValue)) {
|
||
|
// If the accepted value is the same as the default value, mark it as pending since
|
||
|
// we won't necessarily add it to the URL we generate.
|
||
|
pendingParts.Append (Convert.ToString (acceptedParameterValue, CultureInfo.InvariantCulture));
|
||
|
} else {
|
||
|
if (blockAllUriAppends)
|
||
|
return null;
|
||
|
|
||
|
// Add the new part to the URL as well as any pending parts
|
||
|
if (pendingParts.Length > 0) {
|
||
|
// Append any pending literals to the URL
|
||
|
uri.Append (pendingParts.ToString ());
|
||
|
pendingParts.Length = 0;
|
||
|
}
|
||
|
uri.Append (Convert.ToString (acceptedParameterValue, CultureInfo.InvariantCulture));
|
||
|
|
||
|
addedAnySubsegments = true;
|
||
|
}
|
||
|
} else {
|
||
|
Debug.Fail ("Invalid path subsegment type");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (addedAnySubsegments) {
|
||
|
// See comment above about why we add the pending parts
|
||
|
if (pendingParts.Length > 0) {
|
||
|
if (blockAllUriAppends)
|
||
|
return null;
|
||
|
|
||
|
// Append any pending literals to the URL
|
||
|
uri.Append (pendingParts.ToString ());
|
||
|
pendingParts.Length = 0;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (pendingPartsAreAllSafe) {
|
||
|
// Accept
|
||
|
if (pendingParts.Length > 0) {
|
||
|
if (blockAllUriAppends)
|
||
|
return null;
|
||
|
|
||
|
// Append any pending literals to the URI
|
||
|
uri.Append (pendingParts.ToString ());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Process constraints keys
|
||
|
if (constraints != null) {
|
||
|
// If there are any constraints, mark all the keys as being used so that we don't
|
||
|
// generate query string items for custom constraints that don't appear as parameters
|
||
|
// in the URI format.
|
||
|
foreach (var constraintsItem in constraints) {
|
||
|
unusedNewValues.Remove (constraintsItem.Key);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Encode the URI before we append the query string, otherwise we would double encode the query string
|
||
|
var encodedUri = new StringBuilder ();
|
||
|
encodedUri.Append (UriEncode (uri.ToString ()));
|
||
|
uri = encodedUri;
|
||
|
|
||
|
// Add remaining new values as query string parameters to the URI
|
||
|
if (unusedNewValues.Count > 0) {
|
||
|
// Generate the query string
|
||
|
bool firstParam = true;
|
||
|
foreach (string unusedNewValue in unusedNewValues) {
|
||
|
object value;
|
||
|
if (acceptedValues.TryGetValue (unusedNewValue, out value)) {
|
||
|
uri.Append (firstParam ? '?' : '&');
|
||
|
firstParam = false;
|
||
|
uri.Append (Uri.EscapeDataString (unusedNewValue));
|
||
|
uri.Append ('=');
|
||
|
uri.Append (Uri.EscapeDataString (Convert.ToString (value, CultureInfo.InvariantCulture)));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
usedValues = acceptedValues;
|
||
|
return uri.ToString();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|