new DatatypeEnumerationValue(enumValue));
+ issues.put(enumValues, value, "Enum value");
}
}
- private static String orEmpty(String value) {
- return value == null ? "" : value;
- }
-
}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java
index 919d52b..c0be4f7 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java
@@ -1,24 +1,20 @@
package de.uni_stuttgart.ils.reqif4j.datatypes;
-public class DatatypeEnumerationValue {
+import org.w3c.dom.Element;
+
+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 DatatypeEnumerationValue extends Identifiable {
- private java.lang.String id;
- private java.lang.String name;
private java.lang.String key;
private java.lang.String otherContent;
- public String getID() {
- return this.id;
- }
-
- public String getName() {
- return this.name;
- }
-
public String getKey() {
return this.key;
}
@@ -31,11 +27,21 @@ public String getOtherContent() {
public DatatypeEnumerationValue(String id, String name, String key, String otherContent) {
-
- this.id = id;
- this.name = name;
+ super(id, name);
+
this.key = key;
this.otherContent = otherContent;
}
+ /**
+ * Reads an ENUM-VALUE element. A missing KEY or OTHER-CONTENT is read as "".
+ */
+ DatatypeEnumerationValue(Element enumValue) {
+ super(enumValue);
+
+ Element embeddedValue = XmlUtils.firstDescendantByLocalName(enumValue, ReqIFConst.EMBEDDED_VALUE);
+ this.key = XmlUtils.attributeOrEmpty(embeddedValue, ReqIFConst.KEY);
+ this.otherContent = XmlUtils.attributeOrEmpty(embeddedValue, ReqIFConst.OTHER_CONTENT);
+ }
+
}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/Identifiable.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/Identifiable.java
new file mode 100644
index 0000000..b6668da
--- /dev/null
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/Identifiable.java
@@ -0,0 +1,172 @@
+package de.uni_stuttgart.ils.reqif4j.reqif;
+
+import org.w3c.dom.Node;
+
+import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;
+
+/**
+ * Common base of every ReqIF element that has an identity: datatypes, enum
+ * values, spec types, attribute definitions, spec objects, spec relations,
+ * specifications, spec hierarchy nodes and relation groups.
+ *
+ * Mirrors the abstract {@code Identifiable} of the ReqIF metamodel:
+ *
+ *
+ * IDENTIFIER required the element's id, unique within the document
+ * LAST-CHANGE required xsd:dateTime of the last modification
+ * LONG-NAME optional human readable name
+ * DESC optional description
+ * ALTERNATIVE-ID optional a second, tool specific id
+ *
+ *
+ * Values are kept exactly as written in the document; an attribute the
+ * document does not declare is {@code null}, so it is not invented when the
+ * element is written back.
+ *
+ * Creating an element from plain values or changing one through the API
+ * marks it {@link #isModified() modified}; the writer then gives it the time of writing as LAST-CHANGE,
+ * so tools that import the document see what changed, and takes that time
+ * into the model, so writing again gives the same result. Setting LAST-CHANGE
+ * explicitly after the last change takes precedence.
+ */
+public abstract class Identifiable {
+
+ private String id;
+ private String name;
+ private String desc;
+ private String lastChange;
+ private String alternativeID;
+ private boolean modified;
+
+
+ /**
+ * For elements created from plain values instead of a document. A new
+ * element counts as modified, so it is written with the time of writing
+ * as LAST-CHANGE unless one is set; the builder clears that for the
+ * documents it assembles.
+ */
+ protected Identifiable(String id, String name) {
+ this.id = id;
+ this.name = name;
+ this.modified = true;
+ }
+
+ /**
+ * Reads IDENTIFIER, LONG-NAME, DESC, LAST-CHANGE and ALTERNATIVE-ID of a
+ * parsed element.
+ */
+ protected Identifiable(Node element) {
+ readIdentifiable(element);
+ }
+
+ /**
+ * Fills the properties from an element; for elements that are created from
+ * values while a document is read (datatypes).
+ *
+ * @throws ReqIFParseException if the element has no IDENTIFIER, which the
+ * schema requires and every reference relies on
+ */
+ protected final void readIdentifiable(Node element) {
+ this.id = XmlUtils.attribute(element, ReqIFConst.IDENTIFIER);
+ if (this.id == null) {
+ throw new ReqIFParseException(XmlUtils.localName(element) + " without " + ReqIFConst.IDENTIFIER);
+ }
+ this.name = XmlUtils.attribute(element, ReqIFConst.LONG_NAME);
+ this.desc = XmlUtils.attribute(element, ReqIFConst.DESC);
+ this.lastChange = XmlUtils.attribute(element, ReqIFConst.LAST_CHANGE);
+ this.alternativeID = XmlUtils.alternativeID(element);
+ // as read, not changed
+ this.modified = false;
+ }
+
+
+ /**
+ * @return the IDENTIFIER
+ */
+ public String getID() {
+ return this.id;
+ }
+
+ /**
+ * @return the LONG-NAME, or null if the element declares none
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ public Identifiable setName(String name) {
+ this.name = name;
+ markModified();
+ return this;
+ }
+
+ /**
+ * @return the DESC, or null if the element declares none
+ */
+ public String getDesc() {
+ return this.desc;
+ }
+
+ public Identifiable setDesc(String desc) {
+ this.desc = desc;
+ markModified();
+ return this;
+ }
+
+ /**
+ * @return the LAST-CHANGE exactly as written (xsd:dateTime), or null if the
+ * element declares none
+ */
+ public String getLastChange() {
+ return this.lastChange;
+ }
+
+ /**
+ * Sets LAST-CHANGE explicitly; the element is no longer considered
+ * modified, so the writer keeps this value - unless the element is
+ * changed again afterwards.
+ *
+ * @param lastChange xsd:dateTime, e.g. 2026-07-23T10:00:00Z
+ */
+ public Identifiable setLastChange(String lastChange) {
+ this.lastChange = lastChange;
+ this.modified = false;
+ return this;
+ }
+
+ /**
+ * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null
+ */
+ public String getAlternativeID() {
+ return this.alternativeID;
+ }
+
+ public Identifiable setAlternativeID(String alternativeID) {
+ this.alternativeID = alternativeID;
+ markModified();
+ return this;
+ }
+
+ /**
+ * @return true if the element was changed through the API since it was
+ * read or built (and LAST-CHANGE was not set since)
+ */
+ public boolean isModified() {
+ return this.modified;
+ }
+
+ /**
+ * Marks the element changed; done by every setter and structural change.
+ */
+ public void markModified() {
+ this.modified = true;
+ }
+
+ /**
+ * Forgets that the element was changed; the writer then keeps the
+ * LAST-CHANGE it has.
+ */
+ public void clearModified() {
+ this.modified = false;
+ }
+}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ParseIssue.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ParseIssue.java
new file mode 100644
index 0000000..7149e71
--- /dev/null
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ParseIssue.java
@@ -0,0 +1,33 @@
+package de.uni_stuttgart.ils.reqif4j.reqif;
+
+/**
+ * A problem found while reading a document that did not stop the reading,
+ * e.g. a value referencing an attribute definition the document does not
+ * declare. See {@link ReqIFDocument#getParseIssues()}.
+ */
+public final class ParseIssue {
+
+ private final String elementID;
+ private final String message;
+
+ public ParseIssue(String elementID, String message) {
+ this.elementID = elementID;
+ this.message = message;
+ }
+
+ /**
+ * @return the IDENTIFIER of the element the issue was found on, or null
+ */
+ public String getElementID() {
+ return this.elementID;
+ }
+
+ public String getMessage() {
+ return this.message;
+ }
+
+ @Override
+ public String toString() {
+ return (this.elementID == null ? "" : "[" + this.elementID + "] ") + this.message;
+ }
+}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ParseIssues.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ParseIssues.java
new file mode 100644
index 0000000..727f39e
--- /dev/null
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ParseIssues.java
@@ -0,0 +1,93 @@
+package de.uni_stuttgart.ils.reqif4j.reqif;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Receives the problems found while reading a document.
+ *
+ * A {@link #collecting() collecting} instance lets the reader skip what it
+ * cannot use and carry on, so one broken element does not make a whole
+ * document unreadable; the constructors of the model classes that take no
+ * issues use one and discard it. A {@link #strict() strict} instance stops
+ * at the first problem with a {@link ReqIFParseException}.
+ */
+public final class ParseIssues {
+
+ private final boolean strict;
+ private final List issues = new ArrayList();
+
+ private ParseIssues(boolean strict) {
+ this.strict = strict;
+ }
+
+ public static ParseIssues collecting() {
+ return new ParseIssues(false);
+ }
+
+ public static ParseIssues strict() {
+ return new ParseIssues(true);
+ }
+
+ /**
+ * @throws ReqIFParseException if this instance is strict
+ */
+ public void report(String elementID, String message) {
+ ParseIssue issue = new ParseIssue(elementID, message);
+ if (this.strict) {
+ throw new ReqIFParseException(issue.toString());
+ }
+ this.issues.add(issue);
+ }
+
+ /**
+ * @return the issues in the order they were found (read-only)
+ */
+ public List getIssues() {
+ return Collections.unmodifiableList(this.issues);
+ }
+
+ /**
+ * Runs a reader for one element; if the element cannot be read at all (it
+ * has no IDENTIFIER), it is reported and skipped.
+ *
+ * @return what the reader returned, or null if the element was skipped
+ */
+ public T readOrSkip(String kind, java.util.function.Supplier reader) {
+ try {
+ return reader.get();
+ } catch (ReqIFParseException e) {
+ if (this.strict) {
+ throw e;
+ }
+ report(null, e.getMessage() + "; the " + kind + " is skipped");
+ return null;
+ }
+ }
+
+ /**
+ * Adds an element read from the document to the map that holds its kind,
+ * keyed by identifier. A second element with the same identifier replaces
+ * the first, which is reported.
+ *
+ * @param element may be null (skipped element), then nothing happens
+ */
+ public void put(java.util.Map elements, T element, String kind) {
+ if (element == null) {
+ return;
+ }
+ if (elements.containsKey(element.getID())) {
+ reportDuplicate(kind, element.getID());
+ }
+ elements.put(element.getID(), element);
+ }
+
+ /**
+ * Reports a second element with the same identifier, which replaces the
+ * first.
+ */
+ public void reportDuplicate(String kind, String id) {
+ report(id, kind + " " + id + " is declared twice; the later one replaces the earlier");
+ }
+}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java
index ed585db..d2c62b1 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java
@@ -4,12 +4,15 @@ public class ReqIFConst {
public final static String IDENTIFIER = "IDENTIFIER";
public final static String LONG_NAME = "LONG-NAME";
+ public final static String DESC = "DESC";
+ public final static String LAST_CHANGE = "LAST-CHANGE";
public final static String THE_VALUE = "THE-VALUE";
public final static String KEY = "KEY";
public final static String OTHER_CONTENT = "OTHER-CONTENT";
public final static String MIN = "MIN";
public final static String MAX = "MAX";
public final static String MAX_LENGTH = "MAX-LENGTH";
+ public final static String ACCURACY = "ACCURACY";
public final static String _TEXT = "#text";
public final static String ENUM_VALUE = "ENUM-VALUE";
@@ -27,6 +30,9 @@ public class ReqIFConst {
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 REQ_IF_TOOL_EXTENSION = "REQ-IF-TOOL-EXTENSION";
+
+
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";
@@ -55,6 +61,12 @@ public class ReqIFConst {
public final static String REQ_IF_VERSION = "REQ-IF-VERSION";
public final static String COMMENT = "COMMENT";
public final static String CREATION_TIME = "CREATION-TIME";
+ public final static String REPOSITORY_ID = "REPOSITORY-ID";
+ public final static String IS_EDITABLE = "IS-EDITABLE";
+ public final static String IS_TABLE_INTERNAL = "IS-TABLE-INTERNAL";
+ public final static String EDITABLE_ATTS = "EDITABLE-ATTS";
+ public final static String THE_ORIGINAL_VALUE = "THE-ORIGINAL-VALUE";
+ public final static String IS_SIMPLIFIED = "IS-SIMPLIFIED";
public final static String SPECIFICATION_TYPE = "SPECIFICATION-TYPE";
public final static String SPEC_OBJECT_TYPE = "SPEC-OBJECT-TYPE";
@@ -83,4 +95,33 @@ public class ReqIFConst {
public final static String EOR = "EOR";
public final static String EOD = "EOD";
-}
+
+ /**
+ * @return the kind an element name ends with, e.g. STRING for
+ * ATTRIBUTE-VALUE-STRING or REAL for ATTRIBUTE-DEFINITION-REAL
+ */
+ public static String kindOf(String elementName) {
+ return elementName == null ? null : elementName.substring(elementName.lastIndexOf('-') + 1);
+ }
+
+ /**
+ * @return the suffix of the element names belonging to a datatype
+ * category, e.g. "REAL" for DOUBLE as in DATATYPE-DEFINITION-REAL,
+ * or null for a category the parser does not model
+ */
+ public static String datatypeSuffix(String datatypeCategory) {
+ if (datatypeCategory == null) {
+ return null;
+ }
+ switch (datatypeCategory) {
+ case DOUBLE:
+ case REAL: return REAL;
+ case BOOLEAN:
+ case INTEGER:
+ case STRING:
+ case DATE:
+ case XHTML:
+ case ENUMERATION: return datatypeCategory;
+ default: return null;
+ }
+ }}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java
index f8ce6bb..a1feb58 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java
@@ -9,8 +9,6 @@
import de.uni_stuttgart.ils.reqif4j.specification.*;
import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;
import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
public class ReqIFCoreContent {
@@ -21,61 +19,81 @@ public class ReqIFCoreContent {
private Map specRelation = new LinkedHashMap<>();
private Map specifications = new LinkedHashMap();
private Map relationGroups = new LinkedHashMap();
+ private ParseIssues parseIssues = ParseIssues.collecting();
+ /**
+ * @return the dataTypes by IDENTIFIER, in document order (read-only; change
+ * them with the add and remove methods)
+ */
public Map getDatatypes() {
- return this.dataTypes;
+ return java.util.Collections.unmodifiableMap(this.dataTypes);
}
public Datatype getDatatype(String id) {
return this.dataTypes.get(id);
}
+ /**
+ * @return the specTypes by IDENTIFIER, in document order (read-only; change
+ * them with the add and remove methods)
+ */
public Map getSpecTypes() {
- return this.specTypes;
+ return java.util.Collections.unmodifiableMap(this.specTypes);
}
public SpecType getSpecType(String id) {
return this.specTypes.get(id);
}
+ /**
+ * @return the specObjects by IDENTIFIER, in document order (read-only; change
+ * them with the add and remove methods)
+ */
public Map getSpecObjects() {
- return this.specObjects;
+ return java.util.Collections.unmodifiableMap(this.specObjects);
}
public SpecObject getSpecObject(String id) {
return this.specObjects.get(id);
}
+ /**
+ * @return the relations by IDENTIFIER, in document order (read-only; change
+ * them with the add and remove methods)
+ */
public Map getSpecRelation() {
- return this.specRelation;
+ return java.util.Collections.unmodifiableMap(this.specRelation);
}
public SpecRelation getSpecRelation(String id) {
return this.specRelation.get(id);
}
+ /**
+ * @return the specifications by IDENTIFIER, in document order (read-only; change
+ * them with the add and remove methods)
+ */
public Map getSpecifications() {
- return this.specifications;
+ return java.util.Collections.unmodifiableMap(this.specifications);
}
public Specification getSpecification(String id) {
return this.specifications.get(id);
}
+ /**
+ * @return the relationGroups by IDENTIFIER, in document order (read-only; change
+ * them with the add and remove methods)
+ */
public Map getRelationGroups() {
- return this.relationGroups;
+ return java.util.Collections.unmodifiableMap(this.relationGroups);
}
public RelationGroup getRelationGroup(String id) {
return this.relationGroups.get(id);
}
- public ReqIFCoreContent addRelationGroup(RelationGroup relationGroup) {
- this.relationGroups.put(relationGroup.getID(), relationGroup);
- return this;
- }
-
public List getSpecificationsList() {
List specifications = new ArrayList();
for (Specification specification : this.specifications.values()) {
@@ -102,180 +120,578 @@ public List getOrderedSpecHierarchyList() {
public ReqIFCoreContent() {
}
+ /*
+ * Adding and removing elements. The add methods refuse an element whose
+ * IDENTIFIER a datatype, spec type, spec object, relation, specification,
+ * hierarchy node or relation group already has (attribute definitions and
+ * enum values are not checked; the validator reports any clash); the
+ * remove methods keep the references between the elements consistent.
+ */
+
+ /**
+ * @throws IllegalArgumentException if the IDENTIFIER is missing or taken
+ */
public ReqIFCoreContent addDatatype(Datatype datatype) {
- this.dataTypes.put(datatype.getID(), datatype);
- return this;
+ return add(this.dataTypes, datatype);
}
+ /**
+ * @throws IllegalArgumentException if the IDENTIFIER is missing or taken
+ */
public ReqIFCoreContent addSpecType(SpecType specType) {
- this.specTypes.put(specType.getID(), specType);
- return this;
+ return add(this.specTypes, specType);
}
+ /**
+ * @throws IllegalArgumentException if the IDENTIFIER is missing or taken
+ */
public ReqIFCoreContent addSpecObject(SpecObject specObject) {
- this.specObjects.put(specObject.getID(), specObject);
- return this;
+ return add(this.specObjects, specObject);
}
+ /**
+ * @throws IllegalArgumentException if the IDENTIFIER is missing or taken
+ */
public ReqIFCoreContent addSpecRelation(SpecRelation relation) {
- this.specRelation.put(relation.getID(), relation);
- return this;
+ return add(this.specRelation, relation);
}
+ /**
+ * @throws IllegalArgumentException if the IDENTIFIER is missing or taken
+ */
public ReqIFCoreContent addSpecification(Specification specification) {
- this.specifications.put(specification.getID(), specification);
+ if (specification != null && this.specifications.get(specification.getID()) != specification) {
+ for (SpecHierarchy node : specification.getAllSpecHierarchies()) {
+ checkUnused(node.getID());
+ }
+ }
+ return add(this.specifications, specification);
+ }
+
+ /**
+ * @throws IllegalArgumentException if the IDENTIFIER is missing or taken
+ */
+ public ReqIFCoreContent addRelationGroup(RelationGroup relationGroup) {
+ return add(this.relationGroups, relationGroup);
+ }
+
+ private ReqIFCoreContent add(Map elements, T element) {
+
+ if (element == null) {
+ throw new IllegalArgumentException("Cannot add null");
+ }
+ if (elements.get(element.getID()) == element) {
+ return this;
+ }
+ checkUnused(element.getID());
+ elements.put(element.getID(), element);
return this;
}
+ /**
+ * @throws IllegalArgumentException if the identifier is blank or an
+ * element of this content has it
+ */
+ private void checkUnused(String id) {
- public ReqIFCoreContent(Element coreContent) {
- this(coreContent, TypeClassifier.defaultClassifier());
+ if (id == null || id.isBlank()) {
+ throw new IllegalArgumentException("An element needs an identifier");
+ }
+ for (Map map : List.of(this.dataTypes, this.specTypes, this.specObjects,
+ this.specRelation, this.specifications, this.relationGroups)) {
+ if (map.containsKey(id)) {
+ throw new IllegalArgumentException("Identifier " + id + " is already in use");
+ }
+ }
+ if (find(id) != null) {
+ throw new IllegalArgumentException("Identifier " + id + " is already in use by a spec hierarchy");
+ }
}
- public ReqIFCoreContent(Element coreContent, TypeClassifier typeClassifier) {
+ /**
+ * Inserts a new hierarchy node, with any nodes below it. Unlike
+ * {@link HierarchyParent#addChild(int, SpecHierarchy)}, this checks that
+ * no element of the document has the identifiers already.
+ *
+ * @param parentID the specification or hierarchy node to insert it below
+ * @param index the position among the parent's children, 0 for first
+ * @throws IllegalArgumentException if the parent does not exist, an
+ * identifier is taken, the node is already placed somewhere, or
+ * the position is out of range
+ */
+ public SpecHierarchy addSpecHierarchy(String parentID, int index, SpecHierarchy node) {
- if (typeClassifier == null) {
- typeClassifier = TypeClassifier.defaultClassifier();
+ HierarchyParent parent = parent(parentID);
+ if (node == null) {
+ throw new IllegalArgumentException("Cannot add null");
+ }
+ checkUnused(node.getID());
+ for (SpecHierarchy below : node.getAllChildren()) {
+ checkUnused(below.getID());
}
+ parent.addChild(index, node);
+ return node;
+ }
+ /**
+ * @throws IllegalArgumentException if there is no such specification or
+ * hierarchy node
+ */
+ private HierarchyParent parent(String parentID) {
+ HierarchyParent parent = this.specifications.get(parentID);
+ if (parent == null) {
+ Placement placement = find(parentID);
+ if (placement == null) {
+ throw new IllegalArgumentException("There is no specification or spec hierarchy " + parentID);
+ }
+ parent = placement.node;
+ }
+ return parent;
+ }
- // Every element is matched by local name, so documents that put the
- // ReqIF elements into a prefixed namespace are read as well.
- Element datatypesElement = XmlUtils.firstDescendantByLocalName(coreContent, ReqIFConst.DATATYPES);
- if (datatypesElement != null) {
-
- for (Element dataType : XmlUtils.childElements(datatypesElement)) {
-
- String dataTypeNodeName = XmlUtils.localName(dataType);
- {
-
- String dataTypeID = dataType.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent();
- String dataTypeName = dataType.getAttributes().getNamedItem(ReqIFConst.LONG_NAME).getTextContent();
- String dataTypeAlternativeID = XmlUtils.alternativeID(dataType);
-
- switch (dataTypeNodeName.substring(dataTypeNodeName.lastIndexOf("-") + 1)) {
-
- case ReqIFConst.BOOLEAN:
- this.dataTypes.put(dataTypeID, new DatatypeBoolean(dataTypeID, dataTypeName));
- break;
-
- case ReqIFConst.INTEGER:
- // MIN/MAX are optional in ReqIF
- String min = XmlUtils.attribute(dataType, ReqIFConst.MIN);
- String max = XmlUtils.attribute(dataType, ReqIFConst.MAX);
- this.dataTypes.put(dataTypeID, new DatatypeInteger(dataTypeID, dataTypeName, min, max));
- break;
-
- case ReqIFConst.STRING:
- // MAX-LENGTH is optional in ReqIF
- String maxLength = XmlUtils.attribute(dataType, ReqIFConst.MAX_LENGTH);
- this.dataTypes.put(dataTypeID, new DatatypeString(dataTypeID, dataTypeName, maxLength));
- break;
-
- case ReqIFConst.ENUMERATION:
- Node enumeration = dataType;
- this.dataTypes.put(dataTypeID, new DatatypeEnumeration(dataTypeID, dataTypeName, enumeration));
- break;
-
- case ReqIFConst.XHTML:
- this.dataTypes.put(dataTypeID, new DatatypeXHTML(dataTypeID, dataTypeName));
- break;
-
- case ReqIFConst.DATE:
- this.dataTypes.put(dataTypeID, new DatatypeDate(dataTypeID, dataTypeName));
- break;
-
- case ReqIFConst.DOUBLE, ReqIFConst.REAL:
- this.dataTypes.put(dataTypeID, new DatatypeDouble(dataTypeID, dataTypeName));
- break;
-
- default:
- // Register unknown datatypes under their ID (formerly the
- // key was null, making them unresolvable).
- this.dataTypes.put(dataTypeID, new Datatype(dataTypeID, dataTypeName, ReqIFConst.UNDEFINED, dataTypeNodeName));
- break;
+ /**
+ * Removes a spec object, the hierarchy nodes that show it - their children
+ * move up into their place - and the relations from or to it.
+ *
+ * @return the removed object, or null if there is no such object
+ */
+ public SpecObject removeSpecObject(String id) {
+
+ if (!this.specObjects.containsKey(id)) {
+ return null;
+ }
+ // only the parents of the nodes that show the object change: the
+ // nearest ancestor that does not show it itself
+ List parents = new ArrayList();
+ java.util.Set seen = java.util.Collections.newSetFromMap(
+ new java.util.IdentityHashMap());
+ for (Specification specification : this.specifications.values()) {
+ for (SpecHierarchy node : specification.getAllSpecHierarchies()) {
+ if (id.equals(node.getSpecObjectID())) {
+ HierarchyParent parent = node.getParent();
+ while (parent instanceof SpecHierarchy && id.equals(((SpecHierarchy) parent).getSpecObjectID())) {
+ parent = ((SpecHierarchy) parent).getParent();
}
- if (this.dataTypes.get(dataTypeID) != null) {
- this.dataTypes.get(dataTypeID).setAlternativeID(dataTypeAlternativeID);
+ if (seen.add(parent)) {
+ parents.add(parent);
}
}
}
}
+ // checked before anything changes, so a refusal leaves all as it was
+ List> lifted = new ArrayList>();
+ for (HierarchyParent parent : parents) {
+ lifted.add(checkedLifted(parent, id));
+ }
+ SpecObject removed = this.specObjects.remove(id);
+ for (int i = 0; i < parents.size(); i++) {
+ for (SpecHierarchy node : lifted.get(i)) {
+ if (node.getParent() != parents.get(i)) {
+ // below a node that shows the object, which goes
+ node.getParent().removeChild(node.getID());
+ }
+ }
+ // in one step, so the siblings are renumbered once
+ parents.get(i).setChildren(lifted.get(i));
+ }
+ for (SpecRelation relation : new ArrayList(this.specRelation.values())) {
+ if (id.equals(relation.getSourceObjID()) || id.equals(relation.getTargetObjID())) {
+ removeSpecRelation(relation.getID());
+ }
+ }
+ return removed;
+ }
+ /**
+ * @return the children {@code parent} has once the nodes showing the
+ * object are replaced by their children
+ * @throws IllegalStateException if that would put two nodes with the same
+ * IDENTIFIER side by side (only in documents that already repeat
+ * one)
+ */
+ private static List checkedLifted(HierarchyParent parent, String specObjectID) {
+ List lifted = liftedChildren(parent, specObjectID);
+ java.util.Set ids = new java.util.HashSet();
+ for (SpecHierarchy child : lifted) {
+ if (!ids.add(child.getID())) {
+ throw new IllegalStateException("Removing spec object " + specObjectID + " would put spec hierarchy "
+ + child.getID() + " twice below " + parent.getID());
+ }
+ }
+ return lifted;
+ }
- Element specTypesElement = XmlUtils.firstDescendantByLocalName(coreContent, ReqIFConst.SPEC_TYPES);
- if (specTypesElement != null) {
+ /**
+ * @return the children {@code parent} has once the nodes showing the
+ * object are replaced by their children
+ */
+ private static List liftedChildren(HierarchyParent parent, String specObjectID) {
+ List lifted = new ArrayList();
+ for (SpecHierarchy child : parent.getChildren()) {
+ if (specObjectID.equals(child.getSpecObjectID())) {
+ lifted.addAll(liftedChildren(child, specObjectID));
+ } else {
+ lifted.add(child);
+ }
+ }
+ return lifted;
+ }
- for (Element specType : XmlUtils.childElements(specTypesElement)) {
+ /**
+ * Removes a relation, also from the relation groups it belongs to.
+ *
+ * @return the removed relation, or null if there is no such relation
+ */
+ public SpecRelation removeSpecRelation(String id) {
- String specTypeNodeName = XmlUtils.localName(specType);
- {
+ SpecRelation removed = this.specRelation.remove(id);
+ if (removed != null) {
+ for (RelationGroup group : this.relationGroups.values()) {
+ group.removeSpecRelationRef(id);
+ }
+ }
+ return removed;
+ }
- String specTypeID = specType.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent();
+ /**
+ * Removes a specification with its hierarchy, and the relation groups
+ * between it and another specification. The spec objects stay.
+ *
+ * @return the removed specification, or null if there is no such one
+ */
+ public Specification removeSpecification(String id) {
- switch (specTypeNodeName) {
+ Specification removed = this.specifications.remove(id);
+ if (removed != null) {
+ this.relationGroups.values().removeIf(group -> id.equals(group.getSourceSpecificationRef())
+ || id.equals(group.getTargetSpecificationRef()));
+ }
+ return removed;
+ }
- case ReqIFConst.SPECIFICATION_TYPE:
- this.specTypes.put(specTypeID, new SpecificationType(specType, this.dataTypes));
- break;
+ /**
+ * Removes a relation group; its relations stay.
+ *
+ * @return the removed group, or null if there is no such group
+ */
+ public RelationGroup removeRelationGroup(String id) {
+ return this.relationGroups.remove(id);
+ }
- case ReqIFConst.SPEC_OBJECT_TYPE:
- this.specTypes.put(specTypeID, new SpecObjectType(specType, this.dataTypes));
- break;
+ /**
+ * Removes a spec type that no element uses.
+ *
+ * @return the removed type, or null if there is no such type
+ * @throws IllegalStateException if a spec object, relation, specification
+ * or relation group still has this type
+ */
+ public SpecType removeSpecType(String id) {
- case ReqIFConst.SPEC_RELATION_TYPE:
- this.specTypes.put(specTypeID, new SpecRelationType(specType, this.dataTypes));
- break;
+ if (!this.specTypes.containsKey(id)) {
+ return null;
+ }
+ List users = new ArrayList();
+ for (Map elements : List.of(this.specObjects,
+ this.specRelation, this.specifications)) {
+ for (SpecElementWithAttributes element : elements.values()) {
+ if (id.equals(typeID(element))) {
+ users.add(element.getID());
+ }
+ }
+ }
+ for (RelationGroup group : this.relationGroups.values()) {
+ if (id.equals(group.getRelationGroupTypeRef())) {
+ users.add(group.getID());
+ }
+ }
+ if (!users.isEmpty()) {
+ throw new IllegalStateException("Spec type " + id + " is still used by " + String.join(", ", users));
+ }
+ return this.specTypes.remove(id);
+ }
- case ReqIFConst.RELATION_GROUP_TYPE:
- this.specTypes.put(specTypeID, new RelationGroupType(specType, this.dataTypes));
- break;
+ private static String typeID(SpecElementWithAttributes element) {
+ if (element instanceof SpecObject) {
+ return ((SpecObject) element).getSpecTypeID();
+ }
+ return element instanceof Specification ? ((Specification) element).getSpecTypeID() : null;
+ }
- default:
- this.specTypes.put(specTypeID, new SpecType(specType, this.dataTypes));
- break;
- }
+ /**
+ * Removes a datatype that no attribute definition uses.
+ *
+ * @return the removed datatype, or null if there is no such datatype
+ * @throws IllegalStateException if an attribute definition still has
+ * this datatype
+ */
+ public Datatype removeDatatype(String id) {
+
+ Datatype datatype = this.dataTypes.get(id);
+ if (datatype == null) {
+ return null;
+ }
+ List users = new ArrayList();
+ for (SpecType specType : this.specTypes.values()) {
+ for (de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition definition
+ : specType.getAttributeDefinitions().values()) {
+ if (definition.getDataType() == datatype) {
+ users.add(definition.getID());
}
}
}
+ if (!users.isEmpty()) {
+ throw new IllegalStateException("Datatype " + id + " is still used by " + String.join(", ", users));
+ }
+ return this.dataTypes.remove(id);
+ }
+ /**
+ * @return the hierarchy node with this IDENTIFIER in any specification, or
+ * null if there is none
+ */
+ public SpecHierarchy getSpecHierarchy(String id) {
+ Placement placement = find(id);
+ return placement == null ? null : placement.node;
+ }
- for (Element specObj : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.SPEC_OBJECT)) {
+ /**
+ * Removes a hierarchy node with everything below it. The spec objects
+ * stay.
+ *
+ * @return the removed node, or null if there is no such node
+ */
+ public SpecHierarchy removeSpecHierarchy(String id) {
+ Placement placement = find(id);
+ return placement == null ? null : placement.parent.removeChild(id);
+ }
- String specObjID = XmlUtils.attribute(specObj, ReqIFConst.IDENTIFIER);
- Element typeRef = XmlUtils.firstDescendantByLocalName(specObj, ReqIFConst.SPEC_OBJECT_TYPE_REF);
- String specObjTypeRef = typeRef == null ? null : typeRef.getTextContent().trim();
+ /**
+ * Moves a hierarchy node, with everything below it, to another place, in
+ * the same or another specification.
+ *
+ * @param newParentID the specification or hierarchy node to move it to
+ * @param index the position among the new parent's children after
+ * the move, 0 for first
+ * @throws IllegalArgumentException if the node or the new parent does not
+ * exist, the new parent is the node itself or below it, or the
+ * position is out of range
+ */
+ public SpecHierarchy moveSpecHierarchy(String id, String newParentID, int index) {
- this.specObjects.put(specObjID, new SpecObject(specObj, this.specTypes.get(specObjTypeRef), typeClassifier));
+ Placement placement = find(id);
+ if (placement == null) {
+ throw new IllegalArgumentException("There is no spec hierarchy " + id);
+ }
+ HierarchyParent newParent = parent(newParentID);
+ // the new parent must not be the node or below it
+ for (HierarchyParent above = newParent; above instanceof SpecHierarchy;
+ above = ((SpecHierarchy) above).getParent()) {
+ if (above == placement.node) {
+ throw new IllegalArgumentException("Spec hierarchy " + id + " cannot be moved below itself");
+ }
+ }
+ boolean sameParent = newParent == placement.parent;
+ int size = newParent.getChildCount() - (sameParent ? 1 : 0);
+ if (index < 0 || index > size) {
+ throw new IllegalArgumentException("Position " + index + " is outside 0.." + size);
+ }
+ if (!sameParent && newParent.getChild(id) != null) {
+ throw new IllegalArgumentException("Spec hierarchy " + id + " is already a child of " + newParentID);
}
+ placement.parent.removeChild(id);
+ newParent.addChild(index, placement.node);
+ return placement.node;
+ }
+ private static final class Placement {
+ final HierarchyParent parent;
+ final SpecHierarchy node;
- for (Element specRelation : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.SPEC_RELATION)) {
+ Placement(HierarchyParent parent, SpecHierarchy node) {
+ this.parent = parent;
+ this.node = node;
+ }
+ }
- String specRelID = XmlUtils.attribute(specRelation, ReqIFConst.IDENTIFIER);
- Element typeRef = XmlUtils.firstDescendantByLocalName(specRelation, ReqIFConst.SPEC_RELATION_TYPE_REF);
- String specRelTypeRef = typeRef == null ? null : typeRef.getTextContent().trim();
+ private Placement find(String id) {
+ if (id == null) {
+ return null;
+ }
+ for (Specification specification : this.specifications.values()) {
+ SpecHierarchy node = specification.getSpecHierarchy(id);
+ if (node != null) {
+ return new Placement(node.getParent(), node);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Marks every element as unchanged, so that the writer keeps their
+ * LAST-CHANGE instead of giving them the time of writing.
+ */
+ public void clearModified() {
- this.specRelation.put(specRelID, new SpecRelation(specRelation, this.specTypes.get(specRelTypeRef)));
+ List all = new ArrayList();
+ for (Datatype datatype : this.dataTypes.values()) {
+ all.add(datatype);
+ if (datatype instanceof DatatypeEnumeration) {
+ all.addAll(((DatatypeEnumeration) datatype).getEnumValues().values());
+ }
+ }
+ for (SpecType specType : this.specTypes.values()) {
+ all.add(specType);
+ all.addAll(specType.getAttributeDefinitions().values());
+ }
+ all.addAll(this.specObjects.values());
+ all.addAll(this.specRelation.values());
+ for (Specification specification : this.specifications.values()) {
+ all.add(specification);
+ all.addAll(specification.getAllSpecHierarchies());
+ }
+ all.addAll(this.relationGroups.values());
+ for (Identifiable identifiable : all) {
+ identifiable.clearModified();
}
+ }
- for (Element relationGroup : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.RELATION_GROUP)) {
- RelationGroup group = new RelationGroup(relationGroup);
- this.relationGroups.put(group.getID(), group);
+ public ReqIFCoreContent(Element coreContent) {
+ this(coreContent, TypeClassifier.defaultClassifier());
+ }
+
+ public ReqIFCoreContent(Element coreContent, TypeClassifier typeClassifier) {
+ this(coreContent, typeClassifier, ParseIssues.collecting());
+ }
+
+ /**
+ * Reads the content of a document. Elements that cannot be read are
+ * skipped and, like every other problem found, reported to {@code issues}
+ * instead of making the whole document unreadable.
+ */
+ public ReqIFCoreContent(Element coreContent, TypeClassifier typeClassifier, ParseIssues issues) {
+
+ TypeClassifier classifier = typeClassifier == null ? TypeClassifier.defaultClassifier() : typeClassifier;
+ this.parseIssues = issues;
+
+ // Every element is matched by local name, so documents that put the
+ // ReqIF elements into a prefixed namespace are read as well.
+ Element datatypesElement = XmlUtils.firstDescendantByLocalName(coreContent, ReqIFConst.DATATYPES);
+ for (Element dataType : XmlUtils.childElements(datatypesElement)) {
+ issues.put(this.dataTypes, issues.readOrSkip("datatype", () -> readDatatype(dataType, issues)), "Datatype");
+ }
+
+ Element specTypesElement = XmlUtils.firstDescendantByLocalName(coreContent, ReqIFConst.SPEC_TYPES);
+ for (Element specType : XmlUtils.childElements(specTypesElement)) {
+ issues.put(this.specTypes, issues.readOrSkip("spec type", () -> readSpecType(specType, issues)), "Spec type");
+ }
+
+ for (Element specObj : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.SPEC_OBJECT)) {
+ SpecType type = resolveType(specObj, ReqIFConst.SPEC_OBJECT_TYPE_REF, issues);
+ issues.put(this.specObjects, issues.readOrSkip("spec object",
+ () -> new SpecObject(specObj, type, classifier, issues)), "Spec object");
}
+ for (Element specRelation : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.SPEC_RELATION)) {
+ SpecType type = resolveType(specRelation, ReqIFConst.SPEC_RELATION_TYPE_REF, issues);
+ issues.put(this.specRelation, issues.readOrSkip("spec relation",
+ () -> new SpecRelation(specRelation, type, issues)), "Spec relation");
+ }
+
+ for (Element relationGroup : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.RELATION_GROUP)) {
+ issues.put(this.relationGroups, issues.readOrSkip("relation group",
+ () -> new RelationGroup(relationGroup)), "Relation group");
+ }
for (Element specification : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.SPECIFICATION)) {
+ SpecType type = resolveType(specification, ReqIFConst.SPEC_TYPE_REF, issues);
+ issues.put(this.specifications, issues.readOrSkip("specification",
+ () -> new Specification(specification, type, this.specObjects, issues)), "Specification");
+ }
+ }
+
+ /**
+ * @return the problems found while reading this content; empty for content
+ * that was not read from a document
+ */
+ public java.util.List getParseIssues() {
+ return this.parseIssues.getIssues();
+ }
+
+
+ private static Datatype readDatatype(Element dataType, ParseIssues issues) {
+
+ String dataTypeNodeName = XmlUtils.localName(dataType);
+ String dataTypeID = XmlUtils.attribute(dataType, ReqIFConst.IDENTIFIER);
+ // LONG-NAME is optional in ReqIF
+ String dataTypeName = XmlUtils.attribute(dataType, ReqIFConst.LONG_NAME);
+
+ Datatype datatype;
+ switch (ReqIFConst.kindOf(dataTypeNodeName)) {
+
+ case ReqIFConst.BOOLEAN:
+ datatype = new DatatypeBoolean(dataTypeID, dataTypeName);
+ break;
- String specID = XmlUtils.attribute(specification, ReqIFConst.IDENTIFIER);
- Element typeRef = XmlUtils.firstDescendantByLocalName(specification, ReqIFConst.SPEC_TYPE_REF);
- String specTypeRef = typeRef == null ? null : typeRef.getTextContent().trim();
+ case ReqIFConst.INTEGER:
+ // MIN/MAX are optional in ReqIF
+ datatype = new DatatypeInteger(dataTypeID, dataTypeName,
+ XmlUtils.attribute(dataType, ReqIFConst.MIN), XmlUtils.attribute(dataType, ReqIFConst.MAX));
+ break;
- this.specifications.put(specID, new Specification(specification, this.specTypes.get(specTypeRef), this.specObjects));
+ case ReqIFConst.STRING:
+ // MAX-LENGTH is optional in ReqIF
+ datatype = new DatatypeString(dataTypeID, dataTypeName, XmlUtils.attribute(dataType, ReqIFConst.MAX_LENGTH));
+ break;
+
+ case ReqIFConst.ENUMERATION:
+ datatype = new DatatypeEnumeration(dataTypeID, dataTypeName, dataType, issues);
+ break;
+
+ case ReqIFConst.XHTML:
+ datatype = new DatatypeXHTML(dataTypeID, dataTypeName);
+ break;
+
+ case ReqIFConst.DATE:
+ datatype = new DatatypeDate(dataTypeID, dataTypeName);
+ break;
+
+ case ReqIFConst.DOUBLE, ReqIFConst.REAL:
+ datatype = new DatatypeDouble(dataTypeID, dataTypeName,
+ XmlUtils.attribute(dataType, ReqIFConst.ACCURACY),
+ XmlUtils.attribute(dataType, ReqIFConst.MIN),
+ XmlUtils.attribute(dataType, ReqIFConst.MAX));
+ break;
+
+ default:
+ // kinds the parser does not model are kept under their element name
+ datatype = new Datatype(dataTypeID, dataTypeName, ReqIFConst.UNDEFINED, dataTypeNodeName);
+ break;
+ }
+ // IDENTIFIER (required, checked here), DESC, LAST-CHANGE and ALTERNATIVE-ID
+ datatype.readIdentifiable(dataType);
+ return datatype;
+ }
+
+ private SpecType readSpecType(Element specType, ParseIssues issues) {
+
+ switch (XmlUtils.localName(specType)) {
+ case ReqIFConst.SPECIFICATION_TYPE: return new SpecificationType(specType, this.dataTypes, issues);
+ case ReqIFConst.SPEC_OBJECT_TYPE: return new SpecObjectType(specType, this.dataTypes, issues);
+ case ReqIFConst.SPEC_RELATION_TYPE: return new SpecRelationType(specType, this.dataTypes, issues);
+ case ReqIFConst.RELATION_GROUP_TYPE: return new RelationGroupType(specType, this.dataTypes, issues);
+ default: return new SpecType(specType, this.dataTypes, issues);
}
+ }
+ /**
+ * @return the spec type the element's TYPE refers to, or null if the
+ * reference cannot be resolved (the element reports that)
+ */
+ private SpecType resolveType(Element element, String refName, ParseIssues issues) {
+ Element typeRef = XmlUtils.firstDescendantByLocalName(element, refName);
+ return typeRef == null ? null : this.specTypes.get(typeRef.getTextContent().trim());
}
}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java
index 5af0731..46da5cb 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java
@@ -3,6 +3,7 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
+import javax.xml.XMLConstants;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
@@ -25,6 +26,8 @@ public class ReqIFDocument {
private ReqIFHeader header;
private ReqIFCoreContent content;
private final java.util.List toolExtensions = new java.util.ArrayList<>();
+ private String lang;
+ private String schemaLocation;
public String getFilePath() {
@@ -43,6 +46,42 @@ public ReqIFCoreContent getCoreContent() {
return this.content;
}
+ /**
+ * @return the problems found while reading the document - skipped
+ * elements, unresolved references, identifiers used twice - in the
+ * order they were found; empty for a document that was not read
+ * from a file. {@code ReqIFValidator} reports them as warnings.
+ */
+ public java.util.List getParseIssues() {
+ return this.content == null ? java.util.List.of() : this.content.getParseIssues();
+ }
+
+ /**
+ * @return the document language (xml:lang of REQ-IF), e.g. "de", or null
+ * if the document declares none
+ */
+ public String getLang() {
+ return this.lang;
+ }
+
+ /**
+ * @return xsi:schemaLocation of REQ-IF: pairs of namespace and schema file
+ * that tell a validator where to find the schemas, or null
+ */
+ public String getSchemaLocation() {
+ return this.schemaLocation;
+ }
+
+ public ReqIFDocument setSchemaLocation(String schemaLocation) {
+ this.schemaLocation = schemaLocation;
+ return this;
+ }
+
+ public ReqIFDocument setLang(String lang) {
+ this.lang = lang;
+ return this;
+ }
+
/**
* Tool-specific extensions of the document. They are not interpreted, only
* kept as their original nodes so they survive a round trip.
@@ -143,8 +182,22 @@ private void readDocument() {
}
this.content = new ReqIFCoreContent(coreContent, this.typeClassifier);
+ Element root = this.reqifDocument.getDocumentElement();
+ if (root.hasAttributeNS(XMLConstants.XML_NS_URI, "lang")) {
+ this.lang = root.getAttributeNS(XMLConstants.XML_NS_URI, "lang");
+ }
+ if (root.hasAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "schemaLocation")) {
+ this.schemaLocation = root.getAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "schemaLocation");
+ }
+
// Tool extensions are kept verbatim; the parser does not interpret them.
- this.toolExtensions.addAll(XmlUtils.descendantsByLocalName(this.reqifDocument, ReqIFConst.TOOL_EXTENSIONS));
+ // Only the TOOL-EXTENSIONS of REQ-IF itself count: a vendor extension may
+ // well contain an element of that name, and the schema allows one only.
+ for (Element child : XmlUtils.childElements(root)) {
+ if (ReqIFConst.TOOL_EXTENSIONS.equals(XmlUtils.localName(child))) {
+ this.toolExtensions.add(child);
+ }
+ }
}
private static String extractFileName(String path) {
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java
index 59b7e59..663c5c9 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java
@@ -16,6 +16,7 @@ public class ReqIFHeader {
private String comment = "";
private String creationDate = "";
private String creationTime = "";
+ private String repositoryID;
@@ -64,6 +65,19 @@ public String getCreationDate() {
public String getCreationTime() {
return this.creationTime;
}
+
+ /**
+ * @return the optional REPOSITORY-ID, the repository the document was
+ * exported from, or null if the document declares none
+ */
+ public String getRepositoryID() {
+ return this.repositoryID;
+ }
+
+ public ReqIFHeader setRepositoryID(String repositoryID) {
+ this.repositoryID = repositoryID;
+ return this;
+ }
@@ -92,6 +106,7 @@ public ReqIFHeader(Element theHeader) {
this.creationTime = creationTime;
this.creationDate = creationDateOf(creationTime);
}
+ this.repositoryID = textOf(theHeader, ReqIFConst.REPOSITORY_ID);
String title = textOf(theHeader, ReqIFConst.TITLE);
if(title != null) {
// The title is returned as written in the document; stripping a
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java
index 18d6f62..ad65039 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java
@@ -2,35 +2,66 @@
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Comparator;
import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import de.uni_stuttgart.ils.reqif4j.specification.TypeClassifier;
+/**
+ * A .reqifz archive: one or more ReqIF documents plus the files they
+ * reference - images, embedded OLE objects, PDFs or anything else.
+ *
+ * The archive is extracted into a temporary directory, which {@link #close()}
+ * deletes again; use try-with-resources. Every entry that is not a ReqIF
+ * document is kept as an attachment, so re-packing the archive with
+ * {@code ReqIFzWriter} loses nothing.
+ */
public class ReqIFz extends ReqIFFile {
- private static final String EXTRACTION_SUFFIX = "_unzipped";
+ private static final String[] PICTURE_EXTENSIONS = {
+ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".tif", ".tiff", ".webp", ".emf", ".wmf" };
- private final Map pictureFiles = new java.util.LinkedHashMap<>();
- private final Map reqifFiles = new java.util.LinkedHashMap<>();
+ private Path extractionDirectory;
+ private final Map attachmentFiles = new LinkedHashMap<>();
+ private final Map pictureFiles = new LinkedHashMap<>();
+ private final Map reqifFiles = new LinkedHashMap<>();
/**
- * @return the extracted picture files, keyed by their archive entry name
- * (forward slashes, as referenced by xhtml object data attributes).
- * Unlike the input streams these can be read repeatedly, which is
- * what re-packing an archive needs.
+ * @return every entry that is not a ReqIF document, keyed by its archive
+ * entry name (forward slashes, as referenced by xhtml object data
+ * attributes), in archive order. Valid until {@link #close()}.
+ */
+ public Map getAttachmentFiles() {
+ return this.attachmentFiles;
+ }
+
+ /**
+ * @return the attachments that are pictures (png, jpg, gif, svg, ... by
+ * file extension), keyed like
+ * {@link #getAttachmentFiles()}. Unlike the input streams these can
+ * be read repeatedly. Valid until {@link #close()}.
*/
public Map getPictureFiles() {
return this.pictureFiles;
}
/**
- * @return the extracted ReqIF files, keyed by their archive entry name
+ * @return the extracted ReqIF files, keyed by their archive entry name.
+ * Valid until {@link #close()}.
*/
public Map getReqIFFiles() {
return this.reqifFiles;
@@ -46,73 +77,149 @@ public ReqIFz(String filePath) throws IOException {
* LONG-NAME heuristic
*/
public ReqIFz(String filePath, TypeClassifier typeClassifier) throws IOException {
+ this(new FileInputStream(filePath), filePath, typeClassifier);
+ }
+
+ /**
+ * Reads an archive from a stream, e.g. an upload or a classpath resource.
+ * The stream is closed.
+ *
+ * @param name the archive's name or path, used in messages and as
+ * {@link #getPath()}
+ */
+ public ReqIFz(InputStream archive, String name) throws IOException {
+ this(archive, name, TypeClassifier.defaultClassifier());
+ }
+
+ public ReqIFz(InputStream archive, String name, TypeClassifier typeClassifier) throws IOException {
- this.path = filePath;
- this.name = extractFileName(filePath);
+ this.path = name;
+ this.name = extractFileName(name);
- File destDir = new File(removeExtension(new File(filePath).getAbsolutePath()) + EXTRACTION_SUFFIX);
- if (!destDir.exists() && !destDir.mkdirs()) {
- throw new IOException("Failed to create extraction directory " + destDir);
+ try (InputStream in = archive) {
+ this.extractionDirectory = Files.createTempDirectory("reqifz-");
+ extract(in, typeClassifier);
+ } catch (IOException | RuntimeException e) {
+ try {
+ close();
+ } catch (IOException cleanup) {
+ e.addSuppressed(cleanup);
+ }
+ throw e;
}
- String canonicalDestDir = destDir.getCanonicalPath();
+ }
- byte[] buffer = new byte[8192];
- Map picturesIS = new HashMap<>();
+ private void extract(InputStream archive, TypeClassifier typeClassifier) throws IOException {
- try (ZipInputStream zis = new ZipInputStream(new FileInputStream(filePath))) {
- ZipEntry zipEntry = zis.getNextEntry();
+ Path destination = this.extractionDirectory.toRealPath();
- while (zipEntry != null) {
- File newFile = new File(destDir, zipEntry.getName());
+ try (ZipInputStream zis = new ZipInputStream(archive)) {
+ for (ZipEntry zipEntry = zis.getNextEntry(); zipEntry != null; zipEntry = zis.getNextEntry()) {
+ // zip entries use forward slashes; archives made on Windows
+ // sometimes contain backslashes instead
+ String entryName = zipEntry.getName().replace('\\', '/');
+ Path target = destination.resolve(entryName).normalize();
// Guard against zip-slip: entries must not escape the
// extraction directory (e.g. via "../" path segments).
- if (!newFile.getCanonicalPath().startsWith(canonicalDestDir + File.separator)) {
- throw new IOException("Zip entry outside of extraction directory: " + zipEntry.getName());
+ if (!target.startsWith(destination) || target.equals(destination)) {
+ throw new IOException("Zip entry outside of extraction directory: " + entryName);
}
-
if (zipEntry.isDirectory()) {
- if (!newFile.isDirectory() && !newFile.mkdirs()) {
- throw new IOException("Failed to create directory " + newFile);
+ Files.createDirectories(target);
+ continue;
+ }
+ // also for archives that list no directory entries
+ Files.createDirectories(target.getParent());
+ // a zip may hold an entry twice (tools append updated
+ // entries); the later one wins, as when unzipping
+ Files.copy(zis, target, StandardCopyOption.REPLACE_EXISTING);
+
+ File file = target.toFile();
+ if (entryName.toLowerCase(Locale.ROOT).endsWith(".reqif")) {
+ this.numberOfReqIFDocuments++;
+ this.reqifFiles.put(entryName, file);
+ try (InputStream reqifIS = new FileInputStream(file)) {
+ this.reqifDocuments.put(entryName, new ReqIFDocument(reqifIS, this.path, entryName, typeClassifier));
}
} else {
- // Fix for Windows-created archives
- File parent = newFile.getParentFile();
- if (!parent.isDirectory() && !parent.mkdirs()) {
- throw new IOException("Failed to create directory " + parent);
- }
-
- // Write file content
- try (FileOutputStream fos = new FileOutputStream(newFile)) {
- int len;
- while ((len = zis.read(buffer)) > 0) {
- fos.write(buffer, 0, len);
- }
- }
-
- // Process reqif files and associated images
- if (zipEntry.getName().endsWith("reqif")) {
- this.numberOfReqIFDocuments++;
- this.reqifFiles.put(zipEntry.getName(), newFile);
- try (InputStream reqifIS = new FileInputStream(newFile)) {
- this.reqifDocuments.put(zipEntry.getName(), new ReqIFDocument(reqifIS, filePath, zipEntry.getName(), typeClassifier));
- }
- } else if (zipEntry.getName().endsWith("png") || zipEntry.getName().endsWith("jpeg") || zipEntry.getName().endsWith("jpg")) {
- // Keyed by the archive entry path (forward slashes), which is
- // what xhtml object data attributes reference.
- this.pictureFiles.put(zipEntry.getName(), newFile);
- picturesIS.put(zipEntry.getName(), new FileInputStream(newFile));
+ this.attachmentFiles.put(entryName, file);
+ if (isPicture(entryName)) {
+ this.pictureFiles.put(entryName, file);
}
}
- zipEntry = zis.getNextEntry();
}
}
- // Associate the archive's pictures with each contained ReqIF document,
- // using the same keys as reqifDocuments (full entry name).
+ // Opened only now, so a failure while extracting leaves no stream open
+ // that would keep the files from being deleted. Associated with each
+ // contained ReqIF document, using the same keys as reqifDocuments.
+ // The map is registered before the streams are opened, so close()
+ // reaches every stream also if opening one of them fails.
+ Map picturesIS = new HashMap<>();
for (String reqifDocumentName : this.reqifDocuments.keySet()) {
this.picturesInReqIFDocument.put(reqifDocumentName, picturesIS);
}
+ if (!this.reqifDocuments.isEmpty()) {
+ for (Map.Entry picture : this.pictureFiles.entrySet()) {
+ picturesIS.put(picture.getKey(), new FileInputStream(picture.getValue()));
+ }
+ }
+ }
+
+ /**
+ * Closes the picture streams and deletes the extracted files.
+ */
+ @Override
+ public void close() throws IOException {
+
+ IOException failure = null;
+ try {
+ super.close();
+ } catch (IOException e) {
+ failure = e;
+ }
+ if (this.extractionDirectory != null && Files.exists(this.extractionDirectory)) {
+ List files;
+ try (Stream walk = Files.walk(this.extractionDirectory)) {
+ // children before their directories
+ files = walk.sorted(Comparator.reverseOrder()).collect(Collectors.toList());
+ } catch (IOException | UncheckedIOException e) {
+ files = List.of();
+ failure = addFailure(failure, e);
+ }
+ // a file that cannot be deleted must not keep the others
+ for (Path file : files) {
+ try {
+ Files.deleteIfExists(file);
+ } catch (IOException e) {
+ failure = addFailure(failure, e);
+ }
+ }
+ }
+ if (failure != null) {
+ throw failure;
+ }
+ }
+
+ private static boolean isPicture(String entryName) {
+ String lower = entryName.toLowerCase(Locale.ROOT);
+ for (String extension : PICTURE_EXTENSIONS) {
+ if (lower.endsWith(extension)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static IOException addFailure(IOException failure, Exception next) {
+ IOException asIO = next instanceof UncheckedIOException ? ((UncheckedIOException) next).getCause()
+ : next instanceof IOException ? (IOException) next : new IOException(next);
+ if (failure == null) {
+ return asIO;
+ }
+ failure.addSuppressed(asIO);
+ return failure;
}
public static String removeFileExtension(String filename, boolean removeAllExtensions) {
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/AttributeValueReader.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/AttributeValueReader.java
new file mode 100644
index 0000000..627e921
--- /dev/null
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/AttributeValueReader.java
@@ -0,0 +1,283 @@
+package de.uni_stuttgart.ils.reqif4j.specification;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinitionEnumeration;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueBoolean;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueDate;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueDouble;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueEnumeration;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueInteger;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueString;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML;
+import de.uni_stuttgart.ils.reqif4j.attributes.UninterpretedAttributeValue;
+import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype;
+import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues;
+import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
+import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;
+import de.uni_stuttgart.ils.reqif4j.util.XsdTypes;
+
+/**
+ * Reads the VALUES of an element with attributes - spec objects, spec
+ * relations and specifications share the same structure - and creates the
+ * values that stand in for absent ones from the attribute definition defaults.
+ */
+final class AttributeValueReader {
+
+ private AttributeValueReader() {
+ }
+
+ /**
+ * Reads the explicit values, the ones the element itself carries.
+ *
+ * A value whose definition cannot be found, whose definition has no
+ * datatype, or whose kind does not match its definition is reported and
+ * kept as an {@link UninterpretedAttributeValue}, which is written back
+ * unchanged. A value that does not fit its datatype (e.g.
+ * THE-VALUE="abc" for an integer) is kept as written, as a plain
+ * {@link AttributeValue}, so it is written back unchanged and the
+ * validator can report it.
+ *
+ * @param element the SPEC-OBJECT, SPEC-RELATION or SPECIFICATION
+ * @param specType the element's resolved spec type
+ * @param values receives the values
+ * @param issues receives the problems found
+ */
+ static void read(Node element, SpecType specType, AttributeValues values, ParseIssues issues) {
+
+ // VALUES is located by local name so prefixed ReqIF namespaces work too;
+ // only this element's own VALUES is taken, not a nested one.
+ Element valuesElement = XmlUtils.firstChildElementByLocalName(element, ReqIFConst.VALUES);
+ if(valuesElement == null) {
+ return;
+ }
+ String ownerID = XmlUtils.attribute(element, ReqIFConst.IDENTIFIER);
+
+ for(Element attribute: XmlUtils.childElements(valuesElement)) {
+
+ String attValNodeName = XmlUtils.localName(attribute);
+ Element definitionRef = XmlUtils.firstChildElement(
+ XmlUtils.firstChildElementByLocalName(attribute, ReqIFConst.DEFINITION));
+ if(definitionRef == null) {
+ issues.report(ownerID, attValNodeName + " has no DEFINITION; the value is skipped");
+ continue;
+ }
+ String attributeDefinitionRef = definitionRef.getTextContent().trim();
+ if(values.explicit().containsKey(attributeDefinitionRef)) {
+ issues.report(ownerID, "Attribute definition " + attributeDefinitionRef
+ + " has two values; the later one replaces the earlier");
+ }
+ AttributeDefinition attributeDefinition = specType.getAttributeDefinition(attributeDefinitionRef);
+ String kind = ReqIFConst.kindOf(attValNodeName);
+ String definitionKind = attributeDefinition == null || attributeDefinition.getDataType() == null ? null
+ : ReqIFConst.datatypeSuffix(attributeDefinition.getDataType().getType());
+ String reason = null;
+ if(attributeDefinition == null) {
+ reason = "Value references attribute definition " + attributeDefinitionRef
+ + ", which spec type " + specType.getID() + " does not declare";
+ } else if(attributeDefinition.getDataType() == null) {
+ reason = "Value of attribute definition " + attributeDefinitionRef + ", which has no datatype";
+ } else if(definitionKind != null && !kind.equals(definitionKind)) {
+ reason = attValNodeName + " for attribute definition " + attributeDefinitionRef + ", which is of kind "
+ + definitionKind;
+ }
+ if(reason != null) {
+ // a definition without datatype is reported once, with its values
+ if(attributeDefinition == null || attributeDefinition.getDataType() != null) {
+ issues.report(ownerID, reason + "; the value is kept as written");
+ }
+ values.put(new UninterpretedAttributeValue(attribute,
+ attributeDefinition != null ? attributeDefinition : standIn(attributeDefinitionRef), reason));
+ continue;
+ }
+
+ String theValue = theValue(attribute);
+ if(THE_VALUE_KINDS.contains(kind) && XmlUtils.attribute(attribute, ReqIFConst.THE_VALUE) == null) {
+ // read as the neutral value (false, 0, ""), as earlier versions did
+ issues.report(ownerID, attValNodeName + " for " + attributeDefinitionRef
+ + " has no THE-VALUE; it is read as empty");
+ }
+ switch(kind) {
+
+ case ReqIFConst.BOOLEAN:
+ // any case is accepted, as earlier versions did; other text is kept
+ values.put(theValue.isEmpty() || BOOLEAN_TEXT.contains(theValue.trim().toLowerCase(java.util.Locale.ROOT))
+ ? new AttributeValueBoolean(theValue, attributeDefinition)
+ : asWritten(theValue, attributeDefinition, attValNodeName));
+ break;
+
+ case ReqIFConst.INTEGER:
+ values.put(theValue.isEmpty() || XsdTypes.isInteger(theValue)
+ ? new AttributeValueInteger(theValue, attributeDefinition)
+ : asWritten(theValue, attributeDefinition, attValNodeName));
+ break;
+
+ case ReqIFConst.STRING:
+ values.put(new AttributeValueString(theValue, attributeDefinition));
+ break;
+
+ case ReqIFConst.ENUMERATION:
+ // Multiselect: read ALL ENUM-VALUE-REF children, not just the first
+ List enumRefs = new ArrayList();
+ List enumNames = new ArrayList();
+ for(Element ref : XmlUtils.descendantsByLocalName(attribute, ReqIFConst.ENUM_VALUE_REF)) {
+ String refID = ref.getTextContent().trim();
+ enumRefs.add(refID);
+ enumNames.add(specType.getEnumValueName(refID));
+ }
+ values.put(new AttributeValueEnumeration(enumNames, enumRefs, attributeDefinition));
+ break;
+
+ case ReqIFConst.XHTML:
+ values.put(new AttributeValueXHTML(attribute, attributeDefinition));
+ break;
+
+ case ReqIFConst.DATE:
+ values.put(new AttributeValueDate(theValue, attributeDefinition));
+ break;
+
+ case ReqIFConst.REAL:
+ values.put(theValue.isEmpty() || XsdTypes.isDouble(theValue)
+ ? new AttributeValueDouble(theValue, attributeDefinition)
+ : asWritten(theValue, attributeDefinition, attValNodeName));
+ break;
+
+ // Values of datatype kinds the parser does not model are kept
+ // generically instead of being dropped, so they survive a
+ // round trip.
+ default:
+ values.put(asWritten(theValue, attributeDefinition, attValNodeName));
+ break;
+ }
+ }
+ }
+
+ /**
+ * Keeps the values of an element whose spec type is not declared as
+ * written: without the type, they cannot be interpreted.
+ *
+ * @return the number of values kept
+ */
+ static int readUninterpreted(Node element, String reason, AttributeValues values, ParseIssues issues) {
+
+ int count = 0;
+ String ownerID = XmlUtils.attribute(element, ReqIFConst.IDENTIFIER);
+ for(Element attribute: XmlUtils.childElements(XmlUtils.firstChildElementByLocalName(element, ReqIFConst.VALUES))) {
+ Element definitionRef = XmlUtils.firstChildElement(
+ XmlUtils.firstChildElementByLocalName(attribute, ReqIFConst.DEFINITION));
+ if(definitionRef == null) {
+ issues.report(ownerID, XmlUtils.localName(attribute) + " has no DEFINITION; the value is skipped");
+ continue;
+ }
+ String attributeDefinitionRef = definitionRef.getTextContent().trim();
+ if(values.explicit().containsKey(attributeDefinitionRef)) {
+ issues.report(ownerID, "Attribute definition " + attributeDefinitionRef
+ + " has two values; the later one replaces the earlier");
+ } else {
+ count++;
+ }
+ values.put(new UninterpretedAttributeValue(attribute, standIn(attributeDefinitionRef), reason));
+ }
+ return count;
+ }
+
+ /**
+ * @return an attribute definition that stands in for an undeclared one:
+ * only its IDENTIFIER is known
+ */
+ private static AttributeDefinition standIn(String attributeDefinitionID) {
+ return new AttributeDefinition(attributeDefinitionID, null, null, null);
+ }
+
+ private static final java.util.Set BOOLEAN_TEXT = java.util.Set.of("true", "false", "1", "0");
+
+ /** the value kinds that carry their value in a required THE-VALUE attribute */
+ private static final java.util.Set THE_VALUE_KINDS = java.util.Set.of(
+ ReqIFConst.BOOLEAN, ReqIFConst.INTEGER, ReqIFConst.STRING, ReqIFConst.DATE, ReqIFConst.REAL);
+
+ /**
+ * @return the value exactly as written, for kinds the parser does not model
+ * and for text that does not fit the datatype
+ */
+ private static AttributeValue asWritten(String text, AttributeDefinition definition, String elementName) {
+ return new AttributeValue(text, definition).setSourceElementName(elementName);
+ }
+
+ /**
+ * @return the value that stands in for an absent one: the definition's
+ * default, or the neutral value of its datatype (false, 0, ...) as
+ * earlier versions reported it; null for datatypes without one
+ */
+ static AttributeValue defaultValue(AttributeDefinition attributeDefinition) {
+
+ Datatype datatype = attributeDefinition.getDataType();
+ if(datatype == null) {
+ return null;
+ }
+ String defaultValue = attributeDefinition.getDefaultValue();
+ switch(datatype.getType()) {
+
+ // a default that does not fit its datatype is kept as written, like a
+ // value; the validator reports it
+ case ReqIFConst.BOOLEAN:
+ return defaultValue == null || BOOLEAN_TEXT.contains(defaultValue.trim().toLowerCase(java.util.Locale.ROOT))
+ ? new AttributeValueBoolean(defaultValue, attributeDefinition)
+ : new AttributeValue(defaultValue, attributeDefinition);
+
+ case ReqIFConst.INTEGER:
+ return defaultValue == null || XsdTypes.isInteger(defaultValue)
+ ? new AttributeValueInteger(defaultValue, attributeDefinition)
+ : new AttributeValue(defaultValue, attributeDefinition);
+
+ case ReqIFConst.STRING:
+ return new AttributeValueString(defaultValue, attributeDefinition);
+
+ case ReqIFConst.ENUMERATION:
+ if(attributeDefinition instanceof AttributeDefinitionEnumeration) {
+ AttributeDefinitionEnumeration enumDefinition = (AttributeDefinitionEnumeration) attributeDefinition;
+ return new AttributeValueEnumeration(enumDefinition.getDefaultValues(), enumDefinition.getDefaultValueRefs(), attributeDefinition);
+ }
+ return new AttributeValueEnumeration(defaultValue, attributeDefinition);
+
+ case ReqIFConst.XHTML:
+ return attributeDefinition.getDefaultXhtmlValue() != null
+ ? attributeDefinition.getDefaultXhtmlValue()
+ : new AttributeValueXHTML(defaultValue, attributeDefinition);
+
+ case ReqIFConst.DATE:
+ return new AttributeValueDate(defaultValue, attributeDefinition);
+
+ // the parser normalizes REAL datatypes to DOUBLE
+ case ReqIFConst.DOUBLE:
+ case ReqIFConst.REAL:
+ return defaultValue == null || XsdTypes.isDouble(defaultValue)
+ ? new AttributeValueDouble(defaultValue, attributeDefinition)
+ : new AttributeValue(defaultValue, attributeDefinition);
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Attribute values are kept by the LONG-NAME of their definition. LONG-NAME
+ * is optional, though; definitions without one are keyed by their
+ * IDENTIFIER instead, so their values do not overwrite each other.
+ */
+ static String key(AttributeDefinition attributeDefinition) {
+ return attributeDefinition.getName() != null ? attributeDefinition.getName() : attributeDefinition.getID();
+ }
+
+ /**
+ * @return the THE-VALUE attribute, or "" if the value element has none
+ */
+ private static String theValue(Element attribute) {
+ return XmlUtils.attributeOrEmpty(attribute, ReqIFConst.THE_VALUE);
+ }
+}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/AttributeValues.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/AttributeValues.java
new file mode 100644
index 0000000..3817351
--- /dev/null
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/AttributeValues.java
@@ -0,0 +1,93 @@
+package de.uni_stuttgart.ils.reqif4j.specification;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue;
+
+/**
+ * The attribute values of a spec object, spec relation or specification.
+ *
+ * Two views are kept apart:
+ *
+ *
+ * - explicit values are the ones the element carries itself -
+ * exactly what the document contains and what is written back - keyed by the
+ * IDENTIFIER of their attribute definition, in document order;
+ * - effective values additionally fill in the default value of
+ * every attribute definition the element has no value for. They are keyed by
+ * LONG-NAME for convenient lookup and only ever derived, never written.
+ *
+ */
+final class AttributeValues {
+
+ private final SpecType specType;
+ private final Map explicit = new LinkedHashMap();
+ private final Map explicitView = Collections.unmodifiableMap(this.explicit);
+
+ AttributeValues(SpecType specType) {
+ this.specType = specType;
+ }
+
+ SpecType specType() {
+ return this.specType;
+ }
+
+ /**
+ * Adds an explicit value; a second value for the same definition replaces
+ * the first.
+ */
+ void put(AttributeValue value) {
+ this.explicit.put(value.getAttributeDefinitionType().getID(), value);
+ }
+
+ AttributeValue remove(String attributeDefinitionID) {
+ return this.explicit.remove(attributeDefinitionID);
+ }
+
+ /**
+ * @return the explicit values by attribute definition IDENTIFIER, in
+ * document order (read-only)
+ */
+ Map explicit() {
+ return this.explicitView;
+ }
+
+ /**
+ * @return the effective values by LONG-NAME (IDENTIFIER for definitions
+ * without one), in spec type order: explicit values plus the
+ * defaults of all other definitions. An explicit value always wins
+ * over a default of the same name. Derived on every call, so it
+ * follows later changes of the values or the spec type.
+ */
+ Map effectiveByName() {
+
+ Map effective = new LinkedHashMap();
+ Map undeclared = new LinkedHashMap(this.explicit);
+ Set explicitNames = new HashSet();
+ for (AttributeValue value : this.explicit.values()) {
+ explicitNames.add(AttributeValueReader.key(value.getAttributeDefinitionType()));
+ }
+
+ if (this.specType != null) {
+ for (AttributeDefinition definition : this.specType.getAttributeDefinitions().values()) {
+ String name = AttributeValueReader.key(definition);
+ AttributeValue value = undeclared.remove(definition.getID());
+ if (value == null && !explicitNames.contains(name)) {
+ value = AttributeValueReader.defaultValue(definition);
+ }
+ if (value != null) {
+ effective.putIfAbsent(name, value);
+ }
+ }
+ }
+ for (AttributeValue value : undeclared.values()) {
+ effective.putIfAbsent(AttributeValueReader.key(value.getAttributeDefinitionType()), value);
+ }
+ return Collections.unmodifiableMap(effective);
+ }
+}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/HierarchyChildren.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/HierarchyChildren.java
new file mode 100644
index 0000000..9785d47
--- /dev/null
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/HierarchyChildren.java
@@ -0,0 +1,311 @@
+package de.uni_stuttgart.ils.reqif4j.specification;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The ordered children of a specification or a spec hierarchy node. Each
+ * child knows its parent, so a node is in one place only and cannot be put
+ * below itself.
+ *
+ * Two views of all nodes below are kept, as callers ask for them repeatedly:
+ * the flattened list, rebuilt after the structure below changed, and an
+ * index by IDENTIFIER, kept up to date as nodes come and go, so that looking
+ * nodes up while changing a large hierarchy stays cheap.
+ */
+final class HierarchyChildren {
+
+ private final HierarchyParent owner;
+ private final List ordered = new ArrayList();
+ private final Map byID = new HashMap();
+ private List descendants;
+ private Map index;
+ /** whether two nodes below have the same IDENTIFIER (only read documents) */
+ private boolean duplicates;
+
+ HierarchyChildren(HierarchyParent owner) {
+ this.owner = owner;
+ }
+
+ /**
+ * Adds the children of an owner that is being created; a child with an
+ * IDENTIFIER that is already there replaces the earlier one.
+ *
+ * @throws IllegalArgumentException if one of them is placed elsewhere;
+ * then none is added
+ */
+ void putAll(List children) {
+ for (SpecHierarchy child : children) {
+ checkPlaceable(child, true);
+ }
+ for (SpecHierarchy child : children) {
+ put(child);
+ }
+ }
+
+ /**
+ * Adds a child read from a document; one with an IDENTIFIER that is
+ * already there replaces the earlier one, which is reported.
+ *
+ * @param child may be null (a skipped node), then nothing happens
+ */
+ void put(SpecHierarchy child, de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues issues) {
+ if (child == null) {
+ return;
+ }
+ if (this.byID.containsKey(child.getID())) {
+ issues.reportDuplicate("Spec hierarchy", child.getID());
+ }
+ put(child);
+ }
+
+ private void put(SpecHierarchy child) {
+ SpecHierarchy previous = this.byID.put(child.getID(), child);
+ if (previous == child) {
+ return;
+ }
+ if (previous != null) {
+ // in the place of the earlier one, as a map would keep it
+ this.ordered.set(position(previous), child);
+ previous.setParent(null);
+ removed(previous);
+ } else {
+ this.ordered.add(child);
+ }
+ child.setParent(this.owner);
+ added(child);
+ }
+
+ /**
+ * @return the children in order (a copy)
+ */
+ List list() {
+ return new ArrayList(this.ordered);
+ }
+
+ /**
+ * @return the children in order (a live view, read-only)
+ */
+ List values() {
+ return Collections.unmodifiableList(this.ordered);
+ }
+
+ /**
+ * @param index position among the children, 0 for first; the number of
+ * children appends
+ * @throws IllegalArgumentException if the node is already placed somewhere,
+ * a child with the same IDENTIFIER exists, the node is the owner or
+ * above it, or the index is out of range
+ */
+ void add(int index, SpecHierarchy child) {
+
+ checkPlaceable(child, false);
+ if (this.byID.containsKey(child.getID())) {
+ throw new IllegalArgumentException("Spec hierarchy " + child.getID() + " is already a child here");
+ }
+ if (index < 0 || index > this.ordered.size()) {
+ throw new IllegalArgumentException("Position " + index + " is outside 0.." + this.ordered.size());
+ }
+ this.ordered.add(index, child);
+ this.byID.put(child.getID(), child);
+ child.setParent(this.owner);
+ added(child);
+ }
+
+ /**
+ * Replaces the children by these, in this order, in one step. Former
+ * children that are not among them are removed with everything below them.
+ *
+ * @throws IllegalArgumentException if two have the same IDENTIFIER, one is
+ * placed elsewhere, or one is the owner or above it; then nothing
+ * changes
+ */
+ void replace(List children) {
+
+ Set ids = new HashSet();
+ for (SpecHierarchy child : children) {
+ checkPlaceable(child, true);
+ if (!ids.add(child.getID())) {
+ throw new IllegalArgumentException("Spec hierarchy " + child.getID() + " is given twice");
+ }
+ }
+ Set kept = Collections.newSetFromMap(new IdentityHashMap());
+ kept.addAll(children);
+ for (SpecHierarchy former : this.ordered) {
+ if (!kept.contains(former)) {
+ former.setParent(null);
+ }
+ }
+ this.ordered.clear();
+ this.byID.clear();
+ for (SpecHierarchy child : children) {
+ this.ordered.add(child);
+ this.byID.put(child.getID(), child);
+ child.setParent(this.owner);
+ }
+ // many nodes at once: the views are rebuilt when needed
+ for (HierarchyChildren level = this; level != null; level = level.above()) {
+ level.descendants = null;
+ level.index = null;
+ }
+ }
+
+ /**
+ * @param ownChildToo whether a node that is already a child here may be
+ * placed again (in a replacement of all children)
+ * @throws IllegalArgumentException if the node has no IDENTIFIER, is
+ * placed elsewhere, or is the owner or above it
+ */
+ private void checkPlaceable(SpecHierarchy child, boolean ownChildToo) {
+ if (child == null || child.getID() == null) {
+ throw new IllegalArgumentException("A spec hierarchy node needs an identifier");
+ }
+ if (child.getParent() != null && !(ownChildToo && child.getParent() == this.owner)) {
+ throw new IllegalArgumentException("Spec hierarchy " + child.getID() + " is already placed below "
+ + child.getParent().getID() + "; remove or move it instead");
+ }
+ for (HierarchyParent ancestor = this.owner; ancestor != null; ancestor = parentOf(ancestor)) {
+ if (ancestor == child) {
+ throw new IllegalArgumentException("Spec hierarchy " + child.getID() + " cannot be placed below itself");
+ }
+ }
+ }
+
+ private static HierarchyParent parentOf(HierarchyParent node) {
+ return node instanceof SpecHierarchy ? ((SpecHierarchy) node).getParent() : null;
+ }
+
+ SpecHierarchy remove(String id) {
+ SpecHierarchy removed = this.byID.remove(id);
+ if (removed != null) {
+ this.ordered.remove(position(removed));
+ removed.setParent(null);
+ removed(removed);
+ }
+ return removed;
+ }
+
+ /**
+ * @return the position of the child, or -1
+ */
+ int indexOf(String id) {
+ SpecHierarchy child = this.byID.get(id);
+ return child == null ? -1 : position(child);
+ }
+
+ private int position(SpecHierarchy child) {
+ for (int i = 0; i < this.ordered.size(); i++) {
+ if (this.ordered.get(i) == child) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ int size() {
+ return this.ordered.size();
+ }
+
+ SpecHierarchy get(String id) {
+ return this.byID.get(id);
+ }
+
+ /**
+ * @return all nodes below, depth-first in document order (read-only)
+ */
+ List descendants() {
+ if (this.descendants == null) {
+ List all = new ArrayList();
+ for (SpecHierarchy child : this.ordered) {
+ all.add(child);
+ all.addAll(child.getAllChildren());
+ }
+ this.descendants = Collections.unmodifiableList(all);
+ }
+ return this.descendants;
+ }
+
+ /**
+ * @return the node below with this IDENTIFIER; for an identifier used
+ * twice the first in document order
+ */
+ SpecHierarchy descendant(String id) {
+ if (this.index == null) {
+ this.index = new HashMap();
+ this.duplicates = false;
+ for (SpecHierarchy node : descendants()) {
+ if (this.index.putIfAbsent(node.getID(), node) != null) {
+ this.duplicates = true;
+ }
+ }
+ }
+ return this.index.get(id);
+ }
+
+ /**
+ * Updates the views here and above after a node, with everything below
+ * it, was added.
+ */
+ private void added(SpecHierarchy node) {
+ List subtree = null;
+ for (HierarchyChildren level = this; level != null; level = level.above()) {
+ level.descendants = null;
+ if (level.index != null) {
+ subtree = subtree != null ? subtree : subtree(node);
+ for (SpecHierarchy added : subtree) {
+ if (level.index.containsKey(added.getID())) {
+ // which of the two comes first in document order is not
+ // known here: the index starts over
+ level.index = null;
+ break;
+ }
+ level.index.put(added.getID(), added);
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the views here and above after a node, with everything below
+ * it, was removed.
+ */
+ private void removed(SpecHierarchy node) {
+ List subtree = null;
+ for (HierarchyChildren level = this; level != null; level = level.above()) {
+ level.descendants = null;
+ if (level.index != null && level.duplicates) {
+ // another node may have the identifier: start over
+ level.index = null;
+ } else if (level.index != null) {
+ subtree = subtree != null ? subtree : subtree(node);
+ for (SpecHierarchy removed : subtree) {
+ level.index.remove(removed.getID(), removed);
+ }
+ }
+ }
+ }
+
+ private static List subtree(SpecHierarchy node) {
+ List subtree = new ArrayList();
+ subtree.add(node);
+ subtree.addAll(node.getAllChildren());
+ return subtree;
+ }
+
+ /**
+ * @return the children of the owner's parent, or null at the top
+ */
+ private HierarchyChildren above() {
+ HierarchyParent parent = parentOf(this.owner);
+ if (parent instanceof SpecHierarchy) {
+ return ((SpecHierarchy) parent).hierarchyChildren();
+ }
+ return parent instanceof Specification ? ((Specification) parent).hierarchyChildren() : null;
+ }
+}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/HierarchyParent.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/HierarchyParent.java
new file mode 100644
index 0000000..245f90f
--- /dev/null
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/HierarchyParent.java
@@ -0,0 +1,64 @@
+package de.uni_stuttgart.ils.reqif4j.specification;
+
+import java.util.List;
+
+/**
+ * What holds spec hierarchy nodes: a specification (the top level) or a
+ * hierarchy node (the levels below).
+ */
+public interface HierarchyParent {
+
+ /**
+ * @return the IDENTIFIER of this specification or hierarchy node
+ */
+ String getID();
+
+ /**
+ * @return the direct children in document order (a copy)
+ */
+ List getChildren();
+
+ /**
+ * @return the number of direct children
+ */
+ default int getChildCount() {
+ return getChildren().size();
+ }
+
+ /**
+ * @return the direct child with this IDENTIFIER, or null
+ */
+ default SpecHierarchy getChild(String specHierarchyID) {
+ for (SpecHierarchy child : getChildren()) {
+ if (child.getID() != null && child.getID().equals(specHierarchyID)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Inserts a child node at a position, 0 for first.
+ *
+ * @throws IllegalArgumentException if a child with the same IDENTIFIER
+ * exists or the position is out of range
+ */
+ HierarchyParent addChild(int index, SpecHierarchy child);
+
+ /**
+ * Replaces the children by these, in this order, in one step: to reorder
+ * them, or to add and remove several at once. Former children that are
+ * not among them are removed with everything below them.
+ *
+ * @throws IllegalArgumentException if two have the same IDENTIFIER, one is
+ * placed elsewhere, or one is this node or above it
+ */
+ HierarchyParent setChildren(List children);
+
+ /**
+ * Removes a direct child node, with everything below it.
+ *
+ * @return the removed node, or null if there is no such child
+ */
+ SpecHierarchy removeChild(String specHierarchyID);
+}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/LongNameTypeClassifier.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/LongNameTypeClassifier.java
index fa012f0..c52063c 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/LongNameTypeClassifier.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/LongNameTypeClassifier.java
@@ -45,7 +45,7 @@ public boolean isRequirement(SpecObject specObject) {
for (AttributeValue attributeValue : specObject.getAttributes().values()) {
if (isBoolean(attributeValue)
- && attributeValue.getName().toLowerCase().contains(ReqIFConst.REQ.toLowerCase())) {
+ && lowerCaseName(attributeValue).contains(ReqIFConst.REQ.toLowerCase())) {
return Boolean.TRUE.equals(attributeValue.getValue());
}
}
@@ -61,8 +61,8 @@ public boolean isSubRequirement(SpecObject specObject) {
for (AttributeValue attributeValue : specObject.getAttributes().values()) {
if (isBoolean(attributeValue)
- && attributeValue.getName().toLowerCase().contains(ReqIFConst.SUB.toLowerCase())
- && attributeValue.getName().toLowerCase().contains(ReqIFConst.REQ.toLowerCase())) {
+ && lowerCaseName(attributeValue).contains(ReqIFConst.SUB.toLowerCase())
+ && lowerCaseName(attributeValue).contains(ReqIFConst.REQ.toLowerCase())) {
return Boolean.TRUE.equals(attributeValue.getValue());
}
}
@@ -74,4 +74,11 @@ private static boolean isBoolean(AttributeValue attributeValue) {
&& attributeValue.getAttributeDefinitionType().getDataType() != null
&& ReqIFConst.BOOLEAN.equals(attributeValue.getAttributeDefinitionType().getDataType().getType());
}
+
+ /**
+ * LONG-NAME is optional in ReqIF, so an attribute may have no name.
+ */
+ private static String lowerCaseName(AttributeValue attributeValue) {
+ return attributeValue.getName() == null ? "" : attributeValue.getName().toLowerCase();
+ }
}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroup.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroup.java
index 777fc1b..b9303f6 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroup.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroup.java
@@ -7,6 +7,7 @@
import org.w3c.dom.Element;
import org.w3c.dom.Node;
+import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable;
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;
@@ -18,32 +19,14 @@
* between a source and a target specification, for example a traceability view
* between a system and a software specification.
*/
-public class RelationGroup {
+public class RelationGroup extends Identifiable {
- private String id;
- private String name;
- private String alternativeID;
private String relationGroupTypeRef;
private String sourceSpecificationRef;
private String targetSpecificationRef;
private final List specRelationRefs = new ArrayList();
- public String getID() {
- return this.id;
- }
-
- public String getName() {
- return this.name;
- }
-
- /**
- * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null
- */
- public String getAlternativeID() {
- return this.alternativeID;
- }
-
/**
* @return the IDENTIFIER of the RELATION-GROUP-TYPE, or null
*/
@@ -72,6 +55,40 @@ public List getSpecRelationRefs() {
return Collections.unmodifiableList(this.specRelationRefs);
}
+ public RelationGroup setSourceSpecificationRef(String sourceSpecificationRef) {
+ this.sourceSpecificationRef = sourceSpecificationRef;
+ markModified();
+ return this;
+ }
+
+ public RelationGroup setTargetSpecificationRef(String targetSpecificationRef) {
+ this.targetSpecificationRef = targetSpecificationRef;
+ markModified();
+ return this;
+ }
+
+ /**
+ * Adds a relation to the group, unless it already belongs to it.
+ */
+ public RelationGroup addSpecRelationRef(String specRelationID) {
+ if (!this.specRelationRefs.contains(specRelationID)) {
+ this.specRelationRefs.add(specRelationID);
+ markModified();
+ }
+ return this;
+ }
+
+ /**
+ * @return whether the relation belonged to the group
+ */
+ public boolean removeSpecRelationRef(String specRelationID) {
+ boolean removed = this.specRelationRefs.remove(specRelationID);
+ if (removed) {
+ markModified();
+ }
+ return removed;
+ }
+
@@ -81,9 +98,8 @@ public List getSpecRelationRefs() {
*/
public RelationGroup(String id, String name, String relationGroupTypeRef,
String sourceSpecificationRef, String targetSpecificationRef, List specRelationRefs) {
+ super(id, name);
- this.id = id;
- this.name = name;
this.relationGroupTypeRef = relationGroupTypeRef;
this.sourceSpecificationRef = sourceSpecificationRef;
this.targetSpecificationRef = targetSpecificationRef;
@@ -93,10 +109,8 @@ public RelationGroup(String id, String name, String relationGroupTypeRef,
}
public RelationGroup(Node relationGroup) {
+ super(relationGroup);
- this.id = XmlUtils.attribute(relationGroup, ReqIFConst.IDENTIFIER);
- this.name = XmlUtils.attribute(relationGroup, ReqIFConst.LONG_NAME);
- this.alternativeID = XmlUtils.alternativeID(relationGroup);
this.relationGroupTypeRef = refIn(relationGroup, ReqIFConst.TYPE, ReqIFConst.RELATION_GROUP_TYPE_REF);
this.sourceSpecificationRef = refIn(relationGroup, ReqIFConst.SOURCE_SPECIFICATION, ReqIFConst.SPECIFICATION_REF);
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroupType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroupType.java
index a87a529..9095d16 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroupType.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroupType.java
@@ -5,13 +5,18 @@
import org.w3c.dom.Node;
import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype;
+import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues;
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
public class RelationGroupType extends SpecType {
public RelationGroupType(Node specType, Map dataTypes) {
- super(specType, dataTypes);
+ this(specType, dataTypes, ParseIssues.collecting());
+ }
+
+ public RelationGroupType(Node specType, Map dataTypes, ParseIssues issues) {
+ super(specType, dataTypes, issues);
this.type = ReqIFConst.RELATION_GROUP_TYPE;
}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecElementWithAttributes.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecElementWithAttributes.java
new file mode 100644
index 0000000..38bdcb6
--- /dev/null
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecElementWithAttributes.java
@@ -0,0 +1,207 @@
+package de.uni_stuttgart.ils.reqif4j.specification;
+
+import java.util.List;
+import java.util.Map;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueFactory;
+import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable;
+import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues;
+import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
+import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;
+
+/**
+ * An element that carries attribute values: spec objects, spec relations and
+ * specifications, as in the ReqIF metamodel.
+ *
+ * Values are available in two views. {@link #getValues()} holds what the
+ * element itself carries - exactly what the document contains and what is
+ * written back. {@link #getAttributes()} adds the defaults of all attribute
+ * definitions the element has no value for, keyed by LONG-NAME for
+ * convenient lookup.
+ */
+public abstract class SpecElementWithAttributes extends Identifiable {
+
+ private AttributeValues values = new AttributeValues(null);
+
+
+ protected SpecElementWithAttributes(String id, String name) {
+ super(id, name);
+ }
+
+ protected SpecElementWithAttributes(Node element) {
+ super(element);
+ }
+
+
+ /**
+ * @return the values this element carries itself, by IDENTIFIER of their
+ * attribute definition, in document order (read-only). These are
+ * the values that are written; definition defaults are not
+ * included.
+ */
+ public Map getValues() {
+ return this.values.explicit();
+ }
+
+ /**
+ * @return the value this element carries for the attribute definition, or
+ * null if it has none (the definition's default then applies)
+ */
+ public AttributeValue getValue(String attributeDefinitionID) {
+ return this.values.explicit().get(attributeDefinitionID);
+ }
+
+ /**
+ * Sets a value of this element, replacing the one it has for the same
+ * attribute definition.
+ */
+ public SpecElementWithAttributes setValue(AttributeValue value) {
+ if (value == null || value.getAttributeDefinitionType() == null) {
+ throw new IllegalArgumentException("A value needs an attribute definition");
+ }
+ this.values.put(value);
+ markModified();
+ valuesChanged();
+ return this;
+ }
+
+ /**
+ * Sets a scalar value - boolean, integer, real, string or date - of the
+ * attribute definition with this IDENTIFIER, from e.g. a Boolean, a
+ * Number or a String.
+ *
+ * @throws IllegalArgumentException if the element's spec type has no such
+ * attribute definition, or it is an enumeration or XHTML one
+ */
+ public SpecElementWithAttributes set(String attributeDefinitionID, Object value) {
+ return setValue(AttributeValueFactory.of(definition(attributeDefinitionID), value));
+ }
+
+ /**
+ * Sets an XHTML value from markup, with or without a surrounding div.
+ */
+ public SpecElementWithAttributes setXhtml(String attributeDefinitionID, String markup) {
+ return setValue(AttributeValueFactory.xhtml(definition(attributeDefinitionID), markup));
+ }
+
+ /**
+ * Sets an XHTML value that is a simplified version of the complete
+ * content, e.g. the preview image of an embedded OLE object.
+ *
+ * @param originalMarkup the complete content (THE-ORIGINAL-VALUE)
+ */
+ public SpecElementWithAttributes setXhtml(String attributeDefinitionID, String markup, String originalMarkup) {
+ return setValue(AttributeValueFactory.xhtml(definition(attributeDefinitionID), markup, originalMarkup));
+ }
+
+ /**
+ * Selects enum values by their IDENTIFIER; none for an empty selection.
+ */
+ public SpecElementWithAttributes setEnum(String attributeDefinitionID, String... enumValueIDs) {
+ return setValue(AttributeValueFactory.enumeration(definition(attributeDefinitionID),
+ enumValueIDs == null ? null : java.util.Arrays.asList(enumValueIDs)));
+ }
+
+ /**
+ * Removes the element's own value for the attribute definition, so its
+ * default applies again.
+ *
+ * @return the removed value, or null if the element had none
+ */
+ public AttributeValue removeValue(String attributeDefinitionID) {
+ AttributeValue removed = this.values.remove(attributeDefinitionID);
+ if (removed != null) {
+ markModified();
+ valuesChanged();
+ }
+ return removed;
+ }
+
+ /**
+ * @return the element's spec type, or null if its type reference could
+ * not be resolved
+ */
+ public SpecType getResolvedSpecType() {
+ return this.values.specType();
+ }
+
+ private AttributeDefinition definition(String attributeDefinitionID) {
+ SpecType specType = this.values.specType();
+ AttributeDefinition definition = specType == null ? null : specType.getAttributeDefinition(attributeDefinitionID);
+ if (definition == null) {
+ throw new IllegalArgumentException((specType == null ? "The type of " + getID() + " is unknown"
+ : "Spec type " + specType.getID() + " has no attribute definition " + attributeDefinitionID));
+ }
+ return definition;
+ }
+
+ /**
+ * @return the effective values by attribute LONG-NAME: the element's own
+ * values plus the defaults of all other attribute definitions of
+ * its type (read-only). A convenience view: LONG-NAMEs need not be
+ * unique, so prefer {@link #getValues()} to see what the document
+ * contains.
+ */
+ public Map getAttributes() {
+ return this.values.effectiveByName();
+ }
+
+ /**
+ * @return the effective value of the attribute with this LONG-NAME, or null
+ */
+ public Object getAttribute(String attributeName) {
+ AttributeValue attributeValue = getAttributes().get(attributeName);
+ return attributeValue == null ? null : attributeValue.getValue();
+ }
+
+
+ /**
+ * Starts a new set of values for the given spec type, e.g. before reading
+ * them from a document.
+ */
+ AttributeValues resetValues(SpecType specType) {
+ this.values = new AttributeValues(specType);
+ return this.values;
+ }
+
+ /**
+ * Called after a value was set or removed.
+ */
+ protected void valuesChanged() {
+ }
+
+ /**
+ * Reads the element's values. Without a resolved spec type there are no
+ * attribute definitions to interpret them by; that is reported once,
+ * together with the number of values skipped.
+ *
+ * @param typeRef the unresolved type reference, for the report
+ */
+ void readValues(Node element, SpecType specType, String typeRef, ParseIssues issues) {
+
+ AttributeValues values = resetValues(specType);
+ if (specType != null) {
+ AttributeValueReader.read(element, specType, values, issues);
+ return;
+ }
+ String problem = XmlUtils.localName(element)
+ + (typeRef == null ? " has no type reference" : " references type " + typeRef + ", which is not declared");
+ int count = AttributeValueReader.readUninterpreted(element, problem, values, issues);
+ issues.report(getID(), problem
+ + (count == 0 ? "" : "; its " + count + " value" + (count == 1 ? " is" : "s are") + " kept as written"));
+ }
+
+ /**
+ * @return the IDENTIFIER in TYPE/{refName}, or null
+ */
+ static String typeRef(Node element, String refName) {
+ Element type = XmlUtils.firstChildElementByLocalName(element, ReqIFConst.TYPE);
+ Element ref = XmlUtils.firstChildElementByLocalName(type, refName);
+ return ref == null ? null : ref.getTextContent().trim();
+ }
+}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java
index 220ed87..5fa71fa 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java
@@ -1,7 +1,6 @@
package de.uni_stuttgart.ils.reqif4j.specification;
import java.util.ArrayList;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -9,40 +8,124 @@
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition;
import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue;
import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML;
+import de.uni_stuttgart.ils.reqif4j.attributes.XHTML;
import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTMLElementList;
+import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable;
+import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues;
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;
import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLNode;
import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLElementDiv;
-public class SpecHierarchy {
+public class SpecHierarchy extends Identifiable implements HierarchyParent {
- private String specHierarchyID;
- private String alternativeID;
private int hierarchyLvl;
private int section;
private SpecObject specObject;
- private Map children = new LinkedHashMap();
+ private String specObjectRef;
+ private Boolean tableInternal;
+ private Boolean editable;
+ private final List> editableAttributes = new ArrayList>();
+ private final HierarchyChildren children = new HierarchyChildren(this);
+ private HierarchyParent parent;
+ /**
+ * @return the IDENTIFIER of this hierarchy node, same as {@link #getID()}
+ */
public String getSpecHierarchyID() {
- return this.specHierarchyID;
+ return getID();
+ }
+
+ /**
+ * @return the IDENTIFIER of the referenced spec object; also when the
+ * reference could not be resolved, null if there is none
+ */
+ public String getSpecObjectID(){
+ return this.specObject != null ? this.specObject.getID() : this.specObjectRef;
}
/**
- * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null
+ * @return IS-TABLE-INTERNAL: whether this node is part of a table in the
+ * tool's view, or null if the document does not say
*/
- public String getAlternativeID() {
- return this.alternativeID;
+ public Boolean getTableInternal() {
+ return this.tableInternal;
}
-
- public String getSpecObjectID(){
- return this.specObject.getID();
+
+ public SpecHierarchy setTableInternal(Boolean tableInternal) {
+ this.tableInternal = tableInternal;
+ markModified();
+ return this;
+ }
+
+ /**
+ * @return IS-EDITABLE: whether the referenced object may be edited, or
+ * null if the document does not say
+ */
+ public Boolean getEditable() {
+ return this.editable;
+ }
+
+ public SpecHierarchy setEditable(Boolean editable) {
+ this.editable = editable;
+ markModified();
+ return this;
+ }
+
+ /**
+ * @return the IDENTIFIERs of the attribute definitions listed in
+ * EDITABLE-ATTS (editable on this node), in document order
+ */
+ public List getEditableAttributeIDs() {
+ List ids = new ArrayList();
+ for(Map.Entry ref: this.editableAttributes) {
+ ids.add(ref.getKey());
+ }
+ return ids;
+ }
+
+ /**
+ * @return the EDITABLE-ATTS references as read, in document order:
+ * attribute definition IDENTIFIER and the reference element name,
+ * e.g. ATTRIBUTE-DEFINITION-STRING-REF (read-only)
+ */
+ public List> getEditableAttributeRefs() {
+ return java.util.Collections.unmodifiableList(this.editableAttributes);
+ }
+
+ /**
+ * Sets the attributes of the node's object that are editable on this node
+ * (EDITABLE-ATTS); none or null to remove the list.
+ *
+ * @throws IllegalArgumentException if a definition is null, of a datatype
+ * kind that cannot be written, or not an attribute of the spec
+ * type of the node's object
+ */
+ public SpecHierarchy setEditableAttributes(List attributeDefinitions) {
+ List> refs = new ArrayList>();
+ SpecType objectType = this.specObject == null ? null : this.specObject.getResolvedSpecType();
+ for(AttributeDefinition definition: attributeDefinitions == null ? List.of() : attributeDefinitions) {
+ if(definition == null || definition.getElementName() == null) {
+ throw new IllegalArgumentException("Editable attributes need attribute definitions of a known kind");
+ }
+ if(objectType != null && objectType.getAttributeDefinition(definition.getID()) != definition) {
+ throw new IllegalArgumentException("Attribute definition " + definition.getID()
+ + " is not an attribute of spec type " + objectType.getID() + " of the node's object");
+ }
+ refs.add(new java.util.AbstractMap.SimpleImmutableEntry(
+ definition.getID(), definition.getElementName() + "-REF"));
+ }
+ this.editableAttributes.clear();
+ this.editableAttributes.addAll(refs);
+ markModified();
+ return this;
}
public int getHierarchyLvl() {
@@ -65,30 +148,76 @@ public Object getAttribute(String attributeName) {
return (Object)this.specObject.getAttribute(attributeName);
}
+ /**
+ * @return the token list of the object's first XHTML value - its own values
+ * before defaults, so another attribute's default never stands in
+ * for the object's text - or null if it has none
+ */
public AttributeValueXHTMLElementList getXHTMLContent() {
- for(AttributeValue attributeValue: this.specObject.getAttributes().values()) {
- // getValue() returns the rendered string, so the element list has
- // to be fetched via its dedicated getter (the former cast threw
- // ClassCastException on every call).
- if(attributeValue instanceof AttributeValueXHTML) {
- return ((AttributeValueXHTML)attributeValue).getElementList();
+ AttributeValueXHTML xhtml = firstXhtmlValue();
+ return xhtml == null ? null : xhtml.getElementList();
+ }
+
+ /**
+ * @return the block elements of the object's first XHTML value (see
+ * {@link #getXHTMLContent()}), or null if it has none or no content
+ */
+ public List getXHTMLDivContent() {
+ AttributeValueXHTML xhtml = firstXhtmlValue();
+ return xhtml == null || xhtml.getDivValue() == null ? null : xhtml.getBlocks();
+ }
+
+ /**
+ * @return the object's first XHTML value, in the order of its spec type's
+ * attribute definitions: one of its own values if it has any - one
+ * with content first - otherwise a default; null if there is none
+ * or the node's object is unknown
+ */
+ private AttributeValueXHTML firstXhtmlValue() {
+ if(this.specObject == null) {
+ return null;
+ }
+ Map own = this.specObject.getValues();
+ List ordered = new ArrayList();
+ SpecType type = this.specObject.getResolvedSpecType();
+ if(type != null) {
+ for(String definitionID: type.getAttributeDefinitions().keySet()) {
+ if(own.containsKey(definitionID)) {
+ ordered.add(own.get(definitionID));
+ }
+ }
+ }
+ for(AttributeValue value: own.values()) {
+ if(!ordered.contains(value)) {
+ ordered.add(value);
}
}
- return null;
+ AttributeValueXHTML first = firstXhtml(ordered);
+ // the defaults only matter if the object has no XHTML value of its own
+ return first != null ? first : firstXhtml(this.specObject.getAttributes().values());
}
-
- public List getXHTMLDivContent() {
- for(AttributeValue attributeValue: this.specObject.getAttributes().values()) {
- if(attributeValue instanceof AttributeValueXHTML) {
- XHTMLElementDiv value = ((AttributeValueXHTML)attributeValue).getDivValue();
- if(value != null) {
- return value.getChildren();
+
+ /**
+ * @return the first XHTML value with content, failing that the first
+ * without, as earlier versions returned an empty token list rather
+ * than null for it
+ */
+ private static AttributeValueXHTML firstXhtml(java.util.Collection values) {
+ AttributeValueXHTML withoutContent = null;
+ for(AttributeValue value: values) {
+ if(value instanceof AttributeValueXHTML) {
+ AttributeValueXHTML xhtml = (AttributeValueXHTML) value;
+ if(xhtml.getDivValue() != null) {
+ return xhtml;
+ }
+ if(withoutContent == null) {
+ withoutContent = xhtml;
}
}
}
- return null;
+ return withoutContent;
}
-
+
public String getSpecType() {
return this.specObject.getSpecType();
}
@@ -141,21 +270,116 @@ public boolean isText() {
}
}
+ /**
+ * @return the direct children in document order (a copy)
+ */
+ @Override
public List getChildren() {
- return new ArrayList(this.children.values());
+ return this.children.list();
}
-
+
+ /**
+ * @return all nodes below this one, depth-first in document order
+ */
public List getAllChildren() {
-
- List allChildren = new ArrayList();
- for(SpecHierarchy specHierarchy: this.children.values()) {
- allChildren.add(specHierarchy);
- for(SpecHierarchy child: specHierarchy.getAllChildren()) {
- allChildren.add(child);
- }
+ return this.children.descendants();
+ }
+
+ /**
+ * Appends a child node.
+ *
+ * @throws IllegalArgumentException see {@link #addChild(int, SpecHierarchy)}
+ */
+ public SpecHierarchy addChild(SpecHierarchy child) {
+ return addChild(this.children.size(), child);
+ }
+
+ /**
+ * Inserts a child node at a position, 0 for first.
+ *
+ * @throws IllegalArgumentException if the node is already placed somewhere
+ * (move it with {@link de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent#moveSpecHierarchy}),
+ * a child with the same IDENTIFIER exists, the node is this one or
+ * above it, or the position is out of range
+ */
+ @Override
+ public SpecHierarchy addChild(int index, SpecHierarchy child) {
+ this.children.add(index, child);
+ child.place(this.hierarchyLvl + 1, this.section);
+ markModified();
+ return this;
+ }
+
+ @Override
+ public int getChildCount() {
+ return this.children.size();
+ }
+
+ @Override
+ public SpecHierarchy getChild(String specHierarchyID) {
+ return this.children.get(specHierarchyID);
+ }
+
+ @Override
+ public SpecHierarchy setChildren(List children) {
+ this.children.replace(children);
+ for(SpecHierarchy child: children) {
+ child.place(this.hierarchyLvl + 1, this.section);
+ }
+ markModified();
+ return this;
+ }
+
+ /**
+ * Removes a direct child node, with everything below it.
+ *
+ * @return the removed node, or null if there is no such child
+ */
+ @Override
+ public SpecHierarchy removeChild(String specHierarchyID) {
+ SpecHierarchy removed = this.children.remove(specHierarchyID);
+ if(removed != null) {
+ markModified();
+ }
+ return removed;
+ }
+
+ /**
+ * @return the specification or hierarchy node this node is placed below,
+ * or null if it is not placed
+ */
+ public HierarchyParent getParent() {
+ return this.parent;
+ }
+
+ void setParent(HierarchyParent parent) {
+ this.parent = parent;
+ }
+
+ HierarchyChildren hierarchyChildren() {
+ return this.children;
+ }
+
+ /**
+ * Points this node at another spec object.
+ */
+ public SpecHierarchy setSpecObject(SpecObject specObject) {
+ this.specObject = specObject;
+ this.specObjectRef = specObject == null ? null : specObject.getID();
+ markModified();
+ return this;
+ }
+
+ /**
+ * Sets level and section of this node and everything below it, after it
+ * was inserted somewhere.
+ */
+ void place(int hierarchyLvl, int section) {
+ this.hierarchyLvl = hierarchyLvl;
+ this.section = section;
+ for(SpecHierarchy child: this.children.values()) {
+ child.place(hierarchyLvl + 1, section);
}
-
- return allChildren;
}
@@ -167,42 +391,54 @@ public List getAllChildren() {
*/
public SpecHierarchy(String specHierarchyID, int hierarchyLvl, int section, SpecObject specObject,
List children) {
+ super(specHierarchyID, null);
- this.specHierarchyID = specHierarchyID;
this.hierarchyLvl = hierarchyLvl;
this.section = section;
this.specObject = specObject;
if(children != null) {
- for(SpecHierarchy child: children) {
- this.children.put(child.getSpecHierarchyID(), child);
- }
+ this.children.putAll(children);
}
}
public SpecHierarchy(int hierarchyLvl, int section, Node specHierarchy, Map specObjects) {
+ this(hierarchyLvl, section, specHierarchy, specObjects, ParseIssues.collecting());
+ }
+
+ /**
+ * @param issues receives nested hierarchy nodes that cannot be read or are
+ * declared twice
+ */
+ public SpecHierarchy(int hierarchyLvl, int section, Node specHierarchy, Map specObjects,
+ ParseIssues issues) {
+ super(specHierarchy);
this.hierarchyLvl = hierarchyLvl;
this.section = section;
- this.specHierarchyID = specHierarchy.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent();
- this.alternativeID = XmlUtils.alternativeID(specHierarchy);
Element object = XmlUtils.firstChildElementByLocalName(specHierarchy, ReqIFConst.OBJECT);
Element specObjectRef = XmlUtils.firstDescendantByLocalName(object, ReqIFConst.SPEC_OBJECT_REF);
if(specObjectRef != null) {
- this.specObject = specObjects.get(specObjectRef.getTextContent().trim());
+ // the reference is kept even if it cannot be resolved, so it is not
+ // lost on writing and the validator can report it
+ this.specObjectRef = specObjectRef.getTextContent().trim();
+ this.specObject = specObjects.get(this.specObjectRef);
}
- Element childrenElement = XmlUtils.firstChildElementByLocalName(specHierarchy, ReqIFConst.CHILDREN);
- if(childrenElement != null) {
-
- for(Element newSpecHierarchy: XmlUtils.childElements(childrenElement)) {
-
- String specHierarchyID = XmlUtils.attribute(newSpecHierarchy, ReqIFConst.IDENTIFIER);
+ this.tableInternal = XmlUtils.booleanAttribute(specHierarchy, ReqIFConst.IS_TABLE_INTERNAL);
+ this.editable = XmlUtils.booleanAttribute(specHierarchy, ReqIFConst.IS_EDITABLE);
+ Element editableAtts = XmlUtils.firstChildElementByLocalName(specHierarchy, ReqIFConst.EDITABLE_ATTS);
+ for(Element ref: XmlUtils.childElements(editableAtts)) {
+ this.editableAttributes.add(new java.util.AbstractMap.SimpleImmutableEntry(
+ ref.getTextContent().trim(), XmlUtils.localName(ref)));
+ }
- this.children.put(specHierarchyID, new SpecHierarchy(this.hierarchyLvl+1, section, newSpecHierarchy, specObjects));
- }
+ Element childrenElement = XmlUtils.firstChildElementByLocalName(specHierarchy, ReqIFConst.CHILDREN);
+ for(Element newSpecHierarchy: XmlUtils.childElements(childrenElement)) {
+ SpecHierarchy child = issues.readOrSkip("spec hierarchy",
+ () -> new SpecHierarchy(this.hierarchyLvl+1, section, newSpecHierarchy, specObjects, issues));
+ this.children.put(child, issues);
}
}
-
}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java
index 0273e0a..670cfab 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java
@@ -1,72 +1,52 @@
package de.uni_stuttgart.ils.reqif4j.specification;
-import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import de.uni_stuttgart.ils.reqif4j.attributes.*;
-import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype;
-import org.w3c.dom.Element;
+import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue;
import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
+import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues;
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
-import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;
-public class SpecObject {
+public class SpecObject extends SpecElementWithAttributes {
- protected String id;
protected SpecType specType;
+ private String specTypeRef;
protected String type;
- protected String alternativeID;
protected TypeClassifier typeClassifier = TypeClassifier.defaultClassifier();
- protected Map attributeValues = new HashMap();
- public String getID() {
- return this.id;
- }
-
- /**
- * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null
- */
- public String getAlternativeID() {
- return this.alternativeID;
- }
-
public String getType() {
return this.type;
}
+ /**
+ * @return the kind of the spec type (e.g. SPEC-OBJECT-TYPE), or null if
+ * the type reference could not be resolved
+ */
public String getSpecType() {
- return this.specType.getType();
+ return this.specType == null ? null : this.specType.getType();
}
+ /**
+ * @return the LONG-NAME of the spec type, or null if the type reference
+ * could not be resolved
+ */
public String getSpecTypeName() {
- return this.specType.getName();
+ return this.specType == null ? null : this.specType.getName();
}
/**
- * @return the IDENTIFIER of the spec type this object refers to, or null if
- * the reference could not be resolved
+ * @return the IDENTIFIER of the spec type this object refers to - also if
+ * the reference could not be resolved - or null if it has none
*/
public String getSpecTypeID() {
- return this.specType == null ? null : this.specType.getID();
+ return this.specType != null ? this.specType.getID() : this.specTypeRef;
}
- public Map getAttributes() {
- return this.attributeValues;
- }
-
- public Object getAttribute(String attributeName) {
- AttributeValue attributeValue = this.attributeValues.get(attributeName);
- return attributeValue == null ? null : attributeValue.getValue();
- }
public boolean isReq() {
return this.typeClassifier.isRequirement(this);
@@ -96,8 +76,7 @@ public boolean isText() {
public SpecObject(Node specObject){
- this.id = specObject.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent();
- this.alternativeID = XmlUtils.alternativeID(specObject);
+ super(specObject);
}
/**
@@ -107,14 +86,15 @@ public SpecObject(Node specObject){
*/
public SpecObject(String id, SpecType specType, Collection attributeValues,
TypeClassifier typeClassifier) {
+ super(id, null);
- this.id = id;
this.specType = specType;
this.typeClassifier = typeClassifier == null ? TypeClassifier.defaultClassifier() : typeClassifier;
+ AttributeValues values = resetValues(specType);
if(attributeValues != null) {
for(AttributeValue attributeValue: attributeValues) {
- this.attributeValues.put(attributeValue.getName(), attributeValue);
+ values.put(attributeValue);
}
}
this.type = this.typeClassifier.classify(this);
@@ -129,13 +109,23 @@ public SpecObject(Node specObject, SpecType specType) {
}
public SpecObject(Node specObject, SpecType specType, TypeClassifier typeClassifier) {
+ this(specObject, specType, typeClassifier, ParseIssues.collecting());
+ }
+
+ /**
+ * @param specType the resolved spec type; if null, the object is kept with
+ * its unresolved type reference, but its values cannot be
+ * interpreted and are reported and skipped
+ * @param issues receives the problems found
+ */
+ public SpecObject(Node specObject, SpecType specType, TypeClassifier typeClassifier, ParseIssues issues) {
+ super(specObject);
- this.id = specObject.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent();
- this.alternativeID = XmlUtils.alternativeID(specObject);
this.specType = specType;
+ this.specTypeRef = typeRef(specObject, ReqIFConst.SPEC_OBJECT_TYPE_REF);
this.typeClassifier = typeClassifier == null ? TypeClassifier.defaultClassifier() : typeClassifier;
- readAttributeValues(specObject, specType);
+ readAttributeValues(specObject, specType, issues);
// Classify only after all attribute values are available, so an
// attribute-based classifier (e.g. the ReqIF Implementation Guide
@@ -144,146 +134,28 @@ public SpecObject(Node specObject, SpecType specType, TypeClassifier typeClassif
}
/**
- * Reads all attribute values of the given node and fills in default values
- * for attribute definitions that carry no explicit value. Shared by
+ * The content category may depend on the values (attribute-based
+ * classifiers), so it is derived again.
+ */
+ @Override
+ protected void valuesChanged() {
+ this.type = this.typeClassifier.classify(this);
+ }
+
+ /**
+ * Reads the attribute values of the given node. Shared by
* {@link SpecObject} and {@link SpecRelation}.
*/
protected void readAttributeValues(Node specObject, SpecType specType) {
+ readAttributeValues(specObject, specType, ParseIssues.collecting());
+ }
- // VALUES is located by local name so prefixed ReqIF namespaces work too;
- // only this object's own VALUES is taken, not a nested one.
- Element valuesElement = XmlUtils.firstChildElementByLocalName(specObject, ReqIFConst.VALUES);
- if(valuesElement != null) {
-
- for(Element attribute: XmlUtils.childElements(valuesElement)) {
-
- String attValNodeName = XmlUtils.localName(attribute);
- {
-
- String attributeDefinitionRef = XmlUtils.firstChildElement(
- XmlUtils.firstChildElementByLocalName(attribute, ReqIFConst.DEFINITION)).getTextContent().trim();
- String attributeDefinitionName = specType.getAttributeDefinitions().get(attributeDefinitionRef).getName();
- String attributeValue;
- AttributeDefinition attributeDefinition = specType.getAttributeDefinition(attributeDefinitionRef);
-
- switch(attValNodeName.substring(attValNodeName.lastIndexOf("-")+1)) {
-
- case ReqIFConst.BOOLEAN: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) {
- attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent();
- }else{
- attributeValue = "";
- }
- this.attributeValues.put(attributeDefinitionName, new AttributeValueBoolean(attributeValue, attributeDefinition));
- break;
-
- case ReqIFConst.INTEGER: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) {
- attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent();
- }else{
- attributeValue = "";
- }
- this.attributeValues.put(attributeDefinitionName, new AttributeValueInteger(attributeValue, attributeDefinition));
- break;
-
- case ReqIFConst.STRING: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) {
- attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent();
- }else{
- attributeValue = "";
- }
- this.attributeValues.put(attributeDefinitionName, new AttributeValueString(attributeValue, attributeDefinition));
- break;
-
- case ReqIFConst.ENUMERATION: // Multiselect: read ALL ENUM-VALUE-REF children, not just the first
- List enumRefs = new ArrayList();
- List enumNames = new ArrayList();
- for(Element ref : XmlUtils.descendantsByLocalName(attribute, ReqIFConst.ENUM_VALUE_REF)) {
- String refID = ref.getTextContent().trim();
- enumRefs.add(refID);
- enumNames.add(specType.getEnumValueName(refID));
- }
- this.attributeValues.put(attributeDefinitionName, new AttributeValueEnumeration(enumNames, enumRefs, attributeDefinition));
- break;
-
- case ReqIFConst.XHTML: this.attributeValues.put(attributeDefinitionName, new AttributeValueXHTML(attribute, attributeDefinition));
- break;
-
- case ReqIFConst.DATE: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) {
- attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent();
- }else{
- attributeValue = "";
- }
- this.attributeValues.put(attributeDefinitionName, new AttributeValueDate(attributeValue, attributeDefinition));
- break;
-
- case ReqIFConst.REAL: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) {
- attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent();
- }else{
- attributeValue = "";
- }
- this.attributeValues.put(attributeDefinitionName, new AttributeValueDouble(attributeValue, attributeDefinition));
- break;
-
- // Values of datatype kinds the parser does not model are
- // kept generically instead of being dropped, so they
- // survive a round trip.
- default: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) {
- attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent();
- }else{
- attributeValue = "";
- }
- this.attributeValues.put(attributeDefinitionName,
- new AttributeValue(attributeValue, attributeDefinition)
- .setSourceElementName(XmlUtils.localName(attribute)));
- break;
- }
- }
- }
- }
-
- if(this.attributeValues.size() < specType.getAttributeDefinitions().size()) {
-
- for(AttributeDefinition attributeDefinition: specType.getAttributeDefinitions().values()) {
-
- if(!this.attributeValues.containsKey(attributeDefinition.getName())) {
-
- Datatype datatype = attributeDefinition.getDataType();
-
- if(datatype == null) {
- throw new ExceptionSpecObject("Attribute definition not existing in ReqIF parser\n", attributeDefinition);
- }
-
- switch(attributeDefinition.getDataType().getType()) {
-
- case ReqIFConst.BOOLEAN: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueBoolean(attributeDefinition.getDefaultValue(), attributeDefinition));
- break;
-
- case ReqIFConst.INTEGER: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueInteger(attributeDefinition.getDefaultValue(), attributeDefinition));
- break;
-
- case ReqIFConst.STRING: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueString(attributeDefinition.getDefaultValue(), attributeDefinition));
- break;
-
- case ReqIFConst.ENUMERATION: if(attributeDefinition instanceof AttributeDefinitionEnumeration) {
- AttributeDefinitionEnumeration enumDefinition = (AttributeDefinitionEnumeration) attributeDefinition;
- this.attributeValues.put(attributeDefinition.getName(), new AttributeValueEnumeration(enumDefinition.getDefaultValues(), enumDefinition.getDefaultValueRefs(), attributeDefinition));
- }else{
- this.attributeValues.put(attributeDefinition.getName(), new AttributeValueEnumeration(attributeDefinition.getDefaultValue(), attributeDefinition));
- }
- break;
-
- case ReqIFConst.XHTML: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueXHTML(attributeDefinition.getDefaultValue(), attributeDefinition));
- break;
-
- case ReqIFConst.DATE: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueDate(attributeDefinition.getDefaultValue(), attributeDefinition));
- break;
-
- case ReqIFConst.DOUBLE: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueDouble(attributeDefinition.getDefaultValue(), attributeDefinition));
- break;
-
- default: break;
- }
- }
- }
- }
+ /**
+ * Reads the attribute values of the given node; without a spec type they
+ * cannot be interpreted and are reported and skipped.
+ */
+ protected void readAttributeValues(Node specObject, SpecType specType, ParseIssues issues) {
+ readValues(specObject, specType, getSpecTypeID(), issues);
}
}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java
index 39daf5d..28759a2 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java
@@ -5,13 +5,18 @@
import org.w3c.dom.Node;
import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype;
+import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues;
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
public class SpecObjectType extends SpecType {
public SpecObjectType(Node specType, Map dataTypes) {
- super(specType, dataTypes);
+ this(specType, dataTypes, ParseIssues.collecting());
+ }
+
+ public SpecObjectType(Node specType, Map dataTypes, ParseIssues issues) {
+ super(specType, dataTypes, issues);
this.type = ReqIFConst.SPEC_OBJECT_TYPE;
}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java
index 3a2a053..b79c10e 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java
@@ -3,6 +3,7 @@
import java.util.Collection;
import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue;
+import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues;
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;
import org.w3c.dom.Element;
@@ -40,6 +41,16 @@ public SpecRelation(String id, SpecType specType, String sourceObjID, String tar
}
public SpecRelation(Node specRelation, SpecType specType) {
+ this(specRelation, specType, ParseIssues.collecting());
+ }
+
+ /**
+ * @param specType the resolved relation type; if null, the relation is
+ * kept with its unresolved type reference, but its values
+ * cannot be interpreted and are reported and skipped
+ * @param issues receives the problems found
+ */
+ public SpecRelation(Node specRelation, SpecType specType, ParseIssues issues) {
super(specRelation);
this.specType = specType;
@@ -58,9 +69,16 @@ public SpecRelation(Node specRelation, SpecType specType) {
// relationTypeRef, not in the inherited type field.
this.type = ReqIFConst.UNDEFINED;
- if (specType != null) {
- readAttributeValues(specRelation, specType);
- }
+ readAttributeValues(specRelation, specType, issues);
+ }
+
+ /**
+ * @return the IDENTIFIER of the relation type - also if the reference
+ * could not be resolved - or null if it has none
+ */
+ @Override
+ public String getSpecTypeID() {
+ return this.specType != null ? this.specType.getID() : this.relationTypeRef;
}
public String getSourceObjID() {
@@ -71,6 +89,24 @@ public String getTargetObjID() {
return this.targetObjID;
}
+ /**
+ * Points the relation at another source object.
+ */
+ public SpecRelation setSourceObjID(String sourceObjID) {
+ this.sourceObjID = sourceObjID;
+ markModified();
+ return this;
+ }
+
+ /**
+ * Points the relation at another target object.
+ */
+ public SpecRelation setTargetObjID(String targetObjID) {
+ this.targetObjID = targetObjID;
+ markModified();
+ return this;
+ }
+
/**
* @return the IDENTIFIER of the SPEC-RELATION-TYPE this relation refers to
* (SPEC-RELATION-TYPE-REF), or null if the document declares none
@@ -87,6 +123,11 @@ public String getRelationTypeName() {
return this.specType == null ? null : this.specType.getName();
}
+ // A relation has no content category, whatever its values.
+ @Override
+ protected void valuesChanged() {
+ }
+
// A relation is none of the content categories.
@Override
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java
index f1a3445..10014de 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java
@@ -5,13 +5,18 @@
import org.w3c.dom.Node;
import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype;
+import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues;
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
public class SpecRelationType extends SpecType {
public SpecRelationType(Node specType, Map dataTypes) {
- super(specType, dataTypes);
+ this(specType, dataTypes, ParseIssues.collecting());
+ }
+
+ public SpecRelationType(Node specType, Map dataTypes, ParseIssues issues) {
+ super(specType, dataTypes, issues);
this.type = ReqIFConst.SPEC_RELATION_TYPE;
}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java
index b0c456c..095732e 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java
@@ -9,33 +9,21 @@
import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype;
import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeEnumeration;
+import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable;
+import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues;
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;
-public class SpecType {
+public class SpecType extends Identifiable {
private Map attributeDefinitions = new LinkedHashMap();
- private String id;
- protected String name;
protected String type;
- protected String alternativeID;
protected String sourceElementName;
- public String getID() {
- return this.id;
- }
-
- /**
- * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null
- */
- public String getAlternativeID() {
- return this.alternativeID;
- }
-
/**
* @return the SPEC-TYPE element name this type was read from, or null when
* it was not created from a document. Needed to write back spec type
@@ -46,10 +34,6 @@ public String getSourceElementName() {
return this.sourceElementName;
}
- public String getName() {
- return this.name;
- }
-
public String getType() {
return this.type;
}
@@ -126,9 +110,8 @@ public String getEnumValueOtherContent(String id) {
* @param kind SPEC-OBJECT-TYPE, SPECIFICATION-TYPE or SPEC-RELATION-TYPE
*/
public SpecType(String id, String name, String kind) {
+ super(id, name);
- this.id = id;
- this.name = name;
this.type = kind;
}
@@ -138,60 +121,56 @@ public SpecType(String id, String name, String kind) {
public SpecType addAttributeDefinition(AttributeDefinition attributeDefinition) {
this.attributeDefinitions.put(attributeDefinition.getID(), attributeDefinition);
+ markModified();
return this;
}
public SpecType(Node specType, Map dataTypes) {
-
- this.id = specType.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent();
- this.name = specType.getAttributes().getNamedItem(ReqIFConst.LONG_NAME).getTextContent();
- this.alternativeID = XmlUtils.alternativeID(specType);
+ this(specType, dataTypes, ParseIssues.collecting());
+ }
+
+ /**
+ * @param issues receives attribute definitions that cannot be read or are
+ * declared twice
+ */
+ public SpecType(Node specType, Map dataTypes, ParseIssues issues) {
+ super(specType);
+
this.sourceElementName = XmlUtils.localName(specType);
this.type = ReqIFConst.UNDEFINED;
//Doors relationship definitionen habe keine ChildNodes
Node specAttributes = XmlUtils.firstChildElementByLocalName(specType, ReqIFConst.SPEC_ATTRIBUTES);
- if(specAttributes != null) {
-
- for(Node attributeDefinition: XmlUtils.childElements(specAttributes)) {
-
- String attDefNodeName = XmlUtils.localName(attributeDefinition);
- {
-
- String attDefID = attributeDefinition.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent();
-
- switch(attDefNodeName.substring(attDefNodeName.lastIndexOf("-")+1)) {
-
- case ReqIFConst.BOOLEAN: this.attributeDefinitions.put(attDefID, new AttributeDefinitionBoolean(attributeDefinition, dataTypes));
- break;
-
- case ReqIFConst.INTEGER: this.attributeDefinitions.put(attDefID, new AttributeDefinitionInteger(attributeDefinition, dataTypes));
- break;
-
- case ReqIFConst.STRING: this.attributeDefinitions.put(attDefID, new AttributeDefinitionString(attributeDefinition, dataTypes));
- break;
-
- case ReqIFConst.ENUMERATION: this.attributeDefinitions.put(attDefID, new AttributeDefinitionEnumeration(attributeDefinition, dataTypes));
- break;
-
- case ReqIFConst.XHTML: this.attributeDefinitions.put(attDefID, new AttributeDefinitionXHTML(attributeDefinition, dataTypes));
- break;
-
- case ReqIFConst.DATE: this.attributeDefinitions.put(attDefID, new AttributeDefinitionDate(attributeDefinition, dataTypes));
- break;
-
- case ReqIFConst.DOUBLE: this.attributeDefinitions.put(attDefID, new AttributeDefinitionDouble(attributeDefinition, dataTypes));
- break;
- //Convert REAL to DOUBLE
- case ReqIFConst.REAL: this.attributeDefinitions.put(attDefID, new AttributeDefinitionDouble(attributeDefinition, dataTypes));
- break;
-
- default: this.attributeDefinitions.put(attDefID, new AttributeDefinition(attributeDefinition, dataTypes));
- break;
- }
+ for(Node attributeDefinition: XmlUtils.childElements(specAttributes)) {
+
+ AttributeDefinition definition = issues.readOrSkip("attribute definition",
+ () -> readAttributeDefinition(attributeDefinition, dataTypes));
+ if(definition != null) {
+ if(definition.getDataType() == null) {
+ issues.report(definition.getID(), "Attribute definition references a datatype that is not declared "
+ + "in DATATYPES; it and its values are kept as written");
}
+ issues.put(this.attributeDefinitions, definition, "Attribute definition");
}
}
}
+ private static AttributeDefinition readAttributeDefinition(Node attributeDefinition, Map dataTypes) {
+
+ String attDefNodeName = XmlUtils.localName(attributeDefinition);
+ switch(ReqIFConst.kindOf(attDefNodeName)) {
+
+ case ReqIFConst.BOOLEAN: return new AttributeDefinitionBoolean(attributeDefinition, dataTypes);
+ case ReqIFConst.INTEGER: return new AttributeDefinitionInteger(attributeDefinition, dataTypes);
+ case ReqIFConst.STRING: return new AttributeDefinitionString(attributeDefinition, dataTypes);
+ case ReqIFConst.ENUMERATION: return new AttributeDefinitionEnumeration(attributeDefinition, dataTypes);
+ case ReqIFConst.XHTML: return new AttributeDefinitionXHTML(attributeDefinition, dataTypes);
+ case ReqIFConst.DATE: return new AttributeDefinitionDate(attributeDefinition, dataTypes);
+ // REAL is read as DOUBLE
+ case ReqIFConst.DOUBLE:
+ case ReqIFConst.REAL: return new AttributeDefinitionDouble(attributeDefinition, dataTypes);
+ default: return new AttributeDefinition(attributeDefinition, dataTypes);
+ }
+ }
+
}
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java
index a99c65d..4c82be3 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java
@@ -1,118 +1,184 @@
package de.uni_stuttgart.ils.reqif4j.specification;
import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition;
-import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinitionEnumeration;
import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue;
-import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueBoolean;
-import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueDate;
-import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueDouble;
-import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueEnumeration;
-import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueInteger;
-import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueString;
-import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML;
+import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues;
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;
import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;
-public class Specification {
+public class Specification extends SpecElementWithAttributes implements HierarchyParent {
- private String id;
- private String name;
- private String alternativeID;
private SpecType type;
- private int numberOfSpecObjects;
- private int sectionCounter = 0;
- private Map attributeValues = new HashMap();
- private Map children = new LinkedHashMap();
- private List allSpecHierarchies = new ArrayList();
+ private String specTypeRef;
+ private final HierarchyChildren children = new HierarchyChildren(this);
- public String getID() {
- return this.id;
- }
-
- /**
- * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null
- */
- public String getAlternativeID() {
- return this.alternativeID;
- }
/**
* @return the value of an attribute named "Description", or null if the
* specification has no such attribute
*/
public String getDescription() {
- AttributeValue description = this.attributeValues.get("Description");
+ AttributeValue description = getAttributes().get("Description");
return description == null ? null : (String) description.getValue();
}
- public String getName() {
- return this.name;
- }
public String getSpecType() {
- return this.type.getType();
+ return this.type == null ? null : this.type.getType();
}
public String getSpecTypeName() {
- return this.type.getName();
+ return this.type == null ? null : this.type.getName();
}
/**
- * @return the IDENTIFIER of the specification type, or null if the
- * reference could not be resolved
+ * @return the IDENTIFIER of the specification type - also if the
+ * reference could not be resolved - or null if it has none
*/
public String getSpecTypeID() {
- return this.type == null ? null : this.type.getID();
+ return this.type != null ? this.type.getID() : this.specTypeRef;
}
- public Map getAttributes() {
- return this.attributeValues;
- }
-
- public Object getAttribute(String attributeName) {
- AttributeValue attributeValue = this.attributeValues.get(attributeName);
- return attributeValue == null ? null : attributeValue.getValue();
- }
+ /**
+ * @return the number of hierarchy nodes, at all levels
+ */
public int getNumberOfSpecObjects() {
- return this.numberOfSpecObjects;
+ return this.children.descendants().size();
}
-
+
+ /**
+ * @return the number of top-level hierarchy nodes (sections)
+ */
public int getNumberOfSections() {
- return this.sectionCounter;
+ return this.children.size();
}
+ /**
+ * @return the top-level hierarchy nodes in document order (a copy)
+ */
+ @Override
public List getChildren() {
-
- return new ArrayList(this.children.values());
+ return this.children.list();
}
public List getLvlOneSpecHierarchies() {
List children = new ArrayList();
- for(SpecHierarchy specHierarchy: this.children.values()) {
+ for(SpecHierarchy specHierarchy: this.children.list()) {
children.add(specHierarchy.getSpecObject());
}
return children;
}
-
+
+ /**
+ * @return all hierarchy nodes, depth-first in document order (read-only)
+ */
public List getAllSpecHierarchies() {
- return this.allSpecHierarchies;
+ return this.children.descendants();
+ }
+
+ /**
+ * @return the hierarchy node with this IDENTIFIER at any level, or null
+ */
+ public SpecHierarchy getSpecHierarchy(String specHierarchyID) {
+ return specHierarchyID == null ? null : this.children.descendant(specHierarchyID);
+ }
+
+ /**
+ * Appends a top-level hierarchy node.
+ *
+ * @throws IllegalArgumentException see {@link #addChild(int, SpecHierarchy)}
+ */
+ public Specification addChild(SpecHierarchy child) {
+ return addChild(this.children.size(), child);
+ }
+
+ /**
+ * Inserts a top-level hierarchy node at a position, 0 for first; the
+ * sections of the following nodes shift.
+ *
+ * @throws IllegalArgumentException if the node is already placed somewhere
+ * (move it with {@link de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent#moveSpecHierarchy}),
+ * a node with the same IDENTIFIER is already at the top level, or
+ * the position is out of range
+ */
+ @Override
+ public Specification addChild(int index, SpecHierarchy child) {
+ this.children.add(index, child);
+ if(index == this.children.size() - 1) {
+ // appended: nothing else moves
+ child.place(1, index + 1);
+ } else {
+ renumber(index);
+ }
+ markModified();
+ return this;
+ }
+
+ /**
+ * Removes a top-level hierarchy node, with everything below it.
+ *
+ * @return the removed node, or null if there is no such node
+ */
+ @Override
+ public SpecHierarchy removeChild(String specHierarchyID) {
+ int position = this.children.indexOf(specHierarchyID);
+ SpecHierarchy removed = this.children.remove(specHierarchyID);
+ if(removed != null) {
+ // the sections before it stay as they are
+ renumber(position);
+ markModified();
+ }
+ return removed;
+ }
+
+ @Override
+ public int getChildCount() {
+ return this.children.size();
+ }
+
+ @Override
+ public SpecHierarchy getChild(String specHierarchyID) {
+ return this.children.get(specHierarchyID);
+ }
+
+ @Override
+ public Specification setChildren(List children) {
+ this.children.replace(children);
+ renumber(0);
+ markModified();
+ return this;
+ }
+
+ /**
+ * Top-level nodes are the sections, numbered from 1.
+ *
+ * @param from the first position whose section may have changed
+ */
+ private void renumber(int from) {
+ int index = 0;
+ for(SpecHierarchy child: this.children.values()) {
+ if(index >= from) {
+ child.place(1, index + 1);
+ }
+ index++;
+ }
+ }
+
+ HierarchyChildren hierarchyChildren() {
+ return this.children;
}
@@ -124,191 +190,52 @@ public List getAllSpecHierarchies() {
*/
public Specification(String id, String name, SpecType specType,
java.util.Collection attributeValues, List children) {
+ super(id, name);
- this.id = id;
- this.name = name;
this.type = specType;
+ AttributeValues values = resetValues(specType);
if(attributeValues != null) {
for(AttributeValue attributeValue: attributeValues) {
- this.attributeValues.put(attributeValue.getName(), attributeValue);
+ values.put(attributeValue);
}
}
if(children != null) {
- for(SpecHierarchy child: children) {
- this.children.put(child.getSpecHierarchyID(), child);
- this.sectionCounter++;
- }
+ this.children.putAll(children);
}
-
- List allChildren = new ArrayList();
- for(SpecHierarchy specHierarchy: this.children.values()) {
- allChildren.add(specHierarchy);
- allChildren.addAll(specHierarchy.getAllChildren());
- }
- this.allSpecHierarchies.addAll(allChildren);
- this.numberOfSpecObjects = allChildren.size();
}
public Specification(Node specification, SpecType specType, Map