Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c40ac72
Add gap analysis for full ReqIF import and export
claude Oct 11, 2026
16757cf
Validate against the official ReqIF schema without going online
claude Oct 11, 2026
bce36d7
Introduce Identifiable: read and write LAST-CHANGE, DESC, LONG-NAME
claude Oct 11, 2026
65f1eb7
Report each schema finding once and keep util free of model imports
claude Oct 11, 2026
537d05b
Write every value the ReqIF schema requires
claude Oct 11, 2026
b9e8b4e
Validate what would make the written file schema-invalid
claude Oct 11, 2026
b5627a9
Close the gaps between the validator and the schema
claude Oct 11, 2026
e46f28c
Read back XHTML defaults and xsd boolean "1"
claude Oct 11, 2026
609a005
Keep explicit values by definition and stop writing defaults
claude Oct 11, 2026
25ed066
Read and write every part of the ReqIF schema
claude Oct 11, 2026
b7fdd55
Keep every file of a .reqifz archive
claude Oct 11, 2026
1e5f249
Read imperfect documents instead of failing on them
claude Oct 11, 2026
ab146c4
Close the Phase 3 review: XHTML defaults and hierarchy content
claude Oct 11, 2026
4355c97
Phase 4.1: mutation API with consistent references and LAST-CHANGE
claude Oct 11, 2026
1cf5400
Phase 4.2: builder covers everything the parser reads
claude Oct 11, 2026
600ecd5
Close the Phase 4 review: XHTML content of nodes, attribute rendering
claude Oct 11, 2026
d7db040
Phase 5.1: test corpus of real tool exports
claude Oct 11, 2026
9f09896
Phase 5.2: check the conventions of the ReqIF Implementation Guide
claude Oct 11, 2026
580db15
Phase 5.3: large documents in linear time
claude Oct 11, 2026
4e32a7c
Close the Phase 5 review: namespaces, archive paths, messages
claude Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,17 @@ jobs:
java-version: '17'
cache: maven

# The schema conformance tests download the official OMG ReqIF XSD and its
# imports on first use; the schemas are frozen, so the cache key never changes.
- name: Cache ReqIF schema
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/reqif-schema
key: reqif-schema-20110401

- name: Build and run tests
env:
REQIF_SCHEMA_DIR: ${{ runner.temp }}/reqif-schema
run: mvn -B --no-transfer-progress verify

- name: Upload test reports
Expand Down
397 changes: 397 additions & 0 deletions IST-SOLL-ANALYSE.md

Large diffs are not rendered by default.

327 changes: 307 additions & 20 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,23 @@
import org.w3c.dom.NodeList;

import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype;
import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable;
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;

public class AttributeDefinition {
public class AttributeDefinition extends Identifiable {


private String id;
private String name;
private Datatype type;
private String defaultValue;
private String alternativeID;
private String sourceElementName;
private Boolean editable;
private AttributeValueXHTML defaultXhtml;
private Element unresolvedElement;




public String getID() {
return this.id;
}

public String getName() {
return this.name;
}

public Datatype getDataType() {
return this.type;
}
Expand All @@ -40,10 +33,26 @@ public String getDefaultValue() {
}

/**
* @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null
* @return the default of an XHTML attribute as read from the document, or
* null if there is none or the definition was not read from a
* document (then {@link #getDefaultValue()} holds the markup)
*/
public AttributeValueXHTML getDefaultXhtmlValue() {
return this.defaultXhtml;
}

/**
* @return IS-EDITABLE: whether values of this attribute may be edited in
* the receiving tool, or null if the document does not say
*/
public String getAlternativeID() {
return this.alternativeID;
public Boolean getEditable() {
return this.editable;
}

public AttributeDefinition setEditable(Boolean editable) {
this.editable = editable;
markModified();
return this;
}

/**
Expand All @@ -53,6 +62,27 @@ public String getAlternativeID() {
public String getSourceElementName() {
return this.sourceElementName;
}

/**
* @return the element this definition was read from if its datatype is not
* declared - it is then written back as read - otherwise null
*/
public Element getUnresolvedElement() {
return this.type == null ? this.unresolvedElement : null;
}

/**
* @return the element name this definition is written with, e.g.
* ATTRIBUTE-DEFINITION-STRING; for datatype kinds the parser does
* not model the name it was read with; null if neither is known
*/
public String getElementName() {
String suffix = ReqIFConst.datatypeSuffix(this.type == null ? null : this.type.getType());
if(suffix != null) {
return "ATTRIBUTE-DEFINITION-" + suffix;
}
return this.sourceElementName;
}



Expand All @@ -62,19 +92,17 @@ public String getSourceElementName() {
* generated instead of parsed.
*/
public AttributeDefinition(String id, String name, Datatype type, String defaultValue) {
super(id, name);

this.id = id;
this.name = name;
this.type = type;
this.defaultValue = defaultValue;
}

public AttributeDefinition(Node attributeDefinition, Map<String, Datatype> dataTypes) {

this.id = attributeDefinition.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent();
this.name = attributeDefinition.getAttributes().getNamedItem(ReqIFConst.LONG_NAME).getTextContent();
this.alternativeID = XmlUtils.alternativeID(attributeDefinition);
super(attributeDefinition);

this.sourceElementName = XmlUtils.localName(attributeDefinition);
this.editable = XmlUtils.booleanAttribute(attributeDefinition, ReqIFConst.IS_EDITABLE);

// Navigate by element (not by fixed child index) so both pretty-printed
// and minified ReqIF files are handled.
Expand All @@ -83,13 +111,28 @@ public AttributeDefinition(Node attributeDefinition, Map<String, Datatype> dataT
if(typeRef != null) {
this.type = dataTypes.get(typeRef.getTextContent().trim());
}
if(this.type == null && attributeDefinition instanceof Element) {
// without its datatype the definition cannot be interpreted; it is
// written back as read
this.unresolvedElement = (Element) attributeDefinition;
}

Element defVal = XmlUtils.firstChildElementByLocalName(attributeDefinition, ReqIFConst.DEFAULT_VALUE);
if(defVal != null) {

Node attDefVal = XmlUtils.firstChildElement(defVal);
if(attDefVal != null && attDefVal.hasAttributes() && attDefVal.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) != null) {
this.defaultValue = attDefVal.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent();
Element attDefVal = XmlUtils.firstChildElement(defVal);
String theValue = XmlUtils.attribute(attDefVal, ReqIFConst.THE_VALUE);
if(theValue != null) {
this.defaultValue = theValue;

} else if(XmlUtils.firstChildElementByLocalName(attDefVal, ReqIFConst.THE_VALUE) != null) {
// an XHTML default carries its markup in a THE-VALUE element. The
// parsed value is kept, so it is written back as it was (a p
// instead of a div, THE-ORIGINAL-VALUE); getDefaultValue() offers
// it as rendered XHTML.
this.defaultXhtml = new AttributeValueXHTML(attDefVal, this);
String markup = (String) this.defaultXhtml.getValue();
this.defaultValue = markup.isEmpty() ? null : markup;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ public AttributeDefinition getAttributeDefinitionType() {
return this.type;
}

/**
* @return the category of the definition's datatype, or null if the
* definition has no datatype
*/
public String getDatatype() {
return this.type.getDataType().getType();
return this.type.getDataType() == null ? null : this.type.getDataType().getType();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ public class AttributeValueBoolean extends AttributeValue {
public AttributeValueBoolean(String value, AttributeDefinition type) {
super(value, type);

if(value != null && value.toLowerCase().equals("true")) {
// xsd:boolean: "true" or "1" (lower case "true" only, but older versions
// accepted any case)
String trimmed = value == null ? "" : value.trim();
if(trimmed.equalsIgnoreCase("true") || trimmed.equals("1")) {
this.value = true;
}else{
this.value = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.uni_stuttgart.ils.reqif4j.attributes;

import de.uni_stuttgart.ils.reqif4j.util.XsdTypes;

public class AttributeValueDouble extends AttributeValue{

public AttributeValueDouble(String value, AttributeDefinition type) {
Expand All @@ -9,7 +11,8 @@ public AttributeValueDouble(String value, AttributeDefinition type) {
if(value == null || value.isBlank()){
this.value = 0.0;
}else {
this.value = Double.parseDouble(value.trim());
// xsd:double notation: INF / -INF for infinity
this.value = XsdTypes.parseDouble(value);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package de.uni_stuttgart.ils.reqif4j.attributes;

import java.util.ArrayList;
import java.util.List;

import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeEnumeration;
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;

/**
* Creates attribute values of the class that fits the attribute's datatype,
* so callers need not know AttributeValueBoolean, AttributeValueInteger, ...
* Used by the builder and by the setters of the model elements.
*/
public final class AttributeValueFactory {

private AttributeValueFactory() {
}

/**
* @param value a Boolean, Number, String or anything whose toString() is
* the value's text; null for an empty value
* @throws IllegalArgumentException for enumeration and XHTML attributes,
* which have their own factory methods
*/
public static AttributeValue of(AttributeDefinition definition, Object value) {

String category = categoryOf(definition);
String text = value == null ? null : String.valueOf(value);

if (ReqIFConst.ENUMERATION.equals(category)) {
throw new IllegalArgumentException("Use setEnum(...) for the enumeration attribute " + definition.getID());
}
if (ReqIFConst.XHTML.equals(category)) {
throw new IllegalArgumentException("Use setXhtml(...) for the XHTML attribute " + definition.getID());
}
if (category == null) {
return new AttributeValue(text, definition);
}
try {
switch (category) {
case ReqIFConst.BOOLEAN: return new AttributeValueBoolean(text, definition);
case ReqIFConst.INTEGER: return new AttributeValueInteger(text, definition);
case ReqIFConst.STRING: return new AttributeValueString(text, definition);
case ReqIFConst.DATE: return new AttributeValueDate(text, definition);
case ReqIFConst.DOUBLE:
case ReqIFConst.REAL: return new AttributeValueDouble(text, definition);
default: return new AttributeValue(text, definition);
}
} catch (IllegalArgumentException e) {
// e.g. a NumberFormatException, which does not say which attribute
throw new IllegalArgumentException("'" + text + "' is not a valid " + category
+ " value for attribute " + definition.getID(), e);
}
}

/**
* @param markup XHTML, with or without a surrounding div
* @throws IllegalArgumentException if the attribute is not an XHTML one or
* the markup is not well-formed
*/
public static AttributeValueXHTML xhtml(AttributeDefinition definition, String markup) {
requireCategory(definition, ReqIFConst.XHTML);
return new AttributeValueXHTML(markup, definition);
}

/**
* @param markup the simplified XHTML (THE-VALUE)
* @param originalMarkup the complete XHTML (THE-ORIGINAL-VALUE); the value
* is marked IS-SIMPLIFIED
* @throws IllegalArgumentException if the attribute is not an XHTML one or
* the markup is not well-formed
*/
public static AttributeValueXHTML xhtml(AttributeDefinition definition, String markup, String originalMarkup) {
requireCategory(definition, ReqIFConst.XHTML);
return new AttributeValueXHTML(markup, originalMarkup, definition);
}

/**
* @param enumValueIDs the IDENTIFIERs of the selected enum values; none for
* an empty selection
* @throws IllegalArgumentException if the attribute is not an enumeration
* one or an enum value does not exist
*/
public static AttributeValueEnumeration enumeration(AttributeDefinition definition, List<String> enumValueIDs) {

requireCategory(definition, ReqIFConst.ENUMERATION);
if (enumValueIDs == null || enumValueIDs.contains(null)) {
throw new IllegalArgumentException("The enum values for attribute " + definition.getID()
+ " must not be null; pass none for an empty selection");
}
DatatypeEnumeration datatype = (DatatypeEnumeration) definition.getDataType();
List<String> names = new ArrayList<String>();
for (String enumValueID : enumValueIDs) {
String name = datatype.getEnumValueName(enumValueID);
if (name == null) {
throw new IllegalArgumentException("Unknown enum value " + enumValueID
+ " for attribute " + definition.getID());
}
names.add(name);
}
return new AttributeValueEnumeration(names, new ArrayList<String>(enumValueIDs), definition);
}

private static String categoryOf(AttributeDefinition definition) {
return definition.getDataType() == null ? null : definition.getDataType().getType();
}

private static void requireCategory(AttributeDefinition definition, String expected) {
String category = categoryOf(definition);
if (!expected.equals(category)
|| ReqIFConst.ENUMERATION.equals(expected) && !(definition.getDataType() instanceof DatatypeEnumeration)) {
throw new IllegalArgumentException("Attribute " + definition.getID() + " is of kind " + category
+ ", but " + expected + " was expected");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,53 @@
package de.uni_stuttgart.ils.reqif4j.attributes;

import java.math.BigInteger;

/**
* Value of an INTEGER attribute. ReqIF declares it as {@code xsd:integer},
* which has no size limit, and DOORS regularly uses values beyond the int
* range.
*
* {@link #getValue()} returns an {@link Integer} when the value fits, so
* existing code comparing with int values keeps working, a {@link Long} when
* it fits that, and a {@link BigInteger} otherwise. {@link #getBigInteger()}
* always returns the exact value.
*/
public class AttributeValueInteger extends AttributeValue {



private final BigInteger integer;

public AttributeValueInteger(String value, AttributeDefinition type) {
super(value, type);
// Missing THE-VALUE reaches this constructor as null or "", both of
// which must not crash the parser.
if(value == null || value.isBlank()){
this.value = 0;
}else {
this.value = Integer.parseInt(value.trim());
}
this.integer = value == null || value.isBlank() ? BigInteger.ZERO : new BigInteger(value.trim());
this.value = narrowest(this.integer);
}

/**
* @return the value as Integer, Long or BigInteger, whichever is the
* smallest type that holds it
*/
@Override
public Object getValue() {
return (int)this.value;
return this.value;
}

/**
* @return the exact value
*/
public BigInteger getBigInteger() {
return this.integer;
}

private static Number narrowest(BigInteger value) {
// bitLength excludes the sign bit
if (value.bitLength() < Integer.SIZE) {
return value.intValue();
}
if (value.bitLength() < Long.SIZE) {
return value.longValue();
}
return value;
}

}
Loading
Loading