File: LegacyFeed\V2FeedQueryBuilder.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.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.Packaging.Core;
using NuGet.Protocol.Core.Types;

namespace NuGet.Protocol
{
    /// <summary>
    /// Build the path part of a V2 feed URL. These values are appended to the V2 base URL.
    /// </summary>
    public class V2FeedQueryBuilder
    {
        // shared constants
        private const string IsLatestVersionFilterFlag = "IsLatestVersion";
        private const string IsAbsoluteLatestVersionFilterFlag = "IsAbsoluteLatestVersion";
        private const string IdProperty = "Id";
        private const string SemVerLevel = "semVerLevel=2.0.0";

        // constants for /Packages(ID,VERSION) endpoint
        private const string GetSpecificPackageFormat = "/Packages(Id='{0}',Version='{1}')";

        // constants for /Search() endpoint
        private const string SearchEndpointFormat = "/Search()?{0}{1}searchTerm='{2}'&targetFramework='{3}'&includePrerelease={4}&$skip={5}&$top={6}&" + SemVerLevel;
        private const string QueryDelimiter = "&";

        // constants for /FindPackagesById() endpoint
        private const string FindPackagesByIdFormat = "/FindPackagesById()?id='{0}'&" + SemVerLevel;

        // constants for /Packages() endpoint
        private const string GetPackagesFormat = "/Packages{0}";
        private const string EndpointParenthesis = "()";
        private const string SearchClauseFormat = "({0}%20ne%20null)%20and%20substringof('{1}',tolower({0}))";
        private const string OrFormat = "({0})%20or%20({1})";
        private const string AndFormat = "({0})%20and%20{1}";
        private const string FilterFormat = "$filter={0}";
        private const string OrderByFormat = "$orderby={0}";
        private const string SkipFormat = "$skip={0}";
        private const string TopFormat = "$top={0}";
        private const string TagTermFormat = " {0} ";
        private const string FirstParameterFormat = "?{0}";
        private const string ParameterFormat = "&{0}";
        private const string TagsProperty = "Tags";
        private static readonly string[] _propertiesToSearch = new[]
        {
            IdProperty,
            "Description",
            TagsProperty
        };

        public string BuildSearchUri(
            string searchTerm,
            SearchFilter filters,
            int skip,
            int take)
        {
            var shortFormTargetFramework = string.Join(
                "|",
                filters
                    .SupportedFrameworks
                    .Select(targetFramework => NuGetFramework.Parse(targetFramework).GetShortFolderName()));

            var orderBy = BuildOrderBy(filters.OrderBy);

            var filter = BuildPropertyFilter(filters.Filter);
            if (filter != null)
            {
                filter = string.Format(
                    CultureInfo.InvariantCulture,
                    FilterFormat,
                    filter);
            }

            var uri = string.Format(
                CultureInfo.InvariantCulture,
                SearchEndpointFormat,
                filter != null ? filter + QueryDelimiter : string.Empty,
                orderBy != null ? orderBy + QueryDelimiter : string.Empty,
                UriUtility.UrlEncodeOdataParameter(searchTerm),
                UriUtility.UrlEncodeOdataParameter(shortFormTargetFramework),
                filters.IncludePrerelease.ToString(CultureInfo.CurrentCulture).ToLowerInvariant(),
                skip,
                take);

            return uri;
        }

        public string BuildFindPackagesByIdUri(string id)
        {
            PackageIdValidator.Validate(id);

            var uri = string.Format(
                CultureInfo.InvariantCulture,
                FindPackagesByIdFormat,
                UriUtility.UrlEncodeOdataParameter(id));

            return uri;
        }

        public string BuildGetPackageUri(PackageIdentity package)
        {
            if (package == null)
            {
                throw new ArgumentNullException(nameof(package));
            }

            if (!package.HasVersion)
            {
                throw new ArgumentException(nameof(package.Version));
            }

            PackageIdValidator.Validate(package.Id);

            var uri = string.Format(
                CultureInfo.InvariantCulture,
                GetSpecificPackageFormat,
                UriUtility.UrlEncodeOdataParameter(package.Id),
                UriUtility.UrlEncodeOdataParameter(package.Version.ToNormalizedString()));

            return uri;
        }

        public string BuildGetPackagesUri(
            string searchTerm,
            SearchFilter filters,
            int? skip,
            int? take)
        {
            var filterParameter = BuildFilter(searchTerm, filters.Filter);
            var orderByParameter = BuildOrderBy(filters.OrderBy);
            var skipParameter = BuildSkip(skip);
            var topParameter = BuildTop(take);

            // The parenthesis right after the "/Packages" path in the URL are excluded if the filter, orderby, and
            // top parameters are not used. This is a quirk of the NuGet 2.x implementation.
            var useParenthesis = filterParameter != null || orderByParameter != null || topParameter != null;

            // Start building the URI.
            var builder = new StringBuilder();

            builder.AppendFormat(CultureInfo.InvariantCulture, GetPackagesFormat, useParenthesis ? EndpointParenthesis : string.Empty);

            var hasParameters = false;

            // Append each query parameter.
            if (filterParameter != null)
            {
                builder.AppendFormat(
                    CultureInfo.CurrentCulture,
                    hasParameters ? ParameterFormat : FirstParameterFormat,
                    filterParameter);
                hasParameters = true;
            }

            if (orderByParameter != null)
            {
                builder.AppendFormat(
                    CultureInfo.CurrentCulture,
                    hasParameters ? ParameterFormat : FirstParameterFormat,
                    orderByParameter);
                hasParameters = true;
            }

            if (skipParameter != null)
            {
                builder.AppendFormat(
                    CultureInfo.CurrentCulture,
                    hasParameters ? ParameterFormat : FirstParameterFormat,
                    skipParameter);
                hasParameters = true;
            }

            if (topParameter != null)
            {
                builder.AppendFormat(
                    CultureInfo.CurrentCulture,
                    hasParameters ? ParameterFormat : FirstParameterFormat,
                    topParameter);
                hasParameters = true;
            }

            builder.AppendFormat(
                CultureInfo.CurrentCulture,
                hasParameters ? ParameterFormat : FirstParameterFormat,
                SemVerLevel);
            hasParameters = true;

            return builder.ToString();
        }

        private string BuildTop(int? top)
        {
            if (!top.HasValue)
            {
                return null;
            }

            return string.Format(CultureInfo.InvariantCulture, TopFormat, top);
        }

        private string BuildSkip(int? skip)
        {
            if (!skip.HasValue)
            {
                return null;
            }

            return string.Format(CultureInfo.InvariantCulture, SkipFormat, skip);
        }

        private string BuildFilter(string searchTerm, SearchFilterType? searchFilterType)
        {
            var pieces = new List<string>
            {
                BuildFieldSearchFilter(searchTerm),
                BuildPropertyFilter(searchFilterType)
            }.AsEnumerable();

            pieces = pieces.Where(p => p != null);

            if (!pieces.Any())
            {
                return null;
            }

            var filter = pieces
                .Aggregate((a, b) => string.Format(CultureInfo.InvariantCulture, AndFormat, a, b));

            return string.Format(CultureInfo.InvariantCulture, FilterFormat, filter);
        }

        private string BuildOrderBy(SearchOrderBy? searchOrderBy)
        {
            string orderBy;
            switch (searchOrderBy)
            {
                case SearchOrderBy.Id:
                    orderBy = IdProperty;
                    break;
                case null:
                    orderBy = null;
                    break;
                default:
                    Debug.Fail("Unhandled value of SearchFilterType");
                    orderBy = null;
                    break;
            }

            if (orderBy != null)
            {
                orderBy = string.Format(CultureInfo.InvariantCulture, OrderByFormat, orderBy);
            }

            return orderBy;
        }

        private string BuildPropertyFilter(SearchFilterType? searchFilterType)
        {
            string filter;
            switch (searchFilterType)
            {
                case SearchFilterType.IsLatestVersion:
                    filter = IsLatestVersionFilterFlag;
                    break;
                case SearchFilterType.IsAbsoluteLatestVersion:
                    filter = IsAbsoluteLatestVersionFilterFlag;
                    break;
                case null:
                    filter = null;
                    break;
                default:
                    Debug.Fail("Unhandled value of SearchFilterType");
                    filter = null;
                    break;
            }

            return filter;
        }

        private string BuildFieldSearchFilter(string searchTerm)
        {
            if (searchTerm == null)
            {
                return null;
            }

            var searchTerms = searchTerm.Split();

            var clauses =
                from term in searchTerms
                from property in _propertiesToSearch
                select BuildFieldSearchClause(term, property);

            var fieldSearch = clauses
                .Aggregate((a, b) => string.Format(CultureInfo.InvariantCulture, OrFormat, a, b));

            return fieldSearch;
        }

        private string BuildFieldSearchClause(string term, string property)
        {
            if (property == TagsProperty)
            {
                term = string.Format(CultureInfo.InvariantCulture, TagTermFormat, term);
            }

            return string.Format(
                CultureInfo.InvariantCulture,
                SearchClauseFormat,
                property,
                UriUtility.UrlEncodeOdataParameter(term));
        }
    }
}