File: Settings\Items\PackageSourceMappingSourceItem.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Configuration\NuGet.Configuration.csproj (NuGet.Configuration)
// 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.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using NuGet.Shared;

namespace NuGet.Configuration
{
    /// <summary>
    /// A PackageSourceMappingSourceItem has only a key and at least 1 <see cref="PackagePatternItem"/> child item.
    ///     - [Required] Key
    /// </summary>
    public class PackageSourceMappingSourceItem : SettingItem
    {
        protected override bool CanHaveChildren => true;

        /// <summary>
        /// List of package pattern items part of this package source element.
        /// </summary>
        public IList<PackagePatternItem> Patterns { get; }

        public override string ElementName => ConfigurationConstants.PackageSourceAttribute;

        /// <summary>
        /// Each PackageSourceMappingSourceItem item needs a key.
        /// The key should correspond a package source key.
        /// </summary>
        public virtual string Key => Attributes[ConfigurationConstants.KeyAttribute];

        protected override IReadOnlyCollection<string> RequiredAttributes { get; } = new HashSet<string>(new[] { ConfigurationConstants.KeyAttribute });

        protected void SetKey(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.PropertyCannotBeNullOrEmpty, nameof(Key)));
            }

            UpdateAttribute(ConfigurationConstants.KeyAttribute, value);
        }

        /// <summary>
        /// Creates a package source mapping source item with the given name, which equals the key and non-empty list of package patters items.
        /// </summary>
        /// <param name="name">A non-empty name of the item which corresponds a package source name.</param>
        /// <param name="packagePatternItems">A non empty list of package pattern items.</param>
        /// <exception cref="ArgumentException">If <paramref name="name"/> is null or empty, or <paramref name="packagePatternItems"/> is null or empty.</exception>
        public PackageSourceMappingSourceItem(string name, IEnumerable<PackagePatternItem> packagePatternItems)
            : base()
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentException(Resources.Argument_Cannot_Be_Null_Or_Empty, nameof(name));
            }

            if (packagePatternItems == null || !packagePatternItems.Any())
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Error_ItemNeedsAtLeastOnePackagePattern, name));
            }

            AddAttribute(ConfigurationConstants.KeyAttribute, name);

            Patterns = new List<PackagePatternItem>();

            foreach (PackagePatternItem patternItem in packagePatternItems)
            {
                Patterns.Add(patternItem);
            }
        }

        internal PackageSourceMappingSourceItem(XElement element, SettingsFile origin)
            : base(element, origin)
        {
            var parsedDescendants = element.Nodes().Where(n => n is XElement || n is XText text && !string.IsNullOrWhiteSpace(text.Value))
                .Select(e => SettingFactory.Parse(e, origin));

            var parsedPackagePatternItems = parsedDescendants.OfType<PackagePatternItem>().ToList();

            if (parsedPackagePatternItems.Count == 0)
            {
                throw new NuGetConfigurationException(string.Format(CultureInfo.CurrentCulture, Resources.Error_ItemNeedsAtLeastOnePackagePatternWithPath, Key, origin.ConfigFilePath));
            }

            Patterns = parsedPackagePatternItems;
        }

        internal override void SetOrigin(SettingsFile origin)
        {
            base.SetOrigin(origin);

            foreach (PackagePatternItem packagePatternItem in Patterns)
            {
                packagePatternItem.SetOrigin(origin);
            }
        }

        internal override void RemoveFromSettings()
        {
            base.RemoveFromSettings();

            foreach (PackagePatternItem @namespace in Patterns)
            {
                @namespace.RemoveFromSettings();
            }
        }

        public override SettingBase Clone()
        {
            var newItem = new PackageSourceMappingSourceItem(
                Key,
                Patterns.Select(c => (PackagePatternItem)c.Clone()).ToArray());

            if (Origin != null)
            {
                newItem.SetOrigin(Origin);
            }

            return newItem;
        }

        internal override XNode AsXNode()
        {
            if (Node is XElement)
            {
                return Node;
            }

            var element = new XElement(ElementName);

            foreach (PackagePatternItem packagePatternItem in Patterns)
            {
                element.Add(packagePatternItem.AsXNode());
            }

            foreach (KeyValuePair<string, string> attr in Attributes)
            {
                element.SetAttributeValue(attr.Key, attr.Value);
            }

            return element;
        }

        internal override void Update(SettingItem other)
        {
            var packageSourceMappingSourceItem = (PackageSourceMappingSourceItem)other;

            if (!packageSourceMappingSourceItem.Patterns.Any())
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.Error_ItemNeedsAtLeastOnePackagePattern, packageSourceMappingSourceItem.Key));
            }

            base.Update(other);

            Dictionary<PackagePatternItem, PackagePatternItem> otherPatterns = packageSourceMappingSourceItem.Patterns.GroupBy(item => item).ToDictionary(c => c.Key, c => c.Key);
            var clonedPatterns = new List<PackagePatternItem>(Patterns);
            foreach (PackagePatternItem packagePatternItem in clonedPatterns)
            {
                if (otherPatterns.TryGetValue(packagePatternItem, out PackagePatternItem? otherChild))
                {
                    otherPatterns.Remove(packagePatternItem);
                }

                if (otherChild == null)
                {
                    Patterns.Remove(packagePatternItem);
                    packagePatternItem.RemoveFromSettings();
                }
                else if (packagePatternItem is SettingItem item)
                {
                    item.Update(otherChild);
                }
            }

            foreach (KeyValuePair<PackagePatternItem, PackagePatternItem> newPackagePatternItem in otherPatterns)
            {
                var itemToAdd = newPackagePatternItem.Value;
                Patterns.Add(itemToAdd);

                if (Origin != null)
                {
                    itemToAdd.SetOrigin(Origin);

                    if (Node != null)
                    {
                        itemToAdd.SetNode(itemToAdd.AsXNode());

                        XElementUtility.AddIndented(Node as XElement, itemToAdd.Node);
                        Origin.IsDirty = true;
                    }
                }
            }
        }

        public override bool Equals(object? other)
        {
            // It is important that equality on checks that the package source mapping source item is for the same `key. The content is not important. 
            // The equality here is used for updating patterns.
            if (other is PackageSourceMappingSourceItem item)
            {
                if (ReferenceEquals(this, item))
                {
                    return true;
                }

                return string.Equals(Key, item.Key, StringComparison.OrdinalIgnoreCase);
            }

            return false;
        }

        public override int GetHashCode()
        {
            var combiner = new HashCodeCombiner();

            combiner.AddStringIgnoreCase(Key);

            return combiner.CombinedHash;
        }
    }
}