File: SourceRepository.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Protocol\NuGet.Protocol.csproj (NuGet.Protocol)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Configuration;

namespace NuGet.Protocol.Core.Types
{
    /// <summary>
    /// Represents a Server endpoint. Exposes methods to get a specific resource such as Search, Metrics service
    /// and so on for the given server endpoint.
    /// </summary>
    public class SourceRepository
    {
        internal const int ProviderCacheTypes = 25;
        private readonly Dictionary<Type, IReadOnlyList<INuGetResourceProvider>> _providerCache;
        private readonly PackageSource _source;

        /// <summary>
        /// Pre-determined feed type.
        /// </summary>
        public FeedType FeedTypeOverride { get; }

        /// <summary>
        /// Source Repository
        /// </summary>
        /// <param name="source">source url</param>
        /// <param name="providers">Resource providers</param>
        public SourceRepository(PackageSource source, IEnumerable<INuGetResourceProvider> providers)
            : this(source, providers.Select(p => new Lazy<INuGetResourceProvider>(() => p)))
        {
        }

        /// <summary>
        /// Source Repository
        /// </summary>
        /// <param name="source">source url</param>
        /// <param name="providers">Resource providers</param>
        public SourceRepository(PackageSource source, IEnumerable<Lazy<INuGetResourceProvider>> providers)
            : this(source, providers, GetFeedType(source))
        {
        }

        /// <summary>
        /// Source Repository
        /// </summary>
        /// <param name="source">source url</param>
        /// <param name="providers">Resource providers</param>
        /// <param name="feedTypeOverride">Restrict the source to this feed type.</param>
        public SourceRepository(
            PackageSource source,
            IEnumerable<Lazy<INuGetResourceProvider>> providers,
            FeedType feedTypeOverride)
            : this()
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            if (providers == null)
            {
                throw new ArgumentNullException(nameof(providers));
            }

            _source = source;
            _providerCache = Init(providers);
            FeedTypeOverride = feedTypeOverride;
        }

        /// <summary>
        /// Internal default constructor
        /// </summary>
        protected SourceRepository()
        {
        }

        public override string ToString()
        {
            return _source.Name;
        }

        /// <summary>
        /// Package source
        /// </summary>
        public virtual PackageSource PackageSource
        {
            get { return _source; }
        }

        /// <summary>
        /// Find the FeedType of the source. If overridden FeedTypeOverride is returned.
        /// </summary>
        public virtual async Task<FeedType> GetFeedType(CancellationToken token)
        {
            if (FeedTypeOverride == FeedType.Undefined)
            {
                var resource = await GetResourceAsync<FeedTypeResource>(token);
                return resource.FeedType;
            }
            else
            {
                return FeedTypeOverride;
            }
        }

        /// <summary>
        /// Returns a resource from the SourceRepository if it exists.
        /// </summary>
        /// <typeparam name="T">Expected resource type</typeparam>
        /// <returns>Null if the resource does not exist</returns>
        public virtual T GetResource<T>() where T : class, INuGetResource
        {
            return GetResource<T>(CancellationToken.None);
        }

        /// <summary>
        /// Returns a resource from the SourceRepository if it exists.
        /// </summary>
        /// <typeparam name="T">Expected resource type</typeparam>
        /// <returns>Null if the resource does not exist</returns>
        public virtual T GetResource<T>(CancellationToken token) where T : class, INuGetResource
        {
            var task = GetResourceAsync<T>(token);
            task.Wait(token);

            return task.Result;
        }

        /// <summary>
        /// Returns a resource from the SourceRepository if it exists.
        /// </summary>
        /// <typeparam name="T">Expected resource type</typeparam>
        /// <returns>Null if the resource does not exist</returns>
        public virtual async Task<T> GetResourceAsync<T>() where T : class, INuGetResource
        {
            return await GetResourceAsync<T>(CancellationToken.None);
        }

        /// <summary>
        /// Returns a resource from the SourceRepository if it exists.
        /// </summary>
        /// <typeparam name="T">Expected resource type</typeparam>
        /// <returns>Null if the resource does not exist</returns>
        public virtual async Task<T> GetResourceAsync<T>(CancellationToken token) where T : class, INuGetResource
        {
            var resourceType = typeof(T);
            IReadOnlyList<INuGetResourceProvider> possible;

            if (_providerCache.TryGetValue(resourceType, out possible))
            {
                for (int i = 0; i < possible.Count; i++)
                {
                    var provider = possible[i];
                    var result = await provider.TryCreate(this, token);
                    if (result.Item1)
                    {
                        return (T)result.Item2;
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Initialize provider cache
        /// </summary>
        /// <param name="providers"></param>
        /// <returns></returns>
        private static Dictionary<Type, IReadOnlyList<INuGetResourceProvider>> Init(IEnumerable<Lazy<INuGetResourceProvider>> providers)
        {
            var cache = new Dictionary<Type, IReadOnlyList<INuGetResourceProvider>>(ProviderCacheTypes);

            foreach (var group in providers.GroupBy(p => p.Value.ResourceType))
            {
                cache.Add(group.Key, Sort(group));
            }

            return cache;
        }

        private static IReadOnlyList<INuGetResourceProvider>
            Sort(IEnumerable<Lazy<INuGetResourceProvider>> group)
        {
            var items = new List<INuGetResourceProvider>(group.Count());
            foreach (var lazy in group)
            {
                items.Add(lazy.Value);
            }

            // initial ordering to help make this deterministic
            items.Sort((a, b) =>
            {
                int cmp = StringComparer.Ordinal.Compare(a.Name, b.Name);
                if (cmp != 0) return cmp;
                cmp = a.After.Count().CompareTo(b.After.Count());
                if (cmp != 0) return cmp;
                return a.Before.Count().CompareTo(b.Before.Count());
            });

            var comparer = ProviderComparer.Instance;

            // List.Sort does not work when lists have unsolvable gaps, which can occur here
            for (int start = 0; start < items.Count - 1; start++)
            {
                int bestIndex = start;

                for (int i = start + 1; i < items.Count; i++)
                {
                    if (comparer.Compare(items[i], items[bestIndex]) < 0)
                    {
                        bestIndex = i;
                    }
                }

                if (bestIndex != start)
                {
                    (items[start], items[bestIndex]) = (items[bestIndex], items[start]);
                }
            }

            return items;
        }

        /// <summary>
        /// Get the feed type from the package source.
        /// </summary>
        private static FeedType GetFeedType(PackageSource source)
        {
            var feedTypeSource = source as FeedTypePackageSource;
            return feedTypeSource?.FeedType ?? FeedType.Undefined;
        }
    }
}