From 7052776930c2d698ca23db9b665fa85490a6bfa6 Mon Sep 17 00:00:00 2001 From: samatstarion Date: Sun, 18 Oct 2026 18:58:15 +0200 Subject: [PATCH] [Fix] read ReqIF documents without whitespace between elements; fixes #94 ReadElementContentAsString moves the reader past the end tag. Inside a while (reader.Read()) loop the next Read then skips the following sibling when no whitespace separates the elements, so minified documents lost every other header field and enumeration value, and the spec relations of a relation group were not read or caused an XmlException. - Add XmlReaderExtensions.ReadElementContentAsStringToEndElement(Async), which reads the text content and leaves the reader on the end tag. - Use it in ReqIFHeader, for ENUM-VALUE-REF in AttributeValueEnumeration and for SPEC-RELATION-REF in RelationGroup. - Read SPEC-RELATIONS through a subtree reader. A child other than SPEC-RELATION-REF is skipped with a warning; before, it caused an XmlException. - Add MinifiedDocumentTestFixture and XmlReaderExtensionsTestFixture, and move the shared test-document helpers into TestDocuments. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 + .../MinifiedDocumentTestFixture.cs | 373 ++++++++++++++++++ .../PrefixedNamespaceTestFixture.cs | 134 ++----- ReqIFSharp.Tests/TestDocuments.cs | 127 ++++++ .../XmlReaderExtensionsTestFixture.cs | 201 ++++++++++ .../AttributeValueEnumeration.cs | 4 +- ReqIFSharp/ReqIFHeader.cs | 28 +- .../RelationGroup.cs | 68 +++- ReqIFSharp/XmlReaderExtensions.cs | 131 ++++++ 9 files changed, 930 insertions(+), 138 deletions(-) create mode 100644 ReqIFSharp.Tests/MinifiedDocumentTestFixture.cs create mode 100644 ReqIFSharp.Tests/TestDocuments.cs create mode 100644 ReqIFSharp.Tests/XmlReaderExtensionsTestFixture.cs create mode 100644 ReqIFSharp/XmlReaderExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2637740..1ea700e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,12 @@ The repository ships two NuGet packages that version independently: **ReqIFSharp ### Changed - A `REQ-IF` root element in a namespace other than the ReqIF namespace (`http://www.omg.org/spec/ReqIF/20110401/reqif.xsd`) is no longer read as a ReqIF document; it is skipped and a warning is logged. Before, such an element was read when it had no prefix. A root in no namespace is still read ([#87](https://github.com/STARIONGROUP/ReqIFSharp/issues/87)). +- An element other than `SPEC-RELATION-REF` inside the `SPEC-RELATIONS` of a relation group is now skipped with a warning, and the references after it are read. Before, it caused an `XmlException` ([#94](https://github.com/STARIONGROUP/ReqIFSharp/issues/94)). ### Fixed - A document that binds the ReqIF namespace to a prefix (``) was read as zero documents, and type, source/target and relation-group references in it were not read. Elements are now matched on their local name and namespace ([#87](https://github.com/STARIONGROUP/ReqIFSharp/issues/87)). +- A document without whitespace between its elements (minified XML) lost data without an error: every other header field (such as `CREATION-TIME` and `REQ-IF-TOOL-ID`) and every other enumeration value of an attribute value was skipped, and the spec relations of a relation group were not read or caused an `XmlException` ([#94](https://github.com/STARIONGROUP/ReqIFSharp/issues/94)). ## [10.1.2] - 2026-10-18 diff --git a/ReqIFSharp.Tests/MinifiedDocumentTestFixture.cs b/ReqIFSharp.Tests/MinifiedDocumentTestFixture.cs new file mode 100644 index 0000000..4ee5856 --- /dev/null +++ b/ReqIFSharp.Tests/MinifiedDocumentTestFixture.cs @@ -0,0 +1,373 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2017-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ReqIFSharp.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using System.Xml; + using System.Xml.Linq; + + using Microsoft.Extensions.Logging; + + using NUnit.Framework; + + using ReqIFSharp; + + using Serilog; + using Serilog.Events; + + /// + /// Verifies that a document without whitespace between its elements (minified XML) is read + /// the same as the indented original, see GH-94. + /// + [TestFixture] + public class MinifiedDocumentTestFixture + { + private const string FirstSpecRelationIdentifier = "specobject_1-specobject_2"; + + private const string SecondSpecRelationIdentifier = "second-spec-relation"; + + private const string MinifiedHeaderDocument = + "" + + "" + + "a comment" + + "2017-03-13T10:15:09.017+01:00" + + "a repository" + + "a tool" + + "1.0" + + "a source tool" + + "a title" + + "" + + "" + + ""; + + private const string MinifiedHeaderDocumentWithEmptyElements = + "" + + "" + + "" + + "2017-03-13T10:15:09.017+01:00" + + "" + + "a tool" + + "1.0" + + "a source tool" + + "a title" + + "" + + "" + + ""; + + [Test] + public void Verify_that_every_header_field_of_a_minified_document_is_read() + { + var reqif = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(MinifiedHeaderDocument, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).Single(); + + AssertHeader(reqif.TheHeader); + } + + [Test] + public async Task Verify_that_every_header_field_of_a_minified_document_is_read_async() + { + var reqif = (await new ReqIFDeserializer().DeserializeAsync(TestDocuments.ToStream(MinifiedHeaderDocument, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif, CancellationToken.None)).Single(); + + AssertHeader(reqif.TheHeader); + } + + [TestCaseSource(typeof(TestDocuments), nameof(TestDocuments.ComparableFiles))] + public void Verify_that_a_minified_document_is_read_the_same_as_the_original(string fileName) + { + var original = TestDocuments.Read(fileName); + var minified = CreateMinifiedCopy(original); + + foreach (var kind in TestDocuments.Kinds) + { + var deserializer = new ReqIFDeserializer(); + + var expected = deserializer.Deserialize(TestDocuments.ToStream(original, kind), kind).ToList(); + var actual = deserializer.Deserialize(TestDocuments.ToStream(minified, kind), kind).ToList(); + + TestDocuments.AssertSameGraph(expected, actual); + } + } + + [TestCaseSource(typeof(TestDocuments), nameof(TestDocuments.ComparableFiles))] + public async Task Verify_that_a_minified_document_is_read_the_same_as_the_original_async(string fileName) + { + var original = TestDocuments.Read(fileName); + var minified = CreateMinifiedCopy(original); + + foreach (var kind in TestDocuments.Kinds) + { + var deserializer = new ReqIFDeserializer(); + + var expected = (await deserializer.DeserializeAsync(TestDocuments.ToStream(original, kind), kind, CancellationToken.None)).ToList(); + var actual = (await deserializer.DeserializeAsync(TestDocuments.ToStream(minified, kind), kind, CancellationToken.None)).ToList(); + + TestDocuments.AssertSameGraph(expected, actual); + } + } + + [Test] + public void Verify_that_the_spec_relations_of_a_relation_group_in_a_minified_document_are_read() + { + var original = CreateDocumentWithSpecRelationsInRelationGroup(); + var minified = CreateMinifiedCopy(original); + + foreach (var kind in TestDocuments.Kinds) + { + var deserializer = new ReqIFDeserializer(); + + var expected = deserializer.Deserialize(TestDocuments.ToStream(original, kind), kind).ToList(); + var actual = deserializer.Deserialize(TestDocuments.ToStream(minified, kind), kind).ToList(); + + Assert.That(expected[0].CoreContent.SpecRelationGroups.Single().SpecRelations, Has.Count.EqualTo(2), "the original must be read with both spec relations"); + + TestDocuments.AssertSameGraph(expected, actual); + } + } + + [Test] + public async Task Verify_that_the_spec_relations_of_a_relation_group_in_a_minified_document_are_read_async() + { + var original = CreateDocumentWithSpecRelationsInRelationGroup(); + var minified = CreateMinifiedCopy(original); + + foreach (var kind in TestDocuments.Kinds) + { + var deserializer = new ReqIFDeserializer(); + + var expected = (await deserializer.DeserializeAsync(TestDocuments.ToStream(original, kind), kind, CancellationToken.None)).ToList(); + var actual = (await deserializer.DeserializeAsync(TestDocuments.ToStream(minified, kind), kind, CancellationToken.None)).ToList(); + + Assert.That(expected[0].CoreContent.SpecRelationGroups.Single().SpecRelations, Has.Count.EqualTo(2), "the original must be read with both spec relations"); + + TestDocuments.AssertSameGraph(expected, actual); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task Verify_that_the_header_fields_after_an_empty_header_field_are_read(bool async) + { + var header = (await Deserialize(MinifiedHeaderDocumentWithEmptyElements, SupportedFileExtensionKind.Reqif, async)).Single().TheHeader; + + Assert.Multiple(() => + { + Assert.That(header.Comment, Is.Empty); + Assert.That(header.CreationTime, Is.EqualTo(XmlConvert.ToDateTime("2017-03-13T10:15:09.017+01:00", XmlDateTimeSerializationMode.RoundtripKind))); + Assert.That(header.RepositoryId, Is.Empty); + Assert.That(header.ReqIFToolId, Is.EqualTo("a tool")); + Assert.That(header.ReqIFVersion, Is.EqualTo("1.0")); + Assert.That(header.SourceToolId, Is.EqualTo("a source tool")); + Assert.That(header.Title, Is.EqualTo("a title")); + }); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Verify_that_the_elements_after_the_spec_relations_of_a_relation_group_are_read(bool async) + { + var original = CreateDocumentWithSpecRelationsInRelationGroup(insertBefore: "TYPE"); + + foreach (var xml in new[] { original, CreateMinifiedCopy(original) }) + { + foreach (var kind in TestDocuments.Kinds) + { + var relationGroup = (await Deserialize(xml, kind, async)).Single().CoreContent.SpecRelationGroups.Single(); + + Assert.Multiple(() => + { + Assert.That(relationGroup.SpecRelations.Select(x => x.Identifier), Is.EqualTo(new[] { FirstSpecRelationIdentifier, SecondSpecRelationIdentifier })); + Assert.That(relationGroup.Type?.Identifier, Is.EqualTo("relationgrouptype")); + Assert.That(relationGroup.SourceSpecification?.Identifier, Is.EqualTo("specification-1")); + Assert.That(relationGroup.TargetSpecification?.Identifier, Is.EqualTo("specification-2")); + }); + } + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task Verify_that_an_empty_spec_relations_element_of_a_relation_group_is_read(bool async) + { + var original = CreateDocumentWithSpecRelationsInRelationGroup(insertBefore: "TARGET-SPECIFICATION", empty: true); + + Assert.That(original, Does.Contain(""), "the document must contain an empty SPEC-RELATIONS element"); + + foreach (var xml in new[] { original, CreateMinifiedCopy(original) }) + { + foreach (var kind in TestDocuments.Kinds) + { + var relationGroup = (await Deserialize(xml, kind, async)).Single().CoreContent.SpecRelationGroups.Single(); + + Assert.Multiple(() => + { + Assert.That(relationGroup.SpecRelations, Is.Empty); + Assert.That(relationGroup.SourceSpecification?.Identifier, Is.EqualTo("specification-1")); + Assert.That(relationGroup.TargetSpecification?.Identifier, Is.EqualTo("specification-2")); + }); + } + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task Verify_that_an_unsupported_child_of_spec_relations_is_skipped_with_a_warning(bool async) + { + var ns = TestDocuments.ReqIFNamespace; + + // the SPEC-RELATION-REF inside the unsupported element is not a direct child of SPEC-RELATIONS and must not be read + var unsupported = new XElement(ns + "SPEC-OBJECT-REF", new XElement(ns + "SPEC-RELATION-REF", FirstSpecRelationIdentifier)); + + var original = CreateDocumentWithSpecRelationsInRelationGroup(insertBefore: "TYPE", unsupportedChild: unsupported); + + foreach (var xml in new[] { original, CreateMinifiedCopy(original) }) + { + foreach (var kind in TestDocuments.Kinds) + { + var testLogEventSink = new TestLogEventSink(); + var logger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(testLogEventSink).CreateLogger(); + + using var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(logger, dispose: true)); + + var relationGroup = (await Deserialize(xml, kind, async, loggerFactory)).Single().CoreContent.SpecRelationGroups.Single(); + + var warnings = testLogEventSink.Events + .Where(e => e.Level == LogEventLevel.Warning && e.MessageTemplate.Text.Contains("is not supported in SPEC-RELATIONS")) + .Select(e => e.RenderMessage()) + .ToList(); + + Assert.Multiple(() => + { + Assert.That(relationGroup.SpecRelations.Select(x => x.Identifier), Is.EqualTo(new[] { FirstSpecRelationIdentifier, SecondSpecRelationIdentifier })); + Assert.That(relationGroup.Type?.Identifier, Is.EqualTo("relationgrouptype")); + Assert.That(warnings, Has.Count.EqualTo(1)); + Assert.That(warnings.FirstOrDefault(), Does.Contain("SPEC-OBJECT-REF").And.Contain("relationgroup-1")); + }); + } + } + } + + private static async Task> Deserialize(string xml, SupportedFileExtensionKind kind, bool async, ILoggerFactory loggerFactory = null) + { + var deserializer = new ReqIFDeserializer(loggerFactory); + var stream = TestDocuments.ToStream(xml, kind); + + return async + ? (await deserializer.DeserializeAsync(stream, kind, CancellationToken.None)).ToList() + : deserializer.Deserialize(stream, kind).ToList(); + } + + /// + /// None of the test documents has spec relations in a relation group: this returns reqifsharpgenerated.reqif + /// with a second spec relation and a relation group that refers to both, indented. + /// + /// + /// The local name of the relation group child before which SPEC-RELATIONS is inserted; null to add it as the last child + /// + /// + /// true to write an empty SPEC-RELATIONS element, without references + /// + /// + /// An element that is placed between the two references, or null + /// + private static string CreateDocumentWithSpecRelationsInRelationGroup(string insertBefore = null, bool empty = false, XElement unsupportedChild = null) + { + var ns = TestDocuments.ReqIFNamespace; + + var document = XDocument.Parse(TestDocuments.Read("reqifsharpgenerated.reqif")); + + var specRelation = document.Descendants(ns + "SPEC-RELATION").Single(); + Assert.That(specRelation.Attribute("IDENTIFIER")!.Value, Is.EqualTo(FirstSpecRelationIdentifier)); + + var copy = new XElement(specRelation); + copy.SetAttributeValue("IDENTIFIER", SecondSpecRelationIdentifier); + specRelation.AddAfterSelf(copy); + + var specRelations = new XElement(ns + "SPEC-RELATIONS"); + + if (!empty) + { + specRelations.Add( + new XElement(ns + "SPEC-RELATION-REF", FirstSpecRelationIdentifier), + unsupportedChild, + new XElement(ns + "SPEC-RELATION-REF", SecondSpecRelationIdentifier)); + } + + var relationGroup = document.Descendants(ns + "RELATION-GROUP").Single(); + + if (insertBefore == null) + { + relationGroup.Add(specRelations); + } + else + { + relationGroup.Element(ns + insertBefore)!.AddBeforeSelf(specRelations); + } + + return document.ToString(); + } + + private static void AssertHeader(ReqIFHeader header) + { + Assert.Multiple(() => + { + Assert.That(header.Identifier, Is.EqualTo("header")); + Assert.That(header.Comment, Is.EqualTo("a comment")); + Assert.That(header.CreationTime, Is.EqualTo(XmlConvert.ToDateTime("2017-03-13T10:15:09.017+01:00", XmlDateTimeSerializationMode.RoundtripKind))); + Assert.That(header.RepositoryId, Is.EqualTo("a repository")); + Assert.That(header.ReqIFToolId, Is.EqualTo("a tool")); + Assert.That(header.ReqIFVersion, Is.EqualTo("1.0")); + Assert.That(header.SourceToolId, Is.EqualTo("a source tool")); + Assert.That(header.Title, Is.EqualTo("a title")); + }); + } + + /// + /// Returns a copy of the document without the whitespace between its ReqIF elements. Whitespace inside + /// XHTML values and tool extensions is kept: it is content, read as raw XML. + /// + private static string CreateMinifiedCopy(string original) + { + var document = XDocument.Parse(original, LoadOptions.PreserveWhitespace); + + var whitespace = document.DescendantNodes() + .OfType() + .Where(t => string.IsNullOrWhiteSpace(t.Value) && !IsRawXmlContent(t)) + .ToList(); + + Assert.That(whitespace, Is.Not.Empty, "the original must contain whitespace between elements"); + + whitespace.ForEach(t => t.Remove()); + + return document.ToString(SaveOptions.DisableFormatting); + } + + private static bool IsRawXmlContent(XText text) + { + return text.Ancestors().Any(a => + a.Name.Namespace != TestDocuments.ReqIFNamespace + || a.Name.LocalName == "THE-VALUE" + || a.Name.LocalName == "REQ-IF-TOOL-EXTENSION"); + } + } +} diff --git a/ReqIFSharp.Tests/PrefixedNamespaceTestFixture.cs b/ReqIFSharp.Tests/PrefixedNamespaceTestFixture.cs index cf2c594..6bf0da7 100644 --- a/ReqIFSharp.Tests/PrefixedNamespaceTestFixture.cs +++ b/ReqIFSharp.Tests/PrefixedNamespaceTestFixture.cs @@ -20,11 +20,7 @@ namespace ReqIFSharp.Tests { - using System.Collections.Generic; - using System.IO; - using System.IO.Compression; using System.Linq; - using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; @@ -46,26 +42,12 @@ namespace ReqIFSharp.Tests [TestFixture] public class PrefixedNamespaceTestFixture { - private static readonly XNamespace ReqIFNamespace = DefaultXmlAttributeFactory.ReqIFSchemaUri; - - // output.reqif and testreqif.reqif are left out: they contain unresolved type references, - // so their object graphs cannot be serialized for the comparison - private static readonly string[] TestFiles = - { - "Datatype-Demo.reqif", - "DefaultValueDemo.reqif", - "ProR_Traceability-Template-v1.0.reqif", - "reqifsharpgenerated.reqif" - }; - private static readonly string[] ForeignNamespaceDocuments = { "", "" }; - private static readonly SupportedFileExtensionKind[] Kinds = { SupportedFileExtensionKind.Reqif, SupportedFileExtensionKind.Reqifz }; - private TestLogEventSink testLogEventSink; private ILoggerFactory loggerFactory; @@ -89,71 +71,71 @@ public void TearDown() this.loggerFactory.Dispose(); } - [TestCaseSource(nameof(TestFiles))] + [TestCaseSource(typeof(TestDocuments), nameof(TestDocuments.ComparableFiles))] public void Verify_that_a_prefixed_document_is_read_the_same_as_the_original(string fileName) { - var original = ReadTestFile(fileName); + var original = TestDocuments.Read(fileName); var prefixed = CreatePrefixedCopy(original); - foreach (var kind in Kinds) + foreach (var kind in TestDocuments.Kinds) { var deserializer = new ReqIFDeserializer(); - var expected = deserializer.Deserialize(ToStream(original, kind), kind).ToList(); - var actual = deserializer.Deserialize(ToStream(prefixed, kind), kind).ToList(); + var expected = deserializer.Deserialize(TestDocuments.ToStream(original, kind), kind).ToList(); + var actual = deserializer.Deserialize(TestDocuments.ToStream(prefixed, kind), kind).ToList(); - AssertSameGraph(expected, actual); + TestDocuments.AssertSameGraph(expected, actual); } } - [TestCaseSource(nameof(TestFiles))] + [TestCaseSource(typeof(TestDocuments), nameof(TestDocuments.ComparableFiles))] public async Task Verify_that_a_prefixed_document_is_read_the_same_as_the_original_async(string fileName) { - var original = ReadTestFile(fileName); + var original = TestDocuments.Read(fileName); var prefixed = CreatePrefixedCopy(original); - foreach (var kind in Kinds) + foreach (var kind in TestDocuments.Kinds) { var deserializer = new ReqIFDeserializer(); - var expected = (await deserializer.DeserializeAsync(ToStream(original, kind), kind, CancellationToken.None)).ToList(); - var actual = (await deserializer.DeserializeAsync(ToStream(prefixed, kind), kind, CancellationToken.None)).ToList(); + var expected = (await deserializer.DeserializeAsync(TestDocuments.ToStream(original, kind), kind, CancellationToken.None)).ToList(); + var actual = (await deserializer.DeserializeAsync(TestDocuments.ToStream(prefixed, kind), kind, CancellationToken.None)).ToList(); - AssertSameGraph(expected, actual); + TestDocuments.AssertSameGraph(expected, actual); } } [Test] public void Verify_that_a_document_without_namespace_is_read_the_same_as_the_original() { - var original = ReadTestFile("reqifsharpgenerated.reqif"); + var original = TestDocuments.Read("reqifsharpgenerated.reqif"); var withoutNamespace = CreateCopyWithoutNamespace(original); - foreach (var kind in Kinds) + foreach (var kind in TestDocuments.Kinds) { var deserializer = new ReqIFDeserializer(); - var expected = deserializer.Deserialize(ToStream(original, kind), kind).ToList(); - var actual = deserializer.Deserialize(ToStream(withoutNamespace, kind), kind).ToList(); + var expected = deserializer.Deserialize(TestDocuments.ToStream(original, kind), kind).ToList(); + var actual = deserializer.Deserialize(TestDocuments.ToStream(withoutNamespace, kind), kind).ToList(); - AssertSameGraph(expected, actual); + TestDocuments.AssertSameGraph(expected, actual); } } [Test] public async Task Verify_that_a_document_without_namespace_is_read_the_same_as_the_original_async() { - var original = ReadTestFile("reqifsharpgenerated.reqif"); + var original = TestDocuments.Read("reqifsharpgenerated.reqif"); var withoutNamespace = CreateCopyWithoutNamespace(original); - foreach (var kind in Kinds) + foreach (var kind in TestDocuments.Kinds) { var deserializer = new ReqIFDeserializer(); - var expected = (await deserializer.DeserializeAsync(ToStream(original, kind), kind, CancellationToken.None)).ToList(); - var actual = (await deserializer.DeserializeAsync(ToStream(withoutNamespace, kind), kind, CancellationToken.None)).ToList(); + var expected = (await deserializer.DeserializeAsync(TestDocuments.ToStream(original, kind), kind, CancellationToken.None)).ToList(); + var actual = (await deserializer.DeserializeAsync(TestDocuments.ToStream(withoutNamespace, kind), kind, CancellationToken.None)).ToList(); - AssertSameGraph(expected, actual); + TestDocuments.AssertSameGraph(expected, actual); } } @@ -162,7 +144,7 @@ public void Verify_that_a_root_element_in_another_namespace_is_skipped_with_a_wa { var deserializer = new ReqIFDeserializer(this.loggerFactory); - var reqifs = deserializer.Deserialize(ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif); + var reqifs = deserializer.Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif); Assert.That(reqifs, Is.Empty); this.AssertSkippedWarningLogged(); @@ -173,7 +155,7 @@ public async Task Verify_that_a_root_element_in_another_namespace_is_skipped_wit { var deserializer = new ReqIFDeserializer(this.loggerFactory); - var reqifs = await deserializer.DeserializeAsync(ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif, CancellationToken.None); + var reqifs = await deserializer.DeserializeAsync(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif, CancellationToken.None); Assert.That(reqifs, Is.Empty); this.AssertSkippedWarningLogged(); @@ -189,11 +171,6 @@ private void AssertSkippedWarningLogged() Assert.That(warnings[0].RenderMessage(), Does.Contain("http://example.com/not-reqif")); } - private static string ReadTestFile(string fileName) - { - return File.ReadAllText(Path.Combine(TestContext.CurrentContext.TestDirectory, "TestData", fileName)); - } - /// /// Returns a copy of the document in which every ReqIF element is written as reqif:... /// @@ -204,7 +181,7 @@ private static string CreatePrefixedCopy(string original) RemoveReqIFNamespaceDeclarations(document); - document.Root!.SetAttributeValue(XNamespace.Xmlns + "reqif", ReqIFNamespace.NamespaceName); + document.Root!.SetAttributeValue(XNamespace.Xmlns + "reqif", TestDocuments.ReqIFNamespace.NamespaceName); var prefixed = document.ToString(SaveOptions.DisableFormatting); @@ -223,7 +200,7 @@ private static string CreateCopyWithoutNamespace(string original) RemoveReqIFNamespaceDeclarations(document); - foreach (var element in document.Descendants().Where(e => e.Name.Namespace == ReqIFNamespace)) + foreach (var element in document.Descendants().Where(e => e.Name.Namespace == TestDocuments.ReqIFNamespace)) { element.Name = XNamespace.None + element.Name.LocalName; } @@ -231,7 +208,7 @@ private static string CreateCopyWithoutNamespace(string original) var withoutNamespace = document.ToString(SaveOptions.DisableFormatting); Assert.That(withoutNamespace, Does.Contain(" a.IsNamespaceDeclaration && a.Value == ReqIFNamespace.NamespaceName) + .Where(a => a.IsNamespaceDeclaration && a.Value == TestDocuments.ReqIFNamespace.NamespaceName) .Remove(); } } - - private static MemoryStream ToStream(string xml, SupportedFileExtensionKind kind) - { - var bytes = Encoding.UTF8.GetBytes(xml); - - if (kind == SupportedFileExtensionKind.Reqif) - { - return new MemoryStream(bytes); - } - - var zipStream = new MemoryStream(); - - using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, true)) - { - using var entryStream = archive.CreateEntry("document.reqif").Open(); - entryStream.Write(bytes, 0, bytes.Length); - } - - zipStream.Position = 0; - return zipStream; - } - - /// - /// Compares two object graphs by serializing both and comparing the XML, ignoring namespace declarations - /// (which differ by design between the original and its copy). - /// - private static void AssertSameGraph(IReadOnlyList expected, IReadOnlyList actual) - { - Assert.That(expected, Has.Count.EqualTo(1)); - Assert.That(actual, Has.Count.EqualTo(expected.Count), "the copy must be read as the same number of documents"); - Assert.That(actual[0].CoreContent.SpecObjects, Has.Count.EqualTo(expected[0].CoreContent.SpecObjects.Count)); - - var expectedXml = Normalize(expected); - var actualXml = Normalize(actual); - - Assert.That(actualXml.ToString(), Is.EqualTo(expectedXml.ToString())); - } - - private static XDocument Normalize(IEnumerable reqifs) - { - using var stream = new MemoryStream(); - - new ReqIFSerializer().Serialize(reqifs, stream, SupportedFileExtensionKind.Reqif); - - stream.Position = 0; - - var document = XDocument.Load(stream); - - foreach (var element in document.Descendants()) - { - element.Attributes().Where(a => a.IsNamespaceDeclaration).Remove(); - } - - return document; - } } } diff --git a/ReqIFSharp.Tests/TestDocuments.cs b/ReqIFSharp.Tests/TestDocuments.cs new file mode 100644 index 0000000..3bd814a --- /dev/null +++ b/ReqIFSharp.Tests/TestDocuments.cs @@ -0,0 +1,127 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2017-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ReqIFSharp.Tests +{ + using System.Collections.Generic; + using System.IO; + using System.IO.Compression; + using System.Linq; + using System.Text; + using System.Xml.Linq; + + using NUnit.Framework; + + using ReqIFSharp; + + /// + /// Helpers for tests that read a modified copy of a test document and compare it with the original. + /// + internal static class TestDocuments + { + /// + /// The ReqIF namespace + /// + public static readonly XNamespace ReqIFNamespace = DefaultXmlAttributeFactory.ReqIFSchemaUri; + + /// + /// Test documents whose object graphs can be serialized again, so they can be compared. + /// output.reqif and testreqif.reqif are left out: they contain unresolved type references. + /// + public static readonly string[] ComparableFiles = + { + "Datatype-Demo.reqif", + "DefaultValueDemo.reqif", + "ProR_Traceability-Template-v1.0.reqif", + "reqifsharpgenerated.reqif" + }; + + /// + /// Both file kinds the deserializer reads + /// + public static readonly SupportedFileExtensionKind[] Kinds = { SupportedFileExtensionKind.Reqif, SupportedFileExtensionKind.Reqifz }; + + /// + /// Reads a file from the TestData folder + /// + public static string Read(string fileName) + { + return File.ReadAllText(Path.Combine(TestContext.CurrentContext.TestDirectory, "TestData", fileName)); + } + + /// + /// Returns the XML as a stream, either as is or as the only entry of a reqifz archive + /// + public static MemoryStream ToStream(string xml, SupportedFileExtensionKind kind) + { + var bytes = Encoding.UTF8.GetBytes(xml); + + if (kind == SupportedFileExtensionKind.Reqif) + { + return new MemoryStream(bytes); + } + + var zipStream = new MemoryStream(); + + using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, true)) + { + using var entryStream = archive.CreateEntry("document.reqif").Open(); + entryStream.Write(bytes, 0, bytes.Length); + } + + zipStream.Position = 0; + return zipStream; + } + + /// + /// Compares two object graphs by serializing both and comparing the XML, ignoring namespace declarations + /// (which differ by design between an original and some of its copies). + /// + public static void AssertSameGraph(IReadOnlyList expected, IReadOnlyList actual) + { + Assert.That(expected, Has.Count.EqualTo(1)); + Assert.That(actual, Has.Count.EqualTo(expected.Count), "the copy must be read as the same number of documents"); + Assert.That(actual[0].CoreContent.SpecObjects, Has.Count.EqualTo(expected[0].CoreContent.SpecObjects.Count)); + + var expectedXml = Normalize(expected); + var actualXml = Normalize(actual); + + Assert.That(actualXml.ToString(), Is.EqualTo(expectedXml.ToString())); + } + + private static XDocument Normalize(IEnumerable reqifs) + { + using var stream = new MemoryStream(); + + new ReqIFSerializer().Serialize(reqifs, stream, SupportedFileExtensionKind.Reqif); + + stream.Position = 0; + + var document = XDocument.Load(stream); + + foreach (var element in document.Descendants()) + { + element.Attributes().Where(a => a.IsNamespaceDeclaration).Remove(); + } + + return document; + } + } +} diff --git a/ReqIFSharp.Tests/XmlReaderExtensionsTestFixture.cs b/ReqIFSharp.Tests/XmlReaderExtensionsTestFixture.cs new file mode 100644 index 0000000..d0f5b8f --- /dev/null +++ b/ReqIFSharp.Tests/XmlReaderExtensionsTestFixture.cs @@ -0,0 +1,201 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2017-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ReqIFSharp.Tests +{ + using System.IO; + using System.Threading.Tasks; + using System.Xml; + + using NUnit.Framework; + + using ReqIFSharp; + + /// + /// Suite of tests for the class + /// + [TestFixture] + public class XmlReaderExtensionsTestFixture + { + [TestCase("text", "text")] + [TestCase("one three", "one two three")] + [TestCase("", "")] + public void Verify_that_the_content_is_read_and_the_reader_is_left_on_the_end_tag(string element, string expected) + { + using var reader = CreateReaderOnFirstChild($"{element}"); + + Assert.That(reader.ReadElementContentAsStringToEndElement(), Is.EqualTo(expected)); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.EndElement)); + Assert.That(reader.LocalName, Is.EqualTo("A")); + + reader.Read(); + Assert.That(reader.LocalName, Is.EqualTo("B"), "the next Read must land on the next sibling"); + } + + [TestCase("text", "text")] + [TestCase("one three", "one two three")] + [TestCase("", "")] + public async Task Verify_that_the_content_is_read_and_the_reader_is_left_on_the_end_tag_async(string element, string expected) + { + using var reader = CreateReaderOnFirstChild($"{element}", true); + + Assert.That(await reader.ReadElementContentAsStringToEndElementAsync(), Is.EqualTo(expected)); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.EndElement)); + Assert.That(reader.LocalName, Is.EqualTo("A")); + + await reader.ReadAsync(); + Assert.That(reader.LocalName, Is.EqualTo("B"), "the next Read must land on the next sibling"); + } + + [Test] + public async Task Verify_that_an_empty_element_gives_an_empty_string_and_the_reader_stays_on_it() + { + using (var reader = CreateReaderOnFirstChild("")) + { + Assert.That(reader.ReadElementContentAsStringToEndElement(), Is.Empty); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.Element)); + Assert.That(reader.LocalName, Is.EqualTo("A")); + } + + using (var reader = CreateReaderOnFirstChild("", true)) + { + Assert.That(await reader.ReadElementContentAsStringToEndElementAsync(), Is.Empty); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.Element)); + Assert.That(reader.LocalName, Is.EqualTo("A")); + } + } + + [TestCase("")] + [TestCase("text")] + public void Verify_that_an_element_with_child_elements_throws(string xml) + { + using (var reader = CreateReaderOnFirstChild(xml)) + { + Assert.That(() => reader.ReadElementContentAsStringToEndElement(), + Throws.TypeOf().With.Message.Contains("The A element may only contain text")); + } + + using (var reader = CreateReaderOnFirstChild(xml, true)) + { + Assert.That(async () => await reader.ReadElementContentAsStringToEndElementAsync(), + Throws.TypeOf().With.Message.Contains("The A element may only contain text")); + } + } + + [Test] + public void Verify_that_a_reader_that_is_not_on_an_element_throws() + { + using (var reader = CreateReaderOnFirstChild("text")) + { + reader.Read(); + + Assert.That(() => reader.ReadElementContentAsStringToEndElement(), + Throws.TypeOf().With.Message.Contains("must be positioned on an element")); + } + + using (var reader = CreateReaderOnFirstChild("text", true)) + { + reader.Read(); + + Assert.That(async () => await reader.ReadElementContentAsStringToEndElementAsync(), + Throws.TypeOf().With.Message.Contains("must be positioned on an element")); + } + } + + [Test] + public async Task Verify_that_a_reader_on_whitespace_before_the_element_moves_to_the_element() + { + using (var reader = CreateReaderOnFirstChild(" text")) + { + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.Whitespace), "the reader must start on the whitespace"); + + Assert.That(reader.ReadElementContentAsStringToEndElement(), Is.EqualTo("text")); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.EndElement)); + Assert.That(reader.LocalName, Is.EqualTo("A")); + } + + using (var reader = CreateReaderOnFirstChild(" text", true)) + { + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.Whitespace), "the reader must start on the whitespace"); + + Assert.That(await reader.ReadElementContentAsStringToEndElementAsync(), Is.EqualTo("text")); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.EndElement)); + Assert.That(reader.LocalName, Is.EqualTo("A")); + } + } + + [Test] + public async Task Verify_that_the_exception_has_the_line_and_position_of_the_offending_node() + { + const string xml = "\n\ntext"; + + using (var reader = CreateReaderOnFirstChild(xml)) + { + reader.MoveToContent(); + + var exception = Assert.Throws(() => reader.ReadElementContentAsStringToEndElement()); + + Assert.That(exception.LineNumber, Is.EqualTo(3)); + Assert.That(exception.LinePosition, Is.EqualTo(6), "the position of the C element name"); + } + + using (var reader = CreateReaderOnFirstChild(xml, true)) + { + reader.MoveToContent(); + + var exception = await Assert.ThrowsAsync(async () => await reader.ReadElementContentAsStringToEndElementAsync()); + + Assert.That(exception.LineNumber, Is.EqualTo(3)); + Assert.That(exception.LinePosition, Is.EqualTo(6), "the position of the C element name"); + } + } + + [Test] + public void Verify_that_the_exception_has_no_line_and_position_when_the_reader_has_no_line_information() + { + var document = new XmlDocument(); + document.LoadXml("text"); + + using var reader = new XmlNodeReader(document.DocumentElement!.FirstChild!); + reader.Read(); + + Assert.That(reader, Is.Not.InstanceOf(), "the reader must not have line information"); + + var exception = Assert.Throws(() => reader.ReadElementContentAsStringToEndElement()); + + Assert.That(exception.Message, Does.Contain("The A element may only contain text")); + Assert.That(exception.LineNumber, Is.Zero); + Assert.That(exception.LinePosition, Is.Zero); + } + + /// + /// Creates a reader positioned on the first child element of the root + /// + private static XmlReader CreateReaderOnFirstChild(string xml, bool async = false) + { + var reader = XmlReader.Create(new StringReader(xml), new XmlReaderSettings { Async = async }); + + reader.MoveToContent(); + reader.Read(); + + return reader; + } + } +} diff --git a/ReqIFSharp/AttributeValue/AttributeValueEnumeration.cs b/ReqIFSharp/AttributeValue/AttributeValueEnumeration.cs index 48154cf..5905962 100644 --- a/ReqIFSharp/AttributeValue/AttributeValueEnumeration.cs +++ b/ReqIFSharp/AttributeValue/AttributeValueEnumeration.cs @@ -199,7 +199,7 @@ internal override void ReadXml(XmlReader reader) } break; case "ENUM-VALUE-REF": - reference = subtree.ReadElementContentAsString(); + reference = subtree.ReadElementContentAsStringToEndElement(); var enumValue = this.ReqIFContent.DataTypes.OfType().SelectMany(x => x.SpecifiedValues).SingleOrDefault(x => x.Identifier == reference); if (enumValue != null) @@ -251,7 +251,7 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to } break; case "ENUM-VALUE-REF": - reference = await subtree.ReadElementContentAsStringAsync(); + reference = await subtree.ReadElementContentAsStringToEndElementAsync(); var enumValue = this.ReqIFContent.DataTypes.OfType().SelectMany(x => x.SpecifiedValues).SingleOrDefault(x => x.Identifier == reference); if (enumValue != null) diff --git a/ReqIFSharp/ReqIFHeader.cs b/ReqIFSharp/ReqIFHeader.cs index a533dfa..cf8834b 100644 --- a/ReqIFSharp/ReqIFHeader.cs +++ b/ReqIFSharp/ReqIFHeader.cs @@ -146,10 +146,10 @@ internal void ReadXml(XmlReader reader) this.Identifier = reader.GetAttribute("IDENTIFIER"); break; case "COMMENT": - this.Comment = reader.ReadElementContentAsString(); + this.Comment = reader.ReadElementContentAsStringToEndElement(); break; case "CREATION-TIME": - var creationTimeValue = reader.ReadElementContentAsString(); + var creationTimeValue = reader.ReadElementContentAsStringToEndElement(); if (!string.IsNullOrWhiteSpace(creationTimeValue)) { try @@ -163,19 +163,19 @@ internal void ReadXml(XmlReader reader) } break; case "REPOSITORY-ID": - this.RepositoryId = reader.ReadElementContentAsString(); + this.RepositoryId = reader.ReadElementContentAsStringToEndElement(); break; case "REQ-IF-TOOL-ID": - this.ReqIFToolId = reader.ReadElementContentAsString(); + this.ReqIFToolId = reader.ReadElementContentAsStringToEndElement(); break; case "REQ-IF-VERSION": - this.ReqIFVersion = reader.ReadElementContentAsString(); + this.ReqIFVersion = reader.ReadElementContentAsStringToEndElement(); break; case "SOURCE-TOOL-ID": - this.SourceToolId = reader.ReadElementContentAsString(); + this.SourceToolId = reader.ReadElementContentAsStringToEndElement(); break; case "TITLE": - this.Title = reader.ReadElementContentAsString(); + this.Title = reader.ReadElementContentAsStringToEndElement(); break; default: this.logger.LogWarning("The {LocalName} element at line:position {LineNumber}:{LinePosition} is not supported", reader.LocalName, xmlLineInfo?.LineNumber, xmlLineInfo?.LinePosition); @@ -210,10 +210,10 @@ internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token) this.Identifier = reader.GetAttribute("IDENTIFIER"); break; case "COMMENT": - this.Comment = await reader.ReadElementContentAsStringAsync(); + this.Comment = await reader.ReadElementContentAsStringToEndElementAsync(); break; case "CREATION-TIME": - var creationTimeValue = await reader.ReadElementContentAsStringAsync(); + var creationTimeValue = await reader.ReadElementContentAsStringToEndElementAsync(); if (!string.IsNullOrWhiteSpace(creationTimeValue)) { try @@ -227,19 +227,19 @@ internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token) } break; case "REPOSITORY-ID": - this.RepositoryId = await reader.ReadElementContentAsStringAsync(); + this.RepositoryId = await reader.ReadElementContentAsStringToEndElementAsync(); break; case "REQ-IF-TOOL-ID": - this.ReqIFToolId = await reader.ReadElementContentAsStringAsync(); + this.ReqIFToolId = await reader.ReadElementContentAsStringToEndElementAsync(); break; case "REQ-IF-VERSION": - this.ReqIFVersion = await reader.ReadElementContentAsStringAsync(); + this.ReqIFVersion = await reader.ReadElementContentAsStringToEndElementAsync(); break; case "SOURCE-TOOL-ID": - this.SourceToolId = await reader.ReadElementContentAsStringAsync(); + this.SourceToolId = await reader.ReadElementContentAsStringToEndElementAsync(); break; case "TITLE": - this.Title = await reader.ReadElementContentAsStringAsync(); + this.Title = await reader.ReadElementContentAsStringToEndElementAsync(); break; default: this.logger.LogWarning("The {LocalName} element at line:position {LineNumber}:{LinePosition} is not supported", reader.LocalName, xmlLineInfo?.LineNumber, xmlLineInfo?.LinePosition); diff --git a/ReqIFSharp/SpecElementWithAttributes/RelationGroup.cs b/ReqIFSharp/SpecElementWithAttributes/RelationGroup.cs index 7105df9..2aaf086 100644 --- a/ReqIFSharp/SpecElementWithAttributes/RelationGroup.cs +++ b/ReqIFSharp/SpecElementWithAttributes/RelationGroup.cs @@ -209,11 +209,10 @@ protected override void ReadObjectSpecificElements(XmlReader reader) } break; case "SPEC-RELATIONS": - reader.ReadStartElement(); - - this.DeserializeSpecRelations(reader); - - reader.ReadEndElement(); + using (var specRelationsSubtree = reader.ReadSubtree()) + { + this.DeserializeSpecRelations(specRelationsSubtree); + } break; default: this.logger.LogWarning("The {LocalName} element at line:position {LineNumber}:{LinePosition} is not supported", reader.LocalName, xmlLineInfo?.LineNumber, xmlLineInfo?.LinePosition); @@ -297,11 +296,10 @@ private async Task ReadObjectSpecificElementsInternalAsync(XmlReader reader, Can } break; case "SPEC-RELATIONS": - reader.ReadStartElement(); - - await this.DeserializeSpecRelationsAsync(reader, token); - - reader.ReadEndElement(); + using (var specRelationsSubtree = reader.ReadSubtree()) + { + await this.DeserializeSpecRelationsAsync(specRelationsSubtree, token); + } break; default: this.logger.LogWarning("The {LocalName} element at line:position {LineNumber}:{LinePosition} is not supported", reader.LocalName, xmlLineInfo?.LineNumber, xmlLineInfo?.LinePosition); @@ -407,13 +405,18 @@ protected override Task ReadHierarchyAsync(XmlReader reader, CancellationToken t /// Deserialize the s contained by the SPEC-RELATIONS element. /// /// - /// an instance of + /// an over the subtree of the SPEC-RELATIONS element /// private void DeserializeSpecRelations(XmlReader reader) { - while (reader.Read() && reader.MoveToContent() == XmlNodeType.Element && reader.LocalName.StartsWith("SPEC-RELATION-REF", StringComparison.Ordinal)) + while (reader.Read()) { - var reference = reader.ReadElementContentAsString(); + if (!this.IsSpecRelationRef(reader)) + { + continue; + } + + var reference = reader.ReadElementContentAsStringToEndElement(); var specRelation = this.CoreContent.SpecRelations.SingleOrDefault(x => x.Identifier == reference); if (specRelation != null) { @@ -430,18 +433,23 @@ private void DeserializeSpecRelations(XmlReader reader) /// Asynchronously deserialize the s contained by the SPEC-RELATIONS element. /// /// - /// an instance of + /// an over the subtree of the SPEC-RELATIONS element /// /// /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// private async Task DeserializeSpecRelationsAsync(XmlReader reader, CancellationToken token) { - while (await reader.ReadAsync() && await reader.MoveToContentAsync() == XmlNodeType.Element && reader.LocalName.StartsWith("SPEC-RELATION-REF", StringComparison.Ordinal)) + while (await reader.ReadAsync()) { token.ThrowIfCancellationRequested(); - var reference = await reader.ReadElementContentAsStringAsync(); + if (!this.IsSpecRelationRef(reader)) + { + continue; + } + + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var specRelation = this.CoreContent.SpecRelations.SingleOrDefault(x => x.Identifier == reference); if (specRelation != null) { @@ -454,6 +462,34 @@ private async Task DeserializeSpecRelationsAsync(XmlReader reader, CancellationT } } + /// + /// Checks whether the reader is on a SPEC-RELATION-REF child of the SPEC-RELATIONS element. + /// Any other child element is not supported: a warning is logged and the element, with its content, is skipped. + /// + /// + /// an over the subtree of the SPEC-RELATIONS element + /// + /// + /// true when the reader is on a SPEC-RELATION-REF element that is a direct child of SPEC-RELATIONS + /// + private bool IsSpecRelationRef(XmlReader reader) + { + if (reader.NodeType != XmlNodeType.Element || reader.Depth != 1) + { + return false; + } + + if (reader.LocalName.StartsWith("SPEC-RELATION-REF", StringComparison.Ordinal)) + { + return true; + } + + var xmlLineInfo = reader as IXmlLineInfo; + this.logger.LogWarning("The {LocalName} element at line:position {LineNumber}:{LinePosition} is not supported in SPEC-RELATIONS of RelationGroup:{Identifier} and has been skipped", reader.LocalName, xmlLineInfo?.LineNumber, xmlLineInfo?.LinePosition, this.Identifier); + + return false; + } + /// /// Converts a object into its XML representation. /// diff --git a/ReqIFSharp/XmlReaderExtensions.cs b/ReqIFSharp/XmlReaderExtensions.cs new file mode 100644 index 0000000..bb547a2 --- /dev/null +++ b/ReqIFSharp/XmlReaderExtensions.cs @@ -0,0 +1,131 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2017-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ReqIFSharp +{ + using System.Threading.Tasks; + using System.Xml; + + /// + /// Extension methods for + /// + internal static class XmlReaderExtensions + { + /// + /// Reads the text content of the current element and leaves the reader on its end tag + /// (or on the element itself when it is empty). + /// + /// + /// moves the reader past the end tag, onto the next node. + /// In a loop that calls for every iteration, that skips the next sibling + /// element when there is no whitespace between the elements. Use this method in such a loop instead. + /// + /// + /// The , positioned on an element + /// + /// + /// The text content of the element; an empty string when the element is empty + /// + /// + /// The reader is not on an element, or the element contains child elements + /// + internal static string ReadElementContentAsStringToEndElement(this XmlReader reader) + { + if (reader.MoveToContent() != XmlNodeType.Element) + { + throw CreateException(reader, $"The reader must be positioned on an element, not on {reader.NodeType}"); + } + + if (reader.IsEmptyElement) + { + return string.Empty; + } + + var localName = reader.LocalName; + + reader.Read(); + + var content = reader.NodeType == XmlNodeType.Element ? null : reader.ReadContentAsString(); + + if (reader.NodeType != XmlNodeType.EndElement) + { + throw CreateException(reader, $"The {localName} element may only contain text"); + } + + return content; + } + + /// + /// Asynchronously reads the text content of the current element and leaves the reader on its end tag + /// (or on the element itself when it is empty). + /// + /// + /// moves the reader past the end tag, onto the next node. + /// In a loop that calls for every iteration, that skips the next sibling + /// element when there is no whitespace between the elements. Use this method in such a loop instead. + /// + /// + /// The , positioned on an element + /// + /// + /// The text content of the element; an empty string when the element is empty + /// + /// + /// The reader is not on an element, or the element contains child elements + /// + internal static async Task ReadElementContentAsStringToEndElementAsync(this XmlReader reader) + { + if (await reader.MoveToContentAsync() != XmlNodeType.Element) + { + throw CreateException(reader, $"The reader must be positioned on an element, not on {reader.NodeType}"); + } + + if (reader.IsEmptyElement) + { + return string.Empty; + } + + var localName = reader.LocalName; + + await reader.ReadAsync(); + + var content = reader.NodeType == XmlNodeType.Element ? null : await reader.ReadContentAsStringAsync(); + + if (reader.NodeType != XmlNodeType.EndElement) + { + throw CreateException(reader, $"The {localName} element may only contain text"); + } + + return content; + } + + /// + /// Creates an with the line and position of the reader, when it has them + /// + private static XmlException CreateException(XmlReader reader, string message) + { + var lineInfo = reader as IXmlLineInfo; + + return lineInfo != null && lineInfo.HasLineInfo() + ? new XmlException(message, null, lineInfo.LineNumber, lineInfo.LinePosition) + : new XmlException(message); + } + } +}