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

Introduce equality comparers for OpenXmlElement #1476

Merged
merged 18 commits into from
Jul 9, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
@@ -1,12 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;

namespace DocumentFormat.OpenXml
{
/// <summary>
/// Defines the Markup Compatibility Attributes.
/// </summary>
public class MarkupCompatibilityAttributes
public class MarkupCompatibilityAttributes : IEquatable<MarkupCompatibilityAttributes>
{
/// <summary>
/// Gets or sets a whitespace-delimited list of prefixes, where each
Expand Down Expand Up @@ -42,5 +44,47 @@ public class MarkupCompatibilityAttributes
/// a set of namespace names.
/// </summary>
public StringValue? MustUnderstand { get; set; }

/// <inheritdoc/>
public override bool Equals(object? obj)
{
return this.Equals(obj as MarkupCompatibilityAttributes);
}

/// <inheritdoc/>
public bool Equals(MarkupCompatibilityAttributes? other)
{
if (other == null)
{
return false;
}

if (object.ReferenceEquals(this, other))
{
return true;
}

return (this.Ignorable == null == (other.Ignorable == null)) &&
PhDuck marked this conversation as resolved.
Show resolved Hide resolved
(this.Ignorable == null || this.Ignorable.Equals(other.Ignorable)) &&
(this.ProcessContent == null == (other.ProcessContent == null)) &&
(this.ProcessContent == null || this.ProcessContent.Equals(other.ProcessContent)) &&
(this.PreserveElements == null == (other.PreserveElements == null)) &&
(this.PreserveElements == null || this.PreserveElements.Equals(other.PreserveElements)) &&
(this.PreserveAttributes == null == (other.PreserveAttributes == null)) &&
(this.PreserveAttributes == null || this.PreserveAttributes.Equals(other.PreserveAttributes)) &&
(this.MustUnderstand == null == (other.MustUnderstand == null)) &&
(this.MustUnderstand == null || this.MustUnderstand.Equals(other.MustUnderstand));
PhDuck marked this conversation as resolved.
Show resolved Hide resolved
}

/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(
this.Ignorable?.GetHashCode() ?? 0,
this.ProcessContent?.GetHashCode() ?? 0,
this.PreserveElements?.GetHashCode() ?? 0,
this.PreserveAttributes?.GetHashCode() ?? 0,
this.MustUnderstand?.GetHashCode() ?? 0);
}
}
}
221 changes: 218 additions & 3 deletions src/DocumentFormat.OpenXml.Framework/OpenXmlElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ namespace DocumentFormat.OpenXml
/// <remarks>
/// Annotations will not be cloned when calling <see cref="Clone"/> and <see cref="CloneNode(bool)"/>.
/// </remarks>
public abstract partial class OpenXmlElement : IEnumerable<OpenXmlElement>, ICloneable
public abstract partial class OpenXmlElement : IEnumerable<OpenXmlElement>, ICloneable, IEquatable<OpenXmlElement>
{
private IFeatureCollection? _features;

Expand Down Expand Up @@ -280,7 +280,7 @@ public bool HasAttributes
/// <summary>
/// Gets all extended attributes (attributes not defined in the schema) of the current element.
/// </summary>
public IEnumerable<OpenXmlAttribute> ExtendedAttributes
public IReadOnlyList<OpenXmlAttribute> ExtendedAttributes
PhDuck marked this conversation as resolved.
Show resolved Hide resolved
{
get
{
Expand All @@ -291,7 +291,7 @@ public IEnumerable<OpenXmlAttribute> ExtendedAttributes
}
else
{
return Enumerable.Empty<OpenXmlAttribute>();
PhDuck marked this conversation as resolved.
Show resolved Hide resolved
return Array.Empty<OpenXmlAttribute>();
}
}
}
Expand Down Expand Up @@ -473,6 +473,221 @@ internal set

#region public methods

private static bool MoveNextAndTrackCount(ref OpenXmlElementList.Enumerator e1, ref OpenXmlElementList.Enumerator e2, ref int e1ctr, ref int e2ctr)
PhDuck marked this conversation as resolved.
Show resolved Hide resolved
{
if (e1.MoveNext())
{
e1ctr++;
if (e2.MoveNext())
{
e2ctr++;
return true;
}
}
else if (e2.MoveNext())
{
e2ctr++;
}

return false;
}

private bool PrefixAndQNameEqual(OpenXmlElement other)
{
OpenXmlQualifiedName tQName = this.ParsedState.Metadata.QName;
OpenXmlQualifiedName oQName = other.ParsedState.Metadata.QName;

// TODO: If we can just compare Namespace.URI instead of prefix comparsion, then we can save quite a bit here.
// Please verify if that is legal.
if (!tQName.Equals(oQName))
{
return false;
}

string turi = tQName.Namespace.Uri;
string ouri = oQName.Namespace.Uri;

// TODO: Check with Taylor if we can somehow be smart about this, since it is the most expensive part.
// Maybe we can just validate Namespace.URI is the same for both, avoiding the prefix lookup.
var tPrefix = this.LookupPrefixLocal(ouri);
var oPrefix = other.LookupPrefixLocal(ouri);

if (string.IsNullOrEmpty(tPrefix))
{
tPrefix = this.Features.GetNamespaceResolver().LookupPrefix(turi);
}

if (string.IsNullOrEmpty(oPrefix))
{
oPrefix = other.Features.GetNamespaceResolver().LookupPrefix(ouri);
}

return string.Equals(tPrefix, oPrefix, StringComparison.Ordinal);
}

/// <inheritdoc/>
public override bool Equals(object? obj)
{
return this.Equals(obj as OpenXmlElement);
}

/// <inheritdoc/>
public bool Equals(OpenXmlElement? other)
{
if (other == null)
{
return false;
}

if (object.ReferenceEquals(this, other))
{
return true;
}

if (this.GetType() != other.GetType())
{
return false;
}

if (!this.XmlParsed && !other.XmlParsed)
{
return string.Equals(this.RawOuterXml, other.RawOuterXml, StringComparison.Ordinal);
}

this.MakeSureParsed();
other.MakeSureParsed();

if (this.HasChildren != other.HasChildren)
{
return false;
}

var thisHasAttributes = this.HasAttributes;
var otherHasAttributes = other.HasAttributes;
if (thisHasAttributes != otherHasAttributes)
{
return false;
}

if (thisHasAttributes && this.ParsedState.Attributes.Length != other.ParsedState.Attributes.Length)
{
return false;
}

// TODO: This seems strange that this is the only one that requires special behaviour, verify with Taylor.
if (this is OpenXmlLeafTextElement)
{
if (!string.Equals(this.InnerText, other.InnerText, StringComparison.Ordinal))
{
return false;
}
}

if (!this.PrefixAndQNameEqual(other))
{
return false;
}

if (this.HasChildren)
{
// DEVNOTE: Do not refactor this to use "simpler" for construct.
// The indexer on ChildElement walks the linked list for each operation,
// not maintaining state so it turns into a O(n^2) operation.
OpenXmlElementList.Enumerator tChilds = this.ChildElements.GetEnumerator();
OpenXmlElementList.Enumerator oChilds = other.ChildElements.GetEnumerator();

int e1 = 0, e2 = 0;
while (MoveNextAndTrackCount(ref tChilds, ref oChilds, ref e1, ref e2))
{
if (!tChilds.Current.Equals(oChilds.Current))
{
return false;
}
}

// Different amount of children.
if (e1 != e2)
{
return false;
}
}

if (thisHasAttributes)
{
if (this.ExtendedAttributes.Count != other.ExtendedAttributes.Count)
{
return false;
}

for (int i = 0; i < this.ExtendedAttributes.Count; i++)
{
if (!this.ExtendedAttributes[i].Equals(other.ExtendedAttributes[i]))
{
return false;
}
}

for (int i = 0; i < this.ParsedState.Attributes.Length; i++)
{
var tAttr = this.ParsedState.Attributes[i];
var oAttr = other.ParsedState.Attributes[i];

if ((tAttr.Value == null && oAttr.Value != null) || (tAttr.Value != null && oAttr.Value == null))
{
return false;
}

if (tAttr.Value == null)
{
continue;
}

// TODO: Figure out if this assumption is valid.
// looking at it it seems that these are all OpenXmlComparableSimpleValue types, which supports IEqutable.
if (!tAttr.Value.Equals(oAttr.Value))
{
return false;
}
}

if (this.MCAttributes == null != (other.MCAttributes == null) || (this.MCAttributes != null && !this.MCAttributes.Equals(other.MCAttributes)))
{
return false;
}
}

return true;
}

/// <inheritdoc/>
public override int GetHashCode()
{
int hc = this.MCAttributes?.GetHashCode() ?? 0;

for (int i = 0; i < this.ParsedState.Attributes.Length; i++)
{
if (this.ParsedState.Attributes[i].Value != null)
{
hc = HashCode.Combine(hc, this.ParsedState.Attributes[i].Value?.GetHashCode() ?? 0);
}
}

foreach (var attr in this.ExtendedAttributes)
{
hc = HashCode.Combine(hc, attr.GetHashCode());
}

if (this.HasChildren)
{
foreach (var child in this.ChildElements)
{
hc = HashCode.Combine(hc, child.GetHashCode());
}
}

return hc;
}

/// <summary>
/// Gets an Open XML attribute with the specified tag name and namespace URI.
/// </summary>
Expand Down
Loading