File: Settings\SettingElement.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;

namespace NuGet.Configuration
{
    public abstract class SettingElement : SettingBase
    {
        /// <summary>
        /// Text that differentiates element tag
        /// </summary>
        public abstract string ElementName { get; }

        /// <summary>
        /// Specifies the keys for the attributes that the element can have
        /// </summary>
        /// <remarks>If null then all attributes are allowed</remarks>
        protected virtual IReadOnlyCollection<string>? AllowedAttributes { get; } = null;

        /// <summary>
        /// Specifies the keys for the attributes that the element should have
        /// </summary>
        /// <remarks>If null or empty then no attributes are required</remarks>
        protected virtual IReadOnlyCollection<string>? RequiredAttributes { get; } = null;

        /// <summary>
        /// Specifies which values are allowed for a specific attribute.
        /// If an attribute is not defined every value is allowed.
        /// Having allowed values does not imply that the attribute is required.
        /// </summary>
        protected virtual IReadOnlyDictionary<string, IReadOnlyCollection<string>>? AllowedValues { get; } = null;

        /// <summary>
        /// Specifies values that are explicitely disallowed for a specific attribute.
        /// If an attribute is not defined no value is disallowed.
        /// Having disallowed values does not imply that the attribute is required.
        /// </summary>
        protected virtual IReadOnlyDictionary<string, IReadOnlyCollection<string>>? DisallowedValues { get; } = null;

        /// <summary>
        ///  Key-value pairs that give more information about the element
        /// </summary>
        protected Dictionary<string, string> MutableAttributes { get; }

        /// <summary>
        /// Read only key-value pairs that give more information about the element
        /// </summary>
        internal IReadOnlyDictionary<string, string> Attributes => MutableAttributes;

        /// <summary>
        /// Specifies if the element is empty.
        /// Each element defines its own definition of empty.
        /// The default definition of empty is an element without attributes.
        /// </summary>
        public override bool IsEmpty() => !Attributes.Any();

        /// <summary>
        /// Settings file path file of the element.
        /// </summary>
        public string? ConfigPath => Origin?.ConfigFilePath;

        /// <summary>
        /// Default constructor
        /// </summary>
        protected SettingElement()
            : base()
        {
            MutableAttributes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        }

        protected SettingElement(IReadOnlyDictionary<string, string>? attributes)
            : this()
        {
            if (attributes != null)
            {
                foreach (var attribute in attributes)
                {
                    if (IsAttributeValid(attribute.Key, attribute.Value))
                    {
                        MutableAttributes.Add(attribute.Key, attribute.Value);
                    }
                    else
                    {
                        throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.Error_InvalidAttribute, attribute.Key, attribute.Value));
                    }
                }
            }
        }

        /// <summary>
        /// Constructor used when element is read from a file
        /// </summary>
        /// <param name="element">Xelement read from XML file document tree</param>
        /// <param name="origin">Settings file that this element was read from</param>
        internal SettingElement(XElement element, SettingsFile origin)
            : base(element, origin)
        {
            ValidateAttributes(element, origin);

            MutableAttributes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

            foreach (var existingAttribute in element.Attributes())
            {
                MutableAttributes.Add(existingAttribute.Name.LocalName, existingAttribute.Value);
            }
        }

        internal override XNode AsXNode()
        {
            if (Node != null && Node is XElement)
            {
                return Node;
            }

            var element = new XElement(ElementName);

            foreach (var attr in Attributes)
            {
                element.SetAttributeValue(attr.Key, attr.Value);
            }

            return element;
        }

        protected void AddOrUpdateAttribute(string attributeName, string? value)
        {
            if (!UpdateAttribute(attributeName, value))
            {
                if (value != null)
                {
                    AddAttribute(attributeName, value);
                }
            }
        }

        internal bool UpdateAttribute(string attributeName, string? newValue)
        {
            if (string.IsNullOrEmpty(attributeName))
            {
                throw new ArgumentException(Resources.Argument_Cannot_Be_Null_Or_Empty, nameof(attributeName));
            }

            if (!IsAttributeValid(attributeName, newValue))
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.Error_InvalidAttribute, attributeName, newValue));
            }

            if (Attributes.ContainsKey(attributeName))
            {
                if (newValue == null)
                {
                    MutableAttributes.Remove(attributeName);
                }
                else
                {
                    MutableAttributes[attributeName] = newValue;
                }

                return true;
            }

            return false;
        }

        protected void AddAttribute(string attributeName, string value)
        {
            if (string.IsNullOrEmpty(attributeName))
            {
                throw new ArgumentException(Resources.Argument_Cannot_Be_Null_Or_Empty, nameof(attributeName));
            }

            value = value ?? string.Empty;

            if (!IsAttributeValid(attributeName, value))
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.Error_InvalidAttribute, attributeName, value));
            }

            if (!Attributes.ContainsKey(attributeName))
            {
                MutableAttributes[attributeName] = value;
            }
        }

        private bool IsAttributeValid(string attributeName, string? value)
        {
            if (AllowedAttributes != null)
            {
                // No attributes are allowed
                if (!AllowedAttributes.Any())
                {
                    return false;
                }

                if (!AllowedAttributes.Contains(attributeName))
                {
                    return false;
                }
            }

            if (RequiredAttributes != null)
            {
                if (value == null && RequiredAttributes.Contains(attributeName))
                {
                    // Don't delete any required attributes
                    return false;
                }
            }

            if (AllowedValues != null)
            {
                if (AllowedValues.TryGetValue(attributeName, out var allowed) && (value is null || !allowed.Contains(value.Trim())))
                {
                    return false;
                }
            }

            if (DisallowedValues != null)
            {
                if (DisallowedValues.TryGetValue(attributeName, out var disallowed) && value is not null && disallowed.Contains(value.Trim()))
                {
                    return false;
                }
            }

            return true;
        }

        private void ValidateAttributes(XElement element, SettingsFile origin)
        {
            if (AllowedAttributes != null)
            {
                if (!AllowedAttributes.Any() && element.HasAttributes)
                {
                    throw new NuGetConfigurationException(string.Format(CultureInfo.CurrentCulture, Resources.UserSettings_UnableToParseConfigFile,
                        string.Format(CultureInfo.CurrentCulture, Resources.NoAttributesAllowed, element.Name.LocalName, element.Attributes().Count()),
                        origin.ConfigFilePath));
                }

                foreach (var attribute in element.Attributes())
                {
                    if (!AllowedAttributes.Contains(attribute.Name.LocalName))
                    {
                        throw new NuGetConfigurationException(string.Format(CultureInfo.CurrentCulture, Resources.UserSettings_UnableToParseConfigFile,
                            string.Format(CultureInfo.CurrentCulture, Resources.AttributeNotAllowed, attribute.Name.LocalName, element.Name.LocalName),
                            origin.ConfigFilePath));
                    }
                }
            }

            if (RequiredAttributes != null)
            {
                foreach (var requireAttribute in RequiredAttributes)
                {
                    var attribute = element.Attribute(requireAttribute);
                    if (attribute == null)
                    {
                        throw new NuGetConfigurationException(string.Format(CultureInfo.CurrentCulture, Resources.UserSettings_UnableToParseConfigFile,
                            string.Format(CultureInfo.CurrentCulture, Resources.MissingRequiredAttribute, requireAttribute, element.Name.LocalName),
                            origin.ConfigFilePath));
                    }
                }
            }

            if (AllowedValues != null)
            {
                foreach (var attributeValues in AllowedValues)
                {
                    var attribute = element.Attribute(attributeValues.Key);
                    if (attribute != null && !attributeValues.Value.Contains(attribute.Value.Trim()))
                    {
                        throw new NuGetConfigurationException(string.Format(CultureInfo.CurrentCulture, Resources.UserSettings_UnableToParseConfigFile,
                              string.Format(CultureInfo.CurrentCulture, Resources.AttributeValueNotAllowed, attribute.Name.LocalName, attribute.Value.Trim(), element.Name.LocalName),
                            origin.ConfigFilePath));
                    }
                }
            }

            if (DisallowedValues != null)
            {
                foreach (var attributeValues in DisallowedValues)
                {
                    var attribute = element.Attribute(attributeValues.Key);
                    if (attribute != null && attributeValues.Value.Contains(attribute.Value.Trim()))
                    {
                        throw new NuGetConfigurationException(string.Format(CultureInfo.CurrentCulture, Resources.UserSettings_UnableToParseConfigFile,
                            string.Format(CultureInfo.CurrentCulture, Resources.AttributeValueNotAllowed, attribute.Name.LocalName, attribute.Value.Trim(), element.Name.LocalName),
                            origin.ConfigFilePath));
                    }
                }
            }
        }
    }
}