File: Data\StaticWebAssetEndpoint.cs
Web Access
Project: ..\..\..\src\StaticWebAssetsSdk\Tasks\Microsoft.NET.Sdk.StaticWebAssets.Tasks.csproj (Microsoft.NET.Sdk.StaticWebAssets.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Collections;
using System.Collections.Concurrent;
using System.Diagnostics;
using Microsoft.Build.Framework;
 
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
 
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public class StaticWebAssetEndpoint : IEquatable<StaticWebAssetEndpoint>, IComparable<StaticWebAssetEndpoint>, ITaskItem2
{
    private ITaskItem _originalItem;
    private StaticWebAssetEndpointProperty[] _endpointProperties;
    private StaticWebAssetEndpointResponseHeader[] _responseHeaders;
    private StaticWebAssetEndpointSelector[] _selectors;
    private string _assetFile;
    private string _route;
    private bool _modified;
    private string _selectorsString;
    private bool _selectorsModified;
    private string _responseHeadersString;
    private bool _responseHeadersModified;
    private string _endpointPropertiesString;
    private bool _endpointPropertiesModified;
    private Dictionary<string, string> _additionalCustomMetadata;
 
    // Route as it should be registered in the routing table.
    public string Route
    {
        get
        {
            _route ??= _originalItem?.ItemSpec;
            return _route;
        }
 
        set
        {
            _route = value;
            _modified = true;
        }
    }
 
    // Path to the file system as provided by static web assets (BasePath + RelativePath).
    public string AssetFile
    {
        get
        {
            _assetFile ??= _originalItem?.GetMetadata(nameof(AssetFile));
            return _assetFile;
        }
 
        set
        {
            _assetFile = value;
            _modified = true;
        }
    }
 
    private string SelectorsString
    {
        get
        {
            _selectorsString ??= _originalItem?.GetMetadata(nameof(Selectors));
            return _selectorsString;
        }
    }
 
    // Request values that must be compatible for the file to be selected.
    public StaticWebAssetEndpointSelector[] Selectors
    {
        get
        {
            _selectors ??= StaticWebAssetEndpointSelector.FromMetadataValue(SelectorsString);
            return _selectors;
        }
 
        set
        {
            Array.Sort(value);
            _selectors = value;
            _selectorsModified = true;
            _modified = true;
        }
    }
 
    private string ResponseHeadersString
    {
        get
        {
            _responseHeadersString ??= _originalItem?.GetMetadata(nameof(ResponseHeaders));
            return _responseHeadersString;
        }
    }
 
    // Response headers that must be added to the response.
    public StaticWebAssetEndpointResponseHeader[] ResponseHeaders
    {
        get
        {
            _responseHeaders ??= StaticWebAssetEndpointResponseHeader.FromMetadataValue(ResponseHeadersString);
            return _responseHeaders;
        }
        set
        {
            Array.Sort(value);
            _responseHeaders = value;
            _responseHeadersModified = true;
            _modified = true;
        }
    }
 
    private string EndpointPropertiesString
    {
        get
        {
            _endpointPropertiesString ??= _originalItem?.GetMetadata(nameof(EndpointProperties));
            return _endpointPropertiesString;
        }
    }
 
    // Properties associated with the endpoint.
    public StaticWebAssetEndpointProperty[] EndpointProperties
    {
        get
        {
            _endpointProperties ??= StaticWebAssetEndpointProperty.FromMetadataValue(EndpointPropertiesString);
            return _endpointProperties;
        }
        set
        {
            Array.Sort(value);
            _endpointProperties = value;
            _endpointPropertiesModified = true;
            _modified = true;
        }
    }
 
    internal void MarkProperiesAsModified()
    {
        _modified = true;
        _endpointPropertiesModified = true;
    }
 
    public static IEqualityComparer<StaticWebAssetEndpoint> RouteAndAssetComparer { get; } = new RouteAndAssetEqualityComparer();
 
    internal static IDictionary<string, List<StaticWebAssetEndpoint>> ToAssetFileDictionary(ITaskItem[] candidateEndpoints)
    {
        var result = new Dictionary<string, List<StaticWebAssetEndpoint>>(candidateEndpoints.Length / 2);
 
        foreach (var candidate in candidateEndpoints)
        {
            var endpoint = FromTaskItem(candidate);
            var assetFile = endpoint.AssetFile;
            if (!result.TryGetValue(assetFile, out var endpoints))
            {
                endpoints = new List<StaticWebAssetEndpoint>(5);
                result[assetFile] = endpoints;
            }
            endpoints.Add(endpoint);
        }
 
        return result;
    }
 
    public static StaticWebAssetEndpoint[] FromItemGroup(ITaskItem[] endpoints)
    {
        var result = new StaticWebAssetEndpoint[endpoints.Length];
        for (var i = 0; i < endpoints.Length; i++)
        {
            result[i] = FromTaskItem(endpoints[i]);
        }
 
        Array.Sort(result, (a, b) => (a.Route, b.Route) switch
        {
            (null, null) => 0,
            (null, _) => -1,
            (_, null) => 1,
            var (x, y) => string.Compare(x, y, StringComparison.Ordinal) switch
            {
                0 => string.Compare(a.AssetFile, b.AssetFile, StringComparison.Ordinal),
                int result => result
            }
        });
 
        return result;
    }
 
    public static StaticWebAssetEndpoint FromTaskItem(ITaskItem item)
    {
        var result = new StaticWebAssetEndpoint()
        {
            _originalItem = item,
        };
 
        return result;
    }
 
    public static ITaskItem[] ToTaskItems(ICollection<StaticWebAssetEndpoint> endpoints)
    {
        if (endpoints == null || endpoints.Count == 0)
        {
            return [];
        }
 
        var endpointItems = new ITaskItem[endpoints.Count];
        var i = 0;
        foreach (var endpoint in endpoints)
        {
            endpointItems[i++] = endpoint.ToTaskItem();
        }
 
        return endpointItems;
    }
 
    public ITaskItem ToTaskItem()
    {
        if (!_modified && _originalItem != null)
        {
            return _originalItem;
        }
 
        // If we're implementing ITaskItem2, we can just return this instance
        return this;
    }
 
    public override bool Equals(object obj) => Equals(obj as StaticWebAssetEndpoint);
 
    public bool Equals(StaticWebAssetEndpoint other) => other is not null && Route == other.Route &&
        AssetFile == other.AssetFile &&
        Selectors.SequenceEqual(other.Selectors) &&
        ResponseHeaders.SequenceEqual(other.ResponseHeaders) &&
        EndpointProperties.SequenceEqual(other.EndpointProperties);
 
    public override int GetHashCode()
    {
#if NET472_OR_GREATER
        var hashCode = -604019124;
        hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Route);
        hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(AssetFile);
        for (var i = 0; i < Selectors.Length; i++)
        {
            hashCode = hashCode * -1521134295 + EqualityComparer<StaticWebAssetEndpointSelector>.Default.GetHashCode(Selectors[i]);
        }
        for (var i = 0; i < ResponseHeaders.Length; i++)
        {
            hashCode = hashCode * -1521134295 + EqualityComparer<StaticWebAssetEndpointResponseHeader>.Default.GetHashCode(ResponseHeaders[i]);
        }
        for (var i = 0; i < EndpointProperties.Length; i++)
        {
            hashCode = hashCode * -1521134295 + EqualityComparer<StaticWebAssetEndpointProperty>.Default.GetHashCode(EndpointProperties[i]);
        }
        return hashCode;
#else
        var hashCode = new HashCode();
        hashCode.Add(Route);
        hashCode.Add(AssetFile);
        for (var i = 0; i < Selectors.Length; i++)
        {
            hashCode.Add(Selectors[i]);
        }
        for (var i = 0; i < ResponseHeaders.Length; i++)
        {
            hashCode.Add(ResponseHeaders[i]);
        }
        for (var i = 0; i < EndpointProperties.Length; i++)
        {
            hashCode.Add(EndpointProperties[i]);
        }
        return hashCode.ToHashCode();
#endif
    }
 
    private string GetDebuggerDisplay() =>
        $"{nameof(StaticWebAssetEndpoint)}: Route = {Route}, AssetFile = {AssetFile}, Selectors = {StaticWebAssetEndpointSelector.ToMetadataValue(Selectors ?? [])}, ResponseHeaders = {ResponseHeaders?.Length}, EndpointProperties = {StaticWebAssetEndpointProperty.ToMetadataValue(EndpointProperties ?? [])}";
 
    public int CompareTo(StaticWebAssetEndpoint other)
    {
        var routeComparison = StringComparer.Ordinal.Compare(Route, Route);
        if (routeComparison != 0)
        {
            return routeComparison;
        }
 
        var assetFileComparison = StringComparer.Ordinal.Compare(AssetFile, other.AssetFile);
        if (assetFileComparison != 0)
        {
            return assetFileComparison;
        }
 
        if (Selectors.Length > other.Selectors.Length)
        {
            return 1;
        }
        else if (Selectors.Length < other.Selectors.Length)
        {
            return -1;
        }
 
        for (var i = 0; i < Selectors.Length; i++)
        {
            var selectorComparison = Selectors[i].Name.CompareTo(other.Selectors[i].Name);
            if (selectorComparison != 0)
            {
                return selectorComparison;
            }
 
            selectorComparison = Selectors[i].Value.CompareTo(other.Selectors[i].Value);
            if (selectorComparison != 0)
            {
                return selectorComparison;
            }
        }
 
        if (EndpointProperties.Length > other.EndpointProperties.Length)
        {
            return 1;
        }
        else if (EndpointProperties.Length < other.EndpointProperties.Length)
        {
            return -1;
        }
 
        for (var i = 0; i < EndpointProperties.Length; i++)
        {
            var propertyComparison = EndpointProperties[i].Name.CompareTo(other.EndpointProperties[i].Name);
            if (propertyComparison != 0)
            {
                return propertyComparison;
            }
 
            propertyComparison = EndpointProperties[i].Value.CompareTo(other.EndpointProperties[i].Value);
            if (propertyComparison != 0)
            {
                return propertyComparison;
            }
        }
 
        if (ResponseHeaders.Length > other.ResponseHeaders.Length)
        {
            return 1;
        }
        else if (ResponseHeaders.Length < other.ResponseHeaders.Length)
        {
            return -1;
        }
 
        for (var i = 0; i < ResponseHeaders.Length; i++)
        {
            var responseHeaderComparison = ResponseHeaders[i].Name.CompareTo(other.ResponseHeaders[i].Name);
            if (responseHeaderComparison != 0)
            {
                return responseHeaderComparison;
            }
 
            responseHeaderComparison = ResponseHeaders[i].Value.CompareTo(other.ResponseHeaders[i].Value);
            if (responseHeaderComparison != 0)
            {
                return responseHeaderComparison;
            }
        }
 
        return 0;
    }
 
    internal static ITaskItem[] ToTaskItems(ConcurrentBag<StaticWebAssetEndpoint> endpoints)
    {
        if (endpoints == null || endpoints.IsEmpty)
        {
            return [];
        }
 
        var endpointItems = new ITaskItem[endpoints.Count];
        var i = 0;
        foreach (var endpoint in endpoints)
        {
            endpointItems[i++] = endpoint.ToTaskItem();
        }
 
        return endpointItems;
    }
 
    private sealed class RouteAndAssetEqualityComparer : IEqualityComparer<StaticWebAssetEndpoint>
    {
        public bool Equals(StaticWebAssetEndpoint x, StaticWebAssetEndpoint y)
        {
            if (ReferenceEquals(x, y))
            {
                return true;
            }
 
            if (x is null || y is null)
            {
                return false;
            }
 
            return string.Equals(x.Route, y.Route, StringComparison.Ordinal) &&
                string.Equals(x.AssetFile, y.AssetFile, StringComparison.Ordinal);
        }
 
        public int GetHashCode(StaticWebAssetEndpoint obj)
        {
#if NET472_OR_GREATER
            var hashCode = -604019124;
            hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(obj.Route);
            hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(obj.AssetFile);
            return hashCode;
#else
            return HashCode.Combine(obj.Route, obj.AssetFile);
#endif
        }
    }
 
    #region ITaskItem2 implementation
 
    string ITaskItem2.EvaluatedIncludeEscaped { get => Route; set => Route = value; }
    string ITaskItem.ItemSpec { get => Route; set => Route = value; }
 
    private static readonly string[] _defaultPropertyNames = [
        nameof(AssetFile),
        nameof(Selectors),
        nameof(ResponseHeaders),
        nameof(EndpointProperties)
    ];
 
    ICollection ITaskItem.MetadataNames
    {
        get
        {
            if (_additionalCustomMetadata == null)
            {
                return _defaultPropertyNames;
            }
 
            var result = new List<string>(_defaultPropertyNames.Length + _additionalCustomMetadata.Count);
            result.AddRange(_defaultPropertyNames);
 
            foreach (var kvp in _additionalCustomMetadata)
            {
                result.Add(kvp.Key);
            }
 
            return result;
        }
    }
 
    int ITaskItem.MetadataCount => _defaultPropertyNames.Length + (_additionalCustomMetadata?.Count ?? 0);
 
    string ITaskItem2.GetMetadataValueEscaped(string metadataName)
    {
        return metadataName switch
        {
            nameof(AssetFile) => AssetFile ?? "",
            nameof(Selectors) => !_selectorsModified ? SelectorsString ?? "" : StaticWebAssetEndpointSelector.ToMetadataValue(Selectors),
            nameof(ResponseHeaders) => !_responseHeadersModified ? ResponseHeadersString ?? "" : StaticWebAssetEndpointResponseHeader.ToMetadataValue(ResponseHeaders),
            nameof(EndpointProperties) => !_endpointPropertiesModified ? EndpointPropertiesString ?? "" : StaticWebAssetEndpointProperty.ToMetadataValue(EndpointProperties),
            _ => _additionalCustomMetadata?.TryGetValue(metadataName, out var value) == true ? (value ?? "") : "",
        };
    }
 
    void ITaskItem2.SetMetadataValueLiteral(string metadataName, string metadataValue)
    {
        metadataValue ??= "";
        switch (metadataName)
        {
            case nameof(AssetFile):
                AssetFile = metadataValue;
                break;
            case nameof(Selectors):
                _selectorsString = metadataValue;
                _selectors = null;
                _selectorsModified = false;
                break;
            case nameof(ResponseHeaders):
                _responseHeadersString = metadataValue;
                _responseHeaders = null;
                _responseHeadersModified = false;
                break;
            case nameof(EndpointProperties):
                _endpointPropertiesString = metadataValue;
                _endpointProperties = null;
                _endpointPropertiesModified = false;
                break;
            default:
                _additionalCustomMetadata ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                _additionalCustomMetadata[metadataName] = metadataValue;
                break;
        }
        _modified = true;
    }
 
    IDictionary ITaskItem2.CloneCustomMetadataEscaped()
    {
        var result = new Dictionary<string, string>(((ITaskItem)this).MetadataCount)
        {
            { nameof(AssetFile), AssetFile ?? "" },
            { nameof(Selectors), !_selectorsModified ? SelectorsString ?? "" : StaticWebAssetEndpointSelector.ToMetadataValue(Selectors) },
            { nameof(ResponseHeaders), !_responseHeadersModified ? ResponseHeadersString ?? "" : StaticWebAssetEndpointResponseHeader.ToMetadataValue(ResponseHeaders) },
            { nameof(EndpointProperties), !_endpointPropertiesModified ? EndpointPropertiesString ?? "" : StaticWebAssetEndpointProperty.ToMetadataValue(EndpointProperties) }
        };
 
        if (_additionalCustomMetadata != null)
        {
            foreach (var kvp in _additionalCustomMetadata)
            {
                result[kvp.Key] = kvp.Value;
            }
        }
 
        return result;
    }
 
    string ITaskItem.GetMetadata(string metadataName) => ((ITaskItem2)this).GetMetadataValueEscaped(metadataName);
 
    void ITaskItem.SetMetadata(string metadataName, string metadataValue) => ((ITaskItem2)this).SetMetadataValueLiteral(metadataName, metadataValue);
 
    void ITaskItem.RemoveMetadata(string metadataName) => _additionalCustomMetadata?.Remove(metadataName);
 
    void ITaskItem.CopyMetadataTo(ITaskItem destinationItem)
    {
        destinationItem.SetMetadata(nameof(AssetFile), AssetFile ?? "");
        destinationItem.SetMetadata(nameof(Selectors), !_selectorsModified ? SelectorsString ?? "" : StaticWebAssetEndpointSelector.ToMetadataValue(Selectors));
        destinationItem.SetMetadata(nameof(ResponseHeaders), !_responseHeadersModified ? ResponseHeadersString ?? "" : StaticWebAssetEndpointResponseHeader.ToMetadataValue(ResponseHeaders));
        destinationItem.SetMetadata(nameof(EndpointProperties), !_endpointPropertiesModified ? EndpointPropertiesString ?? "" : StaticWebAssetEndpointProperty.ToMetadataValue(EndpointProperties));
 
        if (_additionalCustomMetadata != null)
        {
            foreach (var kvp in _additionalCustomMetadata)
            {
                destinationItem.SetMetadata(kvp.Key, kvp.Value ?? "");
            }
        }
    }
 
    IDictionary ITaskItem.CloneCustomMetadata() => ((ITaskItem2)this).CloneCustomMetadataEscaped();
 
    #endregion
}