File: LegacyFeed\LegacyFeedCapabilityResourceV2Feed.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.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using NuGet.Common;
using NuGet.Protocol.Core.Types;

namespace NuGet.Protocol
{
    /// <summary>
    /// A resource for detecting a V2 feed's capabilities based on the metadata document.
    /// </summary>
    public class LegacyFeedCapabilityResourceV2Feed : LegacyFeedCapabilityResource
    {
        private static readonly ConcurrentDictionary<string, Task<Capabilities>> CachedCapabilities
            = new ConcurrentDictionary<string, Task<Capabilities>>();

        private const string MetadataUriFormat = "{0}/$metadata";

        private readonly string _metadataUri;
        private readonly V2FeedParser _feedParser;

        public LegacyFeedCapabilityResourceV2Feed(V2FeedParser feedParser, string baseAddress)
        {
            _feedParser = feedParser;
            _metadataUri = string.Format(
                CultureInfo.InvariantCulture,
                MetadataUriFormat,
                baseAddress);
        }

        public override async Task<bool> SupportsIsAbsoluteLatestVersionAsync(ILogger log, CancellationToken token)
        {
            var capabilities = await GetCachedCapabilitiesAsync(log, token);

            return capabilities.SupportsIsAbsoluteLatestVersion;
        }

        public override async Task<bool> SupportsSearchAsync(ILogger log, CancellationToken token)
        {
            var capabilities = await GetCachedCapabilitiesAsync(log, token);

            return capabilities.SupportsSearch;
        }

        private async Task<Capabilities> GetCachedCapabilitiesAsync(ILogger log, CancellationToken token)
        {
            var task = CachedCapabilities.GetOrAdd(
                _metadataUri,
                key => GetCapabilitiesAsync(key, log, token));

            return await task;
        }

        private async Task<Capabilities> GetCapabilitiesAsync(string metadataUri, ILogger log, CancellationToken token)
        {
            var capabilities = new Capabilities
            {
                SupportsIsAbsoluteLatestVersion = true,
                SupportsSearch = true
            };

            XDocument document;
            try
            {
                document = await _feedParser.LoadXmlAsync(
                    metadataUri,
                    cacheKey: null,
                    ignoreNotFounds: true,
                    sourceCacheContext: null,
                    log: log,
                    token: token);

                var metadata = DataServiceMetadataExtractor.Extract(document);

                capabilities.SupportsIsAbsoluteLatestVersion = metadata
                    .SupportedProperties
                    .Contains("IsAbsoluteLatestVersion");

                capabilities.SupportsSearch = metadata
                    .SupportedMethodNames
                    .Contains("Search");
            }
            catch
            {
                // If there is a failure getting the metadata, assume default capabilities.
            }

            return capabilities;
        }

        private class Capabilities
        {
            public string MetadataUri { get; set; }
            public bool SupportsIsAbsoluteLatestVersion { get; set; }
            public bool SupportsSearch { get; set; }
        }

        private class DataServiceMetadata
        {
            public HashSet<string> SupportedMethodNames { get; set; }

            public HashSet<string> SupportedProperties { get; set; }
        }

        /// <summary>
        /// This implementation is copied from NuGet 2.x.
        /// </summary>
        private static class DataServiceMetadataExtractor
        {
            public static DataServiceMetadata Extract(XDocument schemaDocument)
            {
                // Get all entity containers
                var entityContainers = from e in schemaDocument.Descendants()
                                       where e.Name.LocalName == "EntityContainer"
                                       select e;

                // Find the entity container with the Packages entity set
                var result = (from e in entityContainers
                              let entitySet = e.Elements().FirstOrDefault(el => el.Name.LocalName == "EntitySet")
                              let name = entitySet != null ? entitySet.Attribute("Name").Value : null
                              where name != null && name.Equals("Packages", StringComparison.OrdinalIgnoreCase)
                              select new { Container = e, EntitySet = entitySet }).FirstOrDefault();

                if (result == null)
                {
                    return null;
                }

                var packageEntityContainer = result.Container;
                var packageEntityTypeAttribute = result.EntitySet.Attribute("EntityType");

                string packageEntityName = null;
                if (packageEntityTypeAttribute != null)
                {
                    packageEntityName = packageEntityTypeAttribute.Value;
                }

                var methodNames =
                    from e in packageEntityContainer.Elements()
                    where e.Name.LocalName == "FunctionImport"
                    select e.Attribute("Name").Value;

                var metadata = new DataServiceMetadata
                {
                    SupportedMethodNames = new HashSet<string>(
                        methodNames,
                        StringComparer.OrdinalIgnoreCase),

                    SupportedProperties = new HashSet<string>(
                        ExtractSupportedProperties(schemaDocument, packageEntityName),
                        StringComparer.OrdinalIgnoreCase)
                };

                return metadata;
            }

            private static IEnumerable<string> ExtractSupportedProperties(
                XDocument schemaDocument,
                string packageEntityName)
            {
                packageEntityName = TrimNamespace(packageEntityName);

                var packageEntity = (from e in schemaDocument.Descendants()
                                     where e.Name.LocalName == "EntityType"
                                     let attribute = e.Attribute("Name")
                                     where attribute != null && attribute.Value.Equals(
                                         packageEntityName,
                                         StringComparison.OrdinalIgnoreCase)
                                     select e).FirstOrDefault();

                if (packageEntity != null)
                {
                    return from e in packageEntity.Elements()
                           where e.Name.LocalName == "Property"
                           select e.Attribute("Name").Value;
                }

                return Enumerable.Empty<string>();
            }

            private static string TrimNamespace(string packageEntityName)
            {
                var lastIndex = packageEntityName.LastIndexOf('.');
                if (lastIndex > 0 && lastIndex < packageEntityName.Length)
                {
                    packageEntityName = packageEntityName.Substring(lastIndex + 1);
                }
                return packageEntityName;
            }
        }
    }
}