diff --git a/CHANGELOG.md b/CHANGELOG.md
index 28eed81..d934bce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,9 +11,11 @@ The repository ships two NuGet packages that version independently: **ReqIFSharp
### Added
- `AttributeValueXHTML.ExtractNormalizedTextFromValue(bool keepLineBreaks = false)` returns the text of an XHTML value as plain text: runs of spaces, tabs and line breaks are collapsed to one space (non-breaking spaces are kept), no space is added around inline elements, and block elements such as paragraphs and list items are separated by a space or, with `keepLineBreaks`, by a line feed. The text of a `pre` element is kept as written, and HTML named entities such as ` ` are turned into their characters. `ExtractUnformattedTextFromValue` is unchanged ([#92](https://github.com/STARIONGROUP/ReqIFSharp/issues/92)).
+- `AccessControlledElement.IsEditableSpecified` tells whether the `IS-EDITABLE` attribute is written. It is true when the attribute was present in the document that was read or when `IsEditable` was set; set it to false to leave out an `IS-EDITABLE` that is false ([#86](https://github.com/STARIONGROUP/ReqIFSharp/issues/86)).
### Changed
+- Setting `IsEditable` to false on a spec hierarchy or attribute definition now writes `IS-EDITABLE="false"`; before, the attribute was left out. Both mean that the element is read-only. Set `IsEditableSpecified` to false to leave the attribute out ([#86](https://github.com/STARIONGROUP/ReqIFSharp/issues/86)).
- The source files carry the licence in short SPDX form (`SPDX-FileCopyrightText: 2017-2026 Starion Group S.A.`, `SPDX-License-Identifier: Apache-2.0`) instead of the full Apache-2.0 notice; the licence itself is unchanged, and `NOTICE` now gives the same years, 2017-2026 ([#103](https://github.com/STARIONGROUP/ReqIFSharp/issues/103)).
- 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)).
@@ -25,6 +27,8 @@ The repository ships two NuGet packages that version independently: **ReqIFSharp
- 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)).
+- An explicit `IS-EDITABLE="false"` on a spec hierarchy or attribute definition was dropped when a document was read and written again, so the receiving tool fell back to its own default. It is now written whenever it was read or `IsEditable` was set, also when it is false; an element without the attribute is still written without it ([#86](https://github.com/STARIONGROUP/ReqIFSharp/issues/86)).
+- Writing a spec hierarchy or attribute definition that has an `ALTERNATIVE-ID` threw an `InvalidOperationException` when it was editable or, for a spec hierarchy, `IS-TABLE-INTERNAL`: those attributes were written after the `ALTERNATIVE-ID` element ([#86](https://github.com/STARIONGROUP/ReqIFSharp/issues/86)).
- `new AttributeValueXHTML(loggerFactory)` left `ExternalObjects` `null`, so `QueryExternalObjects` and `QueryBase64PayloadsAsync` of ReqIFSharp.Extensions threw for a spec element that held such a value. `new AttributeValueXHTML()` left its logger `null`, so reading into it threw ([#98](https://github.com/STARIONGROUP/ReqIFSharp/issues/98)).
## [10.1.2] - 2026-10-18
diff --git a/ReqIFSharp.Tests/Decorators/ModelMetadataTestFixture.cs b/ReqIFSharp.Tests/Decorators/ModelMetadataTestFixture.cs
index af4e19a..7304d5a 100644
--- a/ReqIFSharp.Tests/Decorators/ModelMetadataTestFixture.cs
+++ b/ReqIFSharp.Tests/Decorators/ModelMetadataTestFixture.cs
@@ -39,6 +39,7 @@ public class ModelMetadataTestFixture
"DatatypeDefinition", // AttributeDefinition base accessor that aliases the concrete Type
"AttributeDefinition", // AttributeValue base accessor that aliases the concrete Definition
"ExternalObjects", // derived from the parsed XHTML content
+ "IsEditableSpecified", // records whether IS-EDITABLE is written, not a metamodel feature
"OwningDefinition", // back-reference to the owning AttributeDefinition
"Definition", // see note below - kept, handled explicitly (NOT excluded)
"SpecElAt", // back-reference to the owning SpecElementWithAttributes
diff --git a/ReqIFSharp.Tests/IsEditableTestFixture.cs b/ReqIFSharp.Tests/IsEditableTestFixture.cs
new file mode 100644
index 0000000..c8af9cd
--- /dev/null
+++ b/ReqIFSharp.Tests/IsEditableTestFixture.cs
@@ -0,0 +1,435 @@
+// -------------------------------------------------------------------------------------------------
+//
+//
+// SPDX-FileCopyrightText: 2017-2026 Starion Group S.A.
+// SPDX-License-Identifier: Apache-2.0
+//
+//
+// ------------------------------------------------------------------------------------------------
+
+namespace ReqIFSharp.Tests
+{
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using System.Xml;
+ using System.Xml.Linq;
+
+ using NUnit.Framework;
+
+ using ReqIFSharp;
+
+ ///
+ /// Verifies that the IS-EDITABLE attribute of an (a spec hierarchy
+ /// or an attribute definition) is written as it was read, including an explicit IS-EDITABLE="false".
+ ///
+ [TestFixture]
+ public class IsEditableTestFixture
+ {
+ private const string FileName = "reqifsharpgenerated.reqif";
+
+ ///
+ /// The elements that write attributes of their own next to those of an
+ ///
+ private static readonly string[] ElementsWithOwnAttributes =
+ {
+ "DATATYPE-DEFINITION-INTEGER", "DATATYPE-DEFINITION-REAL", "DATATYPE-DEFINITION-STRING", "ATTRIBUTE-DEFINITION-ENUMERATION"
+ };
+
+ ///
+ /// Returns the test document with IS-EDITABLE="false" on its first spec hierarchy and on its first
+ /// attribute definition, IS-EDITABLE="true" on its second spec hierarchy and no IS-EDITABLE on its third
+ ///
+ private static string CreateDocument(out string[] identifiers)
+ {
+ var ns = TestDocuments.ReqIFNamespace;
+ var document = XDocument.Parse(TestDocuments.Read(FileName));
+
+ var specHierarchies = document.Descendants(ns + "SPEC-HIERARCHY").ToList();
+ Assert.That(specHierarchies, Has.Count.GreaterThanOrEqualTo(3));
+
+ var attributeDefinition = document.Descendants(ns + "SPEC-ATTRIBUTES").Elements().First();
+
+ specHierarchies[0].SetAttributeValue("IS-EDITABLE", "false");
+ specHierarchies[1].SetAttributeValue("IS-EDITABLE", "true");
+ specHierarchies[2].SetAttributeValue("IS-EDITABLE", null);
+ attributeDefinition.SetAttributeValue("IS-EDITABLE", "false");
+
+ identifiers = new[]
+ {
+ specHierarchies[0].Attribute("IDENTIFIER")!.Value,
+ specHierarchies[1].Attribute("IDENTIFIER")!.Value,
+ specHierarchies[2].Attribute("IDENTIFIER")!.Value,
+ attributeDefinition.Attribute("IDENTIFIER")!.Value
+ };
+
+ return document.ToString();
+ }
+
+ ///
+ /// Returns the spec hierarchy with the identifier
+ ///
+ private static SpecHierarchy SpecHierarchyOf(ReqIF reqif, string identifier)
+ {
+ IEnumerable All(IEnumerable hierarchies) =>
+ hierarchies.SelectMany(x => new[] { x }.Concat(All(x.Children)));
+
+ return All(reqif.CoreContent.Specifications.SelectMany(x => x.Children)).Single(x => x.Identifier == identifier);
+ }
+
+ ///
+ /// Returns the access controlled element (spec hierarchy or attribute definition) with the identifier
+ ///
+ private static AccessControlledElement AccessControlledElementOf(ReqIF reqif, string identifier)
+ {
+ return reqif.CoreContent.SpecTypes.SelectMany(x => x.SpecAttributes).SingleOrDefault(x => x.Identifier == identifier)
+ ?? (AccessControlledElement)SpecHierarchyOf(reqif, identifier);
+ }
+
+ ///
+ /// Returns the IS-EDITABLE attribute of the element with the identifier, or null when it has none
+ ///
+ private static string IsEditableOf(XDocument document, string identifier)
+ {
+ return document.Descendants()
+ .Single(x => (string)x.Attribute("IDENTIFIER") == identifier)
+ .Attribute("IS-EDITABLE")?.Value;
+ }
+
+ private static void AssertRoundTrip(MemoryStream output, string[] identifiers)
+ {
+ output.Position = 0;
+ var document = XDocument.Load(output);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(IsEditableOf(document, identifiers[0]), Is.EqualTo("false"));
+ Assert.That(IsEditableOf(document, identifiers[1]), Is.EqualTo("true"));
+ Assert.That(IsEditableOf(document, identifiers[2]), Is.Null);
+ Assert.That(IsEditableOf(document, identifiers[3]), Is.EqualTo("false"));
+ });
+ }
+
+ [TestCaseSource(typeof(TestDocuments), nameof(TestDocuments.Kinds))]
+ public void Verify_that_IS_EDITABLE_is_written_as_it_was_read(SupportedFileExtensionKind kind)
+ {
+ var xml = CreateDocument(out var identifiers);
+
+ var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, kind), kind).ToList();
+
+ var reqif = reqifs.Single();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(SpecHierarchyOf(reqif, identifiers[0]).IsEditableSpecified, Is.True);
+ Assert.That(SpecHierarchyOf(reqif, identifiers[0]).IsEditable, Is.False);
+ Assert.That(SpecHierarchyOf(reqif, identifiers[1]).IsEditableSpecified, Is.True);
+ Assert.That(SpecHierarchyOf(reqif, identifiers[1]).IsEditable, Is.True);
+ Assert.That(SpecHierarchyOf(reqif, identifiers[2]).IsEditableSpecified, Is.False);
+ Assert.That(SpecHierarchyOf(reqif, identifiers[2]).IsEditable, Is.False);
+ Assert.That(AccessControlledElementOf(reqif, identifiers[3]).IsEditableSpecified, Is.True);
+ Assert.That(AccessControlledElementOf(reqif, identifiers[3]).IsEditable, Is.False);
+ });
+
+ using var output = new MemoryStream();
+ new ReqIFSerializer().Serialize(reqifs, output, SupportedFileExtensionKind.Reqif);
+
+ AssertRoundTrip(output, identifiers);
+ }
+
+ [TestCaseSource(typeof(TestDocuments), nameof(TestDocuments.Kinds))]
+ public async Task Verify_that_IS_EDITABLE_is_written_as_it_was_read_async(SupportedFileExtensionKind kind)
+ {
+ var xml = CreateDocument(out var identifiers);
+
+ var reqifs = (await new ReqIFDeserializer().DeserializeAsync(TestDocuments.ToStream(xml, kind), kind, CancellationToken.None)).ToList();
+
+ using var output = new MemoryStream();
+ await new ReqIFSerializer().SerializeAsync(reqifs, output, SupportedFileExtensionKind.Reqif, CancellationToken.None);
+
+ AssertRoundTrip(output, identifiers);
+ }
+
+ [Test]
+ public void Verify_that_IsEditableSpecified_is_false_for_a_new_element_and_set_by_IsEditable()
+ {
+ var specHierarchy = new SpecHierarchy();
+
+ Assert.That(specHierarchy.IsEditableSpecified, Is.False);
+
+ specHierarchy.IsEditable = false;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(specHierarchy.IsEditableSpecified, Is.True);
+ Assert.That(specHierarchy.IsEditable, Is.False);
+ });
+ }
+
+ [TestCase(0, Description = "spec hierarchy")]
+ [TestCase(3, Description = "attribute definition")]
+ public async Task Verify_that_a_false_IS_EDITABLE_that_was_read_is_left_out_when_IsEditableSpecified_is_set_to_false(int index)
+ {
+ var xml = CreateDocument(out var identifiers);
+ var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList();
+
+ var element = AccessControlledElementOf(reqifs.Single(), identifiers[index]);
+ Assert.That(element.IsEditableSpecified, Is.True, "the element should have been read with IS-EDITABLE=\"false\"");
+
+ element.IsEditableSpecified = false;
+
+ foreach (var document in await SerializeBothWays(reqifs))
+ {
+ Assert.That(IsEditableOf(document, identifiers[index]), Is.Null);
+ }
+ }
+
+ [Test]
+ public async Task Verify_that_a_false_IS_EDITABLE_is_written_when_IsEditableSpecified_is_set_to_true()
+ {
+ var xml = CreateDocument(out var identifiers);
+ var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList();
+
+ SpecHierarchyOf(reqifs.Single(), identifiers[2]).IsEditableSpecified = true;
+
+ foreach (var document in await SerializeBothWays(reqifs))
+ {
+ Assert.That(IsEditableOf(document, identifiers[2]), Is.EqualTo("false"));
+ }
+ }
+
+ [TestCase(false, "false")]
+ [TestCase(true, "true")]
+ public async Task Verify_that_IsEditable_set_in_code_is_written(bool isEditable, string expected)
+ {
+ var xml = CreateDocument(out var identifiers);
+ var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList();
+
+ // identifiers[2] was read without IS-EDITABLE; set it in code and leave the others as read
+ SpecHierarchyOf(reqifs.Single(), identifiers[2]).IsEditable = isEditable;
+
+ foreach (var document in await SerializeBothWays(reqifs))
+ {
+ Assert.Multiple(() =>
+ {
+ Assert.That(IsEditableOf(document, identifiers[2]), Is.EqualTo(expected));
+ Assert.That(IsEditableOf(document, identifiers[0]), Is.EqualTo("false"));
+ });
+ }
+ }
+
+ [TestCase(false, "false")]
+ [TestCase(true, "true")]
+ public async Task Verify_that_IsEditable_set_in_code_on_an_attribute_definition_is_written(bool isEditable, string expected)
+ {
+ var xml = CreateDocument(out var identifiers);
+ var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList();
+
+ // an attribute definition that was read without IS-EDITABLE
+ var attributeDefinition = reqifs.Single().CoreContent.SpecTypes
+ .SelectMany(x => x.SpecAttributes)
+ .First(x => x.Identifier != identifiers[3]);
+
+ Assert.That(attributeDefinition.IsEditableSpecified, Is.False);
+
+ attributeDefinition.IsEditable = isEditable;
+
+ foreach (var document in await SerializeBothWays(reqifs))
+ {
+ Assert.That(IsEditableOf(document, attributeDefinition.Identifier), Is.EqualTo(expected));
+ }
+ }
+
+ [Test]
+ public async Task Verify_that_a_true_IS_EDITABLE_is_written_when_IsEditableSpecified_is_false()
+ {
+ var xml = CreateDocument(out var identifiers);
+ var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList();
+
+ SpecHierarchyOf(reqifs.Single(), identifiers[1]).IsEditableSpecified = false;
+
+ foreach (var document in await SerializeBothWays(reqifs))
+ {
+ Assert.That(IsEditableOf(document, identifiers[1]), Is.EqualTo("true"));
+ }
+ }
+
+ [TestCase("true", null)]
+ [TestCase("false", "true")]
+ public async Task Verify_that_the_attributes_of_a_spec_hierarchy_with_an_alternative_id_are_written(string isEditable, string isTableInternal)
+ {
+ var ns = TestDocuments.ReqIFNamespace;
+ var source = XDocument.Parse(TestDocuments.Read(FileName));
+
+ var specHierarchy = source.Descendants(ns + "SPEC-HIERARCHY").First();
+ var identifier = specHierarchy.Attribute("IDENTIFIER")!.Value;
+ specHierarchy.SetAttributeValue("IS-EDITABLE", isEditable);
+ specHierarchy.SetAttributeValue("IS-TABLE-INTERNAL", isTableInternal);
+ specHierarchy.Elements(ns + "ALTERNATIVE-ID").Remove();
+ specHierarchy.AddFirst(new XElement(ns + "ALTERNATIVE-ID", new XElement(ns + "ALTERNATIVE-ID", new XAttribute("IDENTIFIER", "hierarchy_alternative"))));
+
+ var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(source.ToString(), SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList();
+
+ foreach (var document in await SerializeBothWays(reqifs))
+ {
+ var element = document.Descendants(ns + "SPEC-HIERARCHY").Single(x => (string)x.Attribute("IDENTIFIER") == identifier);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That((string)element.Attribute("IS-EDITABLE"), Is.EqualTo(isEditable));
+ Assert.That((string)element.Attribute("IS-TABLE-INTERNAL"), Is.EqualTo(isTableInternal));
+ Assert.That((string)element.Element(ns + "ALTERNATIVE-ID")?.Element(ns + "ALTERNATIVE-ID")?.Attribute("IDENTIFIER"), Is.EqualTo("hierarchy_alternative"));
+ });
+ }
+ }
+
+ [TestCase("true")]
+ [TestCase("false")]
+ public async Task Verify_that_the_attributes_of_an_attribute_definition_with_an_alternative_id_are_written(string isEditable)
+ {
+ var ns = TestDocuments.ReqIFNamespace;
+ var source = XDocument.Parse(TestDocuments.Read(FileName));
+
+ var attributeDefinition = source.Descendants(ns + "ATTRIBUTE-DEFINITION-ENUMERATION").First();
+ var identifier = attributeDefinition.Attribute("IDENTIFIER")!.Value;
+ attributeDefinition.SetAttributeValue("IS-EDITABLE", isEditable);
+ attributeDefinition.SetAttributeValue("MULTI-VALUED", "true");
+ attributeDefinition.Elements(ns + "ALTERNATIVE-ID").Remove();
+ attributeDefinition.AddFirst(new XElement(ns + "ALTERNATIVE-ID", new XElement(ns + "ALTERNATIVE-ID", new XAttribute("IDENTIFIER", "definition_alternative"))));
+
+ var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(source.ToString(), SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList();
+
+ foreach (var document in await SerializeBothWays(reqifs))
+ {
+ var element = document.Descendants(ns + "ATTRIBUTE-DEFINITION-ENUMERATION").Single(x => (string)x.Attribute("IDENTIFIER") == identifier);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That((string)element.Attribute("IS-EDITABLE"), Is.EqualTo(isEditable));
+ Assert.That((string)element.Attribute("MULTI-VALUED"), Is.EqualTo("true"));
+ Assert.That((string)element.Element(ns + "ALTERNATIVE-ID")?.Element(ns + "ALTERNATIVE-ID")?.Attribute("IDENTIFIER"), Is.EqualTo("definition_alternative"));
+ });
+ }
+ }
+
+ [Test]
+ public async Task Verify_that_the_attributes_of_datatypes_and_enumeration_definitions_are_written()
+ {
+ var ns = TestDocuments.ReqIFNamespace;
+ var numericAttributes = new[] { "MIN", "MAX", "ACCURACY", "MAX-LENGTH" };
+
+ var xml = TestDocuments.Read("Datatype-Demo.reqif");
+ var source = XDocument.Parse(xml);
+
+ var elements = source.Descendants()
+ .Where(x => numericAttributes.Any(a => x.Attribute(a) != null) || x.Attribute("MULTI-VALUED") != null)
+ .ToList();
+
+ var localNames = elements.Select(x => x.Name.LocalName).Distinct().ToList();
+ Assert.That(localNames, Is.SupersetOf(ElementsWithOwnAttributes));
+ Assert.That(elements.Where(x => x.Element(ns + "ALTERNATIVE-ID") != null), Is.Not.Empty, "the test document should have a datatype with an alternative id");
+
+ var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(xml, SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList();
+
+ foreach (var document in await SerializeBothWays(reqifs))
+ {
+ foreach (var expected in elements)
+ {
+ var identifier = expected.Attribute("IDENTIFIER")!.Value;
+ var actual = document.Descendants(expected.Name).Single(x => (string)x.Attribute("IDENTIFIER") == identifier);
+
+ Assert.Multiple(() =>
+ {
+ foreach (var name in numericAttributes.Where(a => expected.Attribute(a) != null))
+ {
+ Assert.That(actual.Attribute(name), Is.Not.Null, $"{name} of {identifier} is not written");
+ Assert.That(XmlConvert.ToDouble(actual.Attribute(name)!.Value), Is.EqualTo(XmlConvert.ToDouble(expected.Attribute(name)!.Value)), $"{name} of {identifier}");
+ }
+
+ if (expected.Attribute("MULTI-VALUED") != null)
+ {
+ Assert.That(actual.Attribute("MULTI-VALUED"), Is.Not.Null, $"MULTI-VALUED of {identifier} is not written");
+ Assert.That(XmlConvert.ToBoolean(actual.Attribute("MULTI-VALUED")!.Value), Is.EqualTo(XmlConvert.ToBoolean(expected.Attribute("MULTI-VALUED")!.Value)), $"MULTI-VALUED of {identifier}");
+ }
+
+ Assert.That(actual.Element(ns + "ALTERNATIVE-ID") != null, Is.EqualTo(expected.Element(ns + "ALTERNATIVE-ID") != null), $"ALTERNATIVE-ID of {identifier}");
+ });
+ }
+ }
+ }
+
+ [TestCase("")]
+ [TestCase(" ")]
+ public async Task Verify_that_an_empty_IS_EDITABLE_is_read_as_not_specified_and_left_out(string isEditable)
+ {
+ var ns = TestDocuments.ReqIFNamespace;
+ var source = XDocument.Parse(TestDocuments.Read(FileName));
+
+ var specHierarchy = source.Descendants(ns + "SPEC-HIERARCHY").First();
+ var identifier = specHierarchy.Attribute("IDENTIFIER")!.Value;
+ specHierarchy.SetAttributeValue("IS-EDITABLE", isEditable);
+
+ var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(source.ToString(), SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList();
+
+ var element = SpecHierarchyOf(reqifs.Single(), identifier);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(element.IsEditableSpecified, Is.False);
+ Assert.That(element.IsEditable, Is.False);
+ });
+
+ foreach (var document in await SerializeBothWays(reqifs))
+ {
+ Assert.That(IsEditableOf(document, identifier), Is.Null);
+ }
+ }
+
+ [TestCase("0", false, "false")]
+ [TestCase("1", true, "true")]
+ public async Task Verify_that_a_numeric_IS_EDITABLE_is_read_and_written_as_a_boolean(string isEditable, bool expectedValue, string expectedText)
+ {
+ var ns = TestDocuments.ReqIFNamespace;
+ var source = XDocument.Parse(TestDocuments.Read(FileName));
+
+ var specHierarchy = source.Descendants(ns + "SPEC-HIERARCHY").First();
+ var identifier = specHierarchy.Attribute("IDENTIFIER")!.Value;
+ specHierarchy.SetAttributeValue("IS-EDITABLE", isEditable);
+
+ var reqifs = new ReqIFDeserializer().Deserialize(TestDocuments.ToStream(source.ToString(), SupportedFileExtensionKind.Reqif), SupportedFileExtensionKind.Reqif).ToList();
+
+ var element = SpecHierarchyOf(reqifs.Single(), identifier);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(element.IsEditableSpecified, Is.True);
+ Assert.That(element.IsEditable, Is.EqualTo(expectedValue));
+ });
+
+ foreach (var document in await SerializeBothWays(reqifs))
+ {
+ Assert.That(IsEditableOf(document, identifier), Is.EqualTo(expectedText));
+ }
+ }
+
+ ///
+ /// Writes the documents with the sync and with the async serializer
+ ///
+ private static async Task> SerializeBothWays(IEnumerable reqifs)
+ {
+ var serializer = new ReqIFSerializer();
+
+ using var syncOutput = new MemoryStream();
+ serializer.Serialize(reqifs, syncOutput, SupportedFileExtensionKind.Reqif);
+ syncOutput.Position = 0;
+
+ using var asyncOutput = new MemoryStream();
+ await serializer.SerializeAsync(reqifs, asyncOutput, SupportedFileExtensionKind.Reqif, CancellationToken.None);
+ asyncOutput.Position = 0;
+
+ return new[] { XDocument.Load(syncOutput), XDocument.Load(asyncOutput) };
+ }
+ }
+}
diff --git a/ReqIFSharp/AccessControlledElement.cs b/ReqIFSharp/AccessControlledElement.cs
index 33eee14..7c2ffc2 100644
--- a/ReqIFSharp/AccessControlledElement.cs
+++ b/ReqIFSharp/AccessControlledElement.cs
@@ -49,15 +49,40 @@ protected AccessControlledElement(ILoggerFactory loggerFactory)
this.logger = this.loggerFactory == null ? NullLogger.Instance : this.loggerFactory.CreateLogger();
}
+ ///
+ /// Backing field for the property
+ ///
+ private bool isEditable;
+
///
/// Gets or sets a value indicating whether this is editable
///
///
/// True means that the element’s contents may be modified by the user of a tool containing the element.
/// False or leaving isEditable out means that the element is read-only to the user of a tool containing the element.
+ /// Setting this property also sets to true, so that the value is written.
///
[ReqIfProperty(aggregation: AggregationKind.None, lowerValue: 0, upperValue: 1, isOrdered: false, isReadOnly: false, isDerived: false, isDerivedUnion: false, isUnique: true, defaultValue: null)]
- public bool IsEditable { get; set; }
+ public bool IsEditable
+ {
+ get => this.isEditable;
+ set
+ {
+ this.isEditable = value;
+ this.IsEditableSpecified = true;
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the IS-EDITABLE attribute is written
+ ///
+ ///
+ /// This is true when the IS-EDITABLE attribute was present in the document that was read, or when
+ /// was set. When this and are both false, the attribute is
+ /// left out, which means the element is read-only; an of true is always written.
+ /// Set it to false to leave out an IS-EDITABLE attribute that is false.
+ ///
+ public bool IsEditableSpecified { get; set; }
///
/// Generates a object from its XML representation.
@@ -73,38 +98,38 @@ internal override void ReadXml(XmlReader reader)
this.logger.LogTrace("reading IS-EDITABLE at line:position {LineNumber}:{LinePosition}", xmlLineInfo?.LineNumber, xmlLineInfo?.LinePosition);
- var isEditable = reader.GetAttribute("IS-EDITABLE");
- if (!string.IsNullOrWhiteSpace(isEditable))
+ var isEditableValue = reader.GetAttribute("IS-EDITABLE");
+ if (!string.IsNullOrWhiteSpace(isEditableValue))
{
try
{
- this.IsEditable = XmlConvert.ToBoolean(isEditable);
+ this.IsEditable = XmlConvert.ToBoolean(isEditableValue);
}
catch (Exception e)
{
- throw new SerializationException($"The AccessControlledElement.IS-EDITABLE {isEditable} at line:position {xmlLineInfo?.LineNumber}:{xmlLineInfo?.LinePosition} could not be converted to a BOOLEAN", e);
+ throw new SerializationException($"The AccessControlledElement.IS-EDITABLE {isEditableValue} at line:position {xmlLineInfo?.LineNumber}:{xmlLineInfo?.LinePosition} could not be converted to a BOOLEAN", e);
}
}
}
///
- /// Converts a object into its XML representation.
+ /// Writes the IS-EDITABLE attribute when or is true
///
///
/// an instance of
///
- internal override void WriteXml(XmlWriter writer)
+ internal override void WriteXmlAttributes(XmlWriter writer)
{
- base.WriteXml(writer);
+ base.WriteXmlAttributes(writer);
- if (this.IsEditable)
+ if (this.IsEditableSpecified || this.IsEditable)
{
- writer.WriteAttributeString("IS-EDITABLE", "true");
+ writer.WriteAttributeString("IS-EDITABLE", XmlConvert.ToString(this.IsEditable));
}
}
///
- /// Asynchronously converts a object into its XML representation.
+ /// Asynchronously writes the IS-EDITABLE attribute when or is true
///
///
/// an instance of
@@ -112,13 +137,13 @@ internal override void WriteXml(XmlWriter writer)
///
/// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
///
- internal override async Task WriteXmlAsync(XmlWriter writer, CancellationToken token)
+ internal override async Task WriteXmlAttributesAsync(XmlWriter writer, CancellationToken token)
{
- await base.WriteXmlAsync(writer, token);
+ await base.WriteXmlAttributesAsync(writer, token);
- if (this.IsEditable)
+ if (this.IsEditableSpecified || this.IsEditable)
{
- await writer.WriteAttributeStringAsync(null, "IS-EDITABLE", null, "true");
+ await writer.WriteAttributeStringAsync(null, "IS-EDITABLE", null, XmlConvert.ToString(this.IsEditable));
}
}
}
diff --git a/ReqIFSharp/Identifiable.cs b/ReqIFSharp/Identifiable.cs
index 1b60d46..af4b4e7 100644
--- a/ReqIFSharp/Identifiable.cs
+++ b/ReqIFSharp/Identifiable.cs
@@ -163,6 +163,8 @@ internal virtual void WriteXml(XmlWriter writer)
writer.WriteAttributeString("LONG-NAME", this.LongName);
}
+ this.WriteXmlAttributes(writer);
+
if (this.AlternativeId != null)
{
writer.WriteStartElement("ALTERNATIVE-ID");
@@ -206,6 +208,8 @@ internal virtual async Task WriteXmlAsync(XmlWriter writer, CancellationToken to
await writer.WriteAttributeStringAsync(null, "LONG-NAME", null, this.LongName);
}
+ await this.WriteXmlAttributesAsync(writer, token);
+
if (this.AlternativeId != null)
{
await writer.WriteStartElementAsync(null, "ALTERNATIVE-ID", null);
@@ -214,6 +218,34 @@ internal virtual async Task WriteXmlAsync(XmlWriter writer, CancellationToken to
}
}
+ ///
+ /// Writes the XML attributes that a subclass adds to the element. It is called by
+ /// after the attributes of the and before its ALTERNATIVE-ID element, since an
+ /// attribute cannot be written after a child element.
+ ///
+ ///
+ /// an instance of
+ ///
+ internal virtual void WriteXmlAttributes(XmlWriter writer)
+ {
+ }
+
+ ///
+ /// Asynchronously writes the XML attributes that a subclass adds to the element. It is called by
+ /// after the attributes of the and before its
+ /// ALTERNATIVE-ID element, since an attribute cannot be written after a child element.
+ ///
+ ///
+ /// an instance of
+ ///
+ ///
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ ///
+ internal virtual Task WriteXmlAttributesAsync(XmlWriter writer, CancellationToken token)
+ {
+ return Task.CompletedTask;
+ }
+
///
/// Reads the ALTERNATIVE-ID
///
diff --git a/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs b/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs
index 1de41b5..1a6c593 100644
--- a/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs
+++ b/ReqIFSharp/SpecElementWithAttributes/SpecHierarchy.cs
@@ -308,11 +308,6 @@ internal override void WriteXml(XmlWriter writer)
base.WriteXml(writer);
- if (this.IsTableInternal)
- {
- writer.WriteAttributeString("IS-TABLE-INTERNAL", "true");
- }
-
this.WriteObject(writer);
this.WriteEditableAtts(writer);
@@ -341,11 +336,6 @@ internal override async Task WriteXmlAsync(XmlWriter writer, CancellationToken t
await base.WriteXmlAsync(writer, token);
- if (this.IsTableInternal)
- {
- await writer.WriteAttributeStringAsync(null, "IS-TABLE-INTERNAL",null, "true");
- }
-
await this.WriteObjectAsync(writer, token);
await this.WriteEditableAttsAsync(writer, token);
@@ -353,6 +343,41 @@ internal override async Task WriteXmlAsync(XmlWriter writer, CancellationToken t
await this.WriteChildrenAsync(writer, token);
}
+ ///
+ /// Writes the IS-EDITABLE and IS-TABLE-INTERNAL attributes
+ ///
+ ///
+ /// an instance of
+ ///
+ internal override void WriteXmlAttributes(XmlWriter writer)
+ {
+ base.WriteXmlAttributes(writer);
+
+ if (this.IsTableInternal)
+ {
+ writer.WriteAttributeString("IS-TABLE-INTERNAL", "true");
+ }
+ }
+
+ ///
+ /// Asynchronously writes the IS-EDITABLE and IS-TABLE-INTERNAL attributes
+ ///
+ ///
+ /// an instance of
+ ///
+ ///
+ /// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
+ ///
+ internal override async Task WriteXmlAttributesAsync(XmlWriter writer, CancellationToken token)
+ {
+ await base.WriteXmlAttributesAsync(writer, token);
+
+ if (this.IsTableInternal)
+ {
+ await writer.WriteAttributeStringAsync(null, "IS-TABLE-INTERNAL", null, "true");
+ }
+ }
+
///
/// Initializes the properties of the current instance
///