diff --git a/ReqIFSharp.Tests/NamespaceRetentionTestFixture.cs b/ReqIFSharp.Tests/NamespaceRetentionTestFixture.cs
new file mode 100644
index 0000000..22e096d
--- /dev/null
+++ b/ReqIFSharp.Tests/NamespaceRetentionTestFixture.cs
@@ -0,0 +1,335 @@
+// -------------------------------------------------------------------------------------------------
+//
+//
+// 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.IO.Compression;
+ using System.Linq;
+ using System.Text;
+ using System.Threading;
+ using System.Xml;
+
+ using NUnit.Framework;
+
+ using ReqIFSharp;
+
+ ///
+ /// Suite of tests that verify the namespace declarations and root attributes of a
+ /// document are retained when it is deserialized and serialized to a new destination (issue #44).
+ ///
+ [TestFixture]
+ public class NamespaceRetentionTestFixture
+ {
+ private const string ReqIFNamespace = "http://www.omg.org/spec/ReqIF/20110401/reqif.xsd";
+ private const string XsiNamespace = "http://www.w3.org/2001/XMLSchema-instance";
+
+ private string datatypeDemoPath;
+
+ [SetUp]
+ public void SetUp()
+ {
+ this.datatypeDemoPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestData", "Datatype-Demo.reqif");
+ }
+
+ [Test]
+ public void Verify_that_namespace_declarations_are_retained_on_roundtrip()
+ {
+ var deserializer = new ReqIFDeserializer();
+ var serializer = new ReqIFSerializer();
+
+ var reqif = deserializer.Deserialize(this.datatypeDemoPath).First();
+
+ using var output = new MemoryStream();
+ serializer.Serialize(new[] { reqif }, output, SupportedFileExtensionKind.Reqif);
+
+ var attributes = ExtractRootAttributes(output);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(attributes.ContainsKey("xmlns"), Is.True, "the default ReqIF namespace declaration is missing");
+ Assert.That(attributes["xmlns"], Is.EqualTo(ReqIFNamespace));
+ Assert.That(attributes["xmlns:configuration"], Is.EqualTo("http://eclipse.org/rmf/pror/toolextensions/1.0"));
+ Assert.That(attributes["xmlns:id"], Is.EqualTo("http://pror.org/presentation/id"));
+ Assert.That(attributes["xmlns:xhtml"], Is.EqualTo("http://www.w3.org/1999/xhtml"));
+ });
+ }
+
+ [Test]
+ public async System.Threading.Tasks.Task Verify_that_namespace_declarations_are_retained_on_roundtrip_async()
+ {
+ var deserializer = new ReqIFDeserializer();
+ var serializer = new ReqIFSerializer();
+
+ var reqif = (await deserializer.DeserializeAsync(this.datatypeDemoPath, CancellationToken.None)).First();
+
+ using var output = new MemoryStream();
+ await serializer.SerializeAsync(new[] { reqif }, output, SupportedFileExtensionKind.Reqif, CancellationToken.None);
+
+ var attributes = ExtractRootAttributes(output);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(attributes["xmlns"], Is.EqualTo(ReqIFNamespace));
+ Assert.That(attributes["xmlns:configuration"], Is.EqualTo("http://eclipse.org/rmf/pror/toolextensions/1.0"));
+ Assert.That(attributes["xmlns:id"], Is.EqualTo("http://pror.org/presentation/id"));
+ Assert.That(attributes["xmlns:xhtml"], Is.EqualTo("http://www.w3.org/1999/xhtml"));
+ });
+ }
+
+ [Test]
+ public void Verify_that_prefixed_root_attribute_is_retained_on_roundtrip()
+ {
+ const string schemaLocation = "http://www.omg.org/spec/ReqIF/20110401/reqif.xsd reqif.xsd";
+
+ // start from a known-valid document and add an xsi namespace declaration plus an
+ // xsi:schemaLocation prefixed attribute to the REQ-IF root element
+ var xml = File.ReadAllText(this.datatypeDemoPath)
+ .Replace(
+ $"xmlns=\"{ReqIFNamespace}\"",
+ $"xmlns=\"{ReqIFNamespace}\" xmlns:xsi=\"{XsiNamespace}\" xsi:schemaLocation=\"{schemaLocation}\"");
+
+ var deserializer = new ReqIFDeserializer();
+ var serializer = new ReqIFSerializer();
+
+ ReqIF reqif;
+ using (var input = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
+ {
+ reqif = deserializer.Deserialize(input, SupportedFileExtensionKind.Reqif).First();
+ }
+
+ using var output = new MemoryStream();
+ serializer.Serialize(new[] { reqif }, output, SupportedFileExtensionKind.Reqif);
+
+ var attributes = ExtractRootAttributes(output);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(attributes["xmlns:xsi"], Is.EqualTo(XsiNamespace), "the xsi namespace declaration was not retained");
+ Assert.That(attributes["xsi:schemaLocation"], Is.EqualTo(schemaLocation), "the xsi:schemaLocation attribute was not retained");
+ });
+ }
+
+ [Test]
+ public void Verify_that_repeated_serialization_does_not_duplicate_namespace_declarations()
+ {
+ var deserializer = new ReqIFDeserializer();
+ var serializer = new ReqIFSerializer();
+
+ var reqif = deserializer.Deserialize(this.datatypeDemoPath).First();
+
+ // serialize twice from the same object; the second pass must not accumulate extra declarations
+ using (var first = new MemoryStream())
+ {
+ serializer.Serialize(new[] { reqif }, first, SupportedFileExtensionKind.Reqif);
+ }
+
+ using var second = new MemoryStream();
+ serializer.Serialize(new[] { reqif }, second, SupportedFileExtensionKind.Reqif);
+
+ var attributes = ExtractRootAttributes(second);
+
+ Assert.That(attributes["xmlns:xhtml"], Is.EqualTo("http://www.w3.org/1999/xhtml"));
+ }
+
+ [TestCase("Datatype-Demo.reqif")]
+ [TestCase("ProR_Traceability-Template-v1.0.reqif")]
+ [TestCase("DefaultValueDemo.reqif")]
+ [TestCase("reqifsharpgenerated.reqif")]
+ public void Verify_that_root_attributes_of_reqif_files_survive_roundtrip(string fileName)
+ {
+ var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestData", fileName);
+
+ IDictionary source;
+ using (var sourceStream = File.OpenRead(path))
+ {
+ source = ExtractRootAttributes(sourceStream);
+ }
+
+ var deserializer = new ReqIFDeserializer();
+ var serializer = new ReqIFSerializer();
+
+ var reqif = deserializer.Deserialize(path).First();
+
+ using var output = new MemoryStream();
+ serializer.Serialize(new[] { reqif }, output, SupportedFileExtensionKind.Reqif);
+
+ var serialized = ExtractRootAttributes(output);
+
+ AssertAttributesRetained(source, serialized, fileName);
+ }
+
+ [TestCase("Spielwiese.reqifz", 1)]
+ [TestCase("requirements-and-objects.reqifz", 1)]
+ [TestCase("test-multiple-reqif.reqifz", 2)]
+ public void Verify_that_namespace_declarations_of_reqifz_archives_survive_roundtrip(string fileName, int expectedDocumentCount)
+ {
+ var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestData", fileName);
+
+ IDictionary sourceNamespaces;
+ using (var sourceStream = File.OpenRead(path))
+ {
+ sourceNamespaces = UnionNamespaceDeclarations(ExtractArchiveRootAttributes(sourceStream));
+ }
+
+ var deserializer = new ReqIFDeserializer();
+ var serializer = new ReqIFSerializer();
+
+ var reqifs = deserializer.Deserialize(path).ToList();
+ Assert.That(reqifs, Has.Count.EqualTo(expectedDocumentCount));
+
+ using var output = new MemoryStream();
+ serializer.Serialize(reqifs, output, SupportedFileExtensionKind.Reqifz);
+
+ var outputDocuments = ExtractArchiveRootAttributes(output);
+ Assert.That(outputDocuments, Has.Count.EqualTo(expectedDocumentCount), $"{fileName}: the number of serialized documents changed");
+
+ var outputNamespaces = UnionNamespaceDeclarations(outputDocuments);
+
+ AssertAttributesRetained(sourceNamespaces, outputNamespaces, fileName);
+ }
+
+ [TestCase("Spielwiese.reqifz", 1)]
+ [TestCase("test-multiple-reqif.reqifz", 2)]
+ public async System.Threading.Tasks.Task Verify_that_namespace_declarations_of_reqifz_archives_survive_roundtrip_async(string fileName, int expectedDocumentCount)
+ {
+ var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestData", fileName);
+
+ IDictionary sourceNamespaces;
+ using (var sourceStream = File.OpenRead(path))
+ {
+ sourceNamespaces = UnionNamespaceDeclarations(ExtractArchiveRootAttributes(sourceStream));
+ }
+
+ var deserializer = new ReqIFDeserializer();
+ var serializer = new ReqIFSerializer();
+
+ var reqifs = (await deserializer.DeserializeAsync(path, CancellationToken.None)).ToList();
+ Assert.That(reqifs, Has.Count.EqualTo(expectedDocumentCount));
+
+ using var output = new MemoryStream();
+ await serializer.SerializeAsync(reqifs, output, SupportedFileExtensionKind.Reqifz, CancellationToken.None);
+
+ var outputNamespaces = UnionNamespaceDeclarations(ExtractArchiveRootAttributes(output));
+
+ AssertAttributesRetained(sourceNamespaces, outputNamespaces, fileName);
+ }
+
+ ///
+ /// Asserts that every attribute in is present in
+ /// with the same value (the serializer may add declarations such as the XHTML namespace, but must not drop any).
+ ///
+ private static void AssertAttributesRetained(IDictionary source, IDictionary serialized, string context)
+ {
+ Assert.Multiple(() =>
+ {
+ foreach (var attribute in source)
+ {
+ Assert.That(serialized.ContainsKey(attribute.Key), Is.True, $"{context}: the '{attribute.Key}' attribute was not retained");
+ Assert.That(serialized.TryGetValue(attribute.Key, out var value) ? value : null, Is.EqualTo(attribute.Value), $"{context}: the value of '{attribute.Key}' changed");
+ }
+ });
+ }
+
+ ///
+ /// Reads the REQ-IF root attributes of every .reqif entry contained in a .reqifz archive.
+ ///
+ private static List> ExtractArchiveRootAttributes(Stream reqifzStream)
+ {
+ if (reqifzStream.CanSeek)
+ {
+ reqifzStream.Position = 0;
+ }
+
+ var documents = new List>();
+
+ using var archive = new ZipArchive(reqifzStream, ZipArchiveMode.Read, leaveOpen: true);
+
+ foreach (var entry in archive.Entries)
+ {
+ if (entry.FullName.EndsWith(".reqif", StringComparison.OrdinalIgnoreCase))
+ {
+ using var entryStream = entry.Open();
+ documents.Add(ExtractRootAttributes(entryStream));
+ }
+ }
+
+ return documents;
+ }
+
+ ///
+ /// Collects the union of namespace declarations (xmlns / xmlns:prefix) across the supplied documents.
+ ///
+ private static IDictionary UnionNamespaceDeclarations(IEnumerable> documents)
+ {
+ var union = new Dictionary();
+
+ foreach (var document in documents)
+ {
+ foreach (var attribute in document)
+ {
+ if (attribute.Key == "xmlns" || attribute.Key.StartsWith("xmlns:", StringComparison.Ordinal))
+ {
+ union[attribute.Key] = attribute.Value;
+ }
+ }
+ }
+
+ return union;
+ }
+
+ ///
+ /// Reads the attributes declared on the REQ-IF root element of a serialized document, keyed by
+ /// their qualified name (e.g. xmlns, xmlns:configuration, xsi:schemaLocation).
+ ///
+ private static IDictionary ExtractRootAttributes(Stream stream)
+ {
+ if (stream.CanSeek)
+ {
+ stream.Position = 0;
+ }
+
+ var attributes = new Dictionary();
+
+ using var reader = XmlReader.Create(stream);
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element && reader.Name == "REQ-IF")
+ {
+ if (reader.MoveToFirstAttribute())
+ {
+ do
+ {
+ attributes[reader.Name] = reader.Value;
+ }
+ while (reader.MoveToNextAttribute());
+ }
+
+ break;
+ }
+ }
+
+ return attributes;
+ }
+ }
+}
diff --git a/ReqIFSharp/DefaultXmlAttributeFactory.cs b/ReqIFSharp/DefaultXmlAttributeFactory.cs
index 1275953..29a15c6 100644
--- a/ReqIFSharp/DefaultXmlAttributeFactory.cs
+++ b/ReqIFSharp/DefaultXmlAttributeFactory.cs
@@ -37,6 +37,11 @@ public static class DefaultXmlAttributeFactory
///
public const string XHTMLNameSpaceUri = @"http://www.w3.org/1999/xhtml";
+ ///
+ /// The reserved URI used by the XML parser for namespace declaration attributes (xmlns / xmlns:prefix)
+ ///
+ public const string XmlNamespaceDeclarationUri = @"http://www.w3.org/2000/xmlns/";
+
///
/// Creates the XHTML namespace attribute in case the document
/// contains XHTML data
@@ -56,6 +61,7 @@ internal static XmlAttribute CreateXHTMLNameSpaceAttribute(ReqIF reqIf)
{
LocalName = "xhtml",
Prefix = "xmlns",
+ NamespaceUri = XmlNamespaceDeclarationUri,
Value = XHTMLNameSpaceUri
};
diff --git a/ReqIFSharp/ReqIF.cs b/ReqIFSharp/ReqIF.cs
index 1685ce0..a7bee2e 100644
--- a/ReqIFSharp/ReqIF.cs
+++ b/ReqIFSharp/ReqIF.cs
@@ -116,6 +116,7 @@ internal void ReadXml(XmlReader reader)
{
Prefix = reader.Prefix,
LocalName = reader.LocalName,
+ NamespaceUri = reader.NamespaceURI,
Value = reader.Value
};
@@ -187,6 +188,7 @@ internal async Task ReadXmlAsync(XmlReader reader, CancellationToken token)
{
Prefix = reader.Prefix,
LocalName = reader.LocalName,
+ NamespaceUri = reader.NamespaceURI,
Value = reader.Value
};
@@ -290,30 +292,22 @@ internal async Task WriteXmlAsync(XmlWriter writer, CancellationToken token)
///
private void WriteNameSpaceAttributes(XmlWriter writer)
{
- if (attributes.TrueForAll(x => x.Value != DefaultXmlAttributeFactory.XHTMLNameSpaceUri))
+ foreach (var xmlAttribute in this.QueryNameSpaceAttributes())
{
- var xmlAttribute = DefaultXmlAttributeFactory.CreateXHTMLNameSpaceAttribute(this);
- if (xmlAttribute != null)
+ if (xmlAttribute.Prefix == "xmlns")
{
- this.attributes.Add(xmlAttribute);
+ // prefixed namespace declaration: xmlns:prefix="uri"
+ writer.WriteAttributeString(xmlAttribute.Prefix, xmlAttribute.LocalName, null, xmlAttribute.Value);
}
- }
-
- foreach (var xmlAttribute in this.attributes)
- {
- if (xmlAttribute.Prefix != string.Empty)
+ else if (xmlAttribute.Prefix != string.Empty)
{
- if (xmlAttribute.Prefix == "xmlns")
- {
- writer.WriteAttributeString(xmlAttribute.Prefix, xmlAttribute.LocalName, null, xmlAttribute.Value);
- }
- else
- {
- writer.WriteAttributeString(xmlAttribute.LocalName, xmlAttribute.Prefix, xmlAttribute.Value);
- }
+ // ordinary prefixed attribute (e.g. xsi:schemaLocation): the namespace URI the
+ // prefix is bound to is required to emit it faithfully
+ writer.WriteAttributeString(xmlAttribute.Prefix, xmlAttribute.LocalName, xmlAttribute.NamespaceUri, xmlAttribute.Value);
}
else
{
+ // default namespace declaration (xmlns="...") or an unprefixed attribute
writer.WriteAttributeString(xmlAttribute.LocalName, xmlAttribute.Value);
}
}
@@ -327,35 +321,52 @@ private void WriteNameSpaceAttributes(XmlWriter writer)
///
private async Task WriteNameSpaceAttributesAsync(XmlWriter writer)
{
- if (attributes.TrueForAll(x => x.Value != DefaultXmlAttributeFactory.XHTMLNameSpaceUri))
+ foreach (var xmlAttribute in this.QueryNameSpaceAttributes())
{
- var xmlAttribute = DefaultXmlAttributeFactory.CreateXHTMLNameSpaceAttribute(this);
- if (xmlAttribute != null)
+ if (xmlAttribute.Prefix == "xmlns")
{
- this.attributes.Add(xmlAttribute);
+ // prefixed namespace declaration: xmlns:prefix="uri"
+ await writer.WriteAttributeStringAsync(xmlAttribute.Prefix, xmlAttribute.LocalName, null, xmlAttribute.Value);
}
- }
-
- foreach (var xmlAttribute in this.attributes)
- {
- if (xmlAttribute.Prefix != string.Empty)
+ else if (xmlAttribute.Prefix != string.Empty)
{
- if (xmlAttribute.Prefix == "xmlns")
- {
- await writer.WriteAttributeStringAsync(xmlAttribute.Prefix, xmlAttribute.LocalName, null, xmlAttribute.Value);
- }
- else
- {
- await writer.WriteAttributeStringAsync(null, xmlAttribute.LocalName, xmlAttribute.Prefix, xmlAttribute.Value);
- }
+ // ordinary prefixed attribute (e.g. xsi:schemaLocation): the namespace URI the
+ // prefix is bound to is required to emit it faithfully
+ await writer.WriteAttributeStringAsync(xmlAttribute.Prefix, xmlAttribute.LocalName, xmlAttribute.NamespaceUri, xmlAttribute.Value);
}
else
{
+ // default namespace declaration (xmlns="...") or an unprefixed attribute
await writer.WriteAttributeStringAsync(null, xmlAttribute.LocalName, null, xmlAttribute.Value);
}
}
}
+ ///
+ /// Gets the namespace attributes that are to be written to the REQ-IF XML element: the attributes
+ /// captured when the document was read, plus the XHTML namespace declaration when the document
+ /// contains XHTML data and does not already declare it.
+ ///
+ ///
+ /// the to serialize; the captured
+ /// are not mutated so the can be serialized more than once.
+ ///
+ private IEnumerable QueryNameSpaceAttributes()
+ {
+ var namespaceAttributes = new List(this.attributes);
+
+ if (namespaceAttributes.TrueForAll(x => x.Value != DefaultXmlAttributeFactory.XHTMLNameSpaceUri))
+ {
+ var xmlAttribute = DefaultXmlAttributeFactory.CreateXHTMLNameSpaceAttribute(this);
+ if (xmlAttribute != null)
+ {
+ namespaceAttributes.Add(xmlAttribute);
+ }
+ }
+
+ return namespaceAttributes;
+ }
+
///
/// Writes the
///
diff --git a/ReqIFSharp/ReqIFSerializer.cs b/ReqIFSharp/ReqIFSerializer.cs
index b6084cd..3aab4d9 100644
--- a/ReqIFSharp/ReqIFSerializer.cs
+++ b/ReqIFSharp/ReqIFSerializer.cs
@@ -50,7 +50,7 @@ public void Serialize(IEnumerable reqifs, string fileUri)
var fileExtensionKind = fileUri.ConvertPathToSupportedFileExtensionKind();
- using (var fileStream = new FileStream(fileUri, FileMode.OpenOrCreate))
+ using (var fileStream = new FileStream(fileUri, FileMode.Create))
{
this.Serialize(reqifs, fileStream, fileExtensionKind);
}
@@ -121,7 +121,7 @@ public async Task SerializeAsync(IEnumerable reqifs, string fileUri, Canc
var fileExtensionKind = fileUri.ConvertPathToSupportedFileExtensionKind();
- using (var fileStream = new FileStream(fileUri, FileMode.OpenOrCreate))
+ using (var fileStream = new FileStream(fileUri, FileMode.Create))
{
await this.SerializeAsync(reqifs, fileStream, fileExtensionKind, token);
}
diff --git a/ReqIFSharp/XmlAttribute.cs b/ReqIFSharp/XmlAttribute.cs
index de275e5..da87b84 100644
--- a/ReqIFSharp/XmlAttribute.cs
+++ b/ReqIFSharp/XmlAttribute.cs
@@ -35,6 +35,17 @@ internal class XmlAttribute
///
internal string LocalName { get; set; }
+ ///
+ /// Gets or sets the namespace URI of the attribute.
+ ///
+ ///
+ /// For namespace declarations (xmlns / xmlns:prefix) this is the reserved
+ /// http://www.w3.org/2000/xmlns/ URI; for ordinary prefixed attributes (e.g.
+ /// xsi:schemaLocation) it is the URI the prefix is bound to. Captured on read so the
+ /// attribute can be re-emitted faithfully.
+ ///
+ internal string NamespaceUri { get; set; }
+
///
/// Gets or sets the value of the attribute.
///