Skip to content

Commit 96b52bf

Browse files
committed
Preserve ReqIF kinds the parser does not model
Self review found two silent round-trip defects, both rooted in the original element name being thrown away. 1. Attribute definitions of an unmodelled datatype were dropped when writing (verified: ad-custom parsed, missing from the output), and their values were not even parsed - the value switch had a bare default: break. 2. Spec types of an unmodelled kind were written back as SPEC-OBJECT-TYPE (verified), which is worse than dropping them because the result looks valid but means something else. - AttributeDefinition, AttributeValue and SpecType now remember their source element name, like Datatype already did - SpecObject and Specification keep values of unknown kinds as a generic AttributeValue carrying THE-VALUE - The writer falls back to the remembered element name for attribute definitions, attribute values, the datatype and definition -REF elements, and spec types, instead of skipping or guessing - Tests: UnmodelledElementsTest, incl. an idempotency check - README documents the behavior and the remaining limitation (values stored in child elements rather than THE-VALUE) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011mat2d7AJkouKhXWUYzHxs
1 parent ea73bfc commit 96b52bf

8 files changed

Lines changed: 227 additions & 6 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,17 @@ ValidationResult schemaIssues =
277277
Not covered yet: identifiers duplicated within one category (the parser
278278
keys its maps by identifier, so a duplicate has already replaced its
279279
predecessor by the time the model exists).
280+
281+
# Unmodelled ReqIF kinds
282+
283+
The parser models the datatype and spec type kinds of the standard. Kinds
284+
it does not know - vendor extensions or later ReqIF revisions - are kept
285+
generically rather than dropped: the original element name is remembered
286+
and written back unchanged, and values are carried as their raw
287+
`THE-VALUE`. This applies to datatype definitions, attribute definitions,
288+
attribute values and spec types, so a document round-trips without losing
289+
or altering them.
290+
291+
Limitation: a value of an unknown kind is only preserved when it is
292+
carried in a `THE-VALUE` attribute. Kinds that store their value in child
293+
elements are not covered.

src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class AttributeDefinition {
1818
private Datatype type;
1919
private String defaultValue;
2020
private String alternativeID;
21+
private String sourceElementName;
2122

2223

2324

@@ -44,6 +45,14 @@ public String getDefaultValue() {
4445
public String getAlternativeID() {
4546
return this.alternativeID;
4647
}
48+
49+
/**
50+
* @return the ATTRIBUTE-DEFINITION-* element name this definition was read
51+
* from, or null when it was not created from a document
52+
*/
53+
public String getSourceElementName() {
54+
return this.sourceElementName;
55+
}
4756

4857

4958

@@ -65,6 +74,7 @@ public AttributeDefinition(Node attributeDefinition, Map<String, Datatype> dataT
6574
this.id = attributeDefinition.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent();
6675
this.name = attributeDefinition.getAttributes().getNamedItem(ReqIFConst.LONG_NAME).getTextContent();
6776
this.alternativeID = XmlUtils.alternativeID(attributeDefinition);
77+
this.sourceElementName = XmlUtils.localName(attributeDefinition);
6878

6979
// Navigate by element (not by fixed child index) so both pretty-printed
7080
// and minified ReqIF files are handled.

src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValue.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public class AttributeValue {
66
private String name;
77
protected Object value;
88
private AttributeDefinition type;
9+
private String sourceElementName;
910

1011

1112

@@ -25,6 +26,20 @@ public AttributeDefinition getAttributeDefinitionType() {
2526
public String getDatatype() {
2627
return this.type.getDataType().getType();
2728
}
29+
30+
/**
31+
* @return the ATTRIBUTE-VALUE-* element name this value was read from, or
32+
* null when it was not created from a document. Needed to write back
33+
* values of datatype kinds the parser does not model explicitly.
34+
*/
35+
public String getSourceElementName() {
36+
return this.sourceElementName;
37+
}
38+
39+
public AttributeValue setSourceElementName(String sourceElementName) {
40+
this.sourceElementName = sourceElementName;
41+
return this;
42+
}
2843

2944

3045

src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,19 @@ protected void readAttributeValues(Node specObject, SpecType specType) {
221221
}
222222
this.attributeValues.put(attributeDefinitionName, new AttributeValueDouble(attributeValue, attributeDefinition));
223223
break;
224-
225-
default: break;
224+
225+
// Values of datatype kinds the parser does not model are
226+
// kept generically instead of being dropped, so they
227+
// survive a round trip.
228+
default: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) {
229+
attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent();
230+
}else{
231+
attributeValue = "";
232+
}
233+
this.attributeValues.put(attributeDefinitionName,
234+
new AttributeValue(attributeValue, attributeDefinition)
235+
.setSourceElementName(XmlUtils.localName(attribute)));
236+
break;
226237
}
227238
}
228239
}

src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class SpecType {
2020
protected String name;
2121
protected String type;
2222
protected String alternativeID;
23+
protected String sourceElementName;
2324

2425

2526

@@ -34,6 +35,16 @@ public String getID() {
3435
public String getAlternativeID() {
3536
return this.alternativeID;
3637
}
38+
39+
/**
40+
* @return the SPEC-TYPE element name this type was read from, or null when
41+
* it was not created from a document. Needed to write back spec type
42+
* kinds the parser does not model explicitly, instead of silently
43+
* turning them into a SPEC-OBJECT-TYPE.
44+
*/
45+
public String getSourceElementName() {
46+
return this.sourceElementName;
47+
}
3748

3849
public String getName() {
3950
return this.name;
@@ -135,6 +146,7 @@ public SpecType(Node specType, Map<String, Datatype> dataTypes) {
135146
this.id = specType.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent();
136147
this.name = specType.getAttributes().getNamedItem(ReqIFConst.LONG_NAME).getTextContent();
137148
this.alternativeID = XmlUtils.alternativeID(specType);
149+
this.sourceElementName = XmlUtils.localName(specType);
138150
this.type = ReqIFConst.UNDEFINED;
139151

140152
//Doors relationship definitionen habe keine ChildNodes

src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,17 @@ public Specification(Node specification, SpecType specType, Map<String, SpecObje
231231
this.attributeValues.put(attributeDefinitionName, new AttributeValueDouble(attributeValue, attributeDefinition));
232232
break;
233233

234-
default: break;
234+
// Values of datatype kinds the parser does not model are
235+
// kept generically instead of being dropped.
236+
default: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) {
237+
attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent();
238+
}else{
239+
attributeValue = "";
240+
}
241+
this.attributeValues.put(attributeDefinitionName,
242+
new AttributeValue(attributeValue, attributeDefinition)
243+
.setSourceElementName(XmlUtils.localName(attribute)));
244+
break;
235245
}
236246
}
237247
}

src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFWriter.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,14 @@ private Element enumValue(Document xml, DatatypeEnumerationValue value) {
282282

283283
private Element specType(Document xml, SpecType specType) {
284284

285+
// A spec type kind the parser does not model must keep its original
286+
// element name; writing it as a SPEC-OBJECT-TYPE would silently change
287+
// the document's meaning.
285288
String elementName = specType.getType();
286289
if (elementName == null || ReqIFConst.UNDEFINED.equals(elementName)) {
290+
elementName = specType.getSourceElementName();
291+
}
292+
if (elementName == null) {
287293
elementName = ReqIFConst.SPEC_OBJECT_TYPE;
288294
}
289295

@@ -311,6 +317,10 @@ private Element attributeDefinition(Document xml, AttributeDefinition definition
311317
return null;
312318
}
313319
String elementName = ReqIFElements.attributeDefinition(datatype.getType());
320+
if (elementName == null) {
321+
// datatype kinds the parser does not model: keep the original name
322+
elementName = definition.getSourceElementName();
323+
}
314324
if (elementName == null) {
315325
return null;
316326
}
@@ -326,8 +336,11 @@ private Element attributeDefinition(Document xml, AttributeDefinition definition
326336
}
327337

328338
Element type = element(xml, ReqIFConst.TYPE);
329-
Element datatypeRef = element(xml, ReqIFElements.datatypeDefinitionRef(
330-
ReqIFElements.datatypeDefinition(datatype.getType())));
339+
String datatypeElement = ReqIFElements.datatypeDefinition(datatype.getType());
340+
if (datatypeElement == null) {
341+
datatypeElement = datatype.getSourceElementName();
342+
}
343+
Element datatypeRef = element(xml, ReqIFElements.datatypeDefinitionRef(datatypeElement));
331344
datatypeRef.setTextContent(nullToEmpty(datatype.getID()));
332345
type.appendChild(datatypeRef);
333346
attributeDefinition.appendChild(type);
@@ -478,6 +491,10 @@ private Element attributeValue(Document xml, AttributeValue attributeValue) {
478491
}
479492

480493
String elementName = ReqIFElements.attributeValue(datatypeCategory);
494+
if (elementName == null) {
495+
// datatype kinds the parser does not model: keep the original name
496+
elementName = attributeValue.getSourceElementName();
497+
}
481498
if (elementName == null) {
482499
return null;
483500
}
@@ -561,7 +578,11 @@ private Node parseXhtml(String markup) {
561578
private Element definitionRef(Document xml, AttributeDefinition definition) {
562579

563580
Element definitionElement = element(xml, ReqIFConst.DEFINITION);
564-
Element ref = element(xml, ReqIFElements.attributeDefinitionRef(definition.getDataType().getType()));
581+
String refName = ReqIFElements.attributeDefinitionRef(definition.getDataType().getType());
582+
if (refName == null && definition.getSourceElementName() != null) {
583+
refName = definition.getSourceElementName() + "-REF";
584+
}
585+
Element ref = element(xml, refName);
565586
ref.setTextContent(nullToEmpty(definition.getID()));
566587
definitionElement.appendChild(ref);
567588
return definitionElement;
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package de.uni_stuttgart.ils.reqif4j;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import java.nio.file.Path;
9+
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.io.TempDir;
12+
13+
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF;
14+
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
15+
import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter;
16+
17+
/**
18+
* ReqIF kinds this parser does not model explicitly must survive a round trip
19+
* instead of being dropped (attribute definitions and values) or silently
20+
* turned into something else (spec types written as SPEC-OBJECT-TYPE).
21+
*/
22+
class UnmodelledElementsTest {
23+
24+
/** Fixture with a custom datatype, an attribute of it, and a custom spec type. */
25+
private static String fixtureWithUnmodelledKinds() {
26+
return TestFixtures.REQIF_FIXTURE
27+
.replace("<ATTRIBUTE-DEFINITION-STRING IDENTIFIER=\"ad-title\" LONG-NAME=\"Title\">",
28+
"<ATTRIBUTE-DEFINITION-CUSTOM IDENTIFIER=\"ad-custom\" LONG-NAME=\"Custom\">"
29+
+ "<TYPE><DATATYPE-DEFINITION-CUSTOM-REF>dt-custom</DATATYPE-DEFINITION-CUSTOM-REF></TYPE>"
30+
+ "</ATTRIBUTE-DEFINITION-CUSTOM>"
31+
+ "<ATTRIBUTE-DEFINITION-STRING IDENTIFIER=\"ad-title\" LONG-NAME=\"Title\">")
32+
.replace("<ATTRIBUTE-VALUE-STRING THE-VALUE=\"First requirement\">",
33+
"<ATTRIBUTE-VALUE-CUSTOM THE-VALUE=\"custom payload\">"
34+
+ "<DEFINITION><ATTRIBUTE-DEFINITION-CUSTOM-REF>ad-custom</ATTRIBUTE-DEFINITION-CUSTOM-REF></DEFINITION>"
35+
+ "</ATTRIBUTE-VALUE-CUSTOM>"
36+
+ "<ATTRIBUTE-VALUE-STRING THE-VALUE=\"First requirement\">")
37+
.replace("<SPECIFICATION-TYPE IDENTIFIER=\"st-spec\"",
38+
"<SOME-FUTURE-TYPE IDENTIFIER=\"st-future\" LONG-NAME=\"Future\"><SPEC-ATTRIBUTES/></SOME-FUTURE-TYPE>"
39+
+ "<SPECIFICATION-TYPE IDENTIFIER=\"st-spec\"");
40+
}
41+
42+
private ReqIF roundTrip(Path tempDir, String fixture) throws Exception {
43+
ReqIF original = new ReqIF(TestFixtures.write(tempDir, "in.reqif", fixture).toString());
44+
Path out = tempDir.resolve("out.reqif");
45+
new ReqIFWriter().write(original.getReqIFDocument(), out);
46+
return new ReqIF(out.toString());
47+
}
48+
49+
50+
@Test
51+
void attributeDefinitionOfAnUnmodelledDatatypeSurvives(@TempDir Path tempDir) throws Exception {
52+
ReqIF written = roundTrip(tempDir, fixtureWithUnmodelledKinds());
53+
54+
assertTrue(written.getReqIFCoreContent().getSpecType("st-req").getAttributeDefinitions()
55+
.containsKey("ad-custom"),
56+
"the definition was formerly dropped because its datatype kind is not modelled");
57+
assertEquals("Custom", written.getReqIFCoreContent().getSpecType("st-req")
58+
.getAttributeDefinition("ad-custom").getName());
59+
}
60+
61+
@Test
62+
void attributeValueOfAnUnmodelledDatatypeSurvives(@TempDir Path tempDir) throws Exception {
63+
ReqIF original = new ReqIF(
64+
TestFixtures.write(tempDir, "in.reqif", fixtureWithUnmodelledKinds()).toString());
65+
66+
assertEquals("custom payload", original.getReqIFCoreContent().getSpecObject("so-1")
67+
.getAttribute("Custom"),
68+
"the value was formerly not even parsed");
69+
70+
ReqIF written = roundTrip(tempDir, fixtureWithUnmodelledKinds());
71+
assertEquals("custom payload", written.getReqIFCoreContent().getSpecObject("so-1")
72+
.getAttribute("Custom"));
73+
}
74+
75+
@Test
76+
void unmodelledElementNamesAreWrittenUnchanged(@TempDir Path tempDir) throws Exception {
77+
ReqIF original = new ReqIF(
78+
TestFixtures.write(tempDir, "in.reqif", fixtureWithUnmodelledKinds()).toString());
79+
String xml = new ReqIFWriter().toXml(original.getReqIFDocument());
80+
81+
assertTrue(xml.contains("<ATTRIBUTE-DEFINITION-CUSTOM"), xml);
82+
assertTrue(xml.contains("<DATATYPE-DEFINITION-CUSTOM-REF>dt-custom</DATATYPE-DEFINITION-CUSTOM-REF>"), xml);
83+
assertTrue(xml.contains("<ATTRIBUTE-VALUE-CUSTOM"), xml);
84+
assertTrue(xml.contains("<ATTRIBUTE-DEFINITION-CUSTOM-REF>ad-custom</ATTRIBUTE-DEFINITION-CUSTOM-REF>"), xml);
85+
}
86+
87+
@Test
88+
void unmodelledSpecTypeKeepsItsElementName(@TempDir Path tempDir) throws Exception {
89+
ReqIF original = new ReqIF(
90+
TestFixtures.write(tempDir, "in.reqif", fixtureWithUnmodelledKinds()).toString());
91+
String xml = new ReqIFWriter().toXml(original.getReqIFDocument());
92+
93+
assertTrue(xml.contains("<SOME-FUTURE-TYPE IDENTIFIER=\"st-future\""),
94+
"the original element name must be kept: " + xml);
95+
assertFalse(xml.contains("SPEC-OBJECT-TYPE IDENTIFIER=\"st-future\""),
96+
"it must not be silently rewritten as a spec object type");
97+
}
98+
99+
@Test
100+
void unmodelledSpecTypeStillReadableAfterRoundTrip(@TempDir Path tempDir) throws Exception {
101+
ReqIF written = roundTrip(tempDir, fixtureWithUnmodelledKinds());
102+
103+
assertNotNull(written.getReqIFCoreContent().getSpecType("st-future"));
104+
assertEquals(ReqIFConst.UNDEFINED, written.getReqIFCoreContent().getSpecType("st-future").getType());
105+
assertEquals("Future", written.getReqIFCoreContent().getSpecType("st-future").getName());
106+
}
107+
108+
@Test
109+
void knownKindsAreUnaffected(@TempDir Path tempDir) throws Exception {
110+
ReqIF written = roundTrip(tempDir, fixtureWithUnmodelledKinds());
111+
112+
assertEquals(ReqIFConst.SPEC_OBJECT_TYPE, written.getReqIFCoreContent().getSpecType("st-req").getType());
113+
assertEquals(ReqIFConst.SPECIFICATION_TYPE, written.getReqIFCoreContent().getSpecType("st-spec").getType());
114+
assertEquals(ReqIFConst.RELATION_GROUP_TYPE,
115+
written.getReqIFCoreContent().getSpecType("st-relgroup").getType());
116+
assertEquals("First requirement", written.getReqIFCoreContent().getSpecObject("so-1").getAttribute("Title"));
117+
}
118+
119+
@Test
120+
void roundTripStaysIdempotentWithUnmodelledKinds(@TempDir Path tempDir) throws Exception {
121+
ReqIF original = new ReqIF(
122+
TestFixtures.write(tempDir, "in.reqif", fixtureWithUnmodelledKinds()).toString());
123+
ReqIF written = roundTrip(tempDir, fixtureWithUnmodelledKinds());
124+
125+
assertEquals(new ReqIFWriter().toXml(original.getReqIFDocument()),
126+
new ReqIFWriter().toXml(written.getReqIFDocument()));
127+
}
128+
}

0 commit comments

Comments
 (0)