diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eebd8c..bab06d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,14 @@ The repository ships two NuGet packages that version independently: **ReqIFSharp - 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)). +- An element that has the name of a ReqIF element but is in another namespace (for example ``) is no longer read as that ReqIF element. It is skipped, with its content, and a warning is logged. Elements in the ReqIF namespace or in no namespace are read as before, in any document ([#97](https://github.com/STARIONGROUP/ReqIFSharp/issues/97)). ### 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)). - `AttributeValueXHTML.TheOriginalValue` was not read from `THE-ORIGINAL-VALUE`, so it was always `null` after reading, and a document that was read and written again lost the original values of its simplified XHTML attribute values ([#95](https://github.com/STARIONGROUP/ReqIFSharp/issues/95)). +- Attribute values did not log anything, also when an `ILoggerFactory` was passed to the deserializer: the `AttributeValue` constructors did not keep the logger factory. An enumeration attribute value no longer reports its own element and its `DEFINITION` and `VALUES` elements as not supported ([#97](https://github.com/STARIONGROUP/ReqIFSharp/issues/97)). ## [10.1.2] - 2026-10-18 diff --git a/ReqIFSharp.Tests/AttributeValueTests/AttributeValueEnumerationTestFixture.cs b/ReqIFSharp.Tests/AttributeValueTests/AttributeValueEnumerationTestFixture.cs index de1626d..d403c5e 100644 --- a/ReqIFSharp.Tests/AttributeValueTests/AttributeValueEnumerationTestFixture.cs +++ b/ReqIFSharp.Tests/AttributeValueTests/AttributeValueEnumerationTestFixture.cs @@ -23,8 +23,10 @@ namespace ReqIFSharp.Tests using System; using System.Collections.Generic; using System.IO; + using System.Linq; using System.Runtime.Serialization; using System.Threading; + using System.Threading.Tasks; using System.Xml; using Microsoft.Extensions.Logging; @@ -34,6 +36,7 @@ namespace ReqIFSharp.Tests using ReqIFSharp; using Serilog; + using Serilog.Events; /// /// Suite of tests for the @@ -171,5 +174,68 @@ public void Verify_that_when_ObjectValue_is_not_real_an_exception_is_thrown() Throws.Exception.TypeOf() .With.Message.Contains("The value to set is not an IEnumerable.")); } + + [TestCase(false)] + [TestCase(true)] + public async Task Verify_that_an_unknown_ReqIF_element_in_an_enumeration_value_is_reported_as_not_supported(bool async) + { + const string xml = ""; + + var sink = new TestLogEventSink(); + var serilogLogger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(sink).CreateLogger(); + + using var testLoggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(serilogLogger, dispose: true)); + using var reader = XmlReader.Create(new StringReader(xml), new XmlReaderSettings { Async = async }); + reader.MoveToContent(); + + var attributeValueEnumeration = new AttributeValueEnumeration(testLoggerFactory); + + if (async) + { + await attributeValueEnumeration.ReadXmlAsync(reader, CancellationToken.None); + } + else + { + attributeValueEnumeration.ReadXml(reader); + } + + var warnings = sink.Events.Where(e => e.Level == LogEventLevel.Warning).Select(e => e.RenderMessage()).ToList(); + + // also the positive control of Verify_that_reading_enumeration_values_logs_no_warnings, which filters on the same SourceContext + var fromEnumeration = sink.Events.Where(e => e.Level == LogEventLevel.Warning && e.Properties.TryGetValue("SourceContext", out var source) && source.ToString().Contains(nameof(AttributeValueEnumeration))).ToList(); + + Assert.That(warnings, Has.Count.EqualTo(1)); + Assert.That(warnings[0], Does.Contain("UNKNOWN").And.Contain("is not supported")); + Assert.That(fromEnumeration, Has.Count.EqualTo(1)); + } + + [TestCase("Datatype-Demo.reqif", false)] + [TestCase("Datatype-Demo.reqif", true)] + [TestCase("reqifsharpgenerated.reqif", false)] + [TestCase("reqifsharpgenerated.reqif", true)] + public async Task Verify_that_reading_enumeration_values_logs_no_warnings(string fileName, bool async) + { + var sink = new TestLogEventSink(); + var serilogLogger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(sink).CreateLogger(); + + using var testLoggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(serilogLogger, dispose: true)); + using var stream = File.OpenRead(Path.Combine(TestContext.CurrentContext.TestDirectory, "TestData", fileName)); + + var deserializer = new ReqIFDeserializer(testLoggerFactory); + + var reqifs = async + ? (await deserializer.DeserializeAsync(stream, SupportedFileExtensionKind.Reqif, CancellationToken.None)).ToList() + : deserializer.Deserialize(stream, SupportedFileExtensionKind.Reqif).ToList(); + + var enumerationValues = reqifs.SelectMany(x => x.CoreContent.SpecObjects).SelectMany(x => x.Values).OfType().ToList(); + + var warnings = sink.Events + .Where(e => e.Level == LogEventLevel.Warning && e.Properties.TryGetValue("SourceContext", out var source) && source.ToString().Contains(nameof(AttributeValueEnumeration))) + .Select(e => e.RenderMessage()) + .ToList(); + + Assert.That(enumerationValues, Is.Not.Empty, "the document must contain enumeration values"); + Assert.That(warnings, Is.Empty); + } } } diff --git a/ReqIFSharp.Tests/ForeignNamespaceTestFixture.cs b/ReqIFSharp.Tests/ForeignNamespaceTestFixture.cs new file mode 100644 index 0000000..5d86cf5 --- /dev/null +++ b/ReqIFSharp.Tests/ForeignNamespaceTestFixture.cs @@ -0,0 +1,634 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// 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.IO; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using System.Xml.Linq; + + using Microsoft.Extensions.Logging; + + using NUnit.Framework; + + using ReqIFSharp; + + using Serilog; + using Serilog.Events; + + /// + /// Verifies that an element with the local name of a ReqIF element, but in another namespace, is not read as + /// a ReqIF element: it is skipped, with its content, and a warning is logged, see GH-97. + /// + [TestFixture] + public class ForeignNamespaceTestFixture + { + private const string FileName = "reqifsharpgenerated.reqif"; + + private const string WarningText = "it is not a ReqIF element and has been skipped"; + + private static readonly XNamespace Other = "http://example.com/other"; + + /// + /// Test documents that can be read; the ones that cannot be read on purpose are left out + /// + private static readonly string[] ReadableFiles = + { + "Datatype-Demo.reqif", + "DefaultValueDemo.reqif", + "ProR_Traceability-Template-v1.0.reqif", + "output.reqif", + "reqifsharpgenerated.reqif", + "testreqif.reqif", + "Spielwiese.reqifz", + "requirements-and-objects.reqifz", + "test-multiple-reqif.reqifz" + }; + + /// + /// The places where a foreign element is added, with the test document it is added to. Each foreign element is + /// a copy of a ReqIF element (or a new element) that would change the object graph when it was read as a ReqIF element. + /// + private static readonly object[] Places = + { + new object[] { FileName, "root child" }, + new object[] { FileName, "header field" }, + new object[] { FileName, "content section" }, + new object[] { FileName, "datatype" }, + new object[] { FileName, "enum value" }, + new object[] { FileName, "embedded value" }, + new object[] { FileName, "spec type" }, + new object[] { FileName, "spec type child" }, + new object[] { FileName, "attribute definition" }, + new object[] { FileName, "attribute definition child BOOLEAN" }, + new object[] { FileName, "attribute definition child DATE" }, + new object[] { FileName, "attribute definition child ENUMERATION" }, + new object[] { FileName, "attribute definition child INTEGER" }, + new object[] { FileName, "attribute definition child REAL" }, + new object[] { FileName, "attribute definition child STRING" }, + new object[] { FileName, "attribute definition child XHTML" }, + new object[] { FileName, "spec object" }, + new object[] { FileName, "empty spec object" }, + new object[] { FileName, "attribute value" }, + new object[] { FileName, "enumeration value reference" }, + new object[] { "DefaultValueDemo.reqif", "default value enumeration value reference" }, + new object[] { FileName, "xhtml value" }, + new object[] { FileName, "specification" }, + new object[] { FileName, "spec hierarchy" }, + new object[] { FileName, "spec hierarchy object" }, + new object[] { FileName, "nested spec hierarchy" }, + new object[] { FileName, "spec relation" }, + new object[] { FileName, "relation group" }, + new object[] { FileName, "relation group spec relations" }, + new object[] { FileName, "relation group spec relation ref" }, + new object[] { FileName, "alternative id" }, + new object[] { FileName, "tool extension" }, + new object[] { FileName, "spec object type reference before" }, + new object[] { FileName, "spec object type reference after" }, + new object[] { FileName, "attribute value definition reference before" }, + new object[] { FileName, "attribute value definition reference after" }, + new object[] { FileName, "xhtml value definition reference before" }, + new object[] { FileName, "xhtml value definition reference after" }, + new object[] { FileName, "spec relation source reference before" }, + new object[] { FileName, "spec relation target reference after" }, + new object[] { FileName, "spec hierarchy object reference before" }, + new object[] { FileName, "specification type reference before" }, + new object[] { FileName, "relation group source specification reference before" }, + new object[] { FileName, "relation group type reference after" }, + new object[] { FileName, "spec hierarchy object reference after" }, + new object[] { FileName, "specification type reference after" }, + new object[] { FileName, "spec relation type reference before" }, + new object[] { FileName, "spec relation type reference after" }, + new object[] { FileName, "attribute value definition reference before BOOLEAN" }, + new object[] { FileName, "attribute value definition reference before DATE" }, + new object[] { FileName, "attribute value definition reference before INTEGER" }, + new object[] { FileName, "attribute value definition reference before REAL" }, + new object[] { FileName, "attribute value definition reference after ENUMERATION" }, + new object[] { FileName, "spec relation source wrapper" }, + new object[] { FileName, "relation group target specification wrapper" }, + new object[] { FileName, "attribute value definition wrapper BOOLEAN" }, + new object[] { FileName, "attribute value definition wrapper DATE" }, + new object[] { FileName, "attribute value definition wrapper INTEGER" }, + new object[] { FileName, "attribute value definition wrapper REAL" }, + new object[] { FileName, "attribute value definition wrapper STRING" }, + new object[] { FileName, "attribute definition type reference after BOOLEAN" }, + new object[] { FileName, "attribute definition type reference after DATE" }, + new object[] { FileName, "attribute definition type reference after ENUMERATION" }, + new object[] { FileName, "attribute definition type reference after INTEGER" }, + new object[] { FileName, "attribute definition type reference after REAL" }, + new object[] { FileName, "attribute definition type reference after STRING" }, + new object[] { FileName, "attribute definition type reference after XHTML" }, + new object[] { FileName, "empty element before the xhtml value" }, + new object[] { FileName, "empty element between the xhtml values" } + }; + + [TestCaseSource(nameof(ReadableFiles))] + public async Task Verify_that_no_element_of_a_test_document_is_skipped(string fileName) + { + var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestData", fileName); + var sources = new List<(string Name, Func Open, SupportedFileExtensionKind Kind)>(); + + if (fileName.EndsWith(".reqifz", StringComparison.Ordinal)) + { + sources.Add(("original", () => File.OpenRead(path), SupportedFileExtensionKind.Reqifz)); + } + else + { + var original = TestDocuments.Read(fileName); + + foreach (var (name, xml) in new[] + { + ("original", original), + ("minified", TestDocuments.Minify(original)), + ("prefixed", TestDocuments.Prefix(original)), + ("without namespace", TestDocuments.RemoveNamespace(original)) + }) + { + sources.Add((name, () => TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif)); + } + } + + foreach (var source in sources) + { + foreach (var async in new[] { false, true }) + { + using var stream = source.Open(); + + var (reqifs, warnings) = await Deserialize(stream, source.Kind, async); + + Assert.That(reqifs, Is.Not.Empty); + Assert.That(warnings, Is.Empty, $"{source.Name}, async: {async}"); + } + } + } + + [TestCaseSource(nameof(Places))] + public async Task Verify_that_a_foreign_element_is_skipped_with_a_warning(string fileName, string place) + { + var (original, withForeignElement) = CreateDocuments(fileName, place); + + Assert.That(withForeignElement, Does.Contain("xmlns:other="), "the document must contain a foreign element"); + + foreach (var (name, expectedXml, actualXml) in new[] + { + ("indented", original, withForeignElement), + ("minified", TestDocuments.Minify(original), TestDocuments.Minify(withForeignElement)) + }) + { + foreach (var kind in TestDocuments.Kinds) + { + foreach (var async in new[] { false, true }) + { + var because = $"{place}, {name}, {kind}, async: {async}"; + + var (expected, _) = await Deserialize(TestDocuments.ToStream(expectedXml, kind), kind, async); + var (actual, warnings) = await Deserialize(TestDocuments.ToStream(actualXml, kind), kind, async); + + TestDocuments.AssertSameGraph(expected, actual); + + Assert.That(warnings, Has.Count.EqualTo(1), because); + Assert.That(warnings[0], Does.Contain("other:").And.Contain(Other.NamespaceName), because); + } + } + } + } + + // the emptied container comes before the other one in the test document, so a search that runs past its end finds the other reference + [TestCase("SPEC-RELATION", "TARGET", "SPEC-OBJECT-REF", false)] + [TestCase("SPEC-RELATION", "TARGET", "SPEC-OBJECT-REF", true)] + [TestCase("RELATION-GROUP", "SOURCE-SPECIFICATION", "SPECIFICATION-REF", false)] + [TestCase("RELATION-GROUP", "SOURCE-SPECIFICATION", "SPECIFICATION-REF", true)] + public async Task Verify_that_a_reference_container_with_only_a_foreign_reference_is_read_as_empty(string element, string container, string reference, bool async) + { + var ns = TestDocuments.ReqIFNamespace; + var document = XDocument.Parse(TestDocuments.Read(FileName)); + document.Root!.SetAttributeValue(XNamespace.Xmlns + "other", Other.NamespaceName); + + var owner = document.Root.Descendants(ns + element).First(); + var identifier = owner.Attribute("IDENTIFIER")!.Value; + + // the other reference container of the element (SOURCE for TARGET, TARGET-SPECIFICATION for SOURCE-SPECIFICATION), after the emptied one + var otherContainer = owner.Elements().Single(x => x.Name != ns + container && x.Element(ns + reference) != null); + Assert.That(owner.Element(ns + container)!.IsBefore(otherContainer), Is.True, "the emptied container must come first"); + var otherReference = otherContainer.Element(ns + reference)!.Value; + + // only a foreign reference, to the element the other container refers to + owner.Element(ns + container)!.Element(ns + reference)!.ReplaceWith(new XElement(Other + reference, otherReference)); + + var xml = document.ToString(); + + foreach (var copy in new[] { xml, TestDocuments.Minify(xml) }) + { + var (reqifs, warnings) = await Deserialize(TestDocuments.ToStream(copy, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif, async); + var content = reqifs.Single().CoreContent; + + Assert.Multiple(() => + { + if (element == "SPEC-RELATION") + { + var specRelation = content.SpecRelations.Single(x => x.Identifier == identifier); + Assert.That(specRelation.Target, Is.Null); + Assert.That(specRelation.Source?.Identifier, Is.EqualTo(otherReference)); + } + else + { + var relationGroup = content.SpecRelationGroups.Single(x => x.Identifier == identifier); + Assert.That(relationGroup.SourceSpecification, Is.Null); + Assert.That(relationGroup.TargetSpecification?.Identifier, Is.EqualTo(otherReference)); + } + + Assert.That(warnings, Has.Count.EqualTo(1)); + }); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task Verify_that_the_xhtml_content_of_a_default_value_is_read_and_not_skipped(bool async) + { + var ns = TestDocuments.ReqIFNamespace; + XNamespace xhtml = "http://www.w3.org/1999/xhtml"; + + var document = XDocument.Parse(TestDocuments.Read(FileName)); + var definition = document.Root!.Descendants(ns + "ATTRIBUTE-DEFINITION-XHTML").First(); + var identifier = definition.Attribute("IDENTIFIER")!.Value; + + // as the serializer writes it + definition.Add(new XElement(ns + "DEFAULT-VALUE", + new XElement(ns + "ATTRIBUTE-VALUE-XHTML", + new XElement(ns + "THE-VALUE", new XElement(xhtml + "div", new XElement(xhtml + "p", "default"), new XElement(xhtml + "br"))), + new XElement(ns + "DEFINITION", new XElement(ns + "ATTRIBUTE-DEFINITION-XHTML-REF", identifier))))); + + var xml = document.ToString(); + + foreach (var copy in new[] { xml, TestDocuments.Minify(xml) }) + { + var (reqifs, warnings) = await Deserialize(TestDocuments.ToStream(copy, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif, async); + + var defaultValue = reqifs.Single().CoreContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType().Single(x => x.Identifier == identifier).DefaultValue; + + Assert.That(defaultValue?.TheValue, Does.Contain("default").And.Contain(":br")); + Assert.That(warnings, Is.Empty); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task Verify_that_an_attribute_value_created_with_a_logger_factory_logs_a_skipped_element(bool async) + { + const string enumerationXml = "x"; + const string stringXml = "x"; + const string xhtmlXml = "x"; + + foreach (var (xml, create) in new (string, Func)[] + { + (enumerationXml, factory => new AttributeValueEnumeration(factory)), + (xhtmlXml, factory => new AttributeValueXHTML(factory)), + (stringXml, factory => new AttributeValueString(factory)) + }) + { + var sink = new TestLogEventSink(); + var serilogLogger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(sink).CreateLogger(); + + using var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(serilogLogger, dispose: true)); + using var reader = System.Xml.XmlReader.Create(new StringReader(xml), new System.Xml.XmlReaderSettings { Async = async }); + reader.MoveToContent(); + + var attributeValue = create(loggerFactory); + + if (async) + { + await attributeValue.ReadXmlAsync(reader, CancellationToken.None); + } + else + { + attributeValue.ReadXml(reader); + } + + Assert.That(sink.Events.Count(e => e.Level == LogEventLevel.Warning && e.MessageTemplate.Text.Contains(WarningText)), Is.EqualTo(1), attributeValue.GetType().Name); + } + } + + [TestCase(false)] + [TestCase(true)] + public async Task Verify_that_an_element_in_no_namespace_in_a_document_in_the_ReqIF_namespace_is_read(bool async) + { + // elements in no namespace are read as ReqIF elements in any document, as they were before GH-97 + var document = XDocument.Parse(TestDocuments.Read(FileName)); + var title = document.Root!.Descendants(TestDocuments.ReqIFNamespace + "TITLE").Single(); + title.AddAfterSelf(new XElement(XNamespace.None + "TITLE", "title in no namespace")); + + var xml = document.ToString(); + + Assert.That(xml, Does.Contain(""), "the document must contain a TITLE in no namespace"); + + foreach (var copy in new[] { xml, TestDocuments.Minify(xml) }) + { + var (reqifs, warnings) = await Deserialize(TestDocuments.ToStream(copy, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif, async); + + Assert.That(reqifs.Single().TheHeader.Title, Is.EqualTo("title in no namespace")); + Assert.That(warnings, Is.Empty); + } + } + + /// <summary> + /// Returns the test document without and with one foreign element at the given place. Both are prepared + /// the same way first, when the place needs ReqIF elements that the test document does not have. + /// </summary> + private static (string Expected, string Actual) CreateDocuments(string fileName, string place) + { + var ns = TestDocuments.ReqIFNamespace; + var document = XDocument.Parse(TestDocuments.Read(fileName)); + + if (place == "relation group spec relation ref") + { + var specRelation = document.Root!.Descendants(ns + "SPEC-RELATION").First().Attribute("IDENTIFIER")!.Value; + document.Root.Descendants(ns + "RELATION-GROUP").First().Add(new XElement(ns + "SPEC-RELATIONS", new XElement(ns + "SPEC-RELATION-REF", specRelation))); + } + + var expected = document.ToString(); + + return (expected, AddForeignElement(expected, place)); + } + + /// <summary> + /// Returns a copy of the document with one foreign element at the given place + /// </summary> + private static string AddForeignElement(string original, string place) + { + var ns = TestDocuments.ReqIFNamespace; + var document = XDocument.Parse(original); + var root = document.Root!; + + root.SetAttributeValue(XNamespace.Xmlns + "other", Other.NamespaceName); + + XElement First(string localName) => root.Descendants(ns + localName).First(); + + // a copy of the element, in the foreign namespace, with a new identifier + XElement ForeignCopy(XElement element) + { + var copy = new XElement(element) { Name = Other + element.Name.LocalName }; + + if (copy.Attribute("IDENTIFIER") != null) + { + copy.SetAttributeValue("IDENTIFIER", "foreign-" + copy.Attribute("IDENTIFIER")!.Value); + } + + return copy; + } + + switch (place) + { + case "header field": + First("TITLE").AddAfterSelf(new XElement(Other + "TITLE", "foreign title")); + break; + case "content section": + // a ReqIF spec object with a new identifier, in a foreign SPEC-OBJECTS + var specObject = new XElement(First("SPEC-OBJECT")); + specObject.SetAttributeValue("IDENTIFIER", "foreign-spec-object"); + First("SPEC-OBJECTS").AddAfterSelf(new XElement(Other + "SPEC-OBJECTS", specObject)); + break; + case "datatype": + First("DATATYPE-DEFINITION-STRING").AddAfterSelf(ForeignCopy(First("DATATYPE-DEFINITION-STRING"))); + break; + case "enum value": + First("ENUM-VALUE").AddAfterSelf(ForeignCopy(First("ENUM-VALUE"))); + break; + case "spec type": + First("SPEC-OBJECT-TYPE").AddAfterSelf(ForeignCopy(First("SPEC-OBJECT-TYPE"))); + break; + case "attribute definition": + First("ATTRIBUTE-DEFINITION-STRING").AddAfterSelf(ForeignCopy(First("ATTRIBUTE-DEFINITION-STRING"))); + break; + case "spec object": + First("SPEC-OBJECT").AddAfterSelf(ForeignCopy(First("SPEC-OBJECT"))); + break; + case "empty spec object": + First("SPEC-OBJECT").AddAfterSelf(new XElement(Other + "SPEC-OBJECT", new XAttribute("IDENTIFIER", "foreign-empty"))); + break; + case "attribute value": + var stringValue = First("ATTRIBUTE-VALUE-STRING"); + var foreignValue = ForeignCopy(stringValue); + foreignValue.SetAttributeValue("THE-VALUE", "foreign value"); + stringValue.AddAfterSelf(foreignValue); + break; + case "enumeration value reference": + var enumValueRef = First("ENUM-VALUE-REF"); + var otherEnumValue = root.Descendants(ns + "ENUM-VALUE").Select(x => x.Attribute("IDENTIFIER")!.Value).First(x => x != enumValueRef.Value); + enumValueRef.AddAfterSelf(new XElement(Other + "ENUM-VALUE-REF", otherEnumValue)); + break; + case "xhtml value": + First("ATTRIBUTE-VALUE-XHTML").Element(ns + "THE-VALUE")!.AddAfterSelf(new XElement(Other + "THE-VALUE", new XElement((XNamespace)"http://www.w3.org/1999/xhtml" + "p", "foreign"))); + break; + case "spec hierarchy": + First("SPEC-HIERARCHY").AddAfterSelf(ForeignCopy(First("SPEC-HIERARCHY"))); + break; + case "spec relation": + First("SPEC-RELATION").AddAfterSelf(ForeignCopy(First("SPEC-RELATION"))); + break; + case "relation group spec relations": + First("RELATION-GROUP").Add(new XElement(Other + "SPEC-RELATIONS", new XElement(ns + "SPEC-RELATION-REF", First("SPEC-RELATION").Attribute("IDENTIFIER")!.Value))); + break; + case "alternative id": + var alternativeId = First("ALTERNATIVE-ID").Element(ns + "ALTERNATIVE-ID")!; + // after the ReqIF one, which it would replace when it was read + alternativeId.AddAfterSelf(new XElement(Other + "ALTERNATIVE-ID", new XAttribute("IDENTIFIER", "foreign-alternative-id"))); + break; + case "tool extension": + root.Add(new XElement(ns + "TOOL-EXTENSIONS", + new XElement(Other + "REQ-IF-TOOL-EXTENSION", new XElement(Other + "SETTING", "foreign")))); + break; + case "root child": + // a header with another title, in a foreign THE-HEADER + var header = new XElement(First("REQ-IF-HEADER")); + header.Element(ns + "TITLE")!.Value = "foreign title"; + First("THE-HEADER").AddAfterSelf(new XElement(Other + "THE-HEADER", header)); + break; + case "embedded value": + First("EMBEDDED-VALUE").AddAfterSelf(new XElement(Other + "EMBEDDED-VALUE", new XAttribute("KEY", "2"), new XAttribute("OTHER-CONTENT", "foreign"))); + break; + case "spec type child": + // an attribute definition with a new identifier, in a foreign SPEC-ATTRIBUTES + var attributeDefinition = new XElement(First("ATTRIBUTE-DEFINITION-STRING")); + attributeDefinition.SetAttributeValue("IDENTIFIER", "foreign-attribute-definition"); + First("SPEC-OBJECT-TYPE").Add(new XElement(Other + "SPEC-ATTRIBUTES", attributeDefinition)); + break; + case var _ when place.StartsWith("attribute definition child ", StringComparison.Ordinal): + // an alternative id in a foreign ALTERNATIVE-ID, after the ReqIF one if there is one + var kind = place.Substring("attribute definition child ".Length); + First("ATTRIBUTE-DEFINITION-" + kind).Add(new XElement(Other + "ALTERNATIVE-ID", new XElement(ns + "ALTERNATIVE-ID", new XAttribute("IDENTIFIER", "foreign-alternative-id")))); + break; + case "default value enumeration value reference": + var defaultValueRef = root.Descendants(ns + "DEFAULT-VALUE").Descendants(ns + "ENUM-VALUE-REF").First(); + var otherDefaultValue = root.Descendants(ns + "ENUM-VALUE").Select(x => x.Attribute("IDENTIFIER")!.Value).First(x => x != defaultValueRef.Value); + defaultValueRef.AddAfterSelf(new XElement(Other + "ENUM-VALUE-REF", otherDefaultValue)); + break; + case "specification": + First("SPECIFICATION").AddAfterSelf(ForeignCopy(First("SPECIFICATION"))); + break; + case "spec hierarchy object": + var specHierarchy = First("SPEC-HIERARCHY"); + var objectRef = specHierarchy.Element(ns + "OBJECT")!.Element(ns + "SPEC-OBJECT-REF")!.Value; + var otherSpecObject = root.Descendants(ns + "SPEC-OBJECT").Select(x => x.Attribute("IDENTIFIER")!.Value).First(x => x != objectRef); + specHierarchy.Element(ns + "OBJECT")!.AddAfterSelf(new XElement(Other + "OBJECT", new XElement(ns + "SPEC-OBJECT-REF", otherSpecObject))); + break; + case "nested spec hierarchy": + var nested = root.Descendants(ns + "SPEC-HIERARCHY").First(x => x.Parent!.Parent!.Name == ns + "SPEC-HIERARCHY"); + nested.AddAfterSelf(ForeignCopy(nested)); + break; + case "relation group": + First("RELATION-GROUP").AddAfterSelf(ForeignCopy(First("RELATION-GROUP"))); + break; + case "relation group spec relation ref": + // CreateDocuments added a SPEC-RELATIONS with a reference to the spec relation; a second reference would add it twice + var specRelationRef = First("RELATION-GROUP").Element(ns + "SPEC-RELATIONS")!.Element(ns + "SPEC-RELATION-REF")!; + specRelationRef.AddAfterSelf(new XElement(Other + "SPEC-RELATION-REF", specRelationRef.Value)); + break; + case var _ when place.Contains(" reference "): + AddForeignReference(root, place); + break; + case "empty element before the xhtml value": + SimplifiedXhtmlValue(root).Element(ns + "THE-VALUE")!.AddBeforeSelf(new XElement(Other + "EMPTY")); + break; + case "empty element between the xhtml values": + SimplifiedXhtmlValue(root).Element(ns + "THE-VALUE")!.AddAfterSelf(new XElement(Other + "EMPTY")); + break; + case "spec relation source wrapper": + AddForeignCopyWithOtherReference(root, First("SPEC-RELATION").Element(ns + "SOURCE")!, "SPEC-OBJECT-REF", Identifiers(root, "SPEC-OBJECT")); + break; + case var _ when place.StartsWith("attribute value definition wrapper ", StringComparison.Ordinal): + // a foreign DEFINITION after the ReqIF one, as a direct child of the attribute value + var valueKind = place.Substring("attribute value definition wrapper ".Length); + var valueDefinition = root.Descendants(ns + "SPEC-OBJECT").Descendants(ns + "ATTRIBUTE-VALUE-" + valueKind).First().Element(ns + "DEFINITION")!; + AddForeignCopyWithOtherReference(root, valueDefinition, $"ATTRIBUTE-DEFINITION-{valueKind}-REF", Identifiers(root, "ATTRIBUTE-DEFINITION-" + valueKind)); + break; + case "relation group target specification wrapper": + AddForeignCopyWithOtherReference(root, First("RELATION-GROUP").Element(ns + "TARGET-SPECIFICATION")!, "SPECIFICATION-REF", Identifiers(root, "SPECIFICATION")); + break; + default: + throw new ArgumentOutOfRangeException(nameof(place), place, "unknown place"); + } + + return document.ToString(); + } + + /// <summary> + /// Adds a foreign reference element before or after the ReqIF reference in a reference container + /// (TYPE, DEFINITION, SOURCE, TARGET, OBJECT, ...). It refers to another element of the same kind, so + /// it would change the object graph (or make reading fail) when it was read. + /// </summary> + private static void AddForeignReference(XElement root, string place) + { + var ns = TestDocuments.ReqIFNamespace; + + var index = place.LastIndexOf(" reference ", StringComparison.Ordinal); + var words = place.Substring(index + " reference ".Length).Split(' '); + var before = words[0] == "before"; + var kind = words.Length > 1 ? words[1] : "STRING"; + + (XElement Container, string Reference, IEnumerable<string> Candidates) target = place.Substring(0, index) switch + { + "spec object type" => (root.Descendants(ns + "SPEC-OBJECT").First().Element(ns + "TYPE")!, "SPEC-OBJECT-TYPE-REF", Identifiers(root, "SPEC-OBJECT-TYPE", "SPECIFICATION-TYPE")), + "attribute value definition" => (root.Descendants(ns + "SPEC-OBJECT").Descendants(ns + "ATTRIBUTE-VALUE-" + kind).First().Element(ns + "DEFINITION")!, $"ATTRIBUTE-DEFINITION-{kind}-REF", Identifiers(root, "ATTRIBUTE-DEFINITION-" + kind)), + "attribute definition type" => (root.Descendants(ns + "ATTRIBUTE-DEFINITION-" + kind).First().Element(ns + "TYPE")!, $"DATATYPE-DEFINITION-{kind}-REF", Identifiers(root, "DATATYPE-DEFINITION-" + kind, "DATATYPE-DEFINITION-BOOLEAN", "DATATYPE-DEFINITION-STRING")), + "spec relation type" => (root.Descendants(ns + "SPEC-RELATION").First().Element(ns + "TYPE")!, "SPEC-RELATION-TYPE-REF", Identifiers(root, "SPEC-RELATION-TYPE", "SPEC-OBJECT-TYPE")), + "xhtml value definition" => (SimplifiedXhtmlValue(root).Element(ns + "DEFINITION")!, "ATTRIBUTE-DEFINITION-XHTML-REF", Identifiers(root, "ATTRIBUTE-DEFINITION-XHTML")), + "spec relation source" => (root.Descendants(ns + "SPEC-RELATION").First().Element(ns + "SOURCE")!, "SPEC-OBJECT-REF", Identifiers(root, "SPEC-OBJECT")), + "spec relation target" => (root.Descendants(ns + "SPEC-RELATION").First().Element(ns + "TARGET")!, "SPEC-OBJECT-REF", Identifiers(root, "SPEC-OBJECT")), + "spec hierarchy object" => (root.Descendants(ns + "SPEC-HIERARCHY").First().Element(ns + "OBJECT")!, "SPEC-OBJECT-REF", Identifiers(root, "SPEC-OBJECT")), + "specification type" => (root.Descendants(ns + "SPECIFICATION").First().Element(ns + "TYPE")!, "SPECIFICATION-TYPE-REF", Identifiers(root, "SPECIFICATION-TYPE", "SPEC-OBJECT-TYPE")), + "relation group source specification" => (root.Descendants(ns + "RELATION-GROUP").First().Element(ns + "SOURCE-SPECIFICATION")!, "SPECIFICATION-REF", Identifiers(root, "SPECIFICATION")), + "relation group type" => (root.Descendants(ns + "RELATION-GROUP").First().Element(ns + "TYPE")!, "RELATION-GROUP-TYPE-REF", Identifiers(root, "RELATION-GROUP-TYPE", "SPEC-OBJECT-TYPE")), + _ => throw new ArgumentOutOfRangeException(nameof(place), place, "unknown reference place") + }; + + var reference = target.Container.Element(ns + target.Reference)!; + var other = target.Candidates.First(x => x != reference.Value); + var foreign = new XElement(Other + target.Reference, other); + + if (before) + { + reference.AddBeforeSelf(foreign); + } + else + { + reference.AddAfterSelf(foreign); + } + } + + /// <summary> + /// Adds, after a reference container, a foreign copy of it that refers to another element; it would replace + /// the reference when it was read + /// </summary> + private static void AddForeignCopyWithOtherReference(XElement root, XElement container, string reference, IEnumerable<string> candidates) + { + var ns = TestDocuments.ReqIFNamespace; + var copy = new XElement(container) { Name = Other + container.Name.LocalName }; + var referenceElement = copy.Element(ns + reference)!; + referenceElement.Value = candidates.First(x => x != referenceElement.Value); + container.AddAfterSelf(copy); + } + + /// <summary> + /// The identifiers of the elements with the given local names, in that order + /// </summary> + private static IEnumerable<string> Identifiers(XElement root, params string[] localNames) + { + return localNames.SelectMany(name => root.Descendants(TestDocuments.ReqIFNamespace + name)).Select(x => x.Attribute("IDENTIFIER")!.Value); + } + + /// <summary> + /// The first XHTML attribute value that has a THE-ORIGINAL-VALUE + /// </summary> + private static XElement SimplifiedXhtmlValue(XElement root) + { + var ns = TestDocuments.ReqIFNamespace; + + return root.Descendants(ns + "ATTRIBUTE-VALUE-XHTML").First(x => x.Element(ns + "THE-ORIGINAL-VALUE") != null); + } + + private static async Task<(List<ReqIF> ReqIFs, List<string> Warnings)> Deserialize(Stream stream, SupportedFileExtensionKind kind, bool async) + { + var sink = new TestLogEventSink(); + var logger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(sink).CreateLogger(); + + using var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(logger, dispose: true)); + + var deserializer = new ReqIFDeserializer(loggerFactory); + + // a reader that does not move on would loop forever: fail the test instead of blocking the run + var reqifs = await Task.Run(async () => async + ? (await deserializer.DeserializeAsync(stream, kind, CancellationToken.None)).ToList() + : deserializer.Deserialize(stream, kind).ToList()) + .WaitAsync(TimeSpan.FromSeconds(60)); + + var warnings = sink.Events + .Where(e => e.Level == LogEventLevel.Warning && e.MessageTemplate.Text.Contains(WarningText)) + .Select(e => e.RenderMessage()) + .ToList(); + + return (reqifs, warnings); + } + } +} diff --git a/ReqIFSharp.Tests/PrefixedNamespaceTestFixture.cs b/ReqIFSharp.Tests/PrefixedNamespaceTestFixture.cs index 6bf0da7..448bf50 100644 --- a/ReqIFSharp.Tests/PrefixedNamespaceTestFixture.cs +++ b/ReqIFSharp.Tests/PrefixedNamespaceTestFixture.cs @@ -23,7 +23,6 @@ namespace ReqIFSharp.Tests using System.Linq; using System.Threading; using System.Threading.Tasks; - using System.Xml.Linq; using Microsoft.Extensions.Logging; @@ -75,7 +74,7 @@ public void TearDown() public void Verify_that_a_prefixed_document_is_read_the_same_as_the_original(string fileName) { var original = TestDocuments.Read(fileName); - var prefixed = CreatePrefixedCopy(original); + var prefixed = TestDocuments.Prefix(original); foreach (var kind in TestDocuments.Kinds) { @@ -92,7 +91,7 @@ public void Verify_that_a_prefixed_document_is_read_the_same_as_the_original(str public async Task Verify_that_a_prefixed_document_is_read_the_same_as_the_original_async(string fileName) { var original = TestDocuments.Read(fileName); - var prefixed = CreatePrefixedCopy(original); + var prefixed = TestDocuments.Prefix(original); foreach (var kind in TestDocuments.Kinds) { @@ -109,7 +108,7 @@ public async Task Verify_that_a_prefixed_document_is_read_the_same_as_the_origin public void Verify_that_a_document_without_namespace_is_read_the_same_as_the_original() { var original = TestDocuments.Read("reqifsharpgenerated.reqif"); - var withoutNamespace = CreateCopyWithoutNamespace(original); + var withoutNamespace = TestDocuments.RemoveNamespace(original); foreach (var kind in TestDocuments.Kinds) { @@ -126,7 +125,7 @@ public void Verify_that_a_document_without_namespace_is_read_the_same_as_the_ori public async Task Verify_that_a_document_without_namespace_is_read_the_same_as_the_original_async() { var original = TestDocuments.Read("reqifsharpgenerated.reqif"); - var withoutNamespace = CreateCopyWithoutNamespace(original); + var withoutNamespace = TestDocuments.RemoveNamespace(original); foreach (var kind in TestDocuments.Kinds) { @@ -170,57 +169,5 @@ private void AssertSkippedWarningLogged() Assert.That(warnings, Has.Count.EqualTo(1)); Assert.That(warnings[0].RenderMessage(), Does.Contain("http://example.com/not-reqif")); } - - /// <summary> - /// Returns a copy of the document in which every ReqIF element is written as reqif:... - /// </summary> - private static string CreatePrefixedCopy(string original) - { - // keep the original whitespace, so that the copy differs from the original only in its prefixes - var document = XDocument.Parse(original, LoadOptions.PreserveWhitespace); - - RemoveReqIFNamespaceDeclarations(document); - - document.Root!.SetAttributeValue(XNamespace.Xmlns + "reqif", TestDocuments.ReqIFNamespace.NamespaceName); - - var prefixed = document.ToString(SaveOptions.DisableFormatting); - - Assert.That(prefixed, Does.Contain("<reqif:REQ-IF "), "the copy must use the reqif prefix"); - Assert.That(prefixed, Does.Not.Contain("<REQ-IF "), "the copy must not use the default namespace for ReqIF elements"); - - return prefixed; - } - - /// <summary> - /// Returns a copy of the document in which the ReqIF elements are in no namespace - /// </summary> - private static string CreateCopyWithoutNamespace(string original) - { - var document = XDocument.Parse(original, LoadOptions.PreserveWhitespace); - - RemoveReqIFNamespaceDeclarations(document); - - foreach (var element in document.Descendants().Where(e => e.Name.Namespace == TestDocuments.ReqIFNamespace)) - { - element.Name = XNamespace.None + element.Name.LocalName; - } - - var withoutNamespace = document.ToString(SaveOptions.DisableFormatting); - - Assert.That(withoutNamespace, Does.Contain("<REQ-IF "), "the copy must contain an unprefixed root"); - Assert.That(withoutNamespace, Does.Not.Contain(TestDocuments.ReqIFNamespace.NamespaceName), "the copy must not declare the ReqIF namespace"); - - return withoutNamespace; - } - - private static void RemoveReqIFNamespaceDeclarations(XDocument document) - { - foreach (var element in document.Descendants()) - { - element.Attributes() - .Where(a => a.IsNamespaceDeclaration && a.Value == TestDocuments.ReqIFNamespace.NamespaceName) - .Remove(); - } - } } } diff --git a/ReqIFSharp.Tests/TestDocuments.cs b/ReqIFSharp.Tests/TestDocuments.cs index 9918f92..cf8b0c5 100644 --- a/ReqIFSharp.Tests/TestDocuments.cs +++ b/ReqIFSharp.Tests/TestDocuments.cs @@ -119,6 +119,58 @@ private static bool IsRawXmlContent(XText text) || a.Name.LocalName == "REQ-IF-TOOL-EXTENSION"); } + /// <summary> + /// Returns a copy of the document in which every ReqIF element is written as reqif:... + /// </summary> + public static string Prefix(string original) + { + // keep the original whitespace, so that the copy differs from the original only in its prefixes + var document = XDocument.Parse(original, LoadOptions.PreserveWhitespace); + + RemoveReqIFNamespaceDeclarations(document); + + document.Root!.SetAttributeValue(XNamespace.Xmlns + "reqif", ReqIFNamespace.NamespaceName); + + var prefixed = document.ToString(SaveOptions.DisableFormatting); + + Assert.That(prefixed, Does.Contain("<reqif:REQ-IF "), "the copy must use the reqif prefix"); + Assert.That(prefixed, Does.Not.Contain("<REQ-IF "), "the copy must not use the default namespace for ReqIF elements"); + + return prefixed; + } + + /// <summary> + /// Returns a copy of the document in which the ReqIF elements are in no namespace + /// </summary> + public static string RemoveNamespace(string original) + { + var document = XDocument.Parse(original, LoadOptions.PreserveWhitespace); + + RemoveReqIFNamespaceDeclarations(document); + + foreach (var element in document.Descendants().Where(e => e.Name.Namespace == ReqIFNamespace)) + { + element.Name = XNamespace.None + element.Name.LocalName; + } + + var withoutNamespace = document.ToString(SaveOptions.DisableFormatting); + + Assert.That(withoutNamespace, Does.Contain("<REQ-IF "), "the copy must contain an unprefixed root"); + Assert.That(XDocument.Parse(withoutNamespace).Descendants().Any(e => e.Name.Namespace == ReqIFNamespace), Is.False, "the copy must not contain elements in the ReqIF namespace"); + + return withoutNamespace; + } + + private static void RemoveReqIFNamespaceDeclarations(XDocument document) + { + foreach (var element in document.Descendants()) + { + element.Attributes() + .Where(a => a.IsNamespaceDeclaration && a.Value == ReqIFNamespace.NamespaceName) + .Remove(); + } + } + /// <summary> /// 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). diff --git a/ReqIFSharp.Tests/XmlReaderExtensionsTestFixture.cs b/ReqIFSharp.Tests/XmlReaderExtensionsTestFixture.cs index d0f5b8f..b5a79f6 100644 --- a/ReqIFSharp.Tests/XmlReaderExtensionsTestFixture.cs +++ b/ReqIFSharp.Tests/XmlReaderExtensionsTestFixture.cs @@ -20,14 +20,22 @@ namespace ReqIFSharp.Tests { + using System; using System.IO; + using System.Linq; + using System.Threading; using System.Threading.Tasks; using System.Xml; + using Microsoft.Extensions.Logging; + using NUnit.Framework; using ReqIFSharp; + using Serilog; + using Serilog.Events; + /// <summary> /// Suite of tests for the <see cref="XmlReaderExtensions"/> class /// </summary> @@ -185,6 +193,304 @@ public void Verify_that_the_exception_has_no_line_and_position_when_the_reader_h Assert.That(exception.LinePosition, Is.Zero); } + [TestCase("<ROOT><o:A xmlns:o=\"http://example.com/other\"><B>text</B></o:A><C/></ROOT>")] + [TestCase("<ROOT><A xmlns=\"http://example.com/other\"><B>text</B></A><C/></ROOT>")] + [TestCase("<ROOT><A xmlns=\"HTTP://WWW.OMG.ORG/SPEC/REQIF/20110401/REQIF.XSD\"><B>text</B></A><C/></ROOT>", Description = "namespaces are compared exactly: a different case is another namespace")] + public async Task Verify_that_a_foreign_element_is_skipped_and_the_reader_is_left_on_its_end_tag(string xml) + { + using (var reader = CreateReaderOnFirstChild(xml)) + { + Assert.That(reader.SkipForeignElement(null), Is.True); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.EndElement)); + Assert.That(reader.LocalName, Is.EqualTo("A")); + + reader.Read(); + Assert.That(reader.LocalName, Is.EqualTo("C"), "the next Read must land on the next sibling"); + } + + using (var reader = CreateReaderOnFirstChild(xml, true)) + { + Assert.That(await reader.SkipForeignElementAsync(null, CancellationToken.None), Is.True); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.EndElement)); + Assert.That(reader.LocalName, Is.EqualTo("A")); + + await reader.ReadAsync(); + Assert.That(reader.LocalName, Is.EqualTo("C"), "the next Read must land on the next sibling"); + } + } + + [Test] + public async Task Verify_that_an_empty_foreign_element_is_skipped_and_the_reader_stays_on_it() + { + const string xml = "<ROOT><o:A xmlns:o=\"http://example.com/other\"/><C/></ROOT>"; + + using (var reader = CreateReaderOnFirstChild(xml)) + { + Assert.That(reader.SkipForeignElement(null), Is.True); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.Element)); + Assert.That(reader.LocalName, Is.EqualTo("A")); + + reader.Read(); + Assert.That(reader.LocalName, Is.EqualTo("C")); + } + + using (var reader = CreateReaderOnFirstChild(xml, true)) + { + Assert.That(await reader.SkipForeignElementAsync(null, CancellationToken.None), Is.True); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.Element)); + Assert.That(reader.LocalName, Is.EqualTo("A")); + + await reader.ReadAsync(); + Assert.That(reader.LocalName, Is.EqualTo("C")); + } + } + + [TestCase("<ROOT><A xmlns=\"http://www.omg.org/spec/ReqIF/20110401/reqif.xsd\"><B/></A></ROOT>")] + [TestCase("<ROOT><reqif:A xmlns:reqif=\"http://www.omg.org/spec/ReqIF/20110401/reqif.xsd\"><B/></reqif:A></ROOT>")] + [TestCase("<ROOT><A><B/></A></ROOT>")] + public async Task Verify_that_an_element_in_the_ReqIF_namespace_or_in_no_namespace_is_not_skipped(string xml) + { + using (var reader = CreateReaderOnFirstChild(xml)) + { + Assert.That(reader.SkipForeignElement(null), Is.False); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.Element)); + Assert.That(reader.LocalName, Is.EqualTo("A"), "the reader must not move"); + } + + using (var reader = CreateReaderOnFirstChild(xml, true)) + { + Assert.That(await reader.SkipForeignElementAsync(null, CancellationToken.None), Is.False); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.Element)); + Assert.That(reader.LocalName, Is.EqualTo("A"), "the reader must not move"); + } + } + + [Test] + public async Task Verify_that_a_reader_that_is_not_on_an_element_is_not_skipped() + { + using (var reader = CreateReaderOnFirstChild("<ROOT><A>text</A></ROOT>")) + { + reader.Read(); + + Assert.That(reader.SkipForeignElement(null), Is.False); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.Text)); + } + + using (var reader = CreateReaderOnFirstChild("<ROOT><A>text</A></ROOT>", true)) + { + await reader.ReadAsync(); + + Assert.That(await reader.SkipForeignElementAsync(null, CancellationToken.None), Is.False); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.Text)); + } + } + + [Test] + public async Task Verify_that_a_warning_with_the_name_namespace_and_line_is_logged_for_a_skipped_element() + { + const string xml = "<ROOT>\n <o:A xmlns:o=\"http://example.com/other\"/></ROOT>"; + + foreach (var async in new[] { false, true }) + { + var sink = new TestLogEventSink(); + var serilogLogger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(sink).CreateLogger(); + + using var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(serilogLogger, dispose: true)); + var logger = loggerFactory.CreateLogger<XmlReaderExtensionsTestFixture>(); + + using var reader = CreateReaderOnFirstChild(xml, async); + + var skipped = async ? await reader.SkipForeignElementAsync(logger, CancellationToken.None) : reader.SkipForeignElement(logger); + + var warning = sink.Events.Single(e => e.Level == LogEventLevel.Warning).RenderMessage(); + + Assert.That(skipped, Is.True); + Assert.That(warning, Does.Contain("o:A").And.Contain("2:4").And.Contain("http://example.com/other").And.Contain("has been skipped"), $"async: {async}"); + } + } + + [Test] + public void Verify_that_skipping_a_foreign_element_asynchronously_can_be_cancelled() + { + using var reader = CreateReaderOnFirstChild("<ROOT><o:A xmlns:o=\"http://example.com/other\"><B/><B/></o:A></ROOT>", true); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + Assert.That(async () => await reader.SkipForeignElementAsync(null, cts.Token), Throws.InstanceOf<OperationCanceledException>()); + } + + [Test] + public async Task Verify_that_ReadToReqIFDescendant_finds_a_ReqIF_descendant_and_skips_foreign_ones() + { + const string xml = "<ROOT><A><o:B xmlns:o=\"http://example.com/other\">x</o:B><C><B>y</B></C></A></ROOT>"; + + foreach (var async in new[] { false, true }) + { + var (logger, warnings) = CreateLogger(); + using var reader = CreateReaderOnFirstChild(xml, async); + + var found = async ? await reader.ReadToReqIFDescendantAsync("B", logger, CancellationToken.None) : reader.ReadToReqIFDescendant("B", logger); + + Assert.That(found, Is.True, $"async: {async}"); + Assert.That(reader.LocalName, Is.EqualTo("B")); + Assert.That(reader.NamespaceURI, Is.Empty, "the reader must be on the ReqIF B, not on the foreign one"); + Assert.That(reader.Depth, Is.EqualTo(3), "the B in C"); + Assert.That(warnings(), Is.EqualTo(1)); + } + } + + [Test] + public async Task Verify_that_ReadToReqIFDescendant_does_not_search_past_the_element_it_started_on() + { + const string xml = "<ROOT><A><o:B xmlns:o=\"http://example.com/other\"/></A><B/></ROOT>"; + + foreach (var async in new[] { false, true }) + { + var (logger, warnings) = CreateLogger(); + using var reader = CreateReaderOnFirstChild(xml, async); + + var found = async ? await reader.ReadToReqIFDescendantAsync("B", logger, CancellationToken.None) : reader.ReadToReqIFDescendant("B", logger); + + Assert.That(found, Is.False, $"async: {async}"); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.EndElement)); + Assert.That(reader.LocalName, Is.EqualTo("A"), "the reader must stop on the end tag of A, before its sibling B"); + Assert.That(warnings(), Is.EqualTo(1)); + } + } + + [Test] + public async Task Verify_that_ReadToReqIFDescendant_on_an_empty_element_returns_false_and_does_not_move() + { + foreach (var async in new[] { false, true }) + { + using var reader = CreateReaderOnFirstChild("<ROOT><A/><B/></ROOT>", async); + + var found = async ? await reader.ReadToReqIFDescendantAsync("B", null, CancellationToken.None) : reader.ReadToReqIFDescendant("B", null); + + Assert.That(found, Is.False, $"async: {async}"); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.Element)); + Assert.That(reader.LocalName, Is.EqualTo("A")); + } + } + + [Test] + public async Task Verify_that_ReadToReqIFDescendant_on_a_foreign_element_skips_it_and_returns_false() + { + const string xml = "<ROOT><o:A xmlns:o=\"http://example.com/other\"><B/></o:A><C/></ROOT>"; + + foreach (var async in new[] { false, true }) + { + var (logger, warnings) = CreateLogger(); + using var reader = CreateReaderOnFirstChild(xml, async); + + var found = async ? await reader.ReadToReqIFDescendantAsync("B", logger, CancellationToken.None) : reader.ReadToReqIFDescendant("B", logger); + + Assert.That(found, Is.False, $"async: {async}"); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.EndElement)); + Assert.That(reader.LocalName, Is.EqualTo("A"), "the reader must be on the end tag of the skipped element"); + Assert.That(warnings(), Is.EqualTo(1)); + } + } + + [Test] + public async Task Verify_that_foreign_elements_to_the_end_of_a_subtree_are_skipped_with_a_warning() + { + const string xml = "<ROOT><A><B/><o:X xmlns:o=\"http://example.com/other\"><o:Y/></o:X><o:Z xmlns:o=\"http://example.com/other\"/></A><C/></ROOT>"; + + foreach (var async in new[] { false, true }) + { + var (logger, warnings) = CreateLogger(); + using var reader = CreateReaderOnFirstChild(xml, async); + + using (var subtree = reader.ReadSubtree()) + { + if (async) + { + await subtree.SkipForeignElementsToEndAsync(logger, CancellationToken.None); + } + else + { + subtree.SkipForeignElementsToEnd(logger); + } + + Assert.That(subtree.EOF, Is.True, $"async: {async}"); + } + + Assert.That(warnings(), Is.EqualTo(2), "X and Z, not Y inside X"); + + reader.Read(); + Assert.That(reader.LocalName, Is.EqualTo("C"), "the outer reader must continue after A"); + } + } + + [Test] + public async Task Verify_that_ReadToReqIFDescendant_on_an_end_tag_returns_false_and_does_not_move() + { + foreach (var async in new[] { false, true }) + { + using var reader = CreateReaderOnFirstChild("<ROOT><A><B/></A><B/></ROOT>", async); + + reader.Read(); + reader.Read(); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.EndElement), "the reader must start on the end tag of A"); + + var found = async ? await reader.ReadToReqIFDescendantAsync("B", null, CancellationToken.None) : reader.ReadToReqIFDescendant("B", null); + + Assert.That(found, Is.False, $"async: {async}"); + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.EndElement)); + Assert.That(reader.LocalName, Is.EqualTo("A"), "the reader must not move to the B after A"); + } + } + + [Test] + public async Task Verify_that_ReadToReqIFDescendant_on_whitespace_searches_the_next_element() + { + foreach (var async in new[] { false, true }) + { + using var reader = CreateReaderOnFirstChild("<ROOT> <A><B/></A></ROOT>", async); + + Assert.That(reader.NodeType, Is.EqualTo(XmlNodeType.Whitespace), "the reader must start on the whitespace"); + + var found = async ? await reader.ReadToReqIFDescendantAsync("B", null, CancellationToken.None) : reader.ReadToReqIFDescendant("B", null); + + Assert.That(found, Is.True, $"async: {async}"); + Assert.That(reader.LocalName, Is.EqualTo("B")); + Assert.That(reader.Depth, Is.EqualTo(2), "the B in A"); + } + } + + [Test] + public void Verify_that_the_asynchronous_reference_helpers_can_be_cancelled() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + + using (var reader = CreateReaderOnFirstChild("<ROOT><A><B/><C/></A></ROOT>", true)) + { + Assert.That(async () => await reader.ReadToReqIFDescendantAsync("C", null, cts.Token), Throws.InstanceOf<OperationCanceledException>()); + } + + using (var reader = CreateReaderOnFirstChild("<ROOT><A><B/><C/></A></ROOT>", true)) + { + using var subtree = reader.ReadSubtree(); + + Assert.That(async () => await subtree.SkipForeignElementsToEndAsync(null, cts.Token), Throws.InstanceOf<OperationCanceledException>()); + } + } + + /// <summary> + /// Creates a logger and a function that counts the warnings logged to it + /// </summary> + private static (Microsoft.Extensions.Logging.ILogger Logger, Func<int> Warnings) CreateLogger() + { + var sink = new TestLogEventSink(); + var serilogLogger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(sink).CreateLogger(); + var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(serilogLogger, dispose: true)); + + return (loggerFactory.CreateLogger<XmlReaderExtensionsTestFixture>(), () => sink.Events.Count(e => e.Level == LogEventLevel.Warning)); + } + /// <summary> /// Creates a reader positioned on the first child element of the root /// </summary> diff --git a/ReqIFSharp/AlternativeID.cs b/ReqIFSharp/AlternativeID.cs index c428522..079703e 100644 --- a/ReqIFSharp/AlternativeID.cs +++ b/ReqIFSharp/AlternativeID.cs @@ -25,6 +25,8 @@ namespace ReqIFSharp using System.Threading.Tasks; using System.Xml; + using Microsoft.Extensions.Logging; + /// <summary> /// The purpose of the <see cref="AlternativeId"/> class is to provide an alternative, tool-specific identification. /// </summary> @@ -104,12 +106,20 @@ internal async Task WriteXmlAsync(XmlWriter writer, CancellationToken token) /// <param name="reader"> /// an instance of <see cref="XmlReader"/> /// </param> - internal void ReadXml(XmlReader reader) + /// <param name="logger"> + /// The <see cref="ILogger"/> used to log elements that are skipped, or null + /// </param> + internal void ReadXml(XmlReader reader, ILogger logger = null) { while (reader.Read()) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(logger)) + { + continue; + } + switch (reader.LocalName) { case "ALTERNATIVE-ID": @@ -129,12 +139,20 @@ internal void ReadXml(XmlReader reader) /// <param name="token"> /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// </param> - internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token) + /// <param name="logger"> + /// The <see cref="ILogger"/> used to log elements that are skipped, or null + /// </param> + internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token, ILogger logger = null) { while (await reader.ReadAsync()) { if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(logger, token)) + { + continue; + } + switch (reader.LocalName) { case "ALTERNATIVE-ID": diff --git a/ReqIFSharp/AttributeDefinition/AttributeDefinitionBoolean.cs b/ReqIFSharp/AttributeDefinition/AttributeDefinitionBoolean.cs index 7b80687..3eaad39 100644 --- a/ReqIFSharp/AttributeDefinition/AttributeDefinitionBoolean.cs +++ b/ReqIFSharp/AttributeDefinition/AttributeDefinitionBoolean.cs @@ -140,6 +140,11 @@ internal override void ReadXml(XmlReader reader) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -148,7 +153,7 @@ internal override void ReadXml(XmlReader reader) this.ReadAlternativeId(reader); break; case "DATATYPE-DEFINITION-BOOLEAN-REF": - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var datatypeDefinition = (DatatypeDefinitionBoolean)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; @@ -193,6 +198,11 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -201,7 +211,7 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to await this.ReadAlternativeIdAsync(reader, token); break; case "DATATYPE-DEFINITION-BOOLEAN-REF": - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var datatypeDefinition = (DatatypeDefinitionBoolean)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; diff --git a/ReqIFSharp/AttributeDefinition/AttributeDefinitionDate.cs b/ReqIFSharp/AttributeDefinition/AttributeDefinitionDate.cs index aa48001..5e8eabb 100644 --- a/ReqIFSharp/AttributeDefinition/AttributeDefinitionDate.cs +++ b/ReqIFSharp/AttributeDefinition/AttributeDefinitionDate.cs @@ -141,6 +141,11 @@ internal override void ReadXml(XmlReader reader) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -157,7 +162,7 @@ internal override void ReadXml(XmlReader reader) } break; case "DATATYPE-DEFINITION-DATE-REF": - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var datatypeDefinition = (DatatypeDefinitionDate)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; @@ -193,6 +198,11 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -209,7 +219,7 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to } break; case "DATATYPE-DEFINITION-DATE-REF": - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var datatypeDefinition = (DatatypeDefinitionDate)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; diff --git a/ReqIFSharp/AttributeDefinition/AttributeDefinitionEnumeration.cs b/ReqIFSharp/AttributeDefinition/AttributeDefinitionEnumeration.cs index 76af9c8..6409794 100644 --- a/ReqIFSharp/AttributeDefinition/AttributeDefinitionEnumeration.cs +++ b/ReqIFSharp/AttributeDefinition/AttributeDefinitionEnumeration.cs @@ -156,6 +156,11 @@ internal override void ReadXml(XmlReader reader) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -164,7 +169,7 @@ internal override void ReadXml(XmlReader reader) this.ReadAlternativeId(reader); break; case "DATATYPE-DEFINITION-ENUMERATION-REF": - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var datatypeDefinition = (DatatypeDefinitionEnumeration)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; @@ -211,6 +216,11 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -219,7 +229,7 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to await this.ReadAlternativeIdAsync(reader, token); break; case "DATATYPE-DEFINITION-ENUMERATION-REF": - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var datatypeDefinition = (DatatypeDefinitionEnumeration)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; diff --git a/ReqIFSharp/AttributeDefinition/AttributeDefinitionInteger.cs b/ReqIFSharp/AttributeDefinition/AttributeDefinitionInteger.cs index 5d555d9..6208e36 100644 --- a/ReqIFSharp/AttributeDefinition/AttributeDefinitionInteger.cs +++ b/ReqIFSharp/AttributeDefinition/AttributeDefinitionInteger.cs @@ -142,6 +142,11 @@ internal override void ReadXml(XmlReader reader) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -150,7 +155,7 @@ internal override void ReadXml(XmlReader reader) this.ReadAlternativeId(reader); break; case "DATATYPE-DEFINITION-INTEGER-REF": - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var datatypeDefinition = (DatatypeDefinitionInteger)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; @@ -195,6 +200,11 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -203,7 +213,7 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to await this.ReadAlternativeIdAsync(reader, token); break; case "DATATYPE-DEFINITION-INTEGER-REF": - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var datatypeDefinition = (DatatypeDefinitionInteger)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; diff --git a/ReqIFSharp/AttributeDefinition/AttributeDefinitionReal.cs b/ReqIFSharp/AttributeDefinition/AttributeDefinitionReal.cs index c9f4cbd..15e0e79 100644 --- a/ReqIFSharp/AttributeDefinition/AttributeDefinitionReal.cs +++ b/ReqIFSharp/AttributeDefinition/AttributeDefinitionReal.cs @@ -141,6 +141,11 @@ internal override void ReadXml(XmlReader reader) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -149,7 +154,7 @@ internal override void ReadXml(XmlReader reader) this.ReadAlternativeId(reader); break; case "DATATYPE-DEFINITION-REAL-REF": - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var datatypeDefinition = (DatatypeDefinitionReal)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; @@ -194,6 +199,11 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -202,7 +212,7 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to await this.ReadAlternativeIdAsync(reader, token); break; case "DATATYPE-DEFINITION-REAL-REF": - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var datatypeDefinition = (DatatypeDefinitionReal)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; diff --git a/ReqIFSharp/AttributeDefinition/AttributeDefinitionString.cs b/ReqIFSharp/AttributeDefinition/AttributeDefinitionString.cs index 2465bd0..37928c9 100644 --- a/ReqIFSharp/AttributeDefinition/AttributeDefinitionString.cs +++ b/ReqIFSharp/AttributeDefinition/AttributeDefinitionString.cs @@ -140,6 +140,11 @@ internal override void ReadXml(XmlReader reader) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -148,7 +153,7 @@ internal override void ReadXml(XmlReader reader) this.ReadAlternativeId(reader); break; case "DATATYPE-DEFINITION-STRING-REF": - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var datatypeDefinition = (DatatypeDefinitionString)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; @@ -193,6 +198,11 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -201,7 +211,7 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to await this.ReadAlternativeIdAsync(reader, token); break; case "DATATYPE-DEFINITION-STRING-REF": - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var datatypeDefinition = (DatatypeDefinitionString)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; diff --git a/ReqIFSharp/AttributeDefinition/AttributeDefinitionXHTML.cs b/ReqIFSharp/AttributeDefinition/AttributeDefinitionXHTML.cs index 889ec51..47338f7 100644 --- a/ReqIFSharp/AttributeDefinition/AttributeDefinitionXHTML.cs +++ b/ReqIFSharp/AttributeDefinition/AttributeDefinitionXHTML.cs @@ -140,6 +140,11 @@ internal override void ReadXml(XmlReader reader) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -148,7 +153,7 @@ internal override void ReadXml(XmlReader reader) this.ReadAlternativeId(reader); break; case "DATATYPE-DEFINITION-XHTML-REF": - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var datatypeDefinition = (DatatypeDefinitionXHTML)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; @@ -193,6 +198,11 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -201,7 +211,7 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to await this.ReadAlternativeIdAsync(reader, token); break; case "DATATYPE-DEFINITION-XHTML-REF": - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var datatypeDefinition = (DatatypeDefinitionXHTML)this.SpecType.ReqIFContent.DataTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = datatypeDefinition; diff --git a/ReqIFSharp/AttributeValue/AttributeValue.cs b/ReqIFSharp/AttributeValue/AttributeValue.cs index 33835f9..6707a99 100644 --- a/ReqIFSharp/AttributeValue/AttributeValue.cs +++ b/ReqIFSharp/AttributeValue/AttributeValue.cs @@ -49,6 +49,7 @@ protected AttributeValue() /// </summary> protected AttributeValue(ILoggerFactory loggerFactory) { + this.loggerFactory = loggerFactory; } /// <summary> @@ -65,6 +66,7 @@ protected AttributeValue(ILoggerFactory loggerFactory) /// </param> protected AttributeValue(AttributeDefinition attributeDefinition, ILoggerFactory loggerFactory) { + this.loggerFactory = loggerFactory; this.AttributeDefinition = attributeDefinition; this.ReqIFContent = this.AttributeDefinition.SpecType.ReqIFContent; } @@ -80,6 +82,7 @@ protected AttributeValue(AttributeDefinition attributeDefinition, ILoggerFactory /// </param> protected AttributeValue(SpecElementWithAttributes specElAt, ILoggerFactory loggerFactory) { + this.loggerFactory = loggerFactory; this.SpecElAt = specElAt; this.SpecElAt.Values.Add(this); this.ReqIFContent = this.SpecElAt.ReqIFContent; diff --git a/ReqIFSharp/AttributeValue/AttributeValueBoolean.cs b/ReqIFSharp/AttributeValue/AttributeValueBoolean.cs index b24c900..ecb72da 100644 --- a/ReqIFSharp/AttributeValue/AttributeValueBoolean.cs +++ b/ReqIFSharp/AttributeValue/AttributeValueBoolean.cs @@ -174,9 +174,9 @@ internal override void ReadXml(XmlReader reader) while (reader.Read()) { - if (reader.ReadToDescendant("ATTRIBUTE-DEFINITION-BOOLEAN-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("ATTRIBUTE-DEFINITION-BOOLEAN-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); this.Definition = this.ReqIFContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType<AttributeDefinitionBoolean>().SingleOrDefault(x => x.Identifier == reference); if (this.Definition == null) @@ -204,9 +204,9 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to { token.ThrowIfCancellationRequested(); - if (reader.ReadToDescendant("ATTRIBUTE-DEFINITION-BOOLEAN-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("ATTRIBUTE-DEFINITION-BOOLEAN-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); this.Definition = this.ReqIFContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType<AttributeDefinitionBoolean>().SingleOrDefault(x => x.Identifier == reference); if (this.Definition == null) diff --git a/ReqIFSharp/AttributeValue/AttributeValueDate.cs b/ReqIFSharp/AttributeValue/AttributeValueDate.cs index 16d0b5b..0d0a078 100644 --- a/ReqIFSharp/AttributeValue/AttributeValueDate.cs +++ b/ReqIFSharp/AttributeValue/AttributeValueDate.cs @@ -164,9 +164,9 @@ internal override void ReadXml(XmlReader reader) while (reader.Read()) { - if (reader.ReadToDescendant("ATTRIBUTE-DEFINITION-DATE-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("ATTRIBUTE-DEFINITION-DATE-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); this.Definition = this.ReqIFContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType<AttributeDefinitionDate>().SingleOrDefault(x => x.Identifier == reference); if (this.Definition == null) @@ -194,9 +194,9 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to { token.ThrowIfCancellationRequested(); - if (reader.ReadToDescendant("ATTRIBUTE-DEFINITION-DATE-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("ATTRIBUTE-DEFINITION-DATE-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); this.Definition = this.ReqIFContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType<AttributeDefinitionDate>().SingleOrDefault(x => x.Identifier == reference); if (this.Definition == null) diff --git a/ReqIFSharp/AttributeValue/AttributeValueEnumeration.cs b/ReqIFSharp/AttributeValue/AttributeValueEnumeration.cs index 5905962..420edf4 100644 --- a/ReqIFSharp/AttributeValue/AttributeValueEnumeration.cs +++ b/ReqIFSharp/AttributeValue/AttributeValueEnumeration.cs @@ -183,6 +183,11 @@ internal override void ReadXml(XmlReader reader) { if (subtree.MoveToContent() == XmlNodeType.Element) { + if (subtree.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; string reference; @@ -190,7 +195,7 @@ internal override void ReadXml(XmlReader reader) switch (subtree.LocalName) { case "ATTRIBUTE-DEFINITION-ENUMERATION-REF": - reference = subtree.ReadElementContentAsString(); + reference = subtree.ReadElementContentAsStringToEndElement(); this.Definition = this.ReqIFContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType<AttributeDefinitionEnumeration>().SingleOrDefault(x => x.Identifier == reference); if (this.Definition == null) @@ -207,6 +212,11 @@ internal override void ReadXml(XmlReader reader) this.values.Add(enumValue); } break; + case "ATTRIBUTE-VALUE-ENUMERATION": + case "DEFINITION": + case "VALUES": + // the element itself and the wrappers of the references, which are read below them + break; default: this.logger.LogWarning("The {LocalName} element at line:position {LineNumber}:{LinePosition} is not supported", subtree.LocalName, xmlLineInfo?.LineNumber, xmlLineInfo?.LinePosition); break; @@ -235,6 +245,11 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to if (await subtree.MoveToContentAsync() == XmlNodeType.Element) { + if (await subtree.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; string reference; @@ -242,7 +257,7 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to switch (subtree.LocalName) { case "ATTRIBUTE-DEFINITION-ENUMERATION-REF": - reference = await subtree.ReadElementContentAsStringAsync(); + reference = await subtree.ReadElementContentAsStringToEndElementAsync(); this.Definition = this.ReqIFContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType<AttributeDefinitionEnumeration>().SingleOrDefault(x => x.Identifier == reference); if (this.Definition == null) @@ -259,6 +274,11 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to this.values.Add(enumValue); } break; + case "ATTRIBUTE-VALUE-ENUMERATION": + case "DEFINITION": + case "VALUES": + // the element itself and the wrappers of the references, which are read below them + break; default: this.logger.LogWarning("The {LocalName} element at line:position {LineNumber}:{LinePosition} is not supported", subtree.LocalName, xmlLineInfo?.LineNumber, xmlLineInfo?.LinePosition); break; diff --git a/ReqIFSharp/AttributeValue/AttributeValueInteger.cs b/ReqIFSharp/AttributeValue/AttributeValueInteger.cs index edfb2ef..09a0fce 100644 --- a/ReqIFSharp/AttributeValue/AttributeValueInteger.cs +++ b/ReqIFSharp/AttributeValue/AttributeValueInteger.cs @@ -178,9 +178,9 @@ internal override void ReadXml(XmlReader reader) while (reader.Read()) { - if (reader.ReadToDescendant("ATTRIBUTE-DEFINITION-INTEGER-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("ATTRIBUTE-DEFINITION-INTEGER-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); this.Definition = this.ReqIFContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType<AttributeDefinitionInteger>().SingleOrDefault(x => x.Identifier == reference); if (this.Definition == null) @@ -208,9 +208,9 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to { token.ThrowIfCancellationRequested(); - if (reader.ReadToDescendant("ATTRIBUTE-DEFINITION-INTEGER-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("ATTRIBUTE-DEFINITION-INTEGER-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); this.Definition = this.ReqIFContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType<AttributeDefinitionInteger>().SingleOrDefault(x => x.Identifier == reference); if (this.Definition == null) diff --git a/ReqIFSharp/AttributeValue/AttributeValueReal.cs b/ReqIFSharp/AttributeValue/AttributeValueReal.cs index 7d6e075..338bb48 100644 --- a/ReqIFSharp/AttributeValue/AttributeValueReal.cs +++ b/ReqIFSharp/AttributeValue/AttributeValueReal.cs @@ -175,9 +175,9 @@ internal override void ReadXml(XmlReader reader) while (reader.Read()) { - if (reader.ReadToDescendant("ATTRIBUTE-DEFINITION-REAL-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("ATTRIBUTE-DEFINITION-REAL-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); this.Definition = this.ReqIFContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType<AttributeDefinitionReal>().SingleOrDefault(x => x.Identifier == reference); if (this.Definition == null) @@ -205,9 +205,9 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to { token.ThrowIfCancellationRequested(); - if (reader.ReadToDescendant("ATTRIBUTE-DEFINITION-REAL-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("ATTRIBUTE-DEFINITION-REAL-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); this.Definition = this.ReqIFContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType<AttributeDefinitionReal>().SingleOrDefault(x => x.Identifier == reference); if (this.Definition == null) diff --git a/ReqIFSharp/AttributeValue/AttributeValueString.cs b/ReqIFSharp/AttributeValue/AttributeValueString.cs index aecc635..2d3ac6f 100644 --- a/ReqIFSharp/AttributeValue/AttributeValueString.cs +++ b/ReqIFSharp/AttributeValue/AttributeValueString.cs @@ -36,11 +36,17 @@ namespace ReqIFSharp [ReqIfClass(name: "ATTRIBUTE-VALUE-STRING", isAbstract: false)] public class AttributeValueString : AttributeValueSimple { + /// <summary> + /// The <see cref="ILogger"/> used to log + /// </summary> + private readonly ILogger<AttributeValueString> logger; + /// <summary> /// Initializes a new instance of the <see cref="AttributeValueString"/> class. /// </summary> public AttributeValueString() { + this.logger = NullLogger<AttributeValueString>.Instance; } /// <summary> @@ -51,6 +57,7 @@ public AttributeValueString() /// </param> public AttributeValueString(ILoggerFactory loggerFactory) : base(loggerFactory) { + this.logger = this.loggerFactory == null ? NullLogger<AttributeValueString>.Instance : this.loggerFactory.CreateLogger<AttributeValueString>(); } /// <summary> @@ -66,6 +73,8 @@ public AttributeValueString(ILoggerFactory loggerFactory) : base(loggerFactory) internal AttributeValueString(AttributeDefinitionString attributeDefinition, ILoggerFactory loggerFactory) : base(attributeDefinition, loggerFactory) { + this.logger = this.loggerFactory == null ? NullLogger<AttributeValueString>.Instance : this.loggerFactory.CreateLogger<AttributeValueString>(); + this.OwningDefinition = attributeDefinition; } @@ -81,6 +90,7 @@ internal AttributeValueString(AttributeDefinitionString attributeDefinition, ILo internal AttributeValueString(SpecElementWithAttributes specElAt, ILoggerFactory loggerFactory) : base(specElAt, loggerFactory) { + this.logger = this.loggerFactory == null ? NullLogger<AttributeValueString>.Instance : this.loggerFactory.CreateLogger<AttributeValueString>(); } /// <summary> @@ -156,9 +166,9 @@ internal override void ReadXml(XmlReader reader) while (reader.Read()) { - if (reader.ReadToDescendant("ATTRIBUTE-DEFINITION-STRING-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("ATTRIBUTE-DEFINITION-STRING-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); this.Definition = this.ReqIFContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType<AttributeDefinitionString>().SingleOrDefault(x => x.Identifier == reference); if (this.Definition == null) @@ -186,9 +196,9 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to { token.ThrowIfCancellationRequested(); - if (reader.ReadToDescendant("ATTRIBUTE-DEFINITION-STRING-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("ATTRIBUTE-DEFINITION-STRING-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); this.Definition = this.ReqIFContent.SpecTypes.SelectMany(x => x.SpecAttributes).OfType<AttributeDefinitionString>().SingleOrDefault(x => x.Identifier == reference); if (this.Definition == null) diff --git a/ReqIFSharp/AttributeValue/AttributeValueXHTML.cs b/ReqIFSharp/AttributeValue/AttributeValueXHTML.cs index 7981182..e83207f 100644 --- a/ReqIFSharp/AttributeValue/AttributeValueXHTML.cs +++ b/ReqIFSharp/AttributeValue/AttributeValueXHTML.cs @@ -255,6 +255,12 @@ internal override void ReadXml(XmlReader reader) continue; } + if (subtree.SkipForeignElement(this.logger)) + { + subtree.Read(); + continue; + } + switch (subtree.LocalName) { case "DEFINITION": @@ -320,12 +326,18 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to continue; } + if (await subtree.SkipForeignElementAsync(this.logger, token)) + { + await subtree.ReadAsync(); + continue; + } + switch (subtree.LocalName) { case "DEFINITION": using (var definitionSubtree = subtree.ReadSubtree()) { - await this.ReadDefinitionAsync(definitionSubtree); + await this.ReadDefinitionAsync(definitionSubtree, token); } await subtree.ReadAsync(); @@ -361,10 +373,12 @@ private void ReadDefinition(XmlReader reader) { reader.MoveToContent(); - if (reader.ReadToDescendant("ATTRIBUTE-DEFINITION-XHTML-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("ATTRIBUTE-DEFINITION-XHTML-REF", this.logger)) { - this.SetDefinition(reader.ReadElementContentAsString()); + this.SetDefinition(reader.ReadElementContentAsStringToEndElement()); } + + reader.SkipForeignElementsToEnd(this.logger); } /// <summary> @@ -373,14 +387,19 @@ private void ReadDefinition(XmlReader reader) /// <param name="reader"> /// an <see cref="XmlReader"/> over the subtree of the DEFINITION element /// </param> - private async Task ReadDefinitionAsync(XmlReader reader) + /// <param name="token"> + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// </param> + private async Task ReadDefinitionAsync(XmlReader reader, CancellationToken token) { await reader.MoveToContentAsync(); - if (reader.ReadToDescendant("ATTRIBUTE-DEFINITION-XHTML-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("ATTRIBUTE-DEFINITION-XHTML-REF", this.logger, token)) { - this.SetDefinition(await reader.ReadElementContentAsStringAsync()); + this.SetDefinition(await reader.ReadElementContentAsStringToEndElementAsync()); } + + await reader.SkipForeignElementsToEndAsync(this.logger, token); } /// <summary> diff --git a/ReqIFSharp/Datatype/DatatypeDefinitionEnumeration.cs b/ReqIFSharp/Datatype/DatatypeDefinitionEnumeration.cs index 4082441..da7c711 100644 --- a/ReqIFSharp/Datatype/DatatypeDefinitionEnumeration.cs +++ b/ReqIFSharp/Datatype/DatatypeDefinitionEnumeration.cs @@ -107,6 +107,11 @@ internal override void ReadXml(XmlReader reader) { if (subtree.MoveToContent() == XmlNodeType.Element) { + if (subtree.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (subtree.LocalName) @@ -150,6 +155,11 @@ internal override async Task ReadXmlAsync(XmlReader reader, CancellationToken to if (await subtree.MoveToContentAsync() == XmlNodeType.Element) { + if (await subtree.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (subtree.LocalName) diff --git a/ReqIFSharp/Datatype/EnumValue.cs b/ReqIFSharp/Datatype/EnumValue.cs index 903f83c..2f73003 100644 --- a/ReqIFSharp/Datatype/EnumValue.cs +++ b/ReqIFSharp/Datatype/EnumValue.cs @@ -25,6 +25,7 @@ namespace ReqIFSharp using System.Xml; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; /// <summary> /// The class <see cref="EnumValue"/> represents enumeration literals. @@ -32,11 +33,17 @@ namespace ReqIFSharp [ReqIfClass(name: "ENUM-VALUE", isAbstract: false)] public class EnumValue : Identifiable { + /// <summary> + /// The <see cref="ILogger"/> used to log + /// </summary> + private readonly ILogger<EnumValue> logger; + /// <summary> /// Initializes a new instance of the <see cref="EnumValue"/> class. /// </summary> public EnumValue() { + this.logger = NullLogger<EnumValue>.Instance; } /// <summary> @@ -48,6 +55,7 @@ public EnumValue() public EnumValue(ILoggerFactory loggerFactory) : base(loggerFactory) { + this.logger = this.loggerFactory == null ? NullLogger<EnumValue>.Instance : this.loggerFactory.CreateLogger<EnumValue>(); } /// <summary> @@ -62,6 +70,8 @@ public EnumValue(ILoggerFactory loggerFactory) internal EnumValue(DatatypeDefinitionEnumeration datatypeDefinitionEnumeration, ILoggerFactory loggerFactory) : base(loggerFactory) { + this.logger = this.loggerFactory == null ? NullLogger<EnumValue>.Instance : this.loggerFactory.CreateLogger<EnumValue>(); + this.DataTpeDefEnum = datatypeDefinitionEnumeration; this.DataTpeDefEnum.SpecifiedValues.Add(this); } @@ -91,6 +101,11 @@ internal override void ReadXml(XmlReader reader) { while (subtree.Read()) { + if (subtree.SkipForeignElement(this.logger)) + { + continue; + } + if (subtree.MoveToContent() == XmlNodeType.Element && reader.LocalName == "EMBEDDED-VALUE") { var embeddedValue = new EmbeddedValue(this, this.loggerFactory); @@ -119,6 +134,11 @@ internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token) { token.ThrowIfCancellationRequested(); + if (await subtree.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + if (await subtree.MoveToContentAsync() == XmlNodeType.Element && reader.LocalName == "EMBEDDED-VALUE") { var embeddedValue = new EmbeddedValue(this, this.loggerFactory); diff --git a/ReqIFSharp/Identifiable.cs b/ReqIFSharp/Identifiable.cs index ac22ba2..c386457 100644 --- a/ReqIFSharp/Identifiable.cs +++ b/ReqIFSharp/Identifiable.cs @@ -242,7 +242,7 @@ protected void ReadAlternativeId(XmlReader reader) using (var alternativeIdSubtree = reader.ReadSubtree()) { - alternativeId.ReadXml(alternativeIdSubtree); + alternativeId.ReadXml(alternativeIdSubtree, this.logger); } } @@ -280,7 +280,7 @@ private async Task ReadAlternativeIdInternalAsync(XmlReader reader, Cancellation using (var alternativeIdSubtree = reader.ReadSubtree()) { - await alternativeId.ReadXmlAsync(alternativeIdSubtree, token); + await alternativeId.ReadXmlAsync(alternativeIdSubtree, token, this.logger); } } } diff --git a/ReqIFSharp/ReqIF.cs b/ReqIFSharp/ReqIF.cs index a7bee2e..057306c 100644 --- a/ReqIFSharp/ReqIF.cs +++ b/ReqIFSharp/ReqIF.cs @@ -128,6 +128,11 @@ internal void ReadXml(XmlReader reader) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -147,6 +152,11 @@ internal void ReadXml(XmlReader reader) while (toolExtensionsXmlReader.Read()) { + if (toolExtensionsXmlReader.SkipForeignElement(this.logger)) + { + continue; + } + if (toolExtensionsXmlReader.MoveToContent() == XmlNodeType.Element && toolExtensionsXmlReader.LocalName == "REQ-IF-TOOL-EXTENSION") { var reqIfToolExtensionSubTreeXmlReader = toolExtensionsXmlReader.ReadSubtree(); @@ -202,6 +212,11 @@ internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token) if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -221,6 +236,11 @@ internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token) while (await toolExtensionsXmlReader.ReadAsync()) { + if (await toolExtensionsXmlReader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + if (await toolExtensionsXmlReader.MoveToContentAsync() == XmlNodeType.Element && toolExtensionsXmlReader.LocalName == "REQ-IF-TOOL-EXTENSION") { var reqIfToolExtensionSubTreeXmlReader = toolExtensionsXmlReader.ReadSubtree(); diff --git a/ReqIFSharp/ReqIFContent.cs b/ReqIFSharp/ReqIFContent.cs index b06aebf..b3808ac 100644 --- a/ReqIFSharp/ReqIFContent.cs +++ b/ReqIFSharp/ReqIFContent.cs @@ -149,6 +149,11 @@ internal void ReadXml(XmlReader reader) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -220,6 +225,11 @@ internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token) if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -284,6 +294,11 @@ private void DeserializeDataTypes(XmlReader reader) { while (reader.Read()) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName.StartsWith("DATATYPE-DEFINITION-", StringComparison.Ordinal)) { var datatypeDefinition = ReqIfFactory.DatatypeDefinitionConstruct(reader.LocalName, this, this.loggerFactory); @@ -307,6 +322,11 @@ private async Task DeserializeDataTypesAsync(XmlReader reader, CancellationToken { token.ThrowIfCancellationRequested(); + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + if (await reader.MoveToContentAsync() == XmlNodeType.Element && reader.LocalName.StartsWith("DATATYPE-DEFINITION-", StringComparison.Ordinal)) { var datatypeDefinition = ReqIfFactory.DatatypeDefinitionConstruct(reader.LocalName, this, this.loggerFactory); @@ -327,6 +347,11 @@ private void DeserializeSpecTypes(XmlReader reader) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + var xmlname = reader.LocalName; if (xmlname == "SPEC-OBJECT-TYPE" || xmlname == "SPECIFICATION-TYPE" @@ -361,6 +386,11 @@ private async Task DeserializeSpecTypesAsync(XmlReader reader, CancellationToken if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlname = reader.LocalName; if (xmlname == "SPEC-OBJECT-TYPE" || xmlname == "SPECIFICATION-TYPE" @@ -388,6 +418,11 @@ private void DeserializeSpectObjects(XmlReader reader) { while (reader.Read()) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "SPEC-OBJECT") { using (var subtree = reader.ReadSubtree()) @@ -416,6 +451,11 @@ private async Task DeserializeSpecObjectsAsync(XmlReader reader, CancellationTok { token.ThrowIfCancellationRequested(); + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + if (await reader.MoveToContentAsync() == XmlNodeType.Element && reader.LocalName == "SPEC-OBJECT") { using (var subtree = reader.ReadSubtree()) @@ -439,6 +479,11 @@ private void DeserializeSpecRelations(XmlReader reader) { while (reader.Read()) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "SPEC-RELATION") { using (var subtree = reader.ReadSubtree()) @@ -467,6 +512,11 @@ private async Task DeserializeSpecRelationsAsync(XmlReader reader, CancellationT { token.ThrowIfCancellationRequested(); + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + if (await reader.MoveToContentAsync() == XmlNodeType.Element && reader.LocalName == "SPEC-RELATION") { using (var subtree = reader.ReadSubtree()) @@ -490,6 +540,11 @@ private void DeserializeSpecifications(XmlReader reader) { while (reader.Read()) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "SPECIFICATION") { using (var subtree = reader.ReadSubtree()) @@ -518,6 +573,11 @@ private async Task DeserializeSpecificationsAsync(XmlReader reader, Cancellation { token.ThrowIfCancellationRequested(); + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + if (await reader.MoveToContentAsync() == XmlNodeType.Element && reader.LocalName == "SPECIFICATION") { using (var subtree = reader.ReadSubtree()) @@ -541,6 +601,11 @@ private void DeserializeRelationGroups(XmlReader reader) { while (reader.Read()) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "RELATION-GROUP") { using (var subtree = reader.ReadSubtree()) @@ -567,6 +632,11 @@ private async Task DeserializeRelationGroupsAsync(XmlReader reader, Cancellation { while (await reader.ReadAsync()) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + if (await reader.MoveToContentAsync() == XmlNodeType.Element && reader.LocalName == "RELATION-GROUP") { using (var subtree = reader.ReadSubtree()) diff --git a/ReqIFSharp/ReqIFHeader.cs b/ReqIFSharp/ReqIFHeader.cs index cf8834b..44c6983 100644 --- a/ReqIFSharp/ReqIFHeader.cs +++ b/ReqIFSharp/ReqIFHeader.cs @@ -138,6 +138,11 @@ internal void ReadXml(XmlReader reader) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -202,6 +207,11 @@ internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token) if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) diff --git a/ReqIFSharp/SpecElementWithAttributes/RelationGroup.cs b/ReqIFSharp/SpecElementWithAttributes/RelationGroup.cs index 2aaf086..cb10b81 100644 --- a/ReqIFSharp/SpecElementWithAttributes/RelationGroup.cs +++ b/ReqIFSharp/SpecElementWithAttributes/RelationGroup.cs @@ -173,9 +173,9 @@ protected override void ReadObjectSpecificElements(XmlReader reader) switch (reader.LocalName) { case "SOURCE-SPECIFICATION": - if (reader.ReadToDescendant("SPECIFICATION-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("SPECIFICATION-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var specification = this.CoreContent.Specifications.SingleOrDefault(x => x.Identifier == reference); this.SourceSpecification = specification ?? new Specification(this.ReqIFContent, this.loggerFactory) @@ -191,9 +191,9 @@ protected override void ReadObjectSpecificElements(XmlReader reader) } break; case "TARGET-SPECIFICATION": - if (reader.ReadToDescendant("SPECIFICATION-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("SPECIFICATION-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var specification = this.CoreContent.Specifications.SingleOrDefault(x => x.Identifier == reference); this.TargetSpecification = specification ?? new Specification(this.ReqIFContent, this.loggerFactory) @@ -262,9 +262,9 @@ private async Task ReadObjectSpecificElementsInternalAsync(XmlReader reader, Can switch (reader.LocalName) { case "SOURCE-SPECIFICATION": - if (reader.ReadToDescendant("SPECIFICATION-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("SPECIFICATION-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var specification = this.CoreContent.Specifications.SingleOrDefault(x => x.Identifier == reference); this.SourceSpecification = specification ?? new Specification(this.ReqIFContent, this.loggerFactory) @@ -279,9 +279,9 @@ private async Task ReadObjectSpecificElementsInternalAsync(XmlReader reader, Can } break; case "TARGET-SPECIFICATION": - if (reader.ReadToDescendant("SPECIFICATION-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("SPECIFICATION-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var specification = this.CoreContent.Specifications.SingleOrDefault(x => x.Identifier == reference); this.TargetSpecification = specification ?? new Specification(this.ReqIFContent, this.loggerFactory) @@ -321,9 +321,9 @@ protected override void ReadSpecType(XmlReader reader) throw new ArgumentNullException(nameof(reader)); } - if (reader.ReadToDescendant("RELATION-GROUP-TYPE-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("RELATION-GROUP-TYPE-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var specType = this.ReqIFContent.SpecTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = (RelationGroupType)specType; @@ -332,6 +332,8 @@ protected override void ReadSpecType(XmlReader reader) this.logger.LogTrace("The RelationGroupType:{Reference} could not be found and has been set to null on RelationGroup:{Identifier}", reference, Identifier); } } + + reader.SkipForeignElementsToEnd(this.logger); } /// <summary> @@ -352,7 +354,7 @@ protected override Task ReadSpecTypeAsync(XmlReader reader, CancellationToken to token.ThrowIfCancellationRequested(); - return this.ReadSpecTypeInternalAsync(reader); + return this.ReadSpecTypeInternalAsync(reader, token); } /// <summary> @@ -361,11 +363,14 @@ protected override Task ReadSpecTypeAsync(XmlReader reader, CancellationToken to /// <param name="reader"> /// an instance of <see cref="XmlReader"/> /// </param> - private async Task ReadSpecTypeInternalAsync(XmlReader reader) + /// <param name="token"> + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// </param> + private async Task ReadSpecTypeInternalAsync(XmlReader reader, CancellationToken token) { - if (reader.ReadToDescendant("RELATION-GROUP-TYPE-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("RELATION-GROUP-TYPE-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var specType = this.ReqIFContent.SpecTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = (RelationGroupType)specType; @@ -374,6 +379,8 @@ private async Task ReadSpecTypeInternalAsync(XmlReader reader) this.logger.LogTrace("The RelationGroupType:{Reference} could not be found and has been set to null on RelationGroup:{Identifier}", reference, Identifier); } } + + await reader.SkipForeignElementsToEndAsync(this.logger, token); } /// <summary> @@ -411,7 +418,7 @@ private void DeserializeSpecRelations(XmlReader reader) { while (reader.Read()) { - if (!this.IsSpecRelationRef(reader)) + if (reader.SkipForeignElement(this.logger) || !this.IsSpecRelationRef(reader)) { continue; } @@ -444,7 +451,7 @@ private async Task DeserializeSpecRelationsAsync(XmlReader reader, CancellationT { token.ThrowIfCancellationRequested(); - if (!this.IsSpecRelationRef(reader)) + if (await reader.SkipForeignElementAsync(this.logger, token) || !this.IsSpecRelationRef(reader)) { continue; } diff --git a/ReqIFSharp/SpecElementWithAttributes/SpecElementWithAttributes.cs b/ReqIFSharp/SpecElementWithAttributes/SpecElementWithAttributes.cs index 0c72b84..bf4d267 100644 --- a/ReqIFSharp/SpecElementWithAttributes/SpecElementWithAttributes.cs +++ b/ReqIFSharp/SpecElementWithAttributes/SpecElementWithAttributes.cs @@ -142,6 +142,11 @@ internal override void ReadXml(XmlReader reader) if (specElementWithAttributesReader.MoveToContent() == XmlNodeType.Element) { + if (specElementWithAttributesReader.SkipForeignElement(this.logger)) + { + continue; + } + switch (specElementWithAttributesReader.LocalName) { case "ALTERNATIVE-ID": @@ -249,6 +254,11 @@ internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token) if (await specElementWithAttributesReader.MoveToContentAsync() == XmlNodeType.Element) { + if (await specElementWithAttributesReader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (specElementWithAttributesReader.LocalName) diff --git a/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs b/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs index 38727c5..a665916 100644 --- a/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs +++ b/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs @@ -186,6 +186,11 @@ internal override void ReadXml(XmlReader reader) { if (specHierarchySubtree.MoveToContent() == XmlNodeType.Element) { + if (specHierarchySubtree.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (specHierarchySubtree.LocalName) @@ -236,6 +241,11 @@ internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token) if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -381,9 +391,9 @@ private void Initialize(SpecHierarchy container, Specification root, ReqIFConten /// </param> private void DeserializeObject(XmlReader reader) { - if (reader.ReadToDescendant("SPEC-OBJECT-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("SPEC-OBJECT-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var specObject = this.ReqIfContent.SpecObjects.SingleOrDefault(x => x.Identifier == reference); this.Object = specObject; @@ -392,6 +402,8 @@ private void DeserializeObject(XmlReader reader) this.logger.LogTrace("The SpecObject:{Reference} could not be found and has been set to null on SpecHierarchy:{Identifier}", reference, Identifier); } } + + reader.SkipForeignElementsToEnd(this.logger); } /// <summary> @@ -407,9 +419,9 @@ private async Task DeserializeObjectAsync(XmlReader reader, CancellationToken to { token.ThrowIfCancellationRequested(); - if (reader.ReadToDescendant("SPEC-OBJECT-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("SPEC-OBJECT-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var specObject = this.ReqIfContent.SpecObjects.SingleOrDefault(x => x.Identifier == reference); this.Object = specObject; @@ -418,6 +430,8 @@ private async Task DeserializeObjectAsync(XmlReader reader, CancellationToken to this.logger.LogTrace("The SpecObject:{Reference} could not be found and has been set to null on SpecHierarchy:{Identifier}", reference, Identifier); } } + + await reader.SkipForeignElementsToEndAsync(this.logger, token); } /// <summary> @@ -430,6 +444,11 @@ private void DeserializeSpecHierarchy(XmlReader reader) { while (reader.Read()) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "SPEC-HIERARCHY") { if (bool.TryParse(reader.GetAttribute("IS-TABLE-INTERNAL"), out var isTableInternal)) @@ -462,6 +481,11 @@ private async Task DeserializeSpecHierarchyAsync(XmlReader reader, CancellationT { token.ThrowIfCancellationRequested(); + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + if (await reader.MoveToContentAsync() == XmlNodeType.Element && reader.LocalName == "SPEC-HIERARCHY") { if (bool.TryParse(reader.GetAttribute("IS-TABLE-INTERNAL"), out var isTableInternal)) diff --git a/ReqIFSharp/SpecElementWithAttributes/SpecObject.cs b/ReqIFSharp/SpecElementWithAttributes/SpecObject.cs index 2f62f13..746d0e4 100644 --- a/ReqIFSharp/SpecElementWithAttributes/SpecObject.cs +++ b/ReqIFSharp/SpecElementWithAttributes/SpecObject.cs @@ -134,9 +134,9 @@ protected override void ReadSpecType(XmlReader reader) throw new ArgumentNullException(nameof(reader)); } - if (reader.ReadToDescendant("SPEC-OBJECT-TYPE-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("SPEC-OBJECT-TYPE-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var specType = this.ReqIFContent.SpecTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = (SpecObjectType)specType; @@ -145,6 +145,8 @@ protected override void ReadSpecType(XmlReader reader) this.logger.LogTrace("The SpecObjectType:{Reference} could not be found and has been set to null on SpecObject:{Identifier}", reference, Identifier); } } + + reader.SkipForeignElementsToEnd(this.logger); } /// <summary> @@ -165,7 +167,7 @@ protected override Task ReadSpecTypeAsync(XmlReader reader, CancellationToken to token.ThrowIfCancellationRequested(); - return this.ReadSpecTypeInternalAsync(reader); + return this.ReadSpecTypeInternalAsync(reader, token); } /// <summary> @@ -174,11 +176,14 @@ protected override Task ReadSpecTypeAsync(XmlReader reader, CancellationToken to /// <param name="reader"> /// an instance of <see cref="XmlReader"/> /// </param> - private async Task ReadSpecTypeInternalAsync(XmlReader reader) + /// <param name="token"> + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// </param> + private async Task ReadSpecTypeInternalAsync(XmlReader reader, CancellationToken token) { - if (reader.ReadToDescendant("SPEC-OBJECT-TYPE-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("SPEC-OBJECT-TYPE-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var specType = this.ReqIFContent.SpecTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = (SpecObjectType)specType; @@ -187,6 +192,8 @@ private async Task ReadSpecTypeInternalAsync(XmlReader reader) this.logger.LogTrace("The SpecObjectType:{Reference} could not be found and has been set to null on SpecObject:{Identifier}", reference, Identifier); } } + + await reader.SkipForeignElementsToEndAsync(this.logger, token); } /// <summary> diff --git a/ReqIFSharp/SpecElementWithAttributes/SpecRelation.cs b/ReqIFSharp/SpecElementWithAttributes/SpecRelation.cs index 067c73a..fe1ce0c 100644 --- a/ReqIFSharp/SpecElementWithAttributes/SpecRelation.cs +++ b/ReqIFSharp/SpecElementWithAttributes/SpecRelation.cs @@ -117,9 +117,9 @@ protected override void ReadObjectSpecificElements(XmlReader reader) switch (reader.LocalName) { case "SOURCE": - if (reader.ReadToDescendant("SPEC-OBJECT-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("SPEC-OBJECT-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var specObject = this.ReqIFContent.SpecObjects.SingleOrDefault(x => x.Identifier == reference); this.Source = specObject ?? new SpecObject(this.ReqIFContent, this.loggerFactory) @@ -135,9 +135,9 @@ protected override void ReadObjectSpecificElements(XmlReader reader) } break; case "TARGET": - if (reader.ReadToDescendant("SPEC-OBJECT-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("SPEC-OBJECT-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var specObject = this.ReqIFContent.SpecObjects.SingleOrDefault(x => x.Identifier == reference); this.Target = specObject ?? new SpecObject(this.ReqIFContent, this.loggerFactory) @@ -178,7 +178,7 @@ protected override Task ReadObjectSpecificElementsAsync(XmlReader reader, Cancel token.ThrowIfCancellationRequested(); - return this.ReadObjectSpecificElementsInternalAsync(reader); + return this.ReadObjectSpecificElementsInternalAsync(reader, token); } /// <summary> @@ -188,7 +188,10 @@ protected override Task ReadObjectSpecificElementsAsync(XmlReader reader, Cancel /// <remarks> /// In this case, read the source and target /// </remarks> - private async Task ReadObjectSpecificElementsInternalAsync(XmlReader reader) + /// <param name="token"> + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// </param> + private async Task ReadObjectSpecificElementsInternalAsync(XmlReader reader, CancellationToken token) { if (await reader.MoveToContentAsync() == XmlNodeType.Element) { @@ -197,9 +200,9 @@ private async Task ReadObjectSpecificElementsInternalAsync(XmlReader reader) switch (reader.LocalName) { case "SOURCE": - if (reader.ReadToDescendant("SPEC-OBJECT-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("SPEC-OBJECT-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var specObject = this.ReqIFContent.SpecObjects.SingleOrDefault(x => x.Identifier == reference); this.Source = specObject ?? new SpecObject(this.ReqIFContent, this.loggerFactory) @@ -215,9 +218,9 @@ private async Task ReadObjectSpecificElementsInternalAsync(XmlReader reader) } break; case "TARGET": - if (reader.ReadToDescendant("SPEC-OBJECT-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("SPEC-OBJECT-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var specObject = this.ReqIFContent.SpecObjects.SingleOrDefault(x => x.Identifier == reference); this.Target = specObject ?? new SpecObject(this.ReqIFContent, this.loggerFactory) @@ -284,9 +287,9 @@ protected override void ReadSpecType(XmlReader reader) throw new ArgumentNullException(nameof(reader)); } - if (reader.ReadToDescendant("SPEC-RELATION-TYPE-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("SPEC-RELATION-TYPE-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var specType = this.ReqIFContent.SpecTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = (SpecRelationType)specType; @@ -295,6 +298,8 @@ protected override void ReadSpecType(XmlReader reader) this.logger.LogTrace("The SpecRelationType:{Reference} could not be found and has been set to null on SpecRelation:{Identifier}", reference, Identifier); } } + + reader.SkipForeignElementsToEnd(this.logger); } /// <summary> @@ -315,7 +320,7 @@ protected override Task ReadSpecTypeAsync(XmlReader reader, CancellationToken t token.ThrowIfCancellationRequested(); - return this.ReadSpecTypeInternalAsync(reader); + return this.ReadSpecTypeInternalAsync(reader, token); } /// <summary> @@ -324,11 +329,14 @@ protected override Task ReadSpecTypeAsync(XmlReader reader, CancellationToken t /// <param name="reader"> /// an instance of <see cref="XmlReader"/> /// </param> - private async Task ReadSpecTypeInternalAsync(XmlReader reader) + /// <param name="token"> + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// </param> + private async Task ReadSpecTypeInternalAsync(XmlReader reader, CancellationToken token) { - if (reader.ReadToDescendant("SPEC-RELATION-TYPE-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("SPEC-RELATION-TYPE-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var specType = this.ReqIFContent.SpecTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = (SpecRelationType)specType; @@ -337,6 +345,8 @@ private async Task ReadSpecTypeInternalAsync(XmlReader reader) this.logger.LogTrace("The SpecRelationType:{Reference} could not be found and has been set to null on SpecRelation:{Identifier}", reference, Identifier); } } + + await reader.SkipForeignElementsToEndAsync(this.logger, token); } /// <summary> diff --git a/ReqIFSharp/SpecElementWithAttributes/Specification.cs b/ReqIFSharp/SpecElementWithAttributes/Specification.cs index 4ae7379..0b54161 100644 --- a/ReqIFSharp/SpecElementWithAttributes/Specification.cs +++ b/ReqIFSharp/SpecElementWithAttributes/Specification.cs @@ -43,11 +43,17 @@ public class Specification : SpecElementWithAttributes /// </summary> private readonly List<SpecHierarchy> children = new List<SpecHierarchy>(); + /// <summary> + /// The <see cref="ILogger"/> used to log + /// </summary> + private readonly ILogger<Specification> logger; + /// <summary> /// Initializes a new instance of the <see cref="Specification"/> class. /// </summary> public Specification() { + this.logger = NullLogger<Specification>.Instance; } /// <summary> @@ -59,6 +65,7 @@ public Specification() public Specification(ILoggerFactory loggerFactory) : base(loggerFactory) { + this.logger = this.loggerFactory == null ? NullLogger<Specification>.Instance : this.loggerFactory.CreateLogger<Specification>(); } /// <summary> @@ -73,6 +80,8 @@ public Specification(ILoggerFactory loggerFactory) internal Specification(ReqIFContent reqIfContent, ILoggerFactory loggerFactory) : base(reqIfContent, loggerFactory) { + this.logger = this.loggerFactory == null ? NullLogger<Specification>.Instance : this.loggerFactory.CreateLogger<Specification>(); + this.ReqIFContent.Specifications.Add(this); } @@ -176,12 +185,14 @@ protected override void ReadSpecType(XmlReader reader) throw new ArgumentNullException(nameof(reader)); } - if (reader.ReadToDescendant("SPECIFICATION-TYPE-REF", reader.NamespaceURI)) + if (reader.ReadToReqIFDescendant("SPECIFICATION-TYPE-REF", this.logger)) { - var reference = reader.ReadElementContentAsString(); + var reference = reader.ReadElementContentAsStringToEndElement(); var specType = this.ReqIFContent.SpecTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = (SpecificationType)specType; } + + reader.SkipForeignElementsToEnd(this.logger); } /// <summary> @@ -202,7 +213,7 @@ protected override Task ReadSpecTypeAsync(XmlReader reader, CancellationToken to token.ThrowIfCancellationRequested(); - return this.ReadSpecTypeInternalAsync(reader); + return this.ReadSpecTypeInternalAsync(reader, token); } /// <summary> @@ -211,14 +222,19 @@ protected override Task ReadSpecTypeAsync(XmlReader reader, CancellationToken to /// <param name="reader"> /// an instance of <see cref="XmlReader"/> /// </param> - private async Task ReadSpecTypeInternalAsync(XmlReader reader) + /// <param name="token"> + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// </param> + private async Task ReadSpecTypeInternalAsync(XmlReader reader, CancellationToken token) { - if (reader.ReadToDescendant("SPECIFICATION-TYPE-REF", reader.NamespaceURI)) + if (await reader.ReadToReqIFDescendantAsync("SPECIFICATION-TYPE-REF", this.logger, token)) { - var reference = await reader.ReadElementContentAsStringAsync(); + var reference = await reader.ReadElementContentAsStringToEndElementAsync(); var specType = this.ReqIFContent.SpecTypes.SingleOrDefault(x => x.Identifier == reference); this.Type = (SpecificationType)specType; } + + await reader.SkipForeignElementsToEndAsync(this.logger, token); } /// <summary> /// Reads the <see cref="SpecHierarchy"/> which is specific to the <see cref="Specification"/> class. @@ -235,6 +251,11 @@ protected override void ReadHierarchy(XmlReader reader) while (reader.Read()) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "SPEC-HIERARCHY") { using (var subtree = reader.ReadSubtree()) @@ -284,6 +305,11 @@ private async Task ReadHierarchyInternalAsync(XmlReader reader, CancellationToke { token.ThrowIfCancellationRequested(); + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + if (await reader.MoveToContentAsync() == XmlNodeType.Element && reader.LocalName == "SPEC-HIERARCHY") { token.ThrowIfCancellationRequested(); diff --git a/ReqIFSharp/SpecType/SpecType.cs b/ReqIFSharp/SpecType/SpecType.cs index 765c0f4..1d34ca7 100644 --- a/ReqIFSharp/SpecType/SpecType.cs +++ b/ReqIFSharp/SpecType/SpecType.cs @@ -114,6 +114,11 @@ internal override void ReadXml(XmlReader reader) { if (reader.MoveToContent() == XmlNodeType.Element) { + if (reader.SkipForeignElement(this.logger)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -126,6 +131,11 @@ internal override void ReadXml(XmlReader reader) { while (specAttributesSubTree.Read()) { + if (specAttributesSubTree.SkipForeignElement(this.logger)) + { + continue; + } + if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName.StartsWith("ATTRIBUTE-DEFINITION-", StringComparison.Ordinal)) { this.CreateAttributeDefinition(reader, reader.LocalName); @@ -160,6 +170,11 @@ internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token) if (await reader.MoveToContentAsync() == XmlNodeType.Element) { + if (await reader.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + var xmlLineInfo = reader as IXmlLineInfo; switch (reader.LocalName) @@ -175,6 +190,11 @@ internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token) { token.ThrowIfCancellationRequested(); + if (await specAttributesSubTree.SkipForeignElementAsync(this.logger, token)) + { + continue; + } + if (await reader.MoveToContentAsync() == XmlNodeType.Element && reader.LocalName.StartsWith("ATTRIBUTE-DEFINITION-", StringComparison.Ordinal)) { await this.CreateAttributeDefinitionAsync(reader, reader.LocalName, token); diff --git a/ReqIFSharp/XmlReaderExtensions.cs b/ReqIFSharp/XmlReaderExtensions.cs index bb547a2..45162a8 100644 --- a/ReqIFSharp/XmlReaderExtensions.cs +++ b/ReqIFSharp/XmlReaderExtensions.cs @@ -20,9 +20,12 @@ namespace ReqIFSharp { + using System.Threading; using System.Threading.Tasks; using System.Xml; + using Microsoft.Extensions.Logging; + /// <summary> /// Extension methods for <see cref="XmlReader"/> /// </summary> @@ -116,6 +119,249 @@ internal static async Task<string> ReadElementContentAsStringToEndElementAsync(t return content; } + /// <summary> + /// Skips the current element, with its content, when it is not a ReqIF element: when it is in a namespace + /// other than the ReqIF namespace. Elements in no namespace are ReqIF elements of a document without namespace. + /// </summary> + /// <remarks> + /// A warning is logged for the skipped element, and the reader is left on its end tag (or on the element + /// itself when it is empty), so that the next <see cref="XmlReader.Read"/> moves to the node after it. + /// The reader is first moved to content, see <see cref="XmlReader.MoveToContent"/>. + /// </remarks> + /// <param name="reader"> + /// The <see cref="XmlReader"/> + /// </param> + /// <param name="logger"> + /// The <see cref="ILogger"/> used to log the warning; no warning is logged when it is null + /// </param> + /// <returns> + /// true when the element has been skipped; false when the reader is not on an element or on a ReqIF element + /// </returns> + internal static bool SkipForeignElement(this XmlReader reader, ILogger logger) + { + if (reader.MoveToContent() != XmlNodeType.Element || IsReqIFNamespace(reader.NamespaceURI)) + { + return false; + } + + LogForeignElement(reader, logger); + + if (!reader.IsEmptyElement) + { + using (var subtree = reader.ReadSubtree()) + { + while (subtree.Read()) + { + } + } + } + + return true; + } + + /// <summary> + /// Asynchronously skips the current element, with its content, when it is not a ReqIF element: when it is in a + /// namespace other than the ReqIF namespace. Elements in no namespace are ReqIF elements of a document without namespace. + /// </summary> + /// <remarks> + /// A warning is logged for the skipped element, and the reader is left on its end tag (or on the element + /// itself when it is empty), so that the next <see cref="XmlReader.ReadAsync"/> moves to the node after it. + /// The reader is first moved to content, see <see cref="XmlReader.MoveToContentAsync"/>. + /// </remarks> + /// <param name="reader"> + /// The <see cref="XmlReader"/> + /// </param> + /// <param name="logger"> + /// The <see cref="ILogger"/> used to log the warning; no warning is logged when it is null + /// </param> + /// <param name="token"> + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// </param> + /// <returns> + /// true when the element has been skipped; false when the reader is not on an element or on a ReqIF element + /// </returns> + internal static async Task<bool> SkipForeignElementAsync(this XmlReader reader, ILogger logger, CancellationToken token) + { + if (await reader.MoveToContentAsync() != XmlNodeType.Element || IsReqIFNamespace(reader.NamespaceURI)) + { + return false; + } + + LogForeignElement(reader, logger); + + if (!reader.IsEmptyElement) + { + using (var subtree = reader.ReadSubtree()) + { + while (await subtree.ReadAsync()) + { + token.ThrowIfCancellationRequested(); + } + } + } + + return true; + } + + /// <summary> + /// Advances the reader to the next descendant ReqIF element with the given local name, as + /// <see cref="XmlReader.ReadToDescendant(string, string)"/> does. A ReqIF element is an element in the ReqIF + /// namespace or in no namespace. Elements in another namespace are skipped with their content, and a warning + /// is logged for each, also when the reader is on such an element. + /// </summary> + /// <param name="reader"> + /// The <see cref="XmlReader"/>, positioned on the element whose descendants are searched. The reader is first + /// moved to content (see <see cref="XmlReader.MoveToContent"/>), so on whitespace the next element is searched. + /// </param> + /// <param name="localName"> + /// The local name of the element to find + /// </param> + /// <param name="logger"> + /// The <see cref="ILogger"/> used to log skipped elements; no warning is logged when it is null + /// </param> + /// <returns> + /// true when the element was found, with the reader on it; false when it was not found, with the reader on the + /// end tag of the element it started on (or on that element when it is empty). When that element is not a ReqIF + /// element it is skipped: the reader is on its end tag (or on it when it is empty) and false is returned. + /// </returns> + internal static bool ReadToReqIFDescendant(this XmlReader reader, string localName, ILogger logger) + { + if (reader.SkipForeignElement(logger) || reader.NodeType != XmlNodeType.Element || reader.IsEmptyElement) + { + return false; + } + + var depth = reader.Depth; + + while (reader.Read() && reader.Depth > depth) + { + if (reader.NodeType != XmlNodeType.Element || reader.SkipForeignElement(logger)) + { + continue; + } + + if (reader.LocalName == localName) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Asynchronously advances the reader to the next descendant ReqIF element with the given local name, as + /// <see cref="XmlReader.ReadToDescendant(string, string)"/> does. A ReqIF element is an element in the ReqIF + /// namespace or in no namespace. Elements in another namespace are skipped with their content, and a warning + /// is logged for each, also when the reader is on such an element. + /// </summary> + /// <param name="reader"> + /// The <see cref="XmlReader"/>, positioned on the element whose descendants are searched. The reader is first + /// moved to content (see <see cref="XmlReader.MoveToContent"/>), so on whitespace the next element is searched. + /// </param> + /// <param name="localName"> + /// The local name of the element to find + /// </param> + /// <param name="logger"> + /// The <see cref="ILogger"/> used to log skipped elements; no warning is logged when it is null + /// </param> + /// <param name="token"> + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// </param> + /// <returns> + /// true when the element was found, with the reader on it; false when it was not found, with the reader on the + /// end tag of the element it started on (or on that element when it is empty). When that element is not a ReqIF + /// element it is skipped: the reader is on its end tag (or on it when it is empty) and false is returned. + /// </returns> + internal static async Task<bool> ReadToReqIFDescendantAsync(this XmlReader reader, string localName, ILogger logger, CancellationToken token) + { + if (await reader.SkipForeignElementAsync(logger, token) || reader.NodeType != XmlNodeType.Element || reader.IsEmptyElement) + { + return false; + } + + var depth = reader.Depth; + + while (await reader.ReadAsync() && reader.Depth > depth) + { + token.ThrowIfCancellationRequested(); + + if (reader.NodeType != XmlNodeType.Element || await reader.SkipForeignElementAsync(logger, token)) + { + continue; + } + + if (reader.LocalName == localName) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Reads the rest of a subtree reader, skipping every element in a namespace other than the ReqIF namespace + /// (with a warning, see <see cref="SkipForeignElement"/>), so that such elements are reported also when they + /// come after the content that was read. + /// </summary> + /// <param name="reader"> + /// A subtree reader, see <see cref="XmlReader.ReadSubtree"/>: it is read to its end + /// </param> + /// <param name="logger"> + /// The <see cref="ILogger"/> used to log skipped elements; no warning is logged when it is null + /// </param> + internal static void SkipForeignElementsToEnd(this XmlReader reader, ILogger logger) + { + while (reader.Read()) + { + reader.SkipForeignElement(logger); + } + } + + /// <summary> + /// Asynchronously reads the rest of a subtree reader, skipping every element in a namespace other than the ReqIF + /// namespace (with a warning, see <see cref="SkipForeignElementAsync"/>), so that such elements are reported also + /// when they come after the content that was read. + /// </summary> + /// <param name="reader"> + /// A subtree reader, see <see cref="XmlReader.ReadSubtree"/>: it is read to its end + /// </param> + /// <param name="logger"> + /// The <see cref="ILogger"/> used to log skipped elements; no warning is logged when it is null + /// </param> + /// <param name="token"> + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// </param> + internal static async Task SkipForeignElementsToEndAsync(this XmlReader reader, ILogger logger, CancellationToken token) + { + while (await reader.ReadAsync()) + { + token.ThrowIfCancellationRequested(); + + await reader.SkipForeignElementAsync(logger, token); + } + } + + /// <summary> + /// Checks whether a namespace is the namespace of ReqIF elements: the ReqIF namespace, or no namespace + /// </summary> + private static bool IsReqIFNamespace(string namespaceUri) + { + return namespaceUri.Length == 0 || namespaceUri == DefaultXmlAttributeFactory.ReqIFSchemaUri; + } + + /// <summary> + /// Logs that the element the reader is on is not a ReqIF element and is skipped + /// </summary> + private static void LogForeignElement(XmlReader reader, ILogger logger) + { + var xmlLineInfo = reader as IXmlLineInfo; + + logger?.LogWarning("The {Name} element at line:position {LineNumber}:{LinePosition} is in the namespace {NamespaceUri} instead of {ReqIFNamespaceUri}; it is not a ReqIF element and has been skipped", + reader.Name, xmlLineInfo?.LineNumber, xmlLineInfo?.LinePosition, reader.NamespaceURI, DefaultXmlAttributeFactory.ReqIFSchemaUri); + } + /// <summary> /// Creates an <see cref="XmlException"/> with the line and position of the reader, when it has them /// </summary>