using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text.RegularExpressions; using EmbedIO.Utilities; using Swan; namespace EmbedIO.Routing { /// /// Matches URL paths against a route. /// public sealed class RouteMatcher : IEquatable { private static readonly object SyncRoot = new object(); private static readonly Dictionary<(bool, string), RouteMatcher> Cache = new Dictionary<(bool, string), RouteMatcher>(); private readonly Regex _regex; private RouteMatcher(bool isBaseRoute, string route, string pattern, IReadOnlyList parameterNames) { IsBaseRoute = isBaseRoute; Route = route; ParameterNames = parameterNames; _regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.CultureInvariant); } /// /// Gets a value indicating whether the property /// is a base route. /// public bool IsBaseRoute { get; } /// /// Gets the route this instance matches URL paths against. /// public string Route { get; } /// /// Gets the names of the route's parameters. /// public IReadOnlyList ParameterNames { get; } /// /// Constructs an instance of by parsing the specified route. /// If the same route was previously parsed and the method has not been called since, /// this method obtains an instance from a static cache. /// /// The route to parse. /// if the route to parse /// is a base route; otherwise, . /// A newly-constructed instance of /// that will match URL paths against . /// is . /// is not a valid route. /// /// public static RouteMatcher Parse(string route, bool isBaseRoute) { var exception = TryParseInternal(route, isBaseRoute, out var result); if (exception != null) throw exception; return result!; } /// /// Attempts to obtain an instance of by parsing the specified route. /// If the same route was previously parsed and the method has not been called since, /// this method obtains an instance from a static cache. /// /// The route to parse. /// if the route to parse /// is a base route; otherwise, . /// When this method returns , a newly-constructed instance of /// that will match URL paths against ; otherwise, . /// This parameter is passed uninitialized. /// if parsing was successful; otherwise, . /// /// public static bool TryParse(string route, bool isBaseRoute, out RouteMatcher? result) => TryParseInternal(route, isBaseRoute, out result) == null; /// /// Clears 's internal instance cache. /// /// /// public static void ClearCache() { lock (SyncRoot) { Cache.Clear(); } } /// /// Returns a hash code for this instance. /// /// A hash code for this instance, suitable for use in hashing algorithms /// and data structures like a hash table. public override int GetHashCode() => CompositeHashCode.Using(Route, IsBaseRoute); /// /// Determines whether the specified is equal to this instance. /// /// The to compare with this instance. /// if is equal to this instance; /// otherwise, . public override bool Equals(object? obj) => obj is RouteMatcher other && Equals(other); /// /// Indicates whether this instance is equal to another instance of . /// /// A to compare with this instance. /// if this instance is equal to ; /// otherwise, . public bool Equals(RouteMatcher? other) => other != null && other.Route == Route && other.IsBaseRoute == IsBaseRoute; /// /// Matches the specified URL path against /// and extracts values for the route's parameters. /// /// The URL path to match. /// If the match is successful, a object; /// otherwise, . public RouteMatch? Match(string path) { if (path == null) return null; // Optimize for parameterless base routes if (IsBaseRoute) { if (Route.Length == 1) return RouteMatch.UnsafeFromRoot(path); if (ParameterNames.Count == 0) return RouteMatch.UnsafeFromBasePath(Route, path); } var match = _regex.Match(path); if (!match.Success) return null; return new RouteMatch( path, ParameterNames, match.Groups.Cast().Skip(1).Select(g => WebUtility.UrlDecode(g.Value)).ToArray(), IsBaseRoute ? "/" + path.Substring(match.Groups[0].Length) : null); } private static Exception? TryParseInternal(string route, bool isBaseRoute, out RouteMatcher? result) { lock (SyncRoot) { string? pattern = null; var parameterNames = new List(); var exception = Routing.Route.ParseInternal(route, isBaseRoute, (_, n, p) => { parameterNames.AddRange(n); pattern = p; }); if (exception != null) { result = null; return exception; } route = UrlPath.UnsafeNormalize(route, isBaseRoute); if (Cache.TryGetValue((isBaseRoute, route), out result)) return null; result = new RouteMatcher(isBaseRoute, route, pattern!, parameterNames); Cache.Add((isBaseRoute, route), result); return null; } } } }