Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion FEHLERANALYSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,25 @@ die bei jedem Push in der GitHub-Actions-Pipeline (`.github/workflows/ci.yml`, `
| Multiselect-Enums, Enum-Defaults, Null-Sicherheit | "Fix multiselect enumerations, enum defaults and enum null-safety" | `MultiselectEnumTest` |
| Bild-/Objekt-Pipeline, Bild-Zuordnung, Zip-Slip | "Fix image/object conversion pipeline" | `ImagePipelineTest` |
| Crash-/Robustheitsfehler (Abschnitt 3 + 4.1) | "Fix crash bugs and robustness issues" | `RobustnessTest` |
| LONG-NAME-Heuristik (4.2) → konfigurierbare Strategie | "Make spec object type classification pluggable" | `TypeClassifierTest` |
| Attributbasierte Klassifizierung (Implementor-Guide-Profil) | "Add attribute-based ReqIF Implementation Guide classifier" | `ImplementationGuideClassifierTest` |

Zur Heuristik (4.2): Die Klassifizierung ist jetzt eine Strategie (`TypeClassifier`),
die das fertig geparste `SpecObject` (inkl. Attributwerte) erhält.
Der Default (`LongNameTypeClassifier`) verhält sich exakt wie bisher; eigene Regeln
lassen sich über `new ReqIF(pfad, classifier)` bzw. `new ReqIFz(pfad, classifier)`
injizieren. Zusätzlich liegt mit `ReqIFImplementationGuideClassifier` eine
attributbasierte Implementierung bei, die nach den standardisierten Attributnamen
des ProSTEP-Implementor-Forums (`ReqIF.ChapterName`, `ReqIF.Text`) klassifiziert —
tool-übergreifend robuster als die Namensheuristik und die empfohlene Wahl für
DOORS-/Polarion-Exporte. Details siehe README-Abschnitt „Type classification".

Hinweis: `REQ`/`SUB-REQ`/`HEADLINE`/`TEXT` sind **keine** offiziellen ReqIF-Typen,
sondern parserinterne Inhaltskategorien. Der OMG-Standard definiert nur strukturelle
Typen (`SPEC-OBJECT-TYPE` usw.) mit frei vergebenen Namen/Attributen.

Bewusst (noch) nicht angefasst, da Verhaltensänderungen für bestehende Nutzer:
die LONG-NAME-Heuristik der Typklassifizierung (4.2), die `type`-Semantik von `SpecRelation` (4.3),
die `type`-Semantik von `SpecRelation` (4.3),
HTML-Escaping/Attribut-Erhalt in `toString()` (4.8) sowie die kosmetischen Punkte aus Abschnitt 5
(bis auf den entfernten `javax.xml.crypto.Data`-Import).

Expand Down
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,82 @@ Every push and pull request runs the test suite via GitHub Actions
fixed parser defects documented in `FEHLERANALYSE.md` (namespace-prefixed
XHTML, multiselect enumerations, image/object conversion, picture lookup
in .reqifz archives, and crash robustness).

# Type classification

ReqIF has no semantic "this is a requirement" flag. The categories
`REQ`, `SUB-REQ`, `HEADLINE` and `TEXT` are **this parser's own** content
categories, not official ReqIF types — the standard only defines structural
types (`SPEC-OBJECT-TYPE`, `SPECIFICATION-TYPE`, `SPEC-RELATION-TYPE`) with
free-form `LONG-NAME`s and free-form attributes. What an object *means* is a
convention of the exporting tool or exchange profile.

Classification is therefore a pluggable strategy (`TypeClassifier`). The
strategy receives the fully parsed `SpecObject`, so it may decide based on
the spec type name, the attribute values, or both. Two implementations ship
with the library.

## Default: `LongNameTypeClassifier`

The default applies a substring heuristic on the spec type name
("req", "sub", "headline"). It fits profiles whose type names follow that
convention, but misses e.g. German type names or exports that only encode
the content kind in attributes. It is used automatically when no classifier
is passed.

## Recommended for DOORS/Polarion exports: `ReqIFImplementationGuideClassifier`

The ProSTEP iViP / ReqIF Implementor Forum "ReqIF Implementation Guide"
standardizes **attribute names** (while type names stay free-form). Tools
such as IBM DOORS, PTC and Polarion emit:

- `ReqIF.ChapterName` — set on heading objects
- `ReqIF.Text` — the requirement/description body

This classifier decides by those attributes, independent of the type name,
which is more robust and tool-independent:

```java
import de.uni_stuttgart.ils.reqif4j.specification.ReqIFImplementationGuideClassifier;

ReqIF reqif = new ReqIF("doors-export.reqif", new ReqIFImplementationGuideClassifier());

// custom attribute names, if your profile differs:
ReqIF reqif2 = new ReqIF("export.reqif",
new ReqIFImplementationGuideClassifier("Heading", "Body"));
```

Rule: non-empty `ReqIF.ChapterName` → `HEADLINE`; else non-empty
`ReqIF.Text` → `REQ`; else `TEXT`. Note that the guide does not distinguish
normative requirements from informational text (both use `ReqIF.Text`), so
text-bearing objects are reported as `REQ`; use a custom classifier if your
profile marks requirements with an extra attribute.

## Custom classifier

Any other convention can be implemented directly:

```java
TypeClassifier classifier = new TypeClassifier() {
@Override
public String classify(SpecObject specObject) {
String name = specObject.getSpecTypeName().toLowerCase();
if (name.contains("anforderung")) return ReqIFConst.REQ;
if (name.contains("überschrift")) return ReqIFConst.HEADLINE;
return ReqIFConst.TEXT;
}

@Override
public boolean isRequirement(SpecObject specObject) {
return ReqIFConst.REQ.equals(specObject.getType());
}

@Override
public boolean isSubRequirement(SpecObject specObject) {
return false;
}
};

ReqIF reqif = new ReqIF("spec.reqif", classifier); // .reqif
ReqIFz reqifz = new ReqIFz("archive.reqifz", classifier); // .reqifz
```
13 changes: 12 additions & 1 deletion src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIF.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.io.InputStream;
import java.util.HashMap;

import de.uni_stuttgart.ils.reqif4j.specification.TypeClassifier;

public class ReqIF extends ReqIFFile {


Expand All @@ -26,11 +28,20 @@ public ReqIFCoreContent getReqIFCoreContent() {


public ReqIF(String filePath) throws FileNotFoundException {
this(filePath, TypeClassifier.defaultClassifier());
}

/**
* @param typeClassifier strategy deciding how spec objects are classified
* (requirement/headline/text); null uses the default
* LONG-NAME heuristic
*/
public ReqIF(String filePath, TypeClassifier typeClassifier) throws FileNotFoundException {

this.path = filePath;
this.name = extractFileName(filePath);

this.reqifDocuments.put(this.name, new ReqIFDocument(filePath));
this.reqifDocuments.put(this.name, new ReqIFDocument(filePath, typeClassifier));
this.numberOfReqIFDocuments = 1;

this.picturesIS = new HashMap<String, InputStream>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ public List<SpecHierarchy> getOrderedSpecHierarchyList() {


public ReqIFCoreContent(Element coreContent) {
this(coreContent, TypeClassifier.defaultClassifier());
}

public ReqIFCoreContent(Element coreContent, TypeClassifier typeClassifier) {

if (typeClassifier == null) {
typeClassifier = TypeClassifier.defaultClassifier();
}


if (coreContent.getElementsByTagName("DATATYPES").item(0).hasChildNodes()) {
Expand Down Expand Up @@ -186,7 +194,7 @@ public ReqIFCoreContent(Element coreContent) {
String specObjID = specObj.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent();
String specObjTypeRef = ((Element) specObj).getElementsByTagName(ReqIFConst.SPEC_OBJECT_TYPE_REF).item(0).getTextContent();

this.specObjects.put(specObjID, new SpecObject(specObj, this.specTypes.get(specObjTypeRef)));
this.specObjects.put(specObjID, new SpecObject(specObj, this.specTypes.get(specObjTypeRef), typeClassifier));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import de.uni_stuttgart.ils.reqif4j.specification.TypeClassifier;

public class ReqIFDocument {


private Document reqifDocument;

protected String filePath;
private String fileName;
private TypeClassifier typeClassifier = TypeClassifier.defaultClassifier();

private ReqIFHeader header;
private ReqIFCoreContent content;
Expand All @@ -42,9 +45,14 @@ public ReqIFCoreContent getCoreContent() {


public ReqIFDocument(String filePath) throws FileNotFoundException {
this(filePath, TypeClassifier.defaultClassifier());
}

public ReqIFDocument(String filePath, TypeClassifier typeClassifier) throws FileNotFoundException {

this.filePath = filePath;
this.fileName = extractFileName(filePath);
setTypeClassifier(typeClassifier);

try {
this.reqifDocument = newDocumentBuilder().parse(this.filePath);
Expand All @@ -56,9 +64,14 @@ public ReqIFDocument(String filePath) throws FileNotFoundException {
}

public ReqIFDocument(InputStream is, String filePath) throws FileNotFoundException {
this(is, filePath, TypeClassifier.defaultClassifier());
}

public ReqIFDocument(InputStream is, String filePath, TypeClassifier typeClassifier) throws FileNotFoundException {

this.filePath = filePath;
this.fileName = extractFileName(filePath);
setTypeClassifier(typeClassifier);

try {
this.reqifDocument = newDocumentBuilder().parse(is);
Expand All @@ -70,9 +83,14 @@ public ReqIFDocument(InputStream is, String filePath) throws FileNotFoundExcepti
}

public ReqIFDocument(InputStream is, String zipFilePath, String fileName) {
this(is, zipFilePath, fileName, TypeClassifier.defaultClassifier());
}

public ReqIFDocument(InputStream is, String zipFilePath, String fileName, TypeClassifier typeClassifier) {

this.filePath = zipFilePath;
this.fileName = fileName;
setTypeClassifier(typeClassifier);

try {
this.reqifDocument = newDocumentBuilder().parse(is);
Expand All @@ -83,6 +101,10 @@ public ReqIFDocument(InputStream is, String zipFilePath, String fileName) {
}
}

private void setTypeClassifier(TypeClassifier typeClassifier) {
this.typeClassifier = typeClassifier == null ? TypeClassifier.defaultClassifier() : typeClassifier;
}


/**
* Creates a namespace-aware, XXE-hardened document builder. Namespace
Expand Down Expand Up @@ -119,7 +141,7 @@ private void readDocument() {
if (this.reqifDocument.getElementsByTagName(ReqIFConst.CORE_CONTENT).getLength() == 0) {
throw new ReqIFParseException("Document contains no " + ReqIFConst.CORE_CONTENT + " element: " + this.fileName);
}
this.content = new ReqIFCoreContent((Element) this.reqifDocument.getElementsByTagName(ReqIFConst.CORE_CONTENT).item(0));
this.content = new ReqIFCoreContent((Element) this.reqifDocument.getElementsByTagName(ReqIFConst.CORE_CONTENT).item(0), this.typeClassifier);
}

private static String extractFileName(String path) {
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,22 @@
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import de.uni_stuttgart.ils.reqif4j.specification.TypeClassifier;

public class ReqIFz extends ReqIFFile {

private static final String EXTRACTION_SUFFIX = "_unzipped";

public ReqIFz(String filePath) throws IOException {
this(filePath, TypeClassifier.defaultClassifier());
}

/**
* @param typeClassifier strategy deciding how spec objects are classified
* (requirement/headline/text); null uses the default
* LONG-NAME heuristic
*/
public ReqIFz(String filePath, TypeClassifier typeClassifier) throws IOException {

this.path = filePath;
this.name = extractFileName(filePath);
Expand Down Expand Up @@ -63,7 +74,7 @@ public ReqIFz(String filePath) throws IOException {
if (zipEntry.getName().endsWith("reqif")) {
this.numberOfReqIFDocuments++;
try (InputStream reqifIS = new FileInputStream(newFile)) {
this.reqifDocuments.put(zipEntry.getName(), new ReqIFDocument(reqifIS, filePath, zipEntry.getName()));
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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package de.uni_stuttgart.ils.reqif4j.specification;

import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue;
import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst;

/**
* Default {@link TypeClassifier}: the historic LONG-NAME substring heuristic.
*
* A spec type whose name contains "req" is a requirement type ("sub" + "req"
* a sub-requirement type), a name containing "headline" is a headline type,
* everything else is text. Whether a requirement-typed object really is a
* requirement is decided by the first boolean attribute whose name contains
* "req" (respectively "sub" and "req").
*
* This matches tool profiles with type names like "Req", "SubReq" and
* "Headline". For other conventions provide your own {@link TypeClassifier}.
*/
public class LongNameTypeClassifier implements TypeClassifier {

static final LongNameTypeClassifier INSTANCE = new LongNameTypeClassifier();

@Override
public String classify(SpecObject specObject) {

String name = specObject.getSpecTypeName() == null ? "" : specObject.getSpecTypeName().toLowerCase();

if (name.contains(ReqIFConst.REQ.toLowerCase())) {
if (name.contains(ReqIFConst.SUB.toLowerCase())) {
return ReqIFConst.SUB_REQ;
}
return ReqIFConst.REQ;
}
if (name.contains(ReqIFConst.HEADLINE.toLowerCase())) {
return ReqIFConst.HEADLINE;
}
return ReqIFConst.TEXT;
}

@Override
public boolean isRequirement(SpecObject specObject) {

if (!ReqIFConst.REQ.equals(specObject.getType())) {
return false;
}
for (AttributeValue attributeValue : specObject.getAttributes().values()) {

if (isBoolean(attributeValue)
&& attributeValue.getName().toLowerCase().contains(ReqIFConst.REQ.toLowerCase())) {
return Boolean.TRUE.equals(attributeValue.getValue());
}
}
return false;
}

@Override
public boolean isSubRequirement(SpecObject specObject) {

if (!ReqIFConst.SUB_REQ.equals(specObject.getType())) {
return false;
}
for (AttributeValue attributeValue : specObject.getAttributes().values()) {

if (isBoolean(attributeValue)
&& attributeValue.getName().toLowerCase().contains(ReqIFConst.SUB.toLowerCase())
&& attributeValue.getName().toLowerCase().contains(ReqIFConst.REQ.toLowerCase())) {
return Boolean.TRUE.equals(attributeValue.getValue());
}
}
return false;
}

private static boolean isBoolean(AttributeValue attributeValue) {
return attributeValue.getAttributeDefinitionType() != null
&& attributeValue.getAttributeDefinitionType().getDataType() != null
&& ReqIFConst.BOOLEAN.equals(attributeValue.getAttributeDefinitionType().getDataType().getType());
}
}
Loading
Loading