Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 64 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ This fork is based on https://github.com/bfriebel/requirements-interchange-forma
# Supported Formats
ReqIF file extensions .reqif and .reqifz (compressed).

Elements are matched by their local name, so it does not matter whether a
document puts the ReqIF elements into the default namespace
(`<REQ-IF xmlns="...">`) or into a prefixed one (`<rif:REQ-IF xmlns:rif="...">`).
The same holds for the embedded XHTML (`xhtml:div`, `reqif-xhtml:div`, ...).

# Build & Test
The project builds with Maven (Java 17+):

Expand Down Expand Up @@ -232,5 +237,62 @@ values too. The content category of generated spec objects is derived by
the same `TypeClassifier` used when reading (`typeClassifier(...)` to
override).

Not covered yet: writing .reqifz archives and ReqIF elements the parser
does not model (alternative ids, relation groups, tool extensions).
## Writing .reqifz archives

Images travel with the document, stored under the path the XHTML
`object` elements reference:

```java
Map<String, byte[]> pictures = Map.of("files/diagram.png", pngBytes);
new ReqIFzWriter().write(document, "spec.reqif", pictures, Path.of("spec.reqifz"));
```

An archive that was read can be re-packed - the documents are serialized
from the model (so modifications are included) while the images are
copied from the extracted files, without consuming the one-shot streams:

```java
try (ReqIFz source = new ReqIFz("in.reqifz")) {
new ReqIFzWriter().write(source, Path.of("out.reqifz"));
}
```

# Validation

`ReqIFValidator` checks a document before it is written: identifiers
present and globally unique, resolvable datatype and spec type
references, attribute values matching their spec type, enum value
references, relation endpoints, spec hierarchy targets and relation
group references.

```java
new ReqIFValidator().validate(document).throwIfInvalid();
new ReqIFWriter().write(document, Path.of("out.reqif"));
```

This is a model-level check. The OMG ReqIF XSD is **not bundled** with
this library for licensing reasons; if you have it, run a full XML
Schema validation on top:

```java
ValidationResult schemaIssues =
new ReqIFValidator().validateAgainstSchema(document, Path.of("reqif.xsd"));
```

Not covered yet: identifiers duplicated within one category (the parser
keys its maps by identifier, so a duplicate has already replaced its
predecessor by the time the model exists).

# Unmodelled ReqIF kinds

The parser models the datatype and spec type kinds of the standard. Kinds
it does not know - vendor extensions or later ReqIF revisions - are kept
generically rather than dropped: the original element name is remembered
and written back unchanged, and values are carried as their raw
`THE-VALUE`. This applies to datatype definitions, attribute definitions,
attribute values and spec types, so a document round-trips without losing
or altering them.

Limitation: a value of an unknown kind is only preserved when it is
carried in a `THE-VALUE` attribute. Kinds that store their value in child
elements are not covered.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public class AttributeDefinition {
private String name;
private Datatype type;
private String defaultValue;
private String alternativeID;
private String sourceElementName;



Expand All @@ -36,6 +38,21 @@ public Datatype getDataType() {
public String getDefaultValue() {
return this.defaultValue;
}

/**
* @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null
*/
public String getAlternativeID() {
return this.alternativeID;
}

/**
* @return the ATTRIBUTE-DEFINITION-* element name this definition was read
* from, or null when it was not created from a document
*/
public String getSourceElementName() {
return this.sourceElementName;
}



Expand All @@ -56,6 +73,8 @@ public AttributeDefinition(Node attributeDefinition, Map<String, Datatype> dataT

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

// Navigate by element (not by fixed child index) so both pretty-printed
// and minified ReqIF files are handled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public class AttributeValue {
private String name;
protected Object value;
private AttributeDefinition type;
private String sourceElementName;



Expand All @@ -25,6 +26,20 @@ public AttributeDefinition getAttributeDefinitionType() {
public String getDatatype() {
return this.type.getDataType().getType();
}

/**
* @return the ATTRIBUTE-VALUE-* element name this value was read from, or
* null when it was not created from a document. Needed to write back
* values of datatype kinds the parser does not model explicitly.
*/
public String getSourceElementName() {
return this.sourceElementName;
}

public AttributeValue setSourceElementName(String sourceElementName) {
this.sourceElementName = sourceElementName;
return this;
}



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

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import de.uni_stuttgart.ils.reqif4j.util.XhtmlParser;
import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;
import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLElement;
import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLElementDiv;
Expand Down Expand Up @@ -63,7 +57,6 @@ public class AttributeValueXHTML extends AttributeValue {
private static final String T_LIST_END = "/L";

private static final String VARIABLE_NAME_MISSING = "VARIABLE_NAME_MISSING";
private static final String XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";

XHTMLElementDiv divValue;

Expand Down Expand Up @@ -94,28 +87,11 @@ public AttributeValueXHTML(Node xhtmlContent, AttributeDefinition type) {
public AttributeValueXHTML(String value, AttributeDefinition type) {
super(value, type);

Node div = value == null || value.isBlank() ? null : parseDiv(value);
Node div = value == null || value.isBlank() ? null : XhtmlParser.parseDiv(value);
this.divValue = div == null ? null : new XHTMLElementDiv(div);
this.value = deconstructXHTML(this.divValue);
}

private static Node parseDiv(String markup) {

String trimmed = markup.trim();
String document = trimmed.startsWith("<div")
? trimmed.replaceFirst("<div", "<div xmlns=\"" + XHTML_NAMESPACE + "\"")
: "<div xmlns=\"" + XHTML_NAMESPACE + "\">" + trimmed + "</div>";
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
return factory.newDocumentBuilder()
.parse(new ByteArrayInputStream(document.getBytes(StandardCharsets.UTF_8)))
.getDocumentElement();
} catch (SAXException | IOException | ParserConfigurationException e) {
throw new IllegalArgumentException("XHTML value is not well-formed: " + markup, e);
}
}

public XHTMLElementDiv getDivValue() {
return this.divValue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public class ReqIFBuilder {
private final List<SpecRelation> specRelations = new ArrayList<SpecRelation>();
private final List<Specification> specifications = new ArrayList<Specification>();

private final Map<String, String> identifiers = new LinkedHashMap<String, String>();
private final HeaderBuilder headerBuilder = new HeaderBuilder();
private TypeClassifier typeClassifier = TypeClassifier.defaultClassifier();

Expand Down Expand Up @@ -92,6 +93,7 @@ public ReqIFBuilder header(Consumer<HeaderBuilder> header) {

/** Adds a datatype the builder has no shorthand for. */
public ReqIFBuilder datatype(Datatype datatype) {
claim(datatype.getID(), "datatype");
this.datatypes.put(datatype.getID(), datatype);
return this;
}
Expand Down Expand Up @@ -145,7 +147,8 @@ public ReqIFBuilder specRelationType(String id, String name, Consumer<SpecTypeBu

private ReqIFBuilder specType(SpecType specType, Consumer<SpecTypeBuilder> attributes) {

attributes.accept(new SpecTypeBuilder(specType, this.datatypes));
claim(specType.getID(), "spec type");
attributes.accept(new SpecTypeBuilder(specType, this.datatypes, this::claim));
this.specTypes.put(specType.getID(), specType);
return this;
}
Expand All @@ -155,6 +158,7 @@ private ReqIFBuilder specType(SpecType specType, Consumer<SpecTypeBuilder> attri

public ReqIFBuilder specObject(String id, String specTypeID, Consumer<ValuesBuilder> values) {

claim(id, "spec object");
SpecType specType = requireSpecType(specTypeID);
ValuesBuilder builder = new ValuesBuilder(specType);
values.accept(builder);
Expand All @@ -173,6 +177,7 @@ public ReqIFBuilder specObject(String id, String specTypeID) {
public ReqIFBuilder specRelation(String id, String specTypeID, String sourceID, String targetID,
Consumer<ValuesBuilder> values) {

claim(id, "spec relation");
SpecType specType = requireSpecType(specTypeID);
requireSpecObject(sourceID);
requireSpecObject(targetID);
Expand All @@ -194,6 +199,7 @@ public ReqIFBuilder specRelation(String id, String specTypeID, String sourceID,
public ReqIFBuilder specification(String id, String name, String specTypeID,
Consumer<SpecificationBuilder> content) {

claim(id, "specification");
SpecType specType = requireSpecType(specTypeID);
SpecificationBuilder builder = new SpecificationBuilder(specType);
content.accept(builder);
Expand Down Expand Up @@ -230,6 +236,22 @@ public ReqIFDocument build() {
}


/**
* ReqIF identifiers must be unique across the whole document, so a clash is
* refused instead of silently replacing the earlier element.
*/
void claim(String id, String kind) {

if (id == null || id.isBlank()) {
throw new ReqIFBuildException("A " + kind + " needs an identifier");
}
String previousKind = this.identifiers.put(id, kind);
if (previousKind != null) {
throw new ReqIFBuildException("Identifier '" + id + "' is already used by a " + previousKind
+ "; identifiers must be unique across the document");
}
}

private SpecType requireSpecType(String specTypeID) {

SpecType specType = this.specTypes.get(specTypeID);
Expand Down Expand Up @@ -259,6 +281,11 @@ public EnumerationBuilder value(String id, String name, String key) {
}

public EnumerationBuilder value(String id, String name, String key, String otherContent) {
for (DatatypeEnumerationValue existing : this.values) {
if (existing.getID().equals(id)) {
throw new ReqIFBuildException("Enum value '" + id + "' is declared twice");
}
}
this.values.add(new DatatypeEnumerationValue(id, name, key, otherContent));
return this;
}
Expand Down Expand Up @@ -287,6 +314,7 @@ public SpecificationBuilder child(String hierarchyID, String specObjectID) {
public SpecificationBuilder child(String hierarchyID, String specObjectID,
Consumer<HierarchyBuilder> children) {

claim(hierarchyID, "spec hierarchy");
HierarchyBuilder hierarchy = new HierarchyBuilder(hierarchyID, specObjectID);
children.accept(hierarchy);
this.hierarchies.add(hierarchy);
Expand Down Expand Up @@ -323,6 +351,7 @@ public HierarchyBuilder child(String hierarchyID, String specObjectID) {
public HierarchyBuilder child(String hierarchyID, String specObjectID,
Consumer<HierarchyBuilder> children) {

claim(hierarchyID, "spec hierarchy");
HierarchyBuilder hierarchy = new HierarchyBuilder(hierarchyID, specObjectID);
children.accept(hierarchy);
this.children.add(hierarchy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ public class SpecTypeBuilder {

private final SpecType specType;
private final Map<String, Datatype> datatypes;
private final java.util.function.BiConsumer<String, String> claim;

SpecTypeBuilder(SpecType specType, Map<String, Datatype> datatypes) {
SpecTypeBuilder(SpecType specType, Map<String, Datatype> datatypes,
java.util.function.BiConsumer<String, String> claim) {
this.specType = specType;
this.datatypes = datatypes;
this.claim = claim;
}


Expand Down Expand Up @@ -63,6 +66,7 @@ public SpecTypeBuilder enumerationAttribute(String id, String name, String datat
List<String> defaultValueRefs) {

Datatype datatype = requireDatatype(datatypeID, ReqIFConst.ENUMERATION);
this.claim.accept(id, "attribute definition");
this.specType.addAttributeDefinition(
new AttributeDefinitionEnumeration(id, name, datatype, multiValued, defaultValueRefs));
return this;
Expand All @@ -76,6 +80,7 @@ public SpecTypeBuilder enumerationAttribute(String id, String name, String datat
public SpecTypeBuilder attribute(String id, String name, String datatypeID, String defaultValue) {

Datatype datatype = requireDatatype(datatypeID, null);
this.claim.accept(id, "attribute definition");
this.specType.addAttributeDefinition(new AttributeDefinition(id, name, datatype, defaultValue));
return this;
}
Expand All @@ -85,6 +90,7 @@ private SpecTypeBuilder attribute(String id, String name, String datatypeID, Str
String defaultValue) {

Datatype datatype = requireDatatype(datatypeID, expectedCategory);
this.claim.accept(id, "attribute definition");
this.specType.addAttributeDefinition(new AttributeDefinition(id, name, datatype, defaultValue));
return this;
}
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/Datatype.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class Datatype {
private String name;
private String type;
private String sourceElementName;
private String alternativeID;



Expand All @@ -32,6 +33,18 @@ public String getSourceElementName() {
return this.sourceElementName;
}

/**
* @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null
*/
public String getAlternativeID() {
return this.alternativeID;
}

public Datatype setAlternativeID(String alternativeID) {
this.alternativeID = alternativeID;
return this;
}




Expand Down
10 changes: 10 additions & 0 deletions src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ public class ReqIFConst {
public final static String DEFINITION = "DEFINITION";
public final static String SPEC_TYPES = "SPEC-TYPES";
public final static String SPEC_ATTRIBUTES = "SPEC-ATTRIBUTES";
public final static String ALTERNATIVE_ID = "ALTERNATIVE-ID";
public final static String TOOL_EXTENSIONS = "TOOL-EXTENSIONS";
public final static String SPEC_RELATION_GROUPS = "SPEC-RELATION-GROUPS";
public final static String RELATION_GROUP = "RELATION-GROUP";
public final static String RELATION_GROUP_TYPE = "RELATION-GROUP-TYPE";
public final static String RELATION_GROUP_TYPE_REF = "RELATION-GROUP-TYPE-REF";
public final static String SOURCE_SPECIFICATION = "SOURCE-SPECIFICATION";
public final static String TARGET_SPECIFICATION = "TARGET-SPECIFICATION";
public final static String SPECIFICATION_REF = "SPECIFICATION-REF";
public final static String SPEC_RELATION_REF = "SPEC-RELATION-REF";
public final static String SPEC_OBJECTS = "SPEC-OBJECTS";
public final static String SPEC_OBJECT = "SPEC-OBJECT";
public final static String SPEC_RELATIONS = "SPEC-RELATIONS";
Expand Down
Loading
Loading