Skip to content

Commit

Permalink
Fixed route and url redirections inside route groups
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasherceg committed Nov 3, 2024
1 parent 831cdb1 commit 11784bb
Show file tree
Hide file tree
Showing 17 changed files with 218 additions and 285 deletions.
6 changes: 5 additions & 1 deletion src/Framework/Framework/Configuration/FreezableDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static void Freeze<K, V>([AllowNull] ref IDictionary<K, V> dict)
}
}
}
sealed class FreezableDictionary<K, V> : IDictionary<K, V>, IReadOnlyCollection<KeyValuePair<K, V>>
sealed class FreezableDictionary<K, V> : IDictionary<K, V>, IReadOnlyCollection<KeyValuePair<K, V>>, IReadOnlyDictionary<K, V>
where K : notnull
{
private readonly Dictionary<K, V> dict;
Expand Down Expand Up @@ -102,5 +102,9 @@ public V this[K index]
public ICollection<K> Keys => ((IDictionary<K, V>)dict).Keys.ToArray();

public ICollection<V> Values => ((IDictionary<K, V>)dict).Values.ToArray();

IEnumerable<K> IReadOnlyDictionary<K, V>.Keys => Keys;

IEnumerable<V> IReadOnlyDictionary<K, V>.Values => Values;
}
}
2 changes: 1 addition & 1 deletion src/Framework/Framework/Controls/RouteLinkHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ private static void EnsureValidBindingType(IBinding binding)

private static Dictionary<string, object?> ComposeNewRouteParameters(RouteLink control, IDotvvmRequestContext context, RouteBase route)
{
var parameters = new Dictionary<string, object?>(route.DefaultValues, StringComparer.OrdinalIgnoreCase);
var parameters = route.CloneDefaultValues();
foreach (var param in context.Parameters!)
{
parameters[param.Key] = param.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public LocalResourceUrlManager(DotvvmConfiguration configuration, IResourceHashS
this.resourceRoute = new DotvvmRoute(
url: $"{HostingConstants.ResourceRouteName}/{{{HashParameterName}}}/{{{NameParameterName}:regex(.*)}}",
virtualPath: "",
name: $"_dotvvm_{nameof(LocalResourceUrlManager)}",
defaultValues: null,
presenterFactory: _ => throw new NotSupportedException(),
configuration: configuration);
Expand Down
7 changes: 2 additions & 5 deletions src/Framework/Framework/Routing/DefaultRouteStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,7 @@ protected virtual RouteBase BuildRoute(RouteStrategyMarkupFileInfo file)
var defaultParameters = GetRouteDefaultParameters(file);
var presenterFactory = GetRoutePresenterFactory(file);

return new DotvvmRoute(url, file.AppRelativePath, defaultParameters, presenterFactory, configuration)
{
RouteName = routeName
};
return new DotvvmRoute(url, file.AppRelativePath, routeName, defaultParameters, presenterFactory, configuration);
}

protected virtual string GetRouteName(RouteStrategyMarkupFileInfo file)
Expand Down Expand Up @@ -162,7 +159,7 @@ private static IEnumerable<string> GetRoutesForFile(string fileName)
protected override IEnumerable<RouteBase> BuildRoutes(RouteStrategyMarkupFileInfo file)
{
return getRouteList(file.AppRelativePath)
.Select(url => new DotvvmRoute(url, file.AppRelativePath, GetRouteDefaultParameters(file), GetRoutePresenterFactory(file), this.configuration) { RouteName = url });
.Select(url => new DotvvmRoute(url, file.AppRelativePath, url, GetRouteDefaultParameters(file), GetRoutePresenterFactory(file), this.configuration));
}
}
}
39 changes: 6 additions & 33 deletions src/Framework/Framework/Routing/DotvvmRoute.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DotVVM.Framework.Hosting;
using System.Text.RegularExpressions;
using DotVVM.Framework.Configuration;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using DotVVM.Framework.Utils;

namespace DotVVM.Framework.Routing
{
public sealed class DotvvmRoute : RouteBase
{
private Func<IServiceProvider,IDotvvmPresenter> presenterFactory;

private Regex routeRegex;
private List<Func<Dictionary<string, string?>, string>> urlBuilders;
private List<KeyValuePair<string, Func<string, ParameterParseResult>?>> parameters;
Expand All @@ -35,42 +29,26 @@ public sealed class DotvvmRoute : RouteBase
/// <summary>
/// Initializes a new instance of the <see cref="DotvvmRoute"/> class.
/// </summary>
#pragma warning disable CS8618
public DotvvmRoute(string url, string virtualPath, object? defaultValues, Func<IServiceProvider, IDotvvmPresenter> presenterFactory, DotvvmConfiguration configuration)
: base(url, virtualPath, defaultValues)
{
this.presenterFactory = presenterFactory;

ParseRouteUrl(configuration);
}

/// <summary>
/// Initializes a new instance of the <see cref="DotvvmRoute"/> class.
/// </summary>
public DotvvmRoute(string url, string virtualPath, IDictionary<string, object?>? defaultValues, Func<IServiceProvider, IDotvvmPresenter> presenterFactory, DotvvmConfiguration configuration)
: base(url, virtualPath, defaultValues)
public DotvvmRoute(string url, string? virtualPath, string name, object? defaultValues, Func<IServiceProvider, IDotvvmPresenter> presenterFactory, DotvvmConfiguration configuration)
: base(url, virtualPath, name, defaultValues, presenterFactory)
{
this.presenterFactory = presenterFactory;

ParseRouteUrl(configuration);
}

/// <summary>
/// Initializes a new instance of the <see cref="DotvvmRoute"/> class.
/// </summary>
public DotvvmRoute(string url, string virtualPath, string name, IDictionary<string, object?>? defaultValues, Func<IServiceProvider, IDotvvmPresenter> presenterFactory, DotvvmConfiguration configuration)
: base(url, virtualPath, name, defaultValues)
public DotvvmRoute(string url, string? virtualPath, string name, IDictionary<string, object?>? defaultValues, Func<IServiceProvider, IDotvvmPresenter> presenterFactory, DotvvmConfiguration configuration)
: base(url, virtualPath, name, defaultValues, presenterFactory)
{
this.presenterFactory = presenterFactory;

ParseRouteUrl(configuration);
}
#pragma warning restore CS8618


/// <summary>
/// Parses the route URL and extracts the components.
/// </summary>
[MemberNotNull(nameof(routeRegex), nameof(urlBuilders), nameof(parameters), nameof(parameterMetadata), nameof(urlWithoutTypes))]
private void ParseRouteUrl(DotvvmConfiguration configuration)
{
var parser = new DotvvmRouteParser(configuration.RouteConstraints);
Expand Down Expand Up @@ -99,8 +77,7 @@ public override bool IsMatch(string url, [MaybeNullWhen(false)] out IDictionary<
return false;
}

values = new Dictionary<string, object?>(DefaultValues, StringComparer.OrdinalIgnoreCase);

values = CloneDefaultValues();
foreach (var parameter in parameters)
{
var g = match.Groups["param" + parameter.Key];
Expand Down Expand Up @@ -155,10 +132,6 @@ protected internal override string BuildUrlCore(Dictionary<string, object?> valu
}
}

/// <summary>
/// Processes the request.
/// </summary>
public override IDotvvmPresenter GetPresenter(IServiceProvider provider) => presenterFactory(provider);
protected override void Freeze2()
{
// there is no property that would have to be frozen
Expand Down
4 changes: 2 additions & 2 deletions src/Framework/Framework/Routing/DotvvmRouteParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public DotvvmRouteParser(IDictionary<string, IRouteParameterConstraint> routeCon
this.routeConstraints = routeConstrains;
}

public UrlParserResult ParseRouteUrl(string url, IDictionary<string, object?> defaultValues)
public UrlParserResult ParseRouteUrl(string url, IReadOnlyDictionary<string, object?> defaultValues)
{
if (url.StartsWith("/", StringComparison.Ordinal))
throw new ArgumentException("The route URL must not start with '/'!");
Expand Down Expand Up @@ -85,7 +85,7 @@ void AppendParameterParserResult(UrlParameterParserResult result)
};
}

private UrlParameterParserResult ParseParameter(string url, string prefix, ref int index, IDictionary<string, object?> defaultValues)
private UrlParameterParserResult ParseParameter(string url, string prefix, ref int index, IReadOnlyDictionary<string, object?> defaultValues)
{
// find the end of the route parameter name
var startIndex = index;
Expand Down
129 changes: 67 additions & 62 deletions src/Framework/Framework/Routing/DotvvmRouteTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,8 @@ public IDotvvmPresenter GetDefaultPresenter(IServiceProvider provider)
/// <param name="virtualPath">The virtual path of the Dothtml file.</param>
/// <param name="defaultValues">The default values.</param>
/// <param name="presenterFactory">Delegate creating the presenter handling this route</param>
public void Add(string routeName, string? url, string virtualPath, object? defaultValues = null, Func<IServiceProvider, IDotvvmPresenter>? presenterFactory = null, LocalizedRouteUrl[]? localizedUrls = null)
public void Add(string routeName, string url, string? virtualPath, object? defaultValues = null, Func<IServiceProvider, IDotvvmPresenter>? presenterFactory = null, LocalizedRouteUrl[]? localizedUrls = null)
{
ThrowIfFrozen();

virtualPath = CombinePath(group?.VirtualPathPrefix, virtualPath);
AddCore(routeName, url, virtualPath, defaultValues, presenterFactory, localizedUrls);
}

Expand All @@ -132,26 +129,74 @@ public void Add(string routeName, string? url, string virtualPath, object? defau
/// <param name="url">The URL.</param>
/// <param name="defaultValues">The default values.</param>
/// <param name="presenterFactory">The presenter factory.</param>
public void Add(string routeName, string? url, Func<IServiceProvider, IDotvvmPresenter>? presenterFactory = null, object? defaultValues = null, LocalizedRouteUrl[]? localizedUrls = null)
public void Add(string routeName, string url, Func<IServiceProvider, IDotvvmPresenter> presenterFactory, object? defaultValues = null, LocalizedRouteUrl[]? localizedUrls = null)
{
AddCore(routeName, url, virtualPath: null, defaultValues, presenterFactory, localizedUrls);
}

/// <summary>
/// Adds the specified route name.
/// </summary>
/// <param name="routeName">Name of the route.</param>
/// <param name="url">The URL.</param>
/// <param name="presenterType">The presenter factory.</param>
/// <param name="defaultValues">The default values.</param>
public void Add(string routeName, string url, Type presenterType, object? defaultValues = null, LocalizedRouteUrl[]? localizedUrls = null)
{
if (!typeof(IDotvvmPresenter).IsAssignableFrom(presenterType))
{
throw new ArgumentException($@"{nameof(presenterType)} has to inherit from DotVVM.Framework.Hosting.IDotvvmPresenter.", nameof(presenterType));
}
Func<IServiceProvider, IDotvvmPresenter> presenterFactory = provider => (IDotvvmPresenter)provider.GetRequiredService(presenterType);
AddCore(routeName, url, virtualPath: null, defaultValues, presenterFactory, localizedUrls);
}

/// <summary>
/// Adds the specified name.
/// </summary>
public void Add(RouteBase route)
{
ThrowIfFrozen();
if (dictionary.ContainsKey(route.RouteName))
{
throw new InvalidOperationException($"The route with name '{route.RouteName}' has already been registered!");
}

var virtualPath = group?.VirtualPathPrefix ?? "";
AddCore(routeName, url, virtualPath, defaultValues, presenterFactory, localizedUrls);
group?.AddToParentRouteTable?.Invoke(route);

// The list is used for finding the routes because it keeps the ordering, the dictionary is for checking duplicates
list.Add(new KeyValuePair<string, RouteBase>(route.RouteName, route));
dictionary.Add(route.RouteName, route);

if (route is IPartialMatchRoute partialMatchRoute)
{
partialMatchRoutes.Add(partialMatchRoute);
}
}

private void AddCore(string routeName, string? url, string virtualPath, object? defaultValues, Func<IServiceProvider, IDotvvmPresenter>? presenterFactory, LocalizedRouteUrl[]? localizedUrls)
private void AddCore(string routeName, string url, string? virtualPath, object? defaultValues, Func<IServiceProvider, IDotvvmPresenter>? presenterFactory, LocalizedRouteUrl[]? localizedUrls = null)
{
ThrowIfFrozen();

if (url == null)
throw new ArgumentNullException(nameof(url));
url = CombinePath(group?.UrlPrefix, url);

virtualPath = CombinePath(group?.VirtualPathPrefix, virtualPath);
if (virtualPath == null && presenterFactory == null)
{
throw new ArgumentNullException(nameof(presenterFactory), "The presenterFactory argument must be set when virtualPath is null!");
}

presenterFactory ??= GetDefaultPresenter;
routeName = group?.RouteNamePrefix + routeName;

RouteBase route = localizedUrls == null
? new DotvvmRoute(url, virtualPath, defaultValues, presenterFactory, configuration)
? new DotvvmRoute(url, virtualPath, routeName, defaultValues, presenterFactory, configuration)
: new LocalizedDotvvmRoute(url,
localizedUrls.Select(l => new LocalizedRouteUrl(l.CultureIdentifier, CombinePath(group?.UrlPrefix, l.RouteUrl))).ToArray(),
virtualPath, defaultValues, presenterFactory, configuration);
Add(routeName, route);
virtualPath, routeName, defaultValues, presenterFactory, configuration);
Add(route);
}

/// <summary>
Expand All @@ -171,7 +216,6 @@ public void AddUrlRedirection(string routeName, string urlPattern, string target
/// <param name="targetUrlProvider">URL provider to obtain context-based redirection targets.</param>
public void AddUrlRedirection(string routeName, string urlPattern, Func<IDotvvmRequestContext, string> targetUrlProvider, object? defaultValues = null, bool permanent = false)
{
ThrowIfFrozen();
IDotvvmPresenter presenterFactory(IServiceProvider serviceProvider) => new DelegatePresenter(context =>
{
var targetUrl = targetUrlProvider(context);
Expand All @@ -181,7 +225,7 @@ public void AddUrlRedirection(string routeName, string urlPattern, Func<IDotvvmR
else
context.RedirectToUrl(targetUrl);
});
Add(routeName, new DotvvmRoute(urlPattern, string.Empty, defaultValues, presenterFactory, configuration));
AddCore(routeName, urlPattern, virtualPath: null, defaultValues, presenterFactory);
}

/// <summary>
Expand All @@ -207,7 +251,6 @@ public void AddRouteRedirection(string routeName, string urlPattern, Func<IDotvv
object? defaultValues = null, bool permanent = false, Func<IDotvvmRequestContext, Dictionary<string, object?>>? parametersProvider = null,
Func<IDotvvmRequestContext, string>? urlSuffixProvider = null)
{
ThrowIfFrozen();
IDotvvmPresenter presenterFactory(IServiceProvider serviceProvider) => new DelegatePresenter(context =>
{
var targetRouteName = targetRouteNameProvider(context);
Expand All @@ -219,50 +262,7 @@ public void AddRouteRedirection(string routeName, string urlPattern, Func<IDotvv
else
context.RedirectToRoute(targetRouteName, newParameterValues, urlSuffix: urlSuffix);
});
Add(routeName, new DotvvmRoute(urlPattern, string.Empty, defaultValues, presenterFactory, configuration));
}

/// <summary>
/// Adds the specified route name.
/// </summary>
/// <param name="routeName">Name of the route.</param>
/// <param name="url">The URL.</param>
/// <param name="presenterType">The presenter factory.</param>
/// <param name="defaultValues">The default values.</param>
public void Add(string routeName, string? url, Type presenterType, object? defaultValues = null, LocalizedRouteUrl[]? localizedUrls = null)
{
ThrowIfFrozen();
if (!typeof(IDotvvmPresenter).IsAssignableFrom(presenterType))
{
throw new ArgumentException($@"{nameof(presenterType)} has to inherit from DotVVM.Framework.Hosting.IDotvvmPresenter.", nameof(presenterType));
}
Func<IServiceProvider, IDotvvmPresenter> presenterFactory = provider => (IDotvvmPresenter)provider.GetRequiredService(presenterType);
Add(routeName, url, presenterFactory, defaultValues, localizedUrls);
}

/// <summary>
/// Adds the specified name.
/// </summary>
public void Add(string routeName, RouteBase route)
{
ThrowIfFrozen();
if (dictionary.ContainsKey(routeName))
{
throw new InvalidOperationException($"The route with name '{routeName}' has already been registered!");
}
// internal assign routename
route.RouteName = routeName;

group?.AddToParentRouteTable?.Invoke(routeName, route);

// The list is used for finding the routes because it keeps the ordering, the dictionary is for checking duplicates
list.Add(new KeyValuePair<string, RouteBase>(routeName, route));
dictionary.Add(routeName, route);

if (route is IPartialMatchRoute partialMatchRoute)
{
partialMatchRoutes.Add(partialMatchRoute);
}
AddCore(routeName, urlPattern, virtualPath: null, defaultValues, presenterFactory);
}

public void AddPartialMatchHandler(IPartialMatchRouteHandler handler)
Expand Down Expand Up @@ -309,16 +309,21 @@ IEnumerator IEnumerable.GetEnumerator()
return GetEnumerator();
}

private string CombinePath(string? prefix, string? appendedPath)
[return: NotNullIfNotNull(nameof(appendedPath))]
private string? CombinePath(string? prefix, string? appendedPath)
{
if (string.IsNullOrEmpty(prefix))
{
return appendedPath ?? "";
return appendedPath;
}

if (string.IsNullOrEmpty(appendedPath))
if (appendedPath == null)
{
return null;
}
else if (appendedPath == string.Empty)
{
return prefix!;
return prefix ?? string.Empty;
}

return $"{prefix}/{appendedPath}";
Expand Down
Loading

0 comments on commit 11784bb

Please sign in to comment.