// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Matching; namespace Microsoft.AspNetCore.Routing.Patterns { /// /// Contains factory methods for creating and related types. /// Use to parse a route pattern in /// string format. /// public static class RoutePatternFactory { private static readonly IReadOnlyDictionary EmptyDefaultsDictionary = new ReadOnlyDictionary(new Dictionary()); private static readonly IReadOnlyDictionary> EmptyPoliciesDictionary = new ReadOnlyDictionary>(new Dictionary>()); /// /// Creates a from its string representation. /// /// The route pattern string to parse. /// The . public static RoutePattern Parse(string pattern) { if (pattern == null) { throw new ArgumentNullException(nameof(pattern)); } return RoutePatternParser.Parse(pattern); } /// /// Creates a from its string representation along /// with provided default values and parameter policies. /// /// The route pattern string to parse. /// /// Additional default values to associated with the route pattern. May be null. /// The provided object will be converted to key-value pairs using /// and then merged into the parsed route pattern. /// /// /// Additional parameter policies to associated with the route pattern. May be null. /// The provided object will be converted to key-value pairs using /// and then merged into the parsed route pattern. /// /// The . public static RoutePattern Parse(string pattern, object defaults, object parameterPolicies) { if (pattern == null) { throw new ArgumentNullException(nameof(pattern)); } var original = RoutePatternParser.Parse(pattern); return Pattern(original.RawText, defaults, parameterPolicies, original.PathSegments); } /// /// Creates a new from a collection of segments. /// /// The collection of segments. /// The . public static RoutePattern Pattern(IEnumerable segments) { if (segments == null) { throw new ArgumentNullException(nameof(segments)); } return PatternCore(null, null, null, segments); } /// /// Creates a new from a collection of segments. /// /// The raw text to associate with the route pattern. May be null. /// The collection of segments. /// The . public static RoutePattern Pattern(string rawText, IEnumerable segments) { if (segments == null) { throw new ArgumentNullException(nameof(segments)); } return PatternCore(rawText, null, null, segments); } /// /// Creates a from a collection of segments along /// with provided default values and parameter policies. /// /// /// Additional default values to associated with the route pattern. May be null. /// The provided object will be converted to key-value pairs using /// and then merged into the route pattern. /// /// /// Additional parameter policies to associated with the route pattern. May be null. /// The provided object will be converted to key-value pairs using /// and then merged into the route pattern. /// /// The collection of segments. /// The . public static RoutePattern Pattern( object defaults, object parameterPolicies, IEnumerable segments) { if (segments == null) { throw new ArgumentNullException(nameof(segments)); } return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments); } /// /// Creates a from a collection of segments along /// with provided default values and parameter policies. /// /// The raw text to associate with the route pattern. /// /// Additional default values to associated with the route pattern. May be null. /// The provided object will be converted to key-value pairs using /// and then merged into the route pattern. /// /// /// Additional parameter policies to associated with the route pattern. May be null. /// The provided object will be converted to key-value pairs using /// and then merged into the route pattern. /// /// The collection of segments. /// The . public static RoutePattern Pattern( string rawText, object defaults, object parameterPolicies, IEnumerable segments) { if (segments == null) { throw new ArgumentNullException(nameof(segments)); } return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments); } /// /// Creates a new from a collection of segments. /// /// The collection of segments. /// The . public static RoutePattern Pattern(params RoutePatternPathSegment[] segments) { if (segments == null) { throw new ArgumentNullException(nameof(segments)); } return PatternCore(null, null, null, segments); } /// /// Creates a new from a collection of segments. /// /// The raw text to associate with the route pattern. May be null. /// The collection of segments. /// The . public static RoutePattern Pattern(string rawText, params RoutePatternPathSegment[] segments) { if (segments == null) { throw new ArgumentNullException(nameof(segments)); } return PatternCore(rawText, null, null, segments); } /// /// Creates a from a collection of segments along /// with provided default values and parameter policies. /// /// /// Additional default values to associated with the route pattern. May be null. /// The provided object will be converted to key-value pairs using /// and then merged into the route pattern. /// /// /// Additional parameter policies to associated with the route pattern. May be null. /// The provided object will be converted to key-value pairs using /// and then merged into the route pattern. /// /// The collection of segments. /// The . public static RoutePattern Pattern( object defaults, object parameterPolicies, params RoutePatternPathSegment[] segments) { if (segments == null) { throw new ArgumentNullException(nameof(segments)); } return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments); } /// /// Creates a from a collection of segments along /// with provided default values and parameter policies. /// /// The raw text to associate with the route pattern. /// /// Additional default values to associated with the route pattern. May be null. /// The provided object will be converted to key-value pairs using /// and then merged into the route pattern. /// /// /// Additional parameter policies to associated with the route pattern. May be null. /// The provided object will be converted to key-value pairs using /// and then merged into the route pattern. /// /// The collection of segments. /// The . public static RoutePattern Pattern( string rawText, object defaults, object parameterPolicies, params RoutePatternPathSegment[] segments) { if (segments == null) { throw new ArgumentNullException(nameof(segments)); } return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), segments); } private static RoutePattern PatternCore( string rawText, RouteValueDictionary defaults, RouteValueDictionary parameterPolicies, IEnumerable segments) { // We want to merge the segment data with the 'out of line' defaults and parameter policies. // // This means that for parameters that have 'out of line' defaults we will modify // the parameter to contain the default (same story for parameter policies). // // We also maintain a collection of defaults and parameter policies that will also // contain the values that don't match a parameter. // // It's important that these two views of the data are consistent. We don't want // values specified out of line to have a different behavior. Dictionary updatedDefaults = null; if (defaults != null && defaults.Count > 0) { updatedDefaults = new Dictionary(defaults.Count, StringComparer.OrdinalIgnoreCase); foreach (var kvp in defaults) { updatedDefaults.Add(kvp.Key, kvp.Value); } } Dictionary> updatedParameterPolicies = null; if (parameterPolicies != null && parameterPolicies.Count > 0) { updatedParameterPolicies = new Dictionary>(parameterPolicies.Count, StringComparer.OrdinalIgnoreCase); foreach (var kvp in parameterPolicies) { updatedParameterPolicies.Add(kvp.Key, new List() { kvp.Value is IParameterPolicy parameterPolicy ? ParameterPolicy(parameterPolicy) : Constraint(kvp.Value), // Constraint will convert string values into regex constraints }); } } List parameters = null; var updatedSegments = segments.ToArray(); for (var i = 0; i < updatedSegments.Length; i++) { var segment = VisitSegment(updatedSegments[i]); updatedSegments[i] = segment; for (var j = 0; j < segment.Parts.Count; j++) { if (segment.Parts[j] is RoutePatternParameterPart parameter) { if (parameters == null) { parameters = new List(); } parameters.Add(parameter); } } } return new RoutePattern( rawText, updatedDefaults ?? EmptyDefaultsDictionary, updatedParameterPolicies != null ? updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()) : EmptyPoliciesDictionary, (IReadOnlyList)parameters ?? Array.Empty(), updatedSegments); RoutePatternPathSegment VisitSegment(RoutePatternPathSegment segment) { RoutePatternPart[] updatedParts = null; for (var i = 0; i < segment.Parts.Count; i++) { var part = segment.Parts[i]; var updatedPart = VisitPart(part); if (part != updatedPart) { if (updatedParts == null) { updatedParts = segment.Parts.ToArray(); } updatedParts[i] = updatedPart; } } if (updatedParts == null) { // Segment has not changed return segment; } return new RoutePatternPathSegment(updatedParts); } RoutePatternPart VisitPart(RoutePatternPart part) { if (!part.IsParameter) { return part; } var parameter = (RoutePatternParameterPart)part; var @default = parameter.Default; if (updatedDefaults != null && updatedDefaults.TryGetValue(parameter.Name, out var newDefault)) { if (parameter.Default != null && !Equals(newDefault, parameter.Default)) { var message = Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(parameter.Name); throw new InvalidOperationException(message); } if (parameter.IsOptional) { var message = Resources.TemplateRoute_OptionalCannotHaveDefaultValue; throw new InvalidOperationException(message); } @default = newDefault; } if (parameter.Default != null) { if (updatedDefaults == null) { updatedDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); } updatedDefaults[parameter.Name] = parameter.Default; } List parameterConstraints = null; if ((updatedParameterPolicies == null || !updatedParameterPolicies.TryGetValue(parameter.Name, out parameterConstraints)) && parameter.ParameterPolicies.Count > 0) { if (updatedParameterPolicies == null) { updatedParameterPolicies = new Dictionary>(StringComparer.OrdinalIgnoreCase); } parameterConstraints = new List(); updatedParameterPolicies.Add(parameter.Name, parameterConstraints); } if (parameter.ParameterPolicies.Count > 0) { parameterConstraints.AddRange(parameter.ParameterPolicies); } if (Equals(parameter.Default, @default) && parameter.ParameterPolicies.Count == 0 && (parameterConstraints?.Count ?? 0) == 0) { // Part has not changed return part; } return ParameterPartCore( parameter.Name, @default, parameter.ParameterKind, (IEnumerable)parameterConstraints ?? Array.Empty(), parameter.EncodeSlashes); } } /// /// Creates a from the provided collection /// of parts. /// /// The collection of parts. /// The . public static RoutePatternPathSegment Segment(IEnumerable parts) { if (parts == null) { throw new ArgumentNullException(nameof(parts)); } return SegmentCore(parts); } /// /// Creates a from the provided collection /// of parts. /// /// The collection of parts. /// The . public static RoutePatternPathSegment Segment(params RoutePatternPart[] parts) { if (parts == null) { throw new ArgumentNullException(nameof(parts)); } return SegmentCore(parts); } private static RoutePatternPathSegment SegmentCore(IEnumerable parts) { return new RoutePatternPathSegment(parts.ToArray()); } /// /// Creates a from the provided text /// content. /// /// The text content. /// The . public static RoutePatternLiteralPart LiteralPart(string content) { if (string.IsNullOrEmpty(content)) { throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); } if (content.IndexOf('?') >= 0) { throw new ArgumentException(Resources.FormatTemplateRoute_InvalidLiteral(content)); } return LiteralPartCore(content); } private static RoutePatternLiteralPart LiteralPartCore(string content) { return new RoutePatternLiteralPart(content); } /// /// Creates a from the provided text /// content. /// /// The text content. /// The . public static RoutePatternSeparatorPart SeparatorPart(string content) { if (string.IsNullOrEmpty(content)) { throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); } return SeparatorPartCore(content); } private static RoutePatternSeparatorPart SeparatorPartCore(string content) { return new RoutePatternSeparatorPart(content); } /// /// Creates a from the provided parameter name. /// /// The parameter name. /// The . public static RoutePatternParameterPart ParameterPart(string parameterName) { if (string.IsNullOrEmpty(parameterName)) { throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); } if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) { throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); } return ParameterPartCore( parameterName: parameterName, @default: null, parameterKind: RoutePatternParameterKind.Standard, parameterPolicies: Array.Empty()); } /// /// Creates a from the provided parameter name /// and default value. /// /// The parameter name. /// The parameter default value. May be null. /// The . public static RoutePatternParameterPart ParameterPart(string parameterName, object @default) { if (string.IsNullOrEmpty(parameterName)) { throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); } if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) { throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); } return ParameterPartCore( parameterName: parameterName, @default: @default, parameterKind: RoutePatternParameterKind.Standard, parameterPolicies: Array.Empty()); } /// /// Creates a from the provided parameter name /// and default value, and parameter kind. /// /// The parameter name. /// The parameter default value. May be null. /// The parameter kind. /// The . public static RoutePatternParameterPart ParameterPart( string parameterName, object @default, RoutePatternParameterKind parameterKind) { if (string.IsNullOrEmpty(parameterName)) { throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); } if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) { throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); } if (@default != null && parameterKind == RoutePatternParameterKind.Optional) { throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind)); } return ParameterPartCore( parameterName: parameterName, @default: @default, parameterKind: parameterKind, parameterPolicies: Array.Empty()); } /// /// Creates a from the provided parameter name /// and default value, parameter kind, and parameter policies. /// /// The parameter name. /// The parameter default value. May be null. /// The parameter kind. /// The parameter policies to associated with the parameter. /// The . public static RoutePatternParameterPart ParameterPart( string parameterName, object @default, RoutePatternParameterKind parameterKind, IEnumerable parameterPolicies) { if (string.IsNullOrEmpty(parameterName)) { throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); } if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) { throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); } if (@default != null && parameterKind == RoutePatternParameterKind.Optional) { throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind)); } if (parameterPolicies == null) { throw new ArgumentNullException(nameof(parameterPolicies)); } return ParameterPartCore( parameterName: parameterName, @default: @default, parameterKind: parameterKind, parameterPolicies: parameterPolicies); } /// /// Creates a from the provided parameter name /// and default value, parameter kind, and parameter policies. /// /// The parameter name. /// The parameter default value. May be null. /// The parameter kind. /// The parameter policies to associated with the parameter. /// The . public static RoutePatternParameterPart ParameterPart( string parameterName, object @default, RoutePatternParameterKind parameterKind, params RoutePatternParameterPolicyReference[] parameterPolicies) { if (string.IsNullOrEmpty(parameterName)) { throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); } if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) { throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); } if (@default != null && parameterKind == RoutePatternParameterKind.Optional) { throw new ArgumentNullException(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, nameof(parameterKind)); } if (parameterPolicies == null) { throw new ArgumentNullException(nameof(parameterPolicies)); } return ParameterPartCore( parameterName: parameterName, @default: @default, parameterKind: parameterKind, parameterPolicies: parameterPolicies); } private static RoutePatternParameterPart ParameterPartCore( string parameterName, object @default, RoutePatternParameterKind parameterKind, IEnumerable parameterPolicies) { return ParameterPartCore(parameterName, @default, parameterKind, parameterPolicies, encodeSlashes: true); } private static RoutePatternParameterPart ParameterPartCore( string parameterName, object @default, RoutePatternParameterKind parameterKind, IEnumerable parameterPolicies, bool encodeSlashes) { return new RoutePatternParameterPart( parameterName, @default, parameterKind, parameterPolicies.ToArray(), encodeSlashes); } /// /// Creates a from the provided contraint. /// /// /// The constraint object, which must be of type /// or . If the constraint object is a /// then it will be tranformed into an instance of . /// /// The . public static RoutePatternParameterPolicyReference Constraint(object constraint) { // Similar to RouteConstraintBuilder if (constraint is IRouteConstraint policy) { return ParameterPolicyCore(policy); } else if (constraint is string content) { return ParameterPolicyCore(new RegexRouteConstraint("^(" + content + ")$")); } else { throw new InvalidOperationException(Resources.FormatRoutePattern_InvalidConstraintReference( constraint ?? "null", typeof(IRouteConstraint))); } } /// /// Creates a from the provided constraint. /// /// /// The constraint object. /// /// The . public static RoutePatternParameterPolicyReference Constraint(IRouteConstraint constraint) { if (constraint == null) { throw new ArgumentNullException(nameof(constraint)); } return ParameterPolicyCore(constraint); } /// /// Creates a from the provided constraint. /// /// /// The constraint text, which will be resolved by . /// /// The . public static RoutePatternParameterPolicyReference Constraint(string constraint) { if (string.IsNullOrEmpty(constraint)) { throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(constraint)); } return ParameterPolicyCore(constraint); } /// /// Creates a from the provided object. /// /// /// The parameter policy object. /// /// The . public static RoutePatternParameterPolicyReference ParameterPolicy(IParameterPolicy parameterPolicy) { if (parameterPolicy == null) { throw new ArgumentNullException(nameof(parameterPolicy)); } return ParameterPolicyCore(parameterPolicy); } /// /// Creates a from the provided object. /// /// /// The parameter policy text, which will be resolved by . /// /// The . public static RoutePatternParameterPolicyReference ParameterPolicy(string parameterPolicy) { if (string.IsNullOrEmpty(parameterPolicy)) { throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterPolicy)); } return ParameterPolicyCore(parameterPolicy); } private static RoutePatternParameterPolicyReference ParameterPolicyCore(string parameterPolicy) { return new RoutePatternParameterPolicyReference(parameterPolicy); } private static RoutePatternParameterPolicyReference ParameterPolicyCore(IParameterPolicy parameterPolicy) { return new RoutePatternParameterPolicyReference(parameterPolicy); } } }