Adding legacy rounting code - mostly unmodified to get it into history
This commit is contained in:
parent
7cfcdcebdc
commit
d4904e8701
|
|
@ -0,0 +1,145 @@
|
|||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.34003
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.AspNet.Routing {
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter..
|
||||
/// </summary>
|
||||
internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment {
|
||||
get {
|
||||
return ResourceManager.GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string..
|
||||
/// </summary>
|
||||
internal static string TemplateRoute_CannotHaveConsecutiveParameters {
|
||||
get {
|
||||
return ResourceManager.GetString("TemplateRoute_CannotHaveConsecutiveParameters", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value..
|
||||
/// </summary>
|
||||
internal static string TemplateRoute_CannotHaveConsecutiveSeparators {
|
||||
get {
|
||||
return ResourceManager.GetString("TemplateRoute_CannotHaveConsecutiveSeparators", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to A catch-all parameter can only appear as the last segment of the route template..
|
||||
/// </summary>
|
||||
internal static string TemplateRoute_CatchAllMustBeLast {
|
||||
get {
|
||||
return ResourceManager.GetString("TemplateRoute_CatchAllMustBeLast", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?".
|
||||
/// </summary>
|
||||
internal static string TemplateRoute_InvalidParameterName {
|
||||
get {
|
||||
return ResourceManager.GetString("TemplateRoute_InvalidParameterName", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The route template cannot start with a '/' or '~' character and it cannot contain a '?' character..
|
||||
/// </summary>
|
||||
internal static string TemplateRoute_InvalidRouteTemplate {
|
||||
get {
|
||||
return ResourceManager.GetString("TemplateRoute_InvalidRouteTemplate", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character..
|
||||
/// </summary>
|
||||
internal static string TemplateRoute_MismatchedParameter {
|
||||
get {
|
||||
return ResourceManager.GetString("TemplateRoute_MismatchedParameter", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The route parameter name '{0}' appears more than one time in the route template..
|
||||
/// </summary>
|
||||
internal static string TemplateRoute_RepeatedParameter {
|
||||
get {
|
||||
return ResourceManager.GetString("TemplateRoute_RepeatedParameter", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'..
|
||||
/// </summary>
|
||||
internal static string TemplateRoute_ValidationMustBeStringOrCustomConstraint {
|
||||
get {
|
||||
return ResourceManager.GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="TemplateRoute_CannotHaveCatchAllInMultiSegment" xml:space="preserve">
|
||||
<value>A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.</value>
|
||||
</data>
|
||||
<data name="TemplateRoute_CannotHaveConsecutiveParameters" xml:space="preserve">
|
||||
<value>A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.</value>
|
||||
</data>
|
||||
<data name="TemplateRoute_CannotHaveConsecutiveSeparators" xml:space="preserve">
|
||||
<value>The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.</value>
|
||||
</data>
|
||||
<data name="TemplateRoute_CatchAllMustBeLast" xml:space="preserve">
|
||||
<value>A catch-all parameter can only appear as the last segment of the route template.</value>
|
||||
</data>
|
||||
<data name="TemplateRoute_InvalidParameterName" xml:space="preserve">
|
||||
<value>The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?"</value>
|
||||
</data>
|
||||
<data name="TemplateRoute_InvalidRouteTemplate" xml:space="preserve">
|
||||
<value>The route template cannot start with a '/' or '~' character and it cannot contain a '?' character.</value>
|
||||
</data>
|
||||
<data name="TemplateRoute_MismatchedParameter" xml:space="preserve">
|
||||
<value>There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character.</value>
|
||||
</data>
|
||||
<data name="TemplateRoute_RepeatedParameter" xml:space="preserve">
|
||||
<value>The route parameter name '{0}' appears more than one time in the route template.</value>
|
||||
</data>
|
||||
<data name="TemplateRoute_ValidationMustBeStringOrCustomConstraint" xml:space="preserve">
|
||||
<value>The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a URI generated from a <see cref="TemplateParsedRoute"/>.
|
||||
/// </summary>
|
||||
public class BoundRouteTemplate
|
||||
{
|
||||
public string BoundTemplate { get; set; }
|
||||
|
||||
public IDictionary<string, object> Values { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
public interface ITemplateRouteConstraint
|
||||
{
|
||||
bool Match(HttpContext context, IRoute route, string parameterName, IDictionary<string, object> values, RouteDirection routeDirection);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
public interface IVirtualPathData
|
||||
{
|
||||
IRoute Route { get; }
|
||||
|
||||
string VirtualPath { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
// Represents a segment of a URI that is not a separator. It contains subsegments such as literals and parameters.
|
||||
internal sealed class PathContentSegment : PathSegment
|
||||
{
|
||||
public PathContentSegment(IList<PathSubsegment> subsegments)
|
||||
{
|
||||
Subsegments = subsegments;
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm.")]
|
||||
public bool IsCatchAll
|
||||
{
|
||||
get
|
||||
{
|
||||
// TODO: Verify this is correct. Maybe add an assert.
|
||||
// Performance sensitive
|
||||
// Caching count is faster for IList<T>
|
||||
int subsegmentCount = Subsegments.Count;
|
||||
for (int i = 0; i < subsegmentCount; i++)
|
||||
{
|
||||
PathSubsegment seg = Subsegments[i];
|
||||
PathParameterSubsegment paramterSubSegment = seg as PathParameterSubsegment;
|
||||
if (paramterSubSegment != null && paramterSubSegment.IsCatchAll)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public IList<PathSubsegment> Subsegments { get; private set; }
|
||||
|
||||
#if ROUTE_DEBUGGING
|
||||
public override string LiteralText
|
||||
{
|
||||
get
|
||||
{
|
||||
List<string> s = new List<string>();
|
||||
foreach (PathSubsegment subsegment in Subsegments)
|
||||
{
|
||||
s.Add(subsegment.LiteralText);
|
||||
}
|
||||
return String.Join(String.Empty, s.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
List<string> s = new List<string>();
|
||||
foreach (PathSubsegment subsegment in Subsegments)
|
||||
{
|
||||
s.Add(subsegment.ToString());
|
||||
}
|
||||
return "[ " + String.Join(", ", s.ToArray()) + " ]";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
// Represents a literal subsegment of a ContentPathSegment
|
||||
internal sealed class PathLiteralSubsegment : PathSubsegment
|
||||
{
|
||||
public PathLiteralSubsegment(string literal)
|
||||
{
|
||||
Literal = literal;
|
||||
}
|
||||
|
||||
public string Literal { get; private set; }
|
||||
|
||||
#if ROUTE_DEBUGGING
|
||||
public override string LiteralText
|
||||
{
|
||||
get
|
||||
{
|
||||
return Literal;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "\"" + Literal + "\"";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
// Represents a parameter subsegment of a ContentPathSegment
|
||||
internal sealed class PathParameterSubsegment : PathSubsegment
|
||||
{
|
||||
public PathParameterSubsegment(string parameterName)
|
||||
{
|
||||
if (parameterName.StartsWith("*", StringComparison.Ordinal))
|
||||
{
|
||||
ParameterName = parameterName.Substring(1);
|
||||
IsCatchAll = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ParameterName = parameterName;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCatchAll { get; private set; }
|
||||
|
||||
public string ParameterName { get; private set; }
|
||||
|
||||
#if ROUTE_DEBUGGING
|
||||
public override string LiteralText
|
||||
{
|
||||
get
|
||||
{
|
||||
return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}";
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
// Represents a segment of a URI such as a separator or content
|
||||
public abstract class PathSegment
|
||||
{
|
||||
#if ROUTE_DEBUGGING
|
||||
public abstract string LiteralText
|
||||
{
|
||||
get;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
// Represents a "/" separator in a URI
|
||||
internal sealed class PathSeparatorSegment : PathSegment
|
||||
{
|
||||
#if ROUTE_DEBUGGING
|
||||
public override string LiteralText
|
||||
{
|
||||
get
|
||||
{
|
||||
return "/";
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "\"/\"";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
// Represents a subsegment of a ContentPathSegment such as a parameter or a literal.
|
||||
internal abstract class PathSubsegment
|
||||
{
|
||||
#if ROUTE_DEBUGGING
|
||||
public abstract string LiteralText
|
||||
{
|
||||
get;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
public enum RouteDirection
|
||||
{
|
||||
UriResolution = 0,
|
||||
UriGeneration
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,842 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
public sealed class TemplateParsedRoute
|
||||
{
|
||||
public TemplateParsedRoute(IList<PathSegment> pathSegments)
|
||||
{
|
||||
Contract.Assert(pathSegments != null);
|
||||
PathSegments = pathSegments;
|
||||
}
|
||||
|
||||
internal IList<PathSegment> PathSegments { get; private set; }
|
||||
|
||||
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Not changing original algorithm")]
|
||||
[SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode", Justification = "Not changing original algorithm")]
|
||||
public BoundRouteTemplate Bind(IDictionary<string, object> currentValues, IDictionary<string, object> values, IDictionary<string, object> defaultValues, IDictionary<string, object> constraints)
|
||||
{
|
||||
if (currentValues == null)
|
||||
{
|
||||
currentValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (values == null)
|
||||
{
|
||||
values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (defaultValues == null)
|
||||
{
|
||||
defaultValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// The set of values we should be using when generating the URI in this route
|
||||
IDictionary<string, object> acceptedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Keep track of which new values have been used
|
||||
HashSet<string> unusedNewValues = new HashSet<string>(values.Keys, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Step 1: Get the list of values we're going to try to use to match and generate this URI
|
||||
|
||||
// Find out which entries in the URI are valid for the URI we want to generate.
|
||||
// If the URI 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>.
|
||||
ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment)
|
||||
{
|
||||
// If it's a parameter subsegment, examine the current value to see if it matches the new value
|
||||
string parameterName = parameterSubsegment.ParameterName;
|
||||
|
||||
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 (!RoutePartsEqual(currentParameterValue, newParameterValue))
|
||||
{
|
||||
// Stop copying current values when we find one that doesn't match
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the parameter is a match, add it to the list of values we will use for URI generation
|
||||
if (hasNewParameterValue)
|
||||
{
|
||||
if (IsRoutePartNonEmpty(newParameterValue))
|
||||
{
|
||||
acceptedValues.Add(parameterName, newParameterValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hasCurrentParameterValue)
|
||||
{
|
||||
acceptedValues.Add(parameterName, currentParameterValue);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Add all remaining new values to the list of values we will use for URI generation
|
||||
foreach (var newValue in values)
|
||||
{
|
||||
if (IsRoutePartNonEmpty(newValue.Value))
|
||||
{
|
||||
if (!acceptedValues.ContainsKey(newValue.Key))
|
||||
{
|
||||
acceptedValues.Add(newValue.Key, newValue.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add all current values that aren't in the URI at all
|
||||
foreach (var currentValue in currentValues)
|
||||
{
|
||||
string parameterName = currentValue.Key;
|
||||
if (!acceptedValues.ContainsKey(parameterName))
|
||||
{
|
||||
PathParameterSubsegment parameterSubsegment = GetParameterSubsegment(PathSegments, parameterName);
|
||||
if (parameterSubsegment == null)
|
||||
{
|
||||
acceptedValues.Add(parameterName, currentValue.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add all remaining default values from the route to the list of values we will use for URI generation
|
||||
ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment)
|
||||
{
|
||||
if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName))
|
||||
{
|
||||
object defaultValue;
|
||||
if (!IsParameterRequired(parameterSubsegment, 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(parameterSubsegment.ParameterName, defaultValue);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// All required parameters in this URI must have values from somewhere (i.e. the accepted values)
|
||||
bool hasAllRequiredValues = ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment)
|
||||
{
|
||||
object defaultValue;
|
||||
if (IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue))
|
||||
{
|
||||
if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName))
|
||||
{
|
||||
// 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 false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!hasAllRequiredValues)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// All other default values must match if they are explicitly defined in the new values
|
||||
IDictionary<string, object> otherDefaultValues = new Dictionary<string, object>(defaultValues, StringComparer.OrdinalIgnoreCase);
|
||||
ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment)
|
||||
{
|
||||
otherDefaultValues.Remove(parameterSubsegment.ParameterName);
|
||||
return true;
|
||||
});
|
||||
|
||||
foreach (var defaultValue in otherDefaultValues)
|
||||
{
|
||||
object value;
|
||||
if (values.TryGetValue(defaultValue.Key, out value))
|
||||
{
|
||||
unusedNewValues.Remove(defaultValue.Key);
|
||||
if (!RoutePartsEqual(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: If the route is a match generate the appropriate URI
|
||||
|
||||
StringBuilder uri = new StringBuilder();
|
||||
StringBuilder pendingParts = new StringBuilder();
|
||||
|
||||
bool pendingPartsAreAllSafe = false;
|
||||
bool blockAllUriAppends = false;
|
||||
|
||||
for (int i = 0; i < PathSegments.Count; i++)
|
||||
{
|
||||
PathSegment pathSegment = PathSegments[i]; // parsedRouteUriPart
|
||||
|
||||
if (pathSegment is PathSeparatorSegment)
|
||||
{
|
||||
if (pendingPartsAreAllSafe)
|
||||
{
|
||||
// Accept
|
||||
if (pendingParts.Length > 0)
|
||||
{
|
||||
if (blockAllUriAppends)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Append any pending literals to the URI
|
||||
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;
|
||||
blockAllUriAppends = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
pendingParts.Append("/");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PathContentSegment contentPathSegment = pathSegment as PathContentSegment;
|
||||
if (contentPathSegment != null)
|
||||
{
|
||||
// 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 URI, 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 (PathSubsegment subsegment in contentPathSegment.Subsegments)
|
||||
{
|
||||
PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment;
|
||||
if (literalSubsegment != null)
|
||||
{
|
||||
// If it's a literal we hold on to it until we are sure we need to add it
|
||||
pendingPartsAreAllSafe = true;
|
||||
pendingParts.Append(literalSubsegment.Literal);
|
||||
}
|
||||
else
|
||||
{
|
||||
PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment;
|
||||
if (parameterSubsegment != null)
|
||||
{
|
||||
if (pendingPartsAreAllSafe)
|
||||
{
|
||||
// Accept
|
||||
if (pendingParts.Length > 0)
|
||||
{
|
||||
if (blockAllUriAppends)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Append any pending literals to the URI
|
||||
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(parameterSubsegment.ParameterName, out acceptedParameterValue);
|
||||
if (hasAcceptedParameterValue)
|
||||
{
|
||||
unusedNewValues.Remove(parameterSubsegment.ParameterName);
|
||||
}
|
||||
|
||||
object defaultParameterValue;
|
||||
defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultParameterValue);
|
||||
|
||||
if (RoutePartsEqual(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 URI we generate.
|
||||
pendingParts.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (blockAllUriAppends)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add the new part to the URI as well as any pending parts
|
||||
if (pendingParts.Length > 0)
|
||||
{
|
||||
// Append any pending literals to the URI
|
||||
uri.Append(pendingParts.ToString());
|
||||
pendingParts.Length = 0;
|
||||
}
|
||||
uri.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture));
|
||||
|
||||
addedAnySubsegments = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Contract.Assert(false, "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 URI
|
||||
uri.Append(pendingParts.ToString());
|
||||
pendingParts.Length = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Contract.Assert(false, "Invalid path segment type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
StringBuilder 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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new BoundRouteTemplate
|
||||
{
|
||||
BoundTemplate = uri.ToString(),
|
||||
Values = acceptedValues
|
||||
};
|
||||
}
|
||||
|
||||
private static string EscapeReservedCharacters(Match m)
|
||||
{
|
||||
return "%" + Convert.ToUInt16(m.Value[0]).ToString("x2", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static bool ForEachParameter(IList<PathSegment> pathSegments, Func<PathParameterSubsegment, bool> action)
|
||||
{
|
||||
for (int i = 0; i < pathSegments.Count; i++)
|
||||
{
|
||||
PathSegment pathSegment = pathSegments[i];
|
||||
|
||||
if (pathSegment is PathSeparatorSegment)
|
||||
{
|
||||
// We only care about parameter subsegments, so skip this
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
PathContentSegment contentPathSegment = pathSegment as PathContentSegment;
|
||||
if (contentPathSegment != null)
|
||||
{
|
||||
foreach (PathSubsegment subsegment in contentPathSegment.Subsegments)
|
||||
{
|
||||
PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment;
|
||||
if (literalSubsegment != null)
|
||||
{
|
||||
// We only care about parameter subsegments, so skip this
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment;
|
||||
if (parameterSubsegment != null)
|
||||
{
|
||||
if (!action(parameterSubsegment))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Contract.Assert(false, "Invalid path subsegment type");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Contract.Assert(false, "Invalid path segment type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static PathParameterSubsegment GetParameterSubsegment(IList<PathSegment> pathSegments, string parameterName)
|
||||
{
|
||||
PathParameterSubsegment foundParameterSubsegment = null;
|
||||
|
||||
ForEachParameter(pathSegments, delegate(PathParameterSubsegment parameterSubsegment)
|
||||
{
|
||||
if (String.Equals(parameterName, parameterSubsegment.ParameterName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foundParameterSubsegment = parameterSubsegment;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return foundParameterSubsegment;
|
||||
}
|
||||
|
||||
private static bool IsParameterRequired(PathParameterSubsegment parameterSubsegment, IDictionary<string, object> defaultValues, out object defaultValue)
|
||||
{
|
||||
if (parameterSubsegment.IsCatchAll)
|
||||
{
|
||||
defaultValue = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return !defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultValue);
|
||||
}
|
||||
|
||||
private static bool IsRoutePartNonEmpty(object routePart)
|
||||
{
|
||||
string routePartString = routePart as string;
|
||||
if (routePartString != null)
|
||||
{
|
||||
return routePartString.Length > 0;
|
||||
}
|
||||
return routePart != null;
|
||||
}
|
||||
|
||||
public IDictionary<string, object> Match(string virtualPath, IDictionary<string, object> defaultValues)
|
||||
{
|
||||
IList<string> requestPathSegments = TemplateRouteParser.SplitUriToPathSegmentStrings(virtualPath);
|
||||
|
||||
if (defaultValues == null)
|
||||
{
|
||||
defaultValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
IDictionary<string, object> matchedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// This flag gets set once all the data in the URI has been parsed through, but
|
||||
// the route we're trying to match against still has more parts. At this point
|
||||
// we'll only continue matching separator characters and parameters that have
|
||||
// default values.
|
||||
bool ranOutOfStuffToParse = false;
|
||||
|
||||
// This value gets set once we start processing a catchall parameter (if there is one
|
||||
// at all). Once we set this value we consume all remaining parts of the URI into its
|
||||
// parameter value.
|
||||
bool usedCatchAllParameter = false;
|
||||
|
||||
for (int i = 0; i < PathSegments.Count; i++)
|
||||
{
|
||||
PathSegment pathSegment = PathSegments[i];
|
||||
|
||||
if (requestPathSegments.Count <= i)
|
||||
{
|
||||
ranOutOfStuffToParse = true;
|
||||
}
|
||||
|
||||
string requestPathSegment = ranOutOfStuffToParse ? null : requestPathSegments[i];
|
||||
|
||||
if (pathSegment is PathSeparatorSegment)
|
||||
{
|
||||
if (ranOutOfStuffToParse)
|
||||
{
|
||||
// If we're trying to match a separator in the route but there's no more content, that's OK
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!String.Equals(requestPathSegment, "/", StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PathContentSegment contentPathSegment = pathSegment as PathContentSegment;
|
||||
if (contentPathSegment != null)
|
||||
{
|
||||
if (contentPathSegment.IsCatchAll)
|
||||
{
|
||||
Contract.Assert(i == (PathSegments.Count - 1), "If we're processing a catch-all, we should be on the last route segment.");
|
||||
MatchCatchAll(contentPathSegment, requestPathSegments.Skip(i), defaultValues, matchedValues);
|
||||
usedCatchAllParameter = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!MatchContentPathSegment(contentPathSegment, requestPathSegment, defaultValues, matchedValues))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Contract.Assert(false, "Invalid path segment type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!usedCatchAllParameter)
|
||||
{
|
||||
if (PathSegments.Count < requestPathSegments.Count)
|
||||
{
|
||||
// If we've already gone through all the parts defined in the route but the URI
|
||||
// still contains more content, check that the remaining content is all separators.
|
||||
for (int i = PathSegments.Count; i < requestPathSegments.Count; i++)
|
||||
{
|
||||
if (!TemplateRouteParser.IsSeparator(requestPathSegments[i]))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy all remaining default values to the route data
|
||||
if (defaultValues != null)
|
||||
{
|
||||
foreach (var defaultValue in defaultValues)
|
||||
{
|
||||
if (!matchedValues.ContainsKey(defaultValue.Key))
|
||||
{
|
||||
matchedValues.Add(defaultValue.Key, defaultValue.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matchedValues;
|
||||
}
|
||||
|
||||
private static void MatchCatchAll(PathContentSegment contentPathSegment, IEnumerable<string> remainingRequestSegments, IDictionary<string, object> defaultValues, IDictionary<string, object> matchedValues)
|
||||
{
|
||||
string remainingRequest = String.Join(String.Empty, remainingRequestSegments.ToArray());
|
||||
|
||||
PathParameterSubsegment catchAllSegment = contentPathSegment.Subsegments[0] as PathParameterSubsegment;
|
||||
|
||||
object catchAllValue;
|
||||
|
||||
if (remainingRequest.Length > 0)
|
||||
{
|
||||
catchAllValue = remainingRequest;
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultValues.TryGetValue(catchAllSegment.ParameterName, out catchAllValue);
|
||||
}
|
||||
|
||||
matchedValues.Add(catchAllSegment.ParameterName, catchAllValue);
|
||||
}
|
||||
|
||||
private static bool MatchContentPathSegment(PathContentSegment routeSegment, string requestPathSegment, IDictionary<string, object> defaultValues, IDictionary<string, object> matchedValues)
|
||||
{
|
||||
if (String.IsNullOrEmpty(requestPathSegment))
|
||||
{
|
||||
// If there's no data to parse, we must have exactly one parameter segment and no other segments - otherwise no match
|
||||
|
||||
if (routeSegment.Subsegments.Count > 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[0] as PathParameterSubsegment;
|
||||
if (parameterSubsegment == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// We must have a default value since there's no value in the request URI
|
||||
object parameterValue;
|
||||
if (defaultValues.TryGetValue(parameterSubsegment.ParameterName, out parameterValue))
|
||||
{
|
||||
// If there's a default value for this parameter, use that default value
|
||||
matchedValues.Add(parameterSubsegment.ParameterName, parameterValue);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If there's no default value, this segment doesn't match
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Optimize for the common case where there is only one subsegment in the segment - either a parameter or a literal
|
||||
if (routeSegment.Subsegments.Count == 1)
|
||||
{
|
||||
return MatchSingleContentPathSegment(routeSegment.Subsegments[0], requestPathSegment, matchedValues);
|
||||
}
|
||||
|
||||
// Find last literal segment and get its last index in the string
|
||||
|
||||
int lastIndex = requestPathSegment.Length;
|
||||
int indexOfLastSegmentUsed = routeSegment.Subsegments.Count - 1;
|
||||
|
||||
PathParameterSubsegment parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value
|
||||
PathLiteralSubsegment lastLiteral = null; // Keeps track of the left-most literal we've encountered
|
||||
|
||||
while (indexOfLastSegmentUsed >= 0)
|
||||
{
|
||||
int newLastIndex = lastIndex;
|
||||
|
||||
PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathParameterSubsegment;
|
||||
if (parameterSubsegment != null)
|
||||
{
|
||||
// Hold on to the parameter so that we can fill it in when we locate the next literal
|
||||
parameterNeedsValue = parameterSubsegment;
|
||||
}
|
||||
else
|
||||
{
|
||||
PathLiteralSubsegment literalSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathLiteralSubsegment;
|
||||
if (literalSubsegment != null)
|
||||
{
|
||||
lastLiteral = literalSubsegment;
|
||||
|
||||
int startIndex = lastIndex - 1;
|
||||
// If we have a pending parameter subsegment, we must leave at least one character for that
|
||||
if (parameterNeedsValue != null)
|
||||
{
|
||||
startIndex--;
|
||||
}
|
||||
|
||||
if (startIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int indexOfLiteral = requestPathSegment.LastIndexOf(literalSubsegment.Literal, startIndex, StringComparison.OrdinalIgnoreCase);
|
||||
if (indexOfLiteral == -1)
|
||||
{
|
||||
// If we couldn't find this literal index, this segment cannot match
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the first subsegment is a literal, it must match at the right-most extent of the request URI.
|
||||
// Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/".
|
||||
// This check is related to the check we do at the very end of this function.
|
||||
if (indexOfLastSegmentUsed == (routeSegment.Subsegments.Count - 1))
|
||||
{
|
||||
if ((indexOfLiteral + literalSubsegment.Literal.Length) != requestPathSegment.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
newLastIndex = indexOfLiteral;
|
||||
}
|
||||
else
|
||||
{
|
||||
Contract.Assert(false, "Invalid path segment type");
|
||||
}
|
||||
}
|
||||
|
||||
if ((parameterNeedsValue != null) && (((lastLiteral != null) && (parameterSubsegment == null)) || (indexOfLastSegmentUsed == 0)))
|
||||
{
|
||||
// If we have a pending parameter that needs a value, grab that value
|
||||
|
||||
int parameterStartIndex;
|
||||
int parameterTextLength;
|
||||
|
||||
if (lastLiteral == null)
|
||||
{
|
||||
if (indexOfLastSegmentUsed == 0)
|
||||
{
|
||||
parameterStartIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
parameterStartIndex = newLastIndex;
|
||||
Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above");
|
||||
}
|
||||
parameterTextLength = lastIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we're getting a value for a parameter that is somewhere in the middle of the segment
|
||||
if ((indexOfLastSegmentUsed == 0) && (parameterSubsegment != null))
|
||||
{
|
||||
parameterStartIndex = 0;
|
||||
parameterTextLength = lastIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
parameterStartIndex = newLastIndex + lastLiteral.Literal.Length;
|
||||
parameterTextLength = lastIndex - parameterStartIndex;
|
||||
}
|
||||
}
|
||||
|
||||
string parameterValueString = requestPathSegment.Substring(parameterStartIndex, parameterTextLength);
|
||||
|
||||
if (String.IsNullOrEmpty(parameterValueString))
|
||||
{
|
||||
// If we're here that means we have a segment that contains multiple sub-segments.
|
||||
// For these segments all parameters must have non-empty values. If the parameter
|
||||
// has an empty value it's not a match.
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If there's a value in the segment for this parameter, use the subsegment value
|
||||
matchedValues.Add(parameterNeedsValue.ParameterName, parameterValueString);
|
||||
}
|
||||
|
||||
parameterNeedsValue = null;
|
||||
lastLiteral = null;
|
||||
}
|
||||
|
||||
lastIndex = newLastIndex;
|
||||
indexOfLastSegmentUsed--;
|
||||
}
|
||||
|
||||
// If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of
|
||||
// the string since the parameter will have consumed all the remaining text anyway. If the last subsegment
|
||||
// is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching
|
||||
// the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire*
|
||||
// request URI in order for it to be a match.
|
||||
// This check is related to the check we do earlier in this function for LiteralSubsegments.
|
||||
return (lastIndex == 0) || (routeSegment.Subsegments[0] is PathParameterSubsegment);
|
||||
}
|
||||
|
||||
private static bool MatchSingleContentPathSegment(PathSubsegment pathSubsegment, string requestPathSegment, IDictionary<string, object> matchedValues)
|
||||
{
|
||||
PathParameterSubsegment parameterSubsegment = pathSubsegment as PathParameterSubsegment;
|
||||
if (parameterSubsegment == null)
|
||||
{
|
||||
// Handle a single literal segment
|
||||
PathLiteralSubsegment literalSubsegment = pathSubsegment as PathLiteralSubsegment;
|
||||
Contract.Assert(literalSubsegment != null, "Invalid path segment type");
|
||||
return literalSubsegment.Literal.Equals(requestPathSegment, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle a single parameter segment
|
||||
matchedValues.Add(parameterSubsegment.ParameterName, requestPathSegment);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RoutePartsEqual(object a, object b)
|
||||
{
|
||||
string sa = a as string;
|
||||
string sb = b as string;
|
||||
if (sa != null && sb != null)
|
||||
{
|
||||
// For strings do a case-insensitive comparison
|
||||
return String.Equals(sa, sb, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (a != null && b != null)
|
||||
{
|
||||
// Explicitly call .Equals() in case it is overridden in the type
|
||||
return a.Equals(b);
|
||||
}
|
||||
else
|
||||
{
|
||||
// At least one of them is null. Return true if they both are
|
||||
return a == b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string UriEncode(string str)
|
||||
{
|
||||
string escape = Uri.EscapeUriString(str);
|
||||
return Regex.Replace(escape, "([#?])", new MatchEvaluator(EscapeReservedCharacters));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNet.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
/// <summary>
|
||||
/// Route class for self-host (i.e. hosted outside of ASP.NET). This class is mostly the
|
||||
/// same as the System.Web.Routing.Route implementation.
|
||||
/// This class has the same URL matching functionality as System.Web.Routing.Route. However,
|
||||
/// in order for this route to match when generating URLs, a special "httproute" key must be
|
||||
/// specified when generating the URL.
|
||||
/// </summary>
|
||||
public class TemplateRoute : IRoute
|
||||
{
|
||||
/// <summary>
|
||||
/// Key used to signify that a route URL generation request should include HTTP routes (e.g. Web API).
|
||||
/// If this key is not specified then no HTTP routes will match.
|
||||
/// </summary>
|
||||
public static readonly string HttpRouteKey = "httproute";
|
||||
|
||||
private string _routeTemplate;
|
||||
private IDictionary<string, object> _defaults;
|
||||
private IDictionary<string, object> _constraints;
|
||||
private IDictionary<string, object> _dataTokens;
|
||||
|
||||
public TemplateRoute()
|
||||
: this(routeTemplate: null, defaults: null, constraints: null, dataTokens: null, handler: null)
|
||||
{
|
||||
}
|
||||
|
||||
public TemplateRoute(string routeTemplate)
|
||||
: this(routeTemplate, defaults: null, constraints: null, dataTokens: null, handler: null)
|
||||
{
|
||||
}
|
||||
|
||||
public TemplateRoute(string routeTemplate, IDictionary<string, object> defaults)
|
||||
: this(routeTemplate, defaults, constraints: null, dataTokens: null, handler: null)
|
||||
{
|
||||
}
|
||||
|
||||
public TemplateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints)
|
||||
: this(routeTemplate, defaults, constraints, dataTokens: null, handler: null)
|
||||
{
|
||||
}
|
||||
|
||||
public TemplateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens)
|
||||
: this(routeTemplate, defaults, constraints, dataTokens, handler: null)
|
||||
{
|
||||
}
|
||||
|
||||
public TemplateRoute(string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens, IRouteEndpoint handler)
|
||||
{
|
||||
_routeTemplate = routeTemplate == null ? String.Empty : routeTemplate;
|
||||
_defaults = defaults ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
_constraints = constraints ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
_dataTokens = dataTokens ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
Handler = handler;
|
||||
|
||||
// The parser will throw for invalid routes.
|
||||
ParsedRoute = TemplateRouteParser.Parse(RouteTemplate);
|
||||
}
|
||||
|
||||
public IDictionary<string, object> Defaults
|
||||
{
|
||||
get { return _defaults; }
|
||||
}
|
||||
|
||||
public IDictionary<string, object> Constraints
|
||||
{
|
||||
get { return _constraints; }
|
||||
}
|
||||
|
||||
public IDictionary<string, object> DataTokens
|
||||
{
|
||||
get { return _dataTokens; }
|
||||
}
|
||||
|
||||
public IRouteEndpoint Handler { get; private set; }
|
||||
|
||||
public string RouteTemplate
|
||||
{
|
||||
get { return _routeTemplate; }
|
||||
}
|
||||
|
||||
internal TemplateParsedRoute ParsedRoute { get; private set; }
|
||||
|
||||
public virtual RouteMatch GetRouteData(HttpContext request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new ArgumentNullException("request");
|
||||
}
|
||||
|
||||
var requestPath = request.Request.Path.Value;
|
||||
if (!String.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
|
||||
{
|
||||
requestPath = requestPath.Substring(1);
|
||||
}
|
||||
|
||||
IDictionary<string, object> values = ParsedRoute.Match(requestPath, _defaults);
|
||||
if (values == null)
|
||||
{
|
||||
// If we got back a null value set, that means the URI did not match
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate the values
|
||||
if (!ProcessConstraints(request, values, RouteDirection.UriResolution))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RouteMatch(null, values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to generate a URI that represents the values passed in based on current
|
||||
/// values from the <see cref="HttpRouteData"/> and new values using the specified <see cref="TemplateRoute"/>.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request message.</param>
|
||||
/// <param name="values">The route values.</param>
|
||||
/// <returns>A <see cref="VirtualPathData"/> instance or null if URI cannot be generated.</returns>
|
||||
public virtual IVirtualPathData GetVirtualPath(HttpContext request, IDictionary<string, object> values)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new ArgumentNullException("request");
|
||||
}
|
||||
|
||||
// Only perform URL generation if the "httproute" key was specified. This allows these
|
||||
// routes to be ignored when a regular MVC app tries to generate URLs. Without this special
|
||||
// key an HTTP route used for Web API would normally take over almost all the routes in a
|
||||
// typical app.
|
||||
if (values != null && !values.Keys.Contains(HttpRouteKey, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// Remove the value from the collection so that it doesn't affect the generated URL
|
||||
var newValues = GetRouteDictionaryWithoutHttpRouteKey(values);
|
||||
|
||||
IRouteValues routeData = request.GetFeature<IRouteValues>();
|
||||
IDictionary<string, object> requestValues = routeData == null ? null : routeData.Values;
|
||||
|
||||
BoundRouteTemplate result = ParsedRoute.Bind(requestValues, newValues, _defaults, _constraints);
|
||||
if (result == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Assert that the route matches the validation rules
|
||||
if (!ProcessConstraints(request, result.Values, RouteDirection.UriGeneration))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new VirtualPathData(this, result.BoundTemplate);
|
||||
}
|
||||
|
||||
private static IDictionary<string, object> GetRouteDictionaryWithoutHttpRouteKey(IDictionary<string, object> routeValues)
|
||||
{
|
||||
var newRouteValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
if (routeValues != null)
|
||||
{
|
||||
foreach (var routeValue in routeValues)
|
||||
{
|
||||
if (!String.Equals(routeValue.Key, HttpRouteKey, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
newRouteValues.Add(routeValue.Key, routeValue.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newRouteValues;
|
||||
}
|
||||
|
||||
protected virtual bool ProcessConstraint(HttpContext request, object constraint, string parameterName, IDictionary<string, object> values, RouteDirection routeDirection)
|
||||
{
|
||||
ITemplateRouteConstraint customConstraint = constraint as ITemplateRouteConstraint;
|
||||
if (customConstraint != null)
|
||||
{
|
||||
return customConstraint.Match(request, this, parameterName, values, routeDirection);
|
||||
}
|
||||
|
||||
// If there was no custom constraint, then treat the constraint as a string which represents a Regex.
|
||||
string constraintsRule = constraint as string;
|
||||
if (constraintsRule == null)
|
||||
{
|
||||
throw new InvalidOperationException(String.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
Resources.TemplateRoute_ValidationMustBeStringOrCustomConstraint,
|
||||
parameterName,
|
||||
RouteTemplate,
|
||||
typeof(ITemplateRouteConstraint).Name));
|
||||
}
|
||||
|
||||
object parameterValue;
|
||||
values.TryGetValue(parameterName, out parameterValue);
|
||||
string parameterValueString = Convert.ToString(parameterValue, CultureInfo.InvariantCulture);
|
||||
string constraintsRegEx = "^(" + constraintsRule + ")$";
|
||||
return Regex.IsMatch(parameterValueString, constraintsRegEx, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
private bool ProcessConstraints(HttpContext request, IDictionary<string, object> values, RouteDirection routeDirection)
|
||||
{
|
||||
if (Constraints != null)
|
||||
{
|
||||
foreach (KeyValuePair<string, object> constraintsItem in Constraints)
|
||||
{
|
||||
if (!ProcessConstraint(request, constraintsItem.Value, constraintsItem.Key, values, routeDirection))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public RouteMatch Match(RouteContext context)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
public static class TemplateRouteParser
|
||||
{
|
||||
private static string GetLiteral(string segmentLiteral)
|
||||
{
|
||||
// Scan for errant single { and } and convert double {{ to { and double }} to }
|
||||
|
||||
// First we eliminate all escaped braces and then check if any other braces are remaining
|
||||
string newLiteral = segmentLiteral.Replace("{{", String.Empty).Replace("}}", String.Empty);
|
||||
if (newLiteral.Contains("{") || newLiteral.Contains("}"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// If it's a valid format, we unescape the braces
|
||||
return segmentLiteral.Replace("{{", "{").Replace("}}", "}");
|
||||
}
|
||||
|
||||
private static int IndexOfFirstOpenParameter(string segment, int startIndex)
|
||||
{
|
||||
// Find the first unescaped open brace
|
||||
while (true)
|
||||
{
|
||||
startIndex = segment.IndexOf('{', startIndex);
|
||||
if (startIndex == -1)
|
||||
{
|
||||
// If there are no more open braces, stop
|
||||
return -1;
|
||||
}
|
||||
if ((startIndex + 1 == segment.Length) ||
|
||||
((startIndex + 1 < segment.Length) && (segment[startIndex + 1] != '{')))
|
||||
{
|
||||
// If we found an open brace that is followed by a non-open brace, it's
|
||||
// a parameter delimiter.
|
||||
// It's also a delimiter if the open brace is the last character - though
|
||||
// it ends up being being called out as invalid later on.
|
||||
return startIndex;
|
||||
}
|
||||
// Increment by two since we want to skip both the open brace that
|
||||
// we're on as well as the subsequent character since we know for
|
||||
// sure that it is part of an escape sequence.
|
||||
startIndex += 2;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsSeparator(string s)
|
||||
{
|
||||
return String.Equals(s, "/", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsValidParameterName(string parameterName)
|
||||
{
|
||||
if (parameterName.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < parameterName.Length; i++)
|
||||
{
|
||||
char c = parameterName[i];
|
||||
if (c == '/' || c == '{' || c == '}')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool IsInvalidRouteTemplate(string routeTemplate)
|
||||
{
|
||||
return routeTemplate.StartsWith("~", StringComparison.Ordinal) ||
|
||||
routeTemplate.StartsWith("/", StringComparison.Ordinal) ||
|
||||
(routeTemplate.IndexOf('?') != -1);
|
||||
}
|
||||
|
||||
public static TemplateParsedRoute Parse(string routeTemplate)
|
||||
{
|
||||
if (routeTemplate == null)
|
||||
{
|
||||
routeTemplate = String.Empty;
|
||||
}
|
||||
|
||||
if (IsInvalidRouteTemplate(routeTemplate))
|
||||
{
|
||||
throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate");
|
||||
}
|
||||
|
||||
IList<string> uriParts = SplitUriToPathSegmentStrings(routeTemplate);
|
||||
Exception ex = ValidateUriParts(uriParts);
|
||||
if (ex != null)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
|
||||
IList<PathSegment> pathSegments = SplitUriToPathSegments(uriParts);
|
||||
|
||||
Contract.Assert(uriParts.Count == pathSegments.Count, "The number of string segments should be the same as the number of path segments");
|
||||
|
||||
return new TemplateParsedRoute(pathSegments);
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
|
||||
Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")]
|
||||
private static IList<PathSubsegment> ParseUriSegment(string segment, out Exception exception)
|
||||
{
|
||||
int startIndex = 0;
|
||||
|
||||
List<PathSubsegment> pathSubsegments = new List<PathSubsegment>();
|
||||
|
||||
while (startIndex < segment.Length)
|
||||
{
|
||||
int nextParameterStart = IndexOfFirstOpenParameter(segment, startIndex);
|
||||
if (nextParameterStart == -1)
|
||||
{
|
||||
// If there are no more parameters in the segment, capture the remainder as a literal and stop
|
||||
string lastLiteralPart = GetLiteral(segment.Substring(startIndex));
|
||||
if (lastLiteralPart == null)
|
||||
{
|
||||
exception = new ArgumentException(
|
||||
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment),
|
||||
"routeTemplate");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lastLiteralPart.Length > 0)
|
||||
{
|
||||
pathSubsegments.Add(new PathLiteralSubsegment(lastLiteralPart));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
int nextParameterEnd = segment.IndexOf('}', nextParameterStart + 1);
|
||||
if (nextParameterEnd == -1)
|
||||
{
|
||||
exception = new ArgumentException(
|
||||
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment),
|
||||
"routeTemplate");
|
||||
return null;
|
||||
}
|
||||
|
||||
string literalPart = GetLiteral(segment.Substring(startIndex, nextParameterStart - startIndex));
|
||||
if (literalPart == null)
|
||||
{
|
||||
exception = new ArgumentException(
|
||||
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment),
|
||||
"routeTemplate");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (literalPart.Length > 0)
|
||||
{
|
||||
pathSubsegments.Add(new PathLiteralSubsegment(literalPart));
|
||||
}
|
||||
|
||||
string parameterName = segment.Substring(nextParameterStart + 1, nextParameterEnd - nextParameterStart - 1);
|
||||
pathSubsegments.Add(new PathParameterSubsegment(parameterName));
|
||||
|
||||
startIndex = nextParameterEnd + 1;
|
||||
}
|
||||
|
||||
exception = null;
|
||||
return pathSubsegments;
|
||||
}
|
||||
|
||||
private static IList<PathSegment> SplitUriToPathSegments(IList<string> uriParts)
|
||||
{
|
||||
List<PathSegment> pathSegments = new List<PathSegment>();
|
||||
|
||||
foreach (string pathSegment in uriParts)
|
||||
{
|
||||
bool isCurrentPartSeparator = IsSeparator(pathSegment);
|
||||
if (isCurrentPartSeparator)
|
||||
{
|
||||
pathSegments.Add(new PathSeparatorSegment());
|
||||
}
|
||||
else
|
||||
{
|
||||
Exception exception;
|
||||
IList<PathSubsegment> subsegments = ParseUriSegment(pathSegment, out exception);
|
||||
Contract.Assert(exception == null, "This only gets called after the path has been validated, so there should never be an exception here");
|
||||
pathSegments.Add(new PathContentSegment(subsegments));
|
||||
}
|
||||
}
|
||||
return pathSegments;
|
||||
}
|
||||
|
||||
internal static IList<string> SplitUriToPathSegmentStrings(string uri)
|
||||
{
|
||||
List<string> parts = new List<string>();
|
||||
|
||||
if (String.IsNullOrEmpty(uri))
|
||||
{
|
||||
return parts;
|
||||
}
|
||||
|
||||
int currentIndex = 0;
|
||||
|
||||
// Split the incoming URI into individual parts
|
||||
while (currentIndex < uri.Length)
|
||||
{
|
||||
int indexOfNextSeparator = uri.IndexOf('/', currentIndex);
|
||||
if (indexOfNextSeparator == -1)
|
||||
{
|
||||
// If there are no more separators, the rest of the string is the last part
|
||||
string finalPart = uri.Substring(currentIndex);
|
||||
if (finalPart.Length > 0)
|
||||
{
|
||||
parts.Add(finalPart);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
string nextPart = uri.Substring(currentIndex, indexOfNextSeparator - currentIndex);
|
||||
if (nextPart.Length > 0)
|
||||
{
|
||||
parts.Add(nextPart);
|
||||
}
|
||||
|
||||
Contract.Assert(uri[indexOfNextSeparator] == '/', "The separator char itself should always be a '/'.");
|
||||
parts.Add("/");
|
||||
currentIndex = indexOfNextSeparator + 1;
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm")]
|
||||
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
|
||||
Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")]
|
||||
private static Exception ValidateUriParts(IList<string> pathSegments)
|
||||
{
|
||||
Contract.Assert(pathSegments != null, "The value should always come from SplitUri(), and that function should never return null.");
|
||||
|
||||
HashSet<string> usedParameterNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
bool? isPreviousPartSeparator = null;
|
||||
|
||||
bool foundCatchAllParameter = false;
|
||||
|
||||
foreach (string pathSegment in pathSegments)
|
||||
{
|
||||
if (foundCatchAllParameter)
|
||||
{
|
||||
// If we ever start an iteration of the loop and we've already found a
|
||||
// catchall parameter then we have an invalid URI format.
|
||||
return new ArgumentException(Resources.TemplateRoute_CatchAllMustBeLast, "routeTemplate");
|
||||
}
|
||||
|
||||
bool isCurrentPartSeparator;
|
||||
if (isPreviousPartSeparator == null)
|
||||
{
|
||||
// Prime the loop with the first value
|
||||
isPreviousPartSeparator = IsSeparator(pathSegment);
|
||||
isCurrentPartSeparator = isPreviousPartSeparator.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
isCurrentPartSeparator = IsSeparator(pathSegment);
|
||||
|
||||
// If both the previous part and the current part are separators, it's invalid
|
||||
if (isCurrentPartSeparator && isPreviousPartSeparator.Value)
|
||||
{
|
||||
return new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, "routeTemplate");
|
||||
}
|
||||
|
||||
Contract.Assert(isCurrentPartSeparator != isPreviousPartSeparator.Value, "This assert should only happen if both the current and previous parts are non-separators. This should never happen because consecutive non-separators are always parsed as a single part.");
|
||||
isPreviousPartSeparator = isCurrentPartSeparator;
|
||||
}
|
||||
|
||||
// If it's not a separator, parse the segment for parameters and validate it
|
||||
if (!isCurrentPartSeparator)
|
||||
{
|
||||
Exception exception;
|
||||
IList<PathSubsegment> subsegments = ParseUriSegment(pathSegment, out exception);
|
||||
if (exception != null)
|
||||
{
|
||||
return exception;
|
||||
}
|
||||
|
||||
exception = ValidateUriSegment(subsegments, usedParameterNames);
|
||||
if (exception != null)
|
||||
{
|
||||
return exception;
|
||||
}
|
||||
|
||||
foundCatchAllParameter = subsegments.Any<PathSubsegment>(seg => (seg is PathParameterSubsegment) && ((PathParameterSubsegment)seg).IsCatchAll);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly",
|
||||
Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")]
|
||||
private static Exception ValidateUriSegment(IList<PathSubsegment> pathSubsegments, HashSet<string> usedParameterNames)
|
||||
{
|
||||
bool segmentContainsCatchAll = false;
|
||||
|
||||
Type previousSegmentType = null;
|
||||
|
||||
foreach (PathSubsegment subsegment in pathSubsegments)
|
||||
{
|
||||
if (previousSegmentType != null)
|
||||
{
|
||||
if (previousSegmentType == subsegment.GetType())
|
||||
{
|
||||
return new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveParameters, "routeTemplate");
|
||||
}
|
||||
}
|
||||
previousSegmentType = subsegment.GetType();
|
||||
|
||||
PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment;
|
||||
if (literalSubsegment != null)
|
||||
{
|
||||
// Nothing to validate for literals - everything is valid
|
||||
}
|
||||
else
|
||||
{
|
||||
PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment;
|
||||
if (parameterSubsegment != null)
|
||||
{
|
||||
string parameterName = parameterSubsegment.ParameterName;
|
||||
|
||||
if (parameterSubsegment.IsCatchAll)
|
||||
{
|
||||
segmentContainsCatchAll = true;
|
||||
}
|
||||
|
||||
// Check for valid characters in the parameter name
|
||||
if (!IsValidParameterName(parameterName))
|
||||
{
|
||||
return new ArgumentException(
|
||||
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName),
|
||||
"routeTemplate");
|
||||
}
|
||||
|
||||
if (usedParameterNames.Contains(parameterName))
|
||||
{
|
||||
return new ArgumentException(
|
||||
String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_RepeatedParameter, parameterName),
|
||||
"routeTemplate");
|
||||
}
|
||||
else
|
||||
{
|
||||
usedParameterNames.Add(parameterName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Contract.Assert(false, "Invalid path subsegment type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (segmentContainsCatchAll && (pathSubsegments.Count != 1))
|
||||
{
|
||||
return new ArgumentException(Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment, "routeTemplate");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
namespace Microsoft.AspNet.Routing.Template
|
||||
{
|
||||
public class VirtualPathData : IVirtualPathData
|
||||
{
|
||||
private string _virtualPath;
|
||||
|
||||
public VirtualPathData(IRoute route, string virtualPath)
|
||||
{
|
||||
if (route == null)
|
||||
{
|
||||
throw new ArgumentNullException("route");
|
||||
}
|
||||
|
||||
if (virtualPath == null)
|
||||
{
|
||||
throw new ArgumentNullException("virtualPath");
|
||||
}
|
||||
|
||||
Route = route;
|
||||
VirtualPath = virtualPath;
|
||||
}
|
||||
|
||||
public IRoute Route { get; private set; }
|
||||
|
||||
public string VirtualPath
|
||||
{
|
||||
get { return _virtualPath; }
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException("value");
|
||||
}
|
||||
|
||||
_virtualPath = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template.Tests
|
||||
{
|
||||
public class Assert : Xunit.Assert
|
||||
{
|
||||
public static T Throws<T>(Assert.ThrowsDelegate action, string message) where T : Exception
|
||||
{
|
||||
T exception = Assert.Throws<T>(action);
|
||||
Assert.Equal<string>(message, exception.Message);
|
||||
return exception;
|
||||
}
|
||||
|
||||
public static T Throws<T>(Assert.ThrowsDelegateWithReturn action, string message) where T : Exception
|
||||
{
|
||||
T exception = Assert.Throws<T>(action);
|
||||
Assert.Equal<string>(message, exception.Message);
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using Xunit.Extensions;
|
||||
|
||||
namespace Microsoft.AspNet.Routing.Template.Tests
|
||||
{
|
||||
public class TemplateRouteParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void InvalidTemplate_WithRepeatedParameter()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("{Controller}.mvc/{id}/{controller}"),
|
||||
"The route parameter name 'controller' appears more than one time in the route template." + Environment.NewLine + "Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("123{a}abc{")]
|
||||
[InlineData("123{a}abc}")]
|
||||
[InlineData("xyz}123{a}abc}")]
|
||||
[InlineData("{{p1}")]
|
||||
[InlineData("{p1}}")]
|
||||
[InlineData("p1}}p2{")]
|
||||
public void InvalidTemplate_WithMismatchedBraces(string template)
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse(template),
|
||||
@"There is an incomplete parameter in this path segment: '" + template + @"'. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_CannotHaveCatchAllInMultiSegment()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("123{a}abc{*moo}"),
|
||||
"A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_CannotHaveMoreThanOneCatchAll()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("{*p1}/{*p2}"),
|
||||
"A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("{*p1}abc{*p2}"),
|
||||
"A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_CannotHaveCatchAllWithNoName()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("foo/{*}"),
|
||||
@"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("foo/{{p1}"),
|
||||
"There is an incomplete parameter in this path segment: '{{p1}'. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_CannotHaveConsecutiveCloseBrace()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("foo/{p1}}"),
|
||||
"There is an incomplete parameter in this path segment: '{p1}}'. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_SameParameterTwiceThrows()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("{aaa}/{AAA}"),
|
||||
"The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("{aaa}/{*AAA}"),
|
||||
"The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("{a}/{aa}a}/{z}"),
|
||||
"There is an incomplete parameter in this path segment: '{aa}a}'. Check that each '{' character has a matching '}' character." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("{a}/{a{aa}/{z}"),
|
||||
@"The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("{a}/{}/{z}"),
|
||||
@"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_InvalidParameterNameWithQuestionThrows()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("{Controller}.mvc/{?}"),
|
||||
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("{a}//{z}"),
|
||||
"The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_WithCatchAllNotAtTheEndThrows()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("foo/{p1}/{*p2}/{p3}"),
|
||||
"A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_RepeatedParametersThrows()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("foo/aa{p1}{p2}"),
|
||||
"A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_CannotStartWithSlash()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("/foo"),
|
||||
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_CannotStartWithTilde()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("~foo"),
|
||||
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidTemplate_CannotContainQuestionMark()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(
|
||||
() => TemplateRouteParser.Parse("foor?bar"),
|
||||
"The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine +
|
||||
"Parameter name: routeTemplate");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,12 +1,15 @@
|
|||
{
|
||||
"version": "0.1-alpha-*",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNet.Abstractions" : "0.1-alpha-*",
|
||||
"Microsoft.AspNet.Routing" : ""
|
||||
},
|
||||
"configurations": {
|
||||
"net45": {
|
||||
"dependencies": {
|
||||
"Owin": "1.0"
|
||||
"Owin": "1.0",
|
||||
"xunit": "1.9.2",
|
||||
"xunit.extensions": "1.9.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue