File: Resources\PackageMetadataResourceV3.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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NuGet.Common;
using NuGet.Packaging.Core;
using NuGet.Protocol.Core.Types;
using NuGet.Protocol.Extensions;
using NuGet.Protocol.Model;
using NuGet.Versioning;

namespace NuGet.Protocol
{
    public class PackageMetadataResourceV3 : PackageMetadataResource
    {
        private readonly RegistrationResourceV3 _regResource;
        private readonly ReportAbuseResourceV3 _reportAbuseResource;
        private readonly ReadmeUriTemplateResource _readmeUriTemplateResource;
        private readonly PackageDetailsUriResourceV3 _packageDetailsUriResource;
        private readonly HttpSource _client;

        public PackageMetadataResourceV3(
            HttpSource client,
            RegistrationResourceV3 regResource,
            ReportAbuseResourceV3 reportAbuseResource,
            PackageDetailsUriResourceV3 packageDetailsUriResource)
        {
            _regResource = regResource;
            _client = client;
            _reportAbuseResource = reportAbuseResource;
            _packageDetailsUriResource = packageDetailsUriResource;
        }

        internal PackageMetadataResourceV3(
            HttpSource client,
            RegistrationResourceV3 regResource,
            ReportAbuseResourceV3 reportAbuseResource,
            PackageDetailsUriResourceV3 packageDetailsUriResource,
            ReadmeUriTemplateResource readmeResource) : this(client, regResource, reportAbuseResource, packageDetailsUriResource)
        {
            _readmeUriTemplateResource = readmeResource;
        }

        /// <param name="packageId">PackageId for package we're looking.</param>
        /// <param name="includePrerelease">Whether to include PreRelease versions into result.</param>
        /// <param name="includeUnlisted">Whether to include Unlisted versions into result.</param>
        /// <param name="sourceCacheContext">SourceCacheContext for cache.</param>
        /// <param name="log">Logger Instance.</param>
        /// <param name="token">Cancellation token.</param>
        /// <returns>List of package metadata.</returns>
        public override async Task<IEnumerable<IPackageSearchMetadata>> GetMetadataAsync(
            string packageId,
            bool includePrerelease,
            bool includeUnlisted,
            SourceCacheContext sourceCacheContext,
            Common.ILogger log,
            CancellationToken token)
        {
            return await GetMetadataAsync(packageId, includePrerelease, includeUnlisted, range: VersionRange.All, sourceCacheContext, log, token);
        }

        /// <summary>
        /// Returns the registration metadata for the id and version
        /// </summary>
        /// <param name="package"></param>
        /// <param name="sourceCacheContext"></param>
        /// <param name="log"></param>
        /// <param name="token"></param>
        /// <returns>Package meta data.</returns>
        /// <remarks>The inlined entries are potentially going away soon</remarks>
        public override async Task<IPackageSearchMetadata> GetMetadataAsync(
            PackageIdentity package,
            SourceCacheContext sourceCacheContext,
            Common.ILogger log,
            CancellationToken token)
        {
            var range = new VersionRange(package.Version, includeMinVersion: true, package.Version, includeMaxVersion: true);
            var packageMetaDatas = await GetMetadataAsync(package.Id, includePrerelease: true, includeUnlisted: true, range, sourceCacheContext, log, token);

            return packageMetaDatas.SingleOrDefault();
        }

        private async Task<IEnumerable<IPackageSearchMetadata>> GetMetadataAsync(
            string packageId,
            bool includePrerelease,
            bool includeUnlisted,
            VersionRange range,
            SourceCacheContext sourceCacheContext,
            ILogger log,
            CancellationToken token)
        {
            var metadataCache = new MetadataReferenceCache();
            var registrationUri = _regResource.GetUri(packageId);

            var (registrationIndex, httpSourceCacheContext) = await LoadRegistrationIndexAsync(
                _client,
                registrationUri,
                packageId,
                sourceCacheContext,
                httpSourceResult => DeserializeStreamDataAsync<RegistrationIndex>(httpSourceResult.Stream, token),
                log,
                token);

            if (registrationIndex == null)
            {
                // The server returned a 404, the package does not exist
                return Enumerable.Empty<PackageSearchMetadataRegistration>();
            }

            var results = new List<PackageSearchMetadataRegistration>();

            foreach (var registrationPage in registrationIndex.Items)
            {
                if (registrationPage == null)
                {
                    throw new InvalidDataException(registrationUri.AbsoluteUri);
                }

                var lower = NuGetVersion.Parse(registrationPage.Lower);
                var upper = NuGetVersion.Parse(registrationPage.Upper);

                if (range.DoesRangeSatisfy(lower, upper))
                {
                    if (registrationPage.Items == null)
                    {
                        var rangeUri = registrationPage.Url;
                        var leafRegistrationPage = await GetRegistratioIndexPageAsync(_client, rangeUri, packageId, lower, upper, httpSourceCacheContext, log, token);

                        if (registrationPage == null)
                        {
                            throw new InvalidDataException(registrationUri.AbsoluteUri);
                        }

                        ProcessRegistrationPage(leafRegistrationPage, results, range, includePrerelease, includeUnlisted, metadataCache);
                    }
                    else
                    {
                        ProcessRegistrationPage(registrationPage, results, range, includePrerelease, includeUnlisted, metadataCache);
                    }
                }
            }

            return results;
        }

        /// <summary>
        /// Deserialize stream from RegistrationIndex/RegistrationPage and return list of RegistrationPages or RegistrationPage.
        /// </summary>
        /// <typeparam name="T">Generic type</typeparam>
        /// <param name="stream">Stream data to read.</param>
        /// <param name="token">Cancellation token.</param>
        /// <returns></returns>
        private async Task<T> DeserializeStreamDataAsync<T>(Stream stream, CancellationToken token)
        {
            token.ThrowIfCancellationRequested();

            if (stream == null)
            {
                return default(T);
            }

            using (var streamReader = new StreamReader(stream))
            using (var jsonReader = new JsonTextReader(streamReader))
            {
                var registrationIndex = JsonExtensions.JsonObjectSerializer
                    .Deserialize<T>(jsonReader);

                return await Task.FromResult(registrationIndex);
            }
        }

        /// <summary>
        /// Query RegistrationIndex from nuget server for Package Manager UI. This implementation optimized for performance so instead of keeping giant JObject in memory we use strong types.
        /// </summary>
        /// <param name="httpSource">Httpsource instance</param>
        /// <param name="registrationUri">Package registration url</param>
        /// <param name="packageId">PackageId for package we're looking.</param>
        /// <param name="cacheContext">CacheContext for cache.</param>
        /// <param name="processAsync">Func expression used for HttpSource.cs</param>
        /// <param name="log">Logger Instance.</param>
        /// <param name="token">Cancellation token.</param>
        /// <returns></returns>
        private async Task<ValueTuple<RegistrationIndex, HttpSourceCacheContext>> LoadRegistrationIndexAsync(
            HttpSource httpSource,
            Uri registrationUri,
            string packageId,
            SourceCacheContext cacheContext,
            Func<HttpSourceResult, Task<RegistrationIndex>> processAsync,
            ILogger log,
            CancellationToken token)
        {
            var packageIdLowerCase = packageId.ToLowerInvariant();
            var retryCount = 0;

            var httpSourceCacheContext = HttpSourceCacheContext.Create(cacheContext, retryCount);

            var index = await httpSource.GetAsync(
                new HttpSourceCachedRequest(
                    registrationUri.OriginalString,
                    $"list_{packageIdLowerCase}_index",
                    httpSourceCacheContext)
                {
                    IgnoreNotFounds = true,
                },
                async httpSourceResult => await processAsync(httpSourceResult),
                log,
                token);

            return new ValueTuple<RegistrationIndex, HttpSourceCacheContext>(index, httpSourceCacheContext);
        }

        /// <summary>
        /// Process RegistrationIndex 
        /// </summary>
        /// <param name="httpSource">Httpsource instance.</param>
        /// <param name="rangeUri">Paged registration index url address.</param>
        /// <param name="packageId">PackageId for package we're checking.</param>
        /// <param name="lower">Lower bound of nuget package.</param>
        /// <param name="upper">Upper bound of nuget package.</param>
        /// <param name="httpSourceCacheContext">SourceCacheContext for cache.</param>
        /// <param name="log">Logger Instance.</param>
        /// <param name="token">Cancellation token.</param>
        /// <returns></returns>
        private Task<RegistrationPage> GetRegistratioIndexPageAsync(
            HttpSource httpSource,
            string rangeUri,
            string packageId,
            NuGetVersion lower,
            NuGetVersion upper,
            HttpSourceCacheContext httpSourceCacheContext,
            ILogger log,
            CancellationToken token)
        {
            var packageIdLowerCase = packageId.ToLowerInvariant();
            var registrationPage = httpSource.GetAsync(
                            new HttpSourceCachedRequest(
                                rangeUri,
                                $"list_{packageIdLowerCase}_range_{lower.ToNormalizedString()}-{upper.ToNormalizedString()}",
                                httpSourceCacheContext)
                            {
                                IgnoreNotFounds = true,
                            },
                            httpSourceResult => DeserializeStreamDataAsync<RegistrationPage>(httpSourceResult.Stream, token),
                            log,
                            token);

            return registrationPage;
        }

        /// <summary>
        /// Process RegistrationPage
        /// </summary>
        /// <param name="registrationPage">Nuget registration page.</param>
        /// <param name="results">Used to return nuget result.</param>
        /// <param name="range">Nuget version range.</param>
        /// <param name="includePrerelease">Whether to include PreRelease versions into result.</param>
        /// <param name="includeUnlisted">Whether to include Unlisted versions into result.</param>
        private void ProcessRegistrationPage(
            RegistrationPage registrationPage,
            List<PackageSearchMetadataRegistration> results,
            VersionRange range, bool includePrerelease,
            bool includeUnlisted,
            MetadataReferenceCache metadataCache)
        {
            foreach (RegistrationLeafItem registrationLeaf in registrationPage.Items)
            {
                PackageSearchMetadataRegistration catalogEntry = registrationLeaf.CatalogEntry;
                NuGetVersion version = catalogEntry.Version;
                bool listed = catalogEntry.IsListed;

                if (range.Satisfies(catalogEntry.Version)
                    && (includePrerelease || !version.IsPrerelease)
                    && (includeUnlisted || listed))
                {
                    catalogEntry.ReportAbuseUrl = _reportAbuseResource?.GetReportAbuseUrl(catalogEntry.PackageId, catalogEntry.Version);
                    catalogEntry.PackageDetailsUrl = _packageDetailsUriResource?.GetUri(catalogEntry.PackageId, catalogEntry.Version);
                    if (string.IsNullOrWhiteSpace(catalogEntry.ReadmeFileUrl))
                    {
                        catalogEntry.ReadmeFileUrl = _readmeUriTemplateResource?.GetReadmeUrl(catalogEntry.PackageId, catalogEntry.Version);
                    }
                    catalogEntry.CacheStrings(metadataCache);
                    results.Add(catalogEntry);
                }
            }
        }
    }
}