Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for repeated XML elements without a name attribute #44608

Merged
merged 23 commits into from
Mar 10, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
01401f0
Add support for repeated XML elements without a name attribute
amoerie Nov 12, 2020
f83dcfe
Disable tests for duplicate handling in XML config, they are incompat…
amoerie Nov 13, 2020
8de7bda
Stop using the 'Name' parameter to use arrays in XML in the tests, th…
amoerie Nov 13, 2020
96ea0b9
Merge branch 'master' of github.com:dotnet/runtime into repeatedeleme…
amoerie Dec 17, 2020
ca7097a
Rework the XML configuration provider
amoerie Dec 18, 2020
c601197
Add test that verifies mixing repeated with non-repeated XML elements…
amoerie Dec 18, 2020
3dddfca
Cleanup nullable annotations in XmlConfigurationElement
amoerie Feb 5, 2021
5b55a5f
Use ordinal ignore case when detecting siblings in XML configuration
amoerie Feb 5, 2021
cd11a03
Remove empty line
amoerie Feb 5, 2021
ff2336f
Remove usage of Linq method '.Any()'
amoerie Feb 5, 2021
de44ff0
Remove dependency on System.Linq in XmlStreamConfigurationProvider
amoerie Feb 5, 2021
2f77e01
Simplify check that detects whether the current element is the root e…
amoerie Feb 5, 2021
5999c1c
Merge branch 'master' of github.com:dotnet/runtime into repeatedeleme…
amoerie Feb 5, 2021
9a1af2a
Apply suggestions from code review
amoerie Feb 8, 2021
6f07563
Add test for array simulation using Name attribute
amoerie Feb 8, 2021
e479aa9
Add tests related to case insensitivity in XML configuration keys
amoerie Feb 8, 2021
f2e56ee
Merge master and resolve conflicts
amoerie Feb 8, 2021
8901143
Improve performance of XML configuration provider
amoerie Feb 28, 2021
862244e
Revert accidental change of solution file
amoerie Mar 1, 2021
49024de
Merge branch 'main' into repeatedelementsinxmlconfig
amoerie Mar 1, 2021
2c2f1b3
Apply suggestion from feedback: simplify children list initialization
amoerie Mar 10, 2021
22c02e8
Rename ProcessAttributes -> ReadAttributes when parsing the XML
amoerie Mar 10, 2021
5a60973
Merge branch 'main' of github.com:dotnet/runtime into repeatedelement…
amoerie Mar 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Extensions.Configuration.Xml
{
/// <summary>
/// Represents a configuration value that was parsed from an XML source
/// </summary>
internal interface IXmlConfigurationValue
{
string Key { get; }
string Value { get; }
string LineInfo { get; }
amoerie marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Microsoft.Extensions.Configuration.Xml
{
internal class XmlConfigurationElement
{
public string ElementName { get; }

public string Name { get; }

public string LineInfo { get; }

public bool Multiple { get; set; }

public int Index { get; set; }

/// <summary>
/// The parent element, or null if this is the root element
/// </summary>
public XmlConfigurationElement Parent { get; }

public XmlConfigurationElement(XmlConfigurationElement parent, string elementName, string name, string lineInfo)
{
Parent = parent;
ElementName = elementName ?? throw new ArgumentNullException(nameof(elementName));
Name = name;
LineInfo = lineInfo;
}

public string Key
{
get
{
var tokens = new List<string>(3);

// the root element does not contribute to the prefix
if (Parent != null) tokens.Add(ElementName);

// the name attribute always contributes to the prefix
if (Name != null) tokens.Add(Name);

// the index only contributes to the prefix when there are multiple elements wih the same name
if (Multiple) tokens.Add(Index.ToString());

// the root element without a name attribute does not contribute to prefix at all
if (!tokens.Any()) return null;
safern marked this conversation as resolved.
Show resolved Hide resolved

return string.Join(ConfigurationPath.KeyDelimiter, tokens);
}
}

public bool IsSibling(XmlConfigurationElement xmlConfigurationElement)
{
if (xmlConfigurationElement is null)
amoerie marked this conversation as resolved.
Show resolved Hide resolved
{
throw new ArgumentNullException(nameof(xmlConfigurationElement));
}

return Parent != null
&& xmlConfigurationElement.Parent == Parent
&& string.Equals(ElementName, xmlConfigurationElement.ElementName)
&& string.Equals(Name, xmlConfigurationElement.Name);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;

namespace Microsoft.Extensions.Configuration.Xml
{
internal class XmlConfigurationElementAttributeValue : IXmlConfigurationValue
{
private readonly XmlConfigurationElement[] _elementPath;
private readonly string _attribute;

public XmlConfigurationElementAttributeValue(Stack<XmlConfigurationElement> elementPath, string attribute, string value, string lineInfo)
{
_elementPath = elementPath?.Reverse()?.ToArray() ?? throw new ArgumentNullException(nameof(elementPath));
_attribute = attribute ?? throw new ArgumentNullException(nameof(attribute));
Value = value ?? throw new ArgumentNullException(nameof(value));
LineInfo = lineInfo;
}

/// <summary>
/// Combines the path to this element with the attribute value to produce a key.
/// Note that this property cannot be computed during construction,
/// because the keys of the elements along the path may change when multiple elements with the same name are encountered
/// </summary>
public string Key => ConfigurationPath.Combine(_elementPath.Select(e => e.Key).Concat(new[] { _attribute }).Where(key => key != null));

public string Value { get; }

public string LineInfo { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;

namespace Microsoft.Extensions.Configuration.Xml
{
internal class XmlConfigurationElementContent
: IXmlConfigurationValue
{
private readonly XmlConfigurationElement[] _elementPath;

public XmlConfigurationElementContent(Stack<XmlConfigurationElement> elementPath, string content, string lineInfo)
{
Value = content ?? throw new ArgumentNullException(nameof(content));
LineInfo = lineInfo ?? throw new ArgumentNullException(nameof(lineInfo));
_elementPath = elementPath?.Reverse().ToArray() ?? throw new ArgumentNullException(nameof(elementPath));
safern marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Combines the path to this element to produce a key.
/// Note that this property cannot be computed during construction,
/// because the keys of the elements along the path may change when multiple elements with the same name are encountered
/// </summary>
public string Key => ConfigurationPath.Combine(_elementPath.Select(e => e.Key).Where(key => key != null));

public string Value { get; }

public string LineInfo { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public XmlStreamConfigurationProvider(XmlStreamConfigurationSource source) : bas
/// <returns>The <see cref="IDictionary{String, String}"/> which was read from the stream.</returns>
public static IDictionary<string, string> Read(Stream stream, XmlDocumentDecryptor decryptor)
{
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var configurationValues = new List<IXmlConfigurationValue>();

var readerSettings = new XmlReaderSettings()
{
Expand All @@ -42,57 +42,62 @@ public static IDictionary<string, string> Read(Stream stream, XmlDocumentDecrypt

using (XmlReader reader = decryptor.CreateDecryptingXmlReader(stream, readerSettings))
{
var prefixStack = new Stack<string>();

SkipUntilRootElement(reader);
// record all elements we encounter to check for repeated elements
var allElements = new List<XmlConfigurationElement>();

// We process the root element individually since it doesn't contribute to prefix
ProcessAttributes(reader, prefixStack, data, AddNamePrefix);
ProcessAttributes(reader, prefixStack, data, AddAttributePair);
// keep track of the tree we followed to get where we are (breadcrumb style)
var currentPath = new Stack<XmlConfigurationElement>();

XmlNodeType preNodeType = reader.NodeType;
while (reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
prefixStack.Push(reader.LocalName);
ProcessAttributes(reader, prefixStack, data, AddNamePrefix);
ProcessAttributes(reader, prefixStack, data, AddAttributePair);
XmlConfigurationElement parent = currentPath.Any() ? currentPath.Peek() : null;

var element = new XmlConfigurationElement(parent, reader.LocalName, GetName(reader), GetLineInfo(reader));

// check if this element has appeared before
XmlConfigurationElement sibling = allElements.Where(e => e.IsSibling(element)).OrderByDescending(e => e.Index).FirstOrDefault();
if (sibling != null)
{
sibling.Multiple = element.Multiple = true;
element.Index = sibling.Index + 1;
}

currentPath.Push(element);
allElements.Add(element);

ProcessAttributes(reader, currentPath, configurationValues);

// If current element is self-closing
if (reader.IsEmptyElement)
{
prefixStack.Pop();
currentPath.Pop();
}
break;

case XmlNodeType.EndElement:
if (prefixStack.Any())
if (currentPath.Any())
{
// If this EndElement node comes right after an Element node,
// it means there is no text/CDATA node in current element
if (preNodeType == XmlNodeType.Element)
{
string key = ConfigurationPath.Combine(prefixStack.Reverse());
data[key] = string.Empty;
var configurationValue = new XmlConfigurationElementContent(currentPath, string.Empty, GetLineInfo(reader));
configurationValues.Add(configurationValue);
}

prefixStack.Pop();
currentPath.Pop();
}
break;

case XmlNodeType.CDATA:
case XmlNodeType.Text:
{
string key = ConfigurationPath.Combine(prefixStack.Reverse());
if (data.ContainsKey(key))
{
throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key,
GetLineInfo(reader)));
}

data[key] = reader.Value;
var configurationValue = new XmlConfigurationElementContent(currentPath, reader.Value, GetLineInfo(reader));
configurationValues.Add(configurationValue);
break;
}
case XmlNodeType.XmlDeclaration:
Expand All @@ -107,6 +112,7 @@ public static IDictionary<string, string> Read(Stream stream, XmlDocumentDecrypt
GetLineInfo(reader)));
}
preNodeType = reader.NodeType;

// If this element is a self-closing element,
// we pretend that we just processed an EndElement node
// because a self-closing element contains an end within itself
Expand All @@ -117,6 +123,19 @@ public static IDictionary<string, string> Read(Stream stream, XmlDocumentDecrypt
}
}
}

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

foreach (var configurationValue in configurationValues)
{
var key = configurationValue.Key;
if (data.ContainsKey(key))
{
throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key, configurationValue.LineInfo));
}
data[key] = configurationValue.Value;
}

return data;
}

Expand All @@ -129,27 +148,14 @@ public override void Load(Stream stream)
Data = Read(stream, XmlDocumentDecryptor.Instance);
}

private static void SkipUntilRootElement(XmlReader reader)
{
while (reader.Read())
{
if (reader.NodeType != XmlNodeType.XmlDeclaration &&
reader.NodeType != XmlNodeType.ProcessingInstruction)
{
break;
}
}
}

private static string GetLineInfo(XmlReader reader)
{
var lineInfo = reader as IXmlLineInfo;
return lineInfo == null ? string.Empty :
SR.Format(SR.Msg_LineInfo, lineInfo.LineNumber, lineInfo.LinePosition);
}

private static void ProcessAttributes(XmlReader reader, Stack<string> prefixStack, IDictionary<string, string> data,
Action<XmlReader, Stack<string>, IDictionary<string, string>, XmlWriter> act, XmlWriter writer = null)
private static void ProcessAttributes(XmlReader reader, Stack<XmlConfigurationElement> elementPath, IList<IXmlConfigurationValue> data)
{
for (int i = 0; i < reader.AttributeCount; i++)
{
Expand All @@ -161,49 +167,38 @@ private static void ProcessAttributes(XmlReader reader, Stack<string> prefixStac
throw new FormatException(SR.Format(SR.Error_NamespaceIsNotSupported, GetLineInfo(reader)));
}

act(reader, prefixStack, data, writer);
data.Add(new XmlConfigurationElementAttributeValue(elementPath, reader.LocalName, reader.Value, GetLineInfo(reader)));
}

// Go back to the element containing the attributes we just processed
reader.MoveToElement();
}

// The special attribute "Name" only contributes to prefix
// This method adds a prefix if current node in reader represents a "Name" attribute
private static void AddNamePrefix(XmlReader reader, Stack<string> prefixStack,
IDictionary<string, string> data, XmlWriter writer)
// This method retrieves the Name of the element, if the attribute is present
// Unfortunately XmlReader.GetAttribute cannot be used, as it does not support looking for attributes in a case insensitive manner
private static string GetName(XmlReader reader)
{
if (!string.Equals(reader.LocalName, NameAttributeKey, StringComparison.OrdinalIgnoreCase))
{
return;
}
string name = null;

// If current element is not root element
if (prefixStack.Any())
{
string lastPrefix = prefixStack.Pop();
prefixStack.Push(ConfigurationPath.Combine(lastPrefix, reader.Value));
}
else
while (reader.MoveToNextAttribute())
{
prefixStack.Push(reader.Value);
if (string.Equals(reader.LocalName, NameAttributeKey, StringComparison.OrdinalIgnoreCase))
{
// If there is a namespace attached to current attribute
if (!string.IsNullOrEmpty(reader.NamespaceURI))
{
throw new FormatException(SR.Format(SR.Error_NamespaceIsNotSupported, GetLineInfo(reader)));
safern marked this conversation as resolved.
Show resolved Hide resolved
}
name = reader.Value;
break;
}
}
}

// Common attributes contribute to key-value pairs
// This method adds a key-value pair if current node in reader represents a common attribute
private static void AddAttributePair(XmlReader reader, Stack<string> prefixStack,
IDictionary<string, string> data, XmlWriter writer)
{
prefixStack.Push(reader.LocalName);
string key = ConfigurationPath.Combine(prefixStack.Reverse());
if (data.ContainsKey(key))
{
throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key, GetLineInfo(reader)));
}
// Go back to the element containing the name we just processed
reader.MoveToElement();

data[key] = reader.Value;
prefixStack.Pop();
return name;
}
}
}
Loading