Files
ichni_Official/Packages/com.tivadar.best.http/Runtime/HTTP/Hosts/Settings/HostSettingsManager.cs
2026-06-15 18:18:16 +08:00

195 lines
9.6 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Best.HTTP.HostSetting;
using Best.HTTP.Shared.PlatformSupport.Threading;
namespace Best.HTTP.Hosts.Settings
{
/**
* Host Settings Hierarchy for the following hosts, settings are stored as leafs:
*
* *.com
* *.example.com
* example.com
*
* '*' matches one or more subdomains so *.example.com
* - matches a.example.com and a.b.example.com
* - but doesn't match example.com!
*
*
*
* [com] [localhost] [org] [*]
* +------+------+ | | |
* | | [setting] [*] [setting]
* [example] [*] |
* / \ | [setting]
* [b] [setting] [setting]
* |
* [a]
* |
* [setting]
* */
/// <summary>
/// Manages host-specific settings for HTTP requests based on hostnames.
/// The HostSettingsManager is a powerful tool for fine-tuning HTTP request and connection behaviors
/// on a per-host basis. It enables you to define custom settings for specific hostnames
/// while maintaining default settings for all other hosts. This level of granularity allows you to
/// optimize and customize HTTP requests for different endpoints within your application.
/// </summary>
/// <remarks>
/// When host-specific settings are not found for a given host variant, the default <see cref="HostSettings"/>
/// associated with the "*" host will be returned.
/// </remarks>
public sealed class HostSettingsManager
{
private const char SPLITTER = '.';
private SortedList<string, Node> _rootNodes = new SortedList<string, Node>(AsteriskStringComparer.Instance);
private List<string> _segments = new List<string>(4);
private ConcurrentDictionary<string, HostSettings> _cache = new ConcurrentDictionary<string, HostSettings>();
/// <summary>
/// Initializes a new instance of the <see cref="HostSettingsManager"/> class with default settings for all hosts ("*").
/// </summary>
public HostSettingsManager() => Add("*", new HostSettings());
/// <summary>
/// Adds default settings for the host part of the specified URI. This is equivalent to calling <see cref="Add(Uri, HostSettings)"/> with the a new <see cref="HostSettings"/>.
/// </summary>
/// <param name="uri">The URI for which default settings should be applied. Only the host part of the URI will be used.</param>
/// <returns>A <see cref="HostSettings"/> instance with default values.</returns>
public HostSettings AddDefault(Uri uri) => Add(uri, new HostSettings());
/// <summary>
/// Adds default settings for the the specified host name. This is equivalent to calling <see cref="Add(string, HostSettings)"/> with the a new <see cref="HostSettings"/>.
/// </summary>
/// <param name="hostname">The hostname for which default settings should be applied.</param>
/// <returns>A <see cref="HostSettings"/> instance with default values.</returns>
public HostSettings AddDefault(string hostname) => Add(hostname, new HostSettings());
/// <summary>
/// Adds host-specific settings for the host part of the specified URI.
/// </summary>
/// <param name="uri">The URI for which settings should be applied. Only the host part of the URI will be used.</param>
/// <param name="settings">The <see cref="HostSettings"/> to apply.</param>
public HostSettings Add(Uri uri, HostSettings settings) => Add(uri.Host, settings);
/// <summary>
/// Adds host-specific settings for the specified hostname.
/// </summary>
/// <param name="hostname">The hostname for which settings should be applied.</param>
/// <param name="settings">The <see cref="HostSettings"/> to apply.</param>
/// <exception cref="ArgumentNullException">Thrown when either the hostname or settings is null.</exception>
/// <exception cref="FormatException">Thrown when the hostname contains more than one asterisk ('*').</exception>
public HostSettings Add(string hostname, HostSettings settings)
{
if (string.IsNullOrEmpty(hostname))
throw new ArgumentNullException(nameof(hostname));
if (settings == null)
throw new ArgumentNullException(nameof(settings));
if (hostname.IndexOf('*') != hostname.LastIndexOf('*'))
throw new FormatException($"{nameof(hostname)} (\"{hostname}\") MUST contain only one '*'!");
// From "a.b.example.com" create a list: [ "com", "example", "b", "a"]
lock(this._segments)
{
this._segments.Clear();
this._segments.AddRange(hostname.Split(SPLITTER, StringSplitOptions.RemoveEmptyEntries));
this._segments.Reverse();
string subKey = this._segments[0];
this._segments.RemoveAt(0);
if (!_rootNodes.TryGetValue(subKey, out var node))
_rootNodes.Add(subKey, node = new Node(subKey, null));
node.Add(this._segments, settings);
this._cache.Clear();
}
return settings;
}
/// <summary>
/// Gets <see cref="HostSettings"/> for the host part of the specified <see cref="HostVariant"/>. Returns the default settings associated with "*" when not found.
/// </summary>
/// <param name="variant">The <see cref="HostVariant"/> for which settings should be retrieved. Only the host part of the variant will be used.</param>
/// <returns>The host settings for the specified host variant or the default settings for "*" if not found.</returns>
public HostSettings Get(HostVariant variant, bool fallbackToWildcard = true) => Get(variant.Host, fallbackToWildcard);
/// <summary>
/// Gets <see cref="HostSettings"/> for the host part of the specified <see cref="HostKey"/>. Returns the default settings associated with "*" when not found.
/// </summary>
/// <param name="hostKey">The <see cref="HostKey"/> for which settings should be retrieved. Only the host part of the host key will be used.</param>
/// <returns>The host settings for the specified host key or the default settings for "*" if not found.</returns>
public HostSettings Get(HostKey hostKey, bool fallbackToWildcard = true) => Get(hostKey.Host, fallbackToWildcard);
/// <summary>
/// Gets <see cref="HostSettings"/> for the host part of the specified <see cref="Uri"/>. Returns the default settings associated with "*" when not found.
/// </summary>
/// <param name="uri">The <see cref="Uri"/> for which settings should be retrieved. Only the host part of the URI will be used.</param>
/// <returns>The host settings for the specified URI or the default settings for "*" if not found.</returns>
public HostSettings Get(Uri uri, bool fallbackToWildcard = true) => Get(uri.Host, fallbackToWildcard);
/// <summary>
/// Gets <see cref="HostSettings"/> for the host part of the specified hostname. Returns the default settings associated with "*" when not found.
/// </summary>
/// <param name="hostname">The hostname for which settings should be retrieved. Only the host part of the hostname will be used.</param>
/// <returns>The host settings for the specified hostname or the default settings for "*" if not found.</returns>
/// <exception cref="ArgumentNullException">Thrown when the hostname is null.</exception>
public HostSettings Get(string hostname, bool fallbackToWildcard = true)
{
if (string.IsNullOrEmpty(hostname))
throw new ArgumentNullException(nameof(hostname));
HostSettings foundSettings = null;
if (this._cache.TryGetValue(hostname, out foundSettings))
return foundSettings;
lock (this._segments)
{
// This splits the hostname (a.b.c.tld) into segments (["a", "b", "c", "tld"]), reverse it (["tld", "c", "b", "a"])
// and creates a final List<string> object.
this._segments.Clear();
this._segments.AddRange(hostname.Split(SPLITTER, StringSplitOptions.RemoveEmptyEntries));
this._segments.Reverse();
string subKey = this._segments[0];
this._segments.RemoveAt(0);
if (_rootNodes.TryGetValue(subKey, out var node))
foundSettings = node.Find(this._segments);
}
if (fallbackToWildcard && foundSettings == null && _rootNodes.TryGetValue("*", out var asteriskNode))
foundSettings = asteriskNode.hostSettings;
if (foundSettings != null)
this._cache.AddOrUpdate(hostname, foundSettings, (key, settings) => foundSettings);
return foundSettings;
}
/// <summary>
/// Clears all host-specific settings and resetting the default ("*") with default values.
/// </summary>
public void Clear()
{
_rootNodes.Clear();
_cache.Clear();
Add("*", new HostSettings());
}
}
}