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;
}
}
}
}