From 9e7c534ecd20affe3cffe96c85c0cd8770545c32 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 17:50:07 +0000 Subject: [PATCH 1/7] Add error analysis of ReqIF parser Documents all identified defects, grouped by severity: - Multiselect enums: only first ENUM-VALUE-REF is parsed, single-string data model, lost enum defaults - Image/object conversion: invalid toString output, path separator corruption, namespace prefix mismatch, broken picture map lookup, resource leaks, zip-slip - Crashes on valid files: null map key for unknown datatypes, missing null checks, NumberFormatExceptions, ClassCastException in getXHTMLContent, System.exit in library code - Robustness and code quality issues Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011mat2d7AJkouKhXWUYzHxs --- FEHLERANALYSE.md | 117 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 FEHLERANALYSE.md diff --git a/FEHLERANALYSE.md b/FEHLERANALYSE.md new file mode 100644 index 0000000..fafdfb7 --- /dev/null +++ b/FEHLERANALYSE.md @@ -0,0 +1,117 @@ +# Fehleranalyse ReqIF-Parser (reqif4j) + +Stand: 2026-07-23 · Analysierte Dateien: alle 62 Java-Klassen unter `de/uni_stuttgart/ils/reqif4j/` + +Die Fehler sind nach Schweregrad gruppiert. Datei- und Zeilenangaben beziehen sich auf den aktuellen Stand des Branches. + +--- + +## 1. Kritisch: Multiselect-Enumerationen (vom Nutzer gemeldet) + +### 1.1 Nur der erste Enum-Wert wird gelesen +- **`specification/SpecObject.java:173`** und identisch **`specification/Specification.java`** (ENUMERATION-Case): + ```java + String enumValueRef = ((Element)attribute).getElementsByTagName(ReqIFConst.VALUES) + .item(0).getChildNodes().item(1).getTextContent(); + ``` + Es wird ausschließlich `item(1)` gelesen — also der **erste** `ENUM-VALUE-REF`. Bei Multiselect-Enums (mehrere ``-Kindelemente unter ``) werden alle weiteren Werte **stillschweigend verworfen**. Das bestätigt auch der TODO-Kommentar in Zeile 174 („check how it is behaving if more than one enum value exits"). + +### 1.2 Datenmodell kann nur einen Wert speichern +- **`attributes/AttributeValueEnumeration.java`**: Die Klasse speichert den Wert als einzelnen `String` (geerbt von `AttributeValue`). Für Multiselect müsste hier eine `List` (bzw. Liste von Enum-Wert-Objekten mit Name/Key/Other-Content) vorgehalten werden. Es gibt außerdem keinen Zugriff auf `KEY`/`OTHER-CONTENT` des aufgelösten Wertes. + +### 1.3 Default-Werte von Enum-Attributen gehen verloren +- **`attributes/AttributeDefinition.java:51–58`**: Default-Werte werden nur über das XML-Attribut `THE-VALUE` gelesen. Bei Enumerationen besteht der `DEFAULT-VALUE` laut ReqIF-Spezifikation aber aus einem eingebetteten `ATTRIBUTE-VALUE-ENUMERATION` mit `ENUM-VALUE-REF`-Kindern → `defaultValue` bleibt `null`. Fehlt der Wert am SpecObject, entsteht eine `AttributeValueEnumeration` mit `value == null`. + +### 1.4 Fehlende Null-Checks in der Enum-Auflösung (NPE-Gefahr) +- **`specification/SpecType.java:63–91`**: `getEnumValueKey()` und `getEnumValueOtherContent()` prüfen — anders als `getEnumValueName()` (Zeile 49) — **nicht** auf `attributeDefinition.getDataType() == null` → `NullPointerException`, sobald eine Attributdefinition einen unbekannten Datentyp referenziert. +- **`datatypes/DatatypeEnumeration.java:35–41`**: `getEnumValueKey()`/`getEnumValueOtherContent()` werfen NPE bei unbekannter ID; nur `getEnumValueName()` ist abgesichert. +- **`specification/SpecType.java:45–61`**: Bei nicht auflösbarem `enumValueRef` wird stillschweigend `""` geliefert — Fehler werden verschluckt statt gemeldet. + +--- + +## 2. Kritisch: Bild-/Objekt-Konvertierung (vom Nutzer gemeldet) + +### 2.1 `toString()` erzeugt ungültiges XHTML +- **`xhtml/XHTMLElementObject.java:28–34`**: + ```java + sb.append('<').append(tagName).append(" ").append(data).append('>'); + ``` + Ausgabe ist z. B. `` — es fehlen Attributname und Anführungszeichen (`data="…"`). Auch `type`, `width`, `height` werden nicht übernommen. Das Ergebnis ist kein gültiges (X)HTML und kann von keinem Renderer als Bild dargestellt werden. + +### 2.2 Pfad-Umschreibung zerstört URIs +- **`xhtml/XHTMLElementObject.java:18, 24`** und **`attributes/AttributeValueXHTML.java:144`**: `data.replace("/", System.getProperty("file.separator"))` wandelt den URI-Pfad des `data`-Attributs in einen OS-Pfad um. Unter Windows wird aus `files/image.png` → `files\image.png`. Im XHTML-Output (2.1) sind Backslashes falsch, und der Schlüssel passt nicht mehr zu den Bild-Maps (siehe 2.5), die mit `/`-Pfaden aus dem Zip befüllt werden. + +### 2.3 NPE bei `object` ohne `data`-Attribut; kein Fallback-Handling +- **`xhtml/XHTMLElementObject.java:18, 24`**: `getNamedItem("data")` wird ohne Null-Check dereferenziert. Die ReqIF-Spezifikation schreibt zudem für Nicht-PNG-Objekte ein verschachteltes Fallback-`object` (PNG-Alternative) vor — dieses wird nicht ausgewertet. + +### 2.4 Namespace-Präfixe: Bilder (und alles andere XHTML) werden gar nicht erst erkannt +- **`xhtml/XHTMLElement.java:52–103`**: Der `switch` vergleicht `getNodeName()` (nach Entfernen von Ziffern) mit **unpräfixierten** Konstanten (`"object"`, `"p"`, `"div"` …). Reale ReqIF-Dateien serialisieren XHTML aber mit Namespace-Präfix (`reqif-xhtml:object`, `xhtml:p` …). Da der `DocumentBuilderFactory` (in `ReqIFDocument`) **nicht namespace-aware** konfiguriert ist, enthält `getNodeName()` das Präfix → kein Case matcht → alle Elemente landen im `default`-Zweig als generische `XHTMLNode`, deren `toString()` nur ein leeres Tag-Paar ausgibt. **Bilder und Textinhalte gehen komplett verloren.** +- Gleiches Problem in **`attributes/AttributeValueXHTML.java:27, 48`**: `getElementsByTagName(XHTML.DIV)` („div") findet `xhtml:div` nicht → `item(0) == null` → NPE im Konstruktor von `XHTMLElementDiv`. + +### 2.5 Bild-Zuordnung über Dateinamen ist inkonsistent und liefert `null` +- **`reqif/ReqIFFile.java:44–46`**: `getPicturesInputStreams(name)` sucht mit `name.split("\\.")[0]` (z. B. `"spec"`), die Map wird aber sowohl in `ReqIF.java:52` als auch in `ReqIFz.java:64–66` mit dem **vollen Dateinamen** (`"spec.reqif"`) befüllt → Lookup liefert **immer `null`**. +- **`reqif/ReqIFz.java:53`**: `reqifBaseName` wird berechnet, aber nie verwendet; stattdessen bekommen **alle** ReqIF-Dokumente im Archiv dieselbe komplette Bild-Map zugeordnet — keine echte Zuordnung Bild ↔ Dokument. + +### 2.6 Fragile Pfad-Logik beim Bilderordner +- **`reqif/ReqIF.java:37`**: `this.path.split("\\.")[0]` soll die Dateierweiterung entfernen, bricht aber bei jedem Punkt im Pfad (z. B. `./data/v1.2/spec.reqif` → `"/data/v1"`) — der Bilderordner wird dann nicht gefunden. +- **`reqif/ReqIF.java:31`**, **`reqif/ReqIFDocument.java:50, 74`**: Dateiname wird per `lastIndexOf(System.getProperty("file.separator"))` extrahiert — schlägt fehl bei `/`-Pfaden unter Windows bzw. gemischten Separatoren. + +### 2.7 Ressourcen-Leaks und Zip-Sicherheit +- **`reqif/ReqIF.java:47`**, **`reqif/ReqIFz.java:56`**: Für jedes Bild werden `FileInputStream`s geöffnet und **nie geschlossen** (Leak; unter Windows bleiben Dateien gesperrt). +- **`reqif/ReqIFz.java:30`**: `new File(destDir, zipEntry.getName())` ohne Kanonisierungs-Check → **Zip-Slip-Schwachstelle** (Einträge wie `../../x` schreiben außerhalb des Zielordners). + +### 2.8 Tote Bild-Extraktion in der „Deconstruction" +- **`attributes/AttributeValueXHTML.java:44–69, 141–145`**: `deconstructXHTML()` übergibt rohe Node-Namen (`"xhtml:p"`, `"xhtml:object"` …), `decostructXHTMLElement()` vergleicht aber mit `"P"`, `"TBL"`, `"L"`, `"H"`, `"OBJ"`. Der Code, der die Namen auf diese Token abbildete, ist **auskommentiert** (Zeilen 54–66) → der `OBJ`-Zweig (Bildpfad-Extraktion) und alle anderen Zweige werden **nie** erreicht; die Elementliste enthält nur leere Inhaltslisten. + +--- + +## 3. Hoch: Crashes / Datenverlust bei validen ReqIF-Dateien + +1. **`reqif/ReqIFCoreContent.java:133`**: Im `default`-Zweig wird `this.dataTypes.put(null, …)` aufgerufen — Key ist `null` statt `dataTypeID`. Unbekannte Datentypen sind damit nicht referenzierbar; Folge sind `ExceptionSpecObject`/NPEs an anderer Stelle. +2. **`reqif/ReqIFCoreContent.java:105–111`**: `MIN`/`MAX` (Integer) und `MAX-LENGTH` (String) werden ohne Null-Check gelesen — laut ReqIF-Spezifikation sind diese Attribute optional → NPE. +3. **`datatypes/DatatypeInteger.java:27–28`**: `Integer.parseInt()` für `MIN`/`MAX` — ReqIF erlaubt `xsd:integer` (beliebig groß); DOORS exportiert häufig Werte im Long-Bereich → `NumberFormatException`. +4. **`attributes/AttributeValueInteger.java:11`** / **`AttributeValueDouble.java:10`**: `parseInt("")`/`parseDouble("")` werfen `NumberFormatException`. `SpecObject` übergibt bei fehlendem `THE-VALUE` genau `""` (INTEGER-Case, Zeile 160). Der REAL-Case nutzt dagegen `"0.0"` — inkonsistent; der INTEGER-Pfad crasht. +5. **`specification/SpecHierarchy.java:59–66`**: `getXHTMLContent()` castet `attributeValue.getValue()` auf `AttributeValueXHTMLElementList`. `AttributeValueXHTML.getValue()` ist aber überschrieben und liefert einen **String** → garantierte `ClassCastException`, sobald die Methode aufgerufen wird. +6. **`specification/Specification.java`**: + - Der Werte-Switch kennt keine `DATE`-, `REAL`-, `DOUBLE`-Cases (anders als `SpecObject`) → diese Attributwerte werden ignoriert; der Default-Werte-Switch kennt ebenfalls kein `DATE`/`DOUBLE` → Attribute fehlen anschließend komplett. + - Im Default-Werte-Zweig fehlt der Null-Check auf `getDataType()` (in `SpecObject:212` vorhanden) → NPE statt aussagekräftiger `ExceptionSpecObject`. +7. **`reqif/ReqIFDocument.java:65–68, 86–89, 109–112`**: Bei Parse-Fehlern wird `System.exit(1)` aufgerufen — eine Bibliothek darf die JVM des Aufrufers nicht beenden; Fehler werden zudem nicht propagiert (deklariertes `FileNotFoundException` wird nie geworfen). +8. **Systemisch — Whitespace-Index-Annahme `item(1)`**: An vielen Stellen wird das zweite Kind (`.getChildNodes().item(1)`) als „erstes Element" angenommen. Das funktioniert nur bei pretty-printed XML mit Whitespace-Textknoten; bei minifizierten Dateien wird das falsche Kind gelesen oder es kommt zur NPE. Betroffen u. a.: `DatatypeEnumeration.java:52, 59, 61`, `AttributeDefinition.java:48, 54`, `SpecObject.java:142, 173`, `Specification.java` (DEFINITION/ENUM-Refs), `SpecType.java:103–105`, `AttributeValueXHTML.java:235` (`decontructTable`). +9. **`reqif/ReqIFHeader.java:70`**: `split("Created by: ")[1]` → `ArrayIndexOutOfBoundsException`, wenn der `COMMENT` diesen (tool-spezifischen) Text nicht enthält. Zeile 73–74: `date[2]` → AIOOBE bei unerwartetem `CREATION-TIME`-Format. + +--- + +## 4. Mittel: Logik- und Robustheitsfehler + +1. **`xhtml/XHTMLElement.java:15–17`**: `hasChildren()` ist invertiert — gibt `children.isEmpty()` zurück statt `!children.isEmpty()`. +2. **`specification/SpecObject.java:117–130`**: Typklassifizierung (REQ/SUB-REQ/HEADLINE/TEXT) per Substring-Heuristik auf dem `LONG-NAME` des SpecTypes — sprach- und toolabhängig, liefert bei fremden Profilen falsche Ergebnisse. Ebenso `isReq()`/`isSubReq()` (Namens-Heuristik auf Attributnamen). +3. **`specification/SpecRelation.java:22`**: `this.type` wird mit der **Referenz-ID** des Relationstyps überschrieben, obwohl `type` in der Basisklasse die Semantik REQ/HEADLINE/TEXT hat (`getType()`/`isReq()` etc. liefern damit Unsinn). Attributwerte der Relation werden gar nicht geparst. +4. **`specification/SpecObject.java:46`** / **`Specification.java` (`getAttribute`, `getDescription`)**: NPE bei unbekanntem Attributnamen; `getDescription()` setzt hart ein Attribut „Description" voraus. +5. **`reqif/ReqIFDocument.java`**: `DocumentBuilderFactory` ohne Schutz gegen XXE/Entity-Expansion (Sicherheitslücke bei fremden Dateien) und ohne `setNamespaceAware(true)` (Ursache von 2.4). +6. **`attributes/AttributeValueXHTML.java:181–184`** (`list()`): Im `var`-Zweig wird versehentlich `child` (das `li`) statt `listChild` (das `var`) ausgewertet; verschachtelte Listen erzeugen zudem unbalancierte `"/L"`-Marker (nur schließend, nie öffnend im Ergebnis). +7. **`attributes/AttributeValueXHTML.java:209–245`** (`decontructTable`): Nur `item(1)` einer Zelle wird gelesen — Zellen mit mehreren Kindelementen (Formatierung, Bilder, Umbrüche) verlieren Inhalt; `thead`/`th` werden nicht von `tbody`/`td` unterschieden. +8. **`xhtml/XHTMLNode.toString()` / alle Element-`toString()`**: Sämtliche XML-Attribute (`style`, `colspan`, `align` …) gehen bei der Ausgabe verloren; `XHTMLElementText` escaped Sonderzeichen (`<`, `&`) nicht; `
` wird als `

` ausgegeben. +9. **`xhtml/XHTMLElement.java:52`**: `replaceAll("[0-9]","")` soll `h1`–`h6` auf `h` mappen, entfernt aber Ziffern aus **allen** Tag-Namen — ein Tag wie `h2` in Kombination mit Präfixen oder exotischen Namen wird falsch klassifiziert. +10. **`reqif/ReqIFz.java:53`**: `zipEntry.getName().split("\\.")[0]` bricht bei Punkten im Verzeichnis-/Dateinamen (gleiches Muster wie 2.6). + +--- + +## 5. Niedrig: Code-Qualität / Kosmetik + +1. **`datatypes/DatatypeBoolean.java` / `DatatypeXHTML.java` / `DatatypeEnumeration.java`**: private Felder `id`/`name` verschatten die Basisklasse (inkl. redundanter Getter). +2. **`datatypes/DatatypeDate.java:5`**: unbenutzter (und sinnfreier) Import `javax.xml.crypto.Data`. +3. **`specification/ExceptionSpecObject.java`**: Meldungstext ohne Trennzeichen/Zeilenumbrüche zwischen ID/Name/Typ. +4. **`reqif/ReqIFCoreContent.java:128`**: `case A, B ->`-Syntax (Java 14+) inmitten von sonst altem Stil — Build bricht auf älteren Toolchains. +5. **`attributes/AttributeValueDate.java`**: Datum wird als roher String gespeichert, keine Validierung/Parsing (`getValue()` castet ggf. `null`). +6. **`reqif/ReqIFHeader.java:77`**: `replace("_Template", "")` — tool-spezifischer Hack im generischen Parser. +7. Große Mengen auskommentierten Codes (`AttributeValueXHTML`, `Specification`, `ReqIFHeader`) und Tippfehler in Methodennamen (`decostructXHTMLElement`, `decontructTable`). + +--- + +## Empfohlene Reihenfolge für die Behebung + +1. **Namespace-Handling** (2.4 + 5.5): `setNamespaceAware(true)` + Vergleich über `getLocalName()` — Voraussetzung dafür, dass XHTML/Bilder überhaupt ankommen. +2. **Multiselect-Enums** (1.1–1.3): Schleife über alle `ENUM-VALUE-REF`s, `AttributeValueEnumeration` auf Listen umstellen, Enum-Default-Werte korrekt lesen. +3. **Bild-Pipeline** (2.1–2.3, 2.5–2.7): korrektes `data="…"`-Attribut, keine Separator-Ersetzung, konsistente Map-Keys, Streams schließen, Zip-Slip-Check. +4. **Crash-Fixes** (Abschnitt 3): Null-Checks, `put(null, …)`-Fix, leere Strings beim Zahlparsen, `System.exit` entfernen, `getXHTMLContent()`-Cast. +5. Danach die Punkte aus Abschnitt 4/5. From b97f2711823c745606a7f4732cbfe2fd457bf8ae Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 18:01:33 +0000 Subject: [PATCH 2/7] Set up Maven build and GitHub Actions CI - Move sources to standard Maven layout (src/main/java) - Add pom.xml (Java 17, JUnit 5, Surefire) - Add CI workflow running mvn verify on every push and pull request Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011mat2d7AJkouKhXWUYzHxs --- .github/workflows/ci.yml | 30 ++++++++++++++ .gitignore | 36 ++--------------- pom.xml | 39 +++++++++++++++++++ .../attributes/AttributeDefinition.java | 0 .../AttributeDefinitionBoolean.java | 0 .../attributes/AttributeDefinitionDate.java | 0 .../attributes/AttributeDefinitionDouble.java | 0 .../AttributeDefinitionEnumeration.java | 0 .../AttributeDefinitionInteger.java | 0 .../attributes/AttributeDefinitionString.java | 0 .../attributes/AttributeDefinitionXHTML.java | 0 .../reqif4j/attributes/AttributeValue.java | 0 .../attributes/AttributeValueBoolean.java | 0 .../attributes/AttributeValueDate.java | 0 .../attributes/AttributeValueDouble.java | 0 .../attributes/AttributeValueEnumeration.java | 0 .../attributes/AttributeValueInteger.java | 0 .../attributes/AttributeValueString.java | 0 .../attributes/AttributeValueXHTML.java | 0 .../AttributeValueXHTMLElementList.java | 0 .../ils/reqif4j/attributes/XHTML.java | 0 .../ils/reqif4j/datatypes/Datatype.java | 0 .../reqif4j/datatypes/DatatypeBoolean.java | 0 .../ils/reqif4j/datatypes/DatatypeDate.java | 0 .../ils/reqif4j/datatypes/DatatypeDouble.java | 0 .../datatypes/DatatypeEnumeration.java | 0 .../datatypes/DatatypeEnumerationValue.java | 0 .../reqif4j/datatypes/DatatypeInteger.java | 0 .../ils/reqif4j/datatypes/DatatypeString.java | 0 .../ils/reqif4j/datatypes/DatatypeXHTML.java | 0 .../ils/reqif4j/reqif/ReqIF.java | 0 .../ils/reqif4j/reqif/ReqIFConst.java | 0 .../ils/reqif4j/reqif/ReqIFCoreContent.java | 0 .../ils/reqif4j/reqif/ReqIFDocument.java | 0 .../ils/reqif4j/reqif/ReqIFFile.java | 0 .../ils/reqif4j/reqif/ReqIFHeader.java | 0 .../ils/reqif4j/reqif/ReqIFz.java | 0 .../specification/ExceptionSpecObject.java | 0 .../reqif4j/specification/SpecHierarchy.java | 0 .../ils/reqif4j/specification/SpecObject.java | 0 .../reqif4j/specification/SpecObjectType.java | 0 .../reqif4j/specification/SpecRelation.java | 0 .../specification/SpecRelationType.java | 0 .../ils/reqif4j/specification/SpecType.java | 0 .../reqif4j/specification/Specification.java | 0 .../specification/SpecificationType.java | 0 .../ils/reqif4j/xhtml/XHTMLElement.java | 0 .../ils/reqif4j/xhtml/XHTMLElementBr.java | 0 .../ils/reqif4j/xhtml/XHTMLElementDiv.java | 0 .../ils/reqif4j/xhtml/XHTMLElementH.java | 0 .../ils/reqif4j/xhtml/XHTMLElementLi.java | 0 .../ils/reqif4j/xhtml/XHTMLElementObject.java | 0 .../ils/reqif4j/xhtml/XHTMLElementP.java | 0 .../ils/reqif4j/xhtml/XHTMLElementSpan.java | 0 .../ils/reqif4j/xhtml/XHTMLElementTBody.java | 0 .../ils/reqif4j/xhtml/XHTMLElementTHead.java | 0 .../ils/reqif4j/xhtml/XHTMLElementTbl.java | 0 .../ils/reqif4j/xhtml/XHTMLElementTd.java | 0 .../ils/reqif4j/xhtml/XHTMLElementText.java | 0 .../ils/reqif4j/xhtml/XHTMLElementTh.java | 0 .../ils/reqif4j/xhtml/XHTMLElementTr.java | 0 .../ils/reqif4j/xhtml/XHTMLElementUl.java | 0 .../ils/reqif4j/xhtml/XHTMLElementVar.java | 0 .../ils/reqif4j/xhtml/XHTMLLeaf.java | 0 .../ils/reqif4j/xhtml/XHTMLNode.java | 0 65 files changed, 72 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 pom.xml rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionBoolean.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionDate.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionDouble.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionInteger.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionString.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionXHTML.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeValue.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeValueBoolean.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDate.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeValueString.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTMLElementList.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/attributes/XHTML.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/datatypes/Datatype.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/datatypes/DatatypeBoolean.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDate.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDouble.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/datatypes/DatatypeXHTML.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/reqif/ReqIF.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/specification/ExceptionSpecObject.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/specification/SpecObject.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/specification/SpecType.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/specification/Specification.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/specification/SpecificationType.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementBr.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementDiv.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementH.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementLi.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementObject.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementP.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementSpan.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTBody.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTHead.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTbl.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTd.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementText.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTh.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTr.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementUl.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementVar.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLLeaf.java (100%) rename {de => src/main/java/de}/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9e2d63a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + pull_request: + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + cache: maven + + - name: Build and run tests + run: mvn -B --no-transfer-progress verify + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: surefire-reports + path: target/surefire-reports + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 7aef1e4..b2e433c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,34 +1,4 @@ -# Test -Main.java - -# Build -out - -# Intellij -.idea -*.iml - -# Compiled class file +target/ *.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* +.idea/ +*.iml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d00ede7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + de.uni_stuttgart.ils + reqif4j + 0.1.0-SNAPSHOT + jar + + reqif4j + Java parser for ReqIF (Requirements Interchange Format) documents + + + 17 + UTF-8 + 5.10.2 + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionBoolean.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionBoolean.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionBoolean.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionBoolean.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionDate.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionDate.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionDate.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionDate.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionDouble.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionDouble.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionDouble.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionDouble.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionInteger.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionInteger.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionInteger.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionInteger.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionString.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionString.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionString.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionString.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionXHTML.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionXHTML.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionXHTML.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionXHTML.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValue.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValue.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeValue.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValue.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueBoolean.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueBoolean.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueBoolean.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueBoolean.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDate.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDate.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDate.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDate.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueString.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueString.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueString.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueString.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTMLElementList.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTMLElementList.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTMLElementList.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTMLElementList.java diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/XHTML.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/XHTML.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/attributes/XHTML.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/XHTML.java diff --git a/de/uni_stuttgart/ils/reqif4j/datatypes/Datatype.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/Datatype.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/datatypes/Datatype.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/Datatype.java diff --git a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeBoolean.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeBoolean.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeBoolean.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeBoolean.java diff --git a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDate.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDate.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDate.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDate.java diff --git a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDouble.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDouble.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDouble.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDouble.java diff --git a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java diff --git a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java diff --git a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java diff --git a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java diff --git a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeXHTML.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeXHTML.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeXHTML.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeXHTML.java diff --git a/de/uni_stuttgart/ils/reqif4j/reqif/ReqIF.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIF.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/reqif/ReqIF.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIF.java diff --git a/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java diff --git a/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java diff --git a/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java diff --git a/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java diff --git a/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java diff --git a/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java diff --git a/de/uni_stuttgart/ils/reqif4j/specification/ExceptionSpecObject.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/ExceptionSpecObject.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/specification/ExceptionSpecObject.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/specification/ExceptionSpecObject.java diff --git a/de/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java diff --git a/de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java diff --git a/de/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java diff --git a/de/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java diff --git a/de/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java diff --git a/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/specification/SpecType.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java diff --git a/de/uni_stuttgart/ils/reqif4j/specification/Specification.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/specification/Specification.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java diff --git a/de/uni_stuttgart/ils/reqif4j/specification/SpecificationType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecificationType.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/specification/SpecificationType.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecificationType.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementBr.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementBr.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementBr.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementBr.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementDiv.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementDiv.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementDiv.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementDiv.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementH.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementH.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementH.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementH.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementLi.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementLi.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementLi.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementLi.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementObject.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementObject.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementObject.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementObject.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementP.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementP.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementP.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementP.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementSpan.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementSpan.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementSpan.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementSpan.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTBody.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTBody.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTBody.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTBody.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTHead.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTHead.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTHead.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTHead.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTbl.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTbl.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTbl.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTbl.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTd.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTd.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTd.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTd.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementText.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementText.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementText.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementText.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTh.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTh.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTh.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTh.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTr.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTr.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTr.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementTr.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementUl.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementUl.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementUl.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementUl.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementVar.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementVar.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementVar.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementVar.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLLeaf.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLLeaf.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLLeaf.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLLeaf.java diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java similarity index 100% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java From 4ec5824ab09d128c4919b51600a8c4450ac9e3b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 18:07:27 +0000 Subject: [PATCH 3/7] Fix XHTML namespace handling and harden the XML parser - Parse documents namespace-aware and match XHTML elements by local name, so prefixed content (xhtml:div, reqif-xhtml:object, ...) is no longer dropped or causes NPEs - Expose XHTML tag names without namespace prefix - Replace System.exit(1) on parse errors with ReqIFParseException - Disallow DOCTYPE and external entities (XXE hardening) - Navigate TYPE/DEFINITION/DEFAULT-VALUE/SPEC-ATTRIBUTES by element instead of fixed child indices, so minified XML no longer breaks - Add XmlUtils helper for namespace/whitespace-independent DOM access - Tests: XHTMLNamespaceTest, ParserHardeningTest Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011mat2d7AJkouKhXWUYzHxs --- .../attributes/AttributeDefinition.java | 21 ++- .../attributes/AttributeValueXHTML.java | 21 ++- .../ils/reqif4j/reqif/ReqIFConst.java | 1 + .../ils/reqif4j/reqif/ReqIFDocument.java | 167 ++++++++++-------- .../reqif4j/reqif/ReqIFParseException.java | 17 ++ .../ils/reqif4j/specification/SpecObject.java | 6 +- .../ils/reqif4j/specification/SpecType.java | 6 +- .../reqif4j/specification/Specification.java | 6 +- .../ils/reqif4j/util/XmlUtils.java | 136 ++++++++++++++ .../ils/reqif4j/xhtml/XHTMLElement.java | 7 +- .../ils/reqif4j/xhtml/XHTMLNode.java | 14 +- .../ils/reqif4j/ParserHardeningTest.java | 55 ++++++ .../ils/reqif4j/TestFixtures.java | 164 +++++++++++++++++ .../ils/reqif4j/XHTMLNamespaceTest.java | 68 +++++++ 14 files changed, 584 insertions(+), 105 deletions(-) create mode 100644 src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFParseException.java create mode 100644 src/main/java/de/uni_stuttgart/ils/reqif4j/util/XmlUtils.java create mode 100644 src/test/java/de/uni_stuttgart/ils/reqif4j/ParserHardeningTest.java create mode 100644 src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java create mode 100644 src/test/java/de/uni_stuttgart/ils/reqif4j/XHTMLNamespaceTest.java diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java index a7bfab8..433f586 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java @@ -8,6 +8,7 @@ import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; public class AttributeDefinition { @@ -44,14 +45,18 @@ public AttributeDefinition(Node attributeDefinition, Map dataT this.id = attributeDefinition.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent(); this.name = attributeDefinition.getAttributes().getNamedItem(ReqIFConst.LONG_NAME).getTextContent(); - Element attDef = (Element) attributeDefinition; - String typeID = attDef.getElementsByTagName(ReqIFConst.TYPE).item(0).getChildNodes().item(1).getTextContent(); - this.type = dataTypes.get(typeID); - - NodeList defVal = attDef.getElementsByTagName(ReqIFConst.DEFAULT_VALUE); - if(defVal.getLength() > 0) { - - Node attDefVal = defVal.item(0).getChildNodes().item(1); + // Navigate by element (not by fixed child index) so both pretty-printed + // and minified ReqIF files are handled. + Element typeElement = XmlUtils.firstChildElementByLocalName(attributeDefinition, ReqIFConst.TYPE); + Element typeRef = XmlUtils.firstChildElement(typeElement); + if(typeRef != null) { + this.type = dataTypes.get(typeRef.getTextContent().trim()); + } + + Element defVal = XmlUtils.firstChildElementByLocalName(attributeDefinition, ReqIFConst.DEFAULT_VALUE); + if(defVal != null) { + + Node attDefVal = XmlUtils.firstChildElement(defVal); if(attDefVal != null && attDefVal.hasAttributes() && attDefVal.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) != null) { this.defaultValue = attDefVal.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java index 4c77424..90ce579 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java @@ -7,10 +7,11 @@ import org.w3c.dom.Node; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLElementDiv; public class AttributeValueXHTML extends AttributeValue { - + XHTMLElementDiv divValue; @Override @@ -23,8 +24,11 @@ public Object getValue() { public AttributeValueXHTML(Node xhtmlContent, AttributeDefinition type) { super(type); - - this.divValue = new XHTMLElementDiv(((Element)xhtmlContent).getElementsByTagName(XHTML.DIV).item(0)); + + // The div may carry a namespace prefix (xhtml:div, reqif-xhtml:div, ...), + // so it has to be located by local name. + Node div = XmlUtils.firstDescendantByLocalName(xhtmlContent, XHTML.DIV); + this.divValue = div == null ? null : new XHTMLElementDiv(div); this.value = deconstructXHTML(xhtmlContent); } @@ -42,11 +46,14 @@ public XHTMLElementDiv getDivValue() { private AttributeValueXHTMLElementList deconstructXHTML(Node xhtmlContent) { - + AttributeValueXHTMLElementList xhtmlElementList = new AttributeValueXHTMLElementList(); - - Node div = ((Element)xhtmlContent).getElementsByTagName(XHTML.DIV).item(0); - + + Node div = XmlUtils.firstDescendantByLocalName(xhtmlContent, XHTML.DIV); + if(div == null) { + return xhtmlElementList; + } + for(int e=0; e < div.getChildNodes().getLength(); e++) { Node xhtmlElement = div.getChildNodes().item(e); String elementName = div.getChildNodes().item(e).getNodeName(); 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 02e39c9..eb4c4f4 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 @@ -18,6 +18,7 @@ public class ReqIFConst { public final static String DEFAULT_VALUE = "DEFAULT-VALUE"; public final static String DEFINITION = "DEFINITION"; public final static String SPEC_TYPES = "SPEC-TYPES"; + public final static String SPEC_ATTRIBUTES = "SPEC-ATTRIBUTES"; public final static String SPEC_OBJECTS = "SPEC-OBJECTS"; public final static String SPEC_OBJECT = "SPEC-OBJECT"; public final static String SPEC_RELATIONS = "SPEC-RELATIONS"; 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 212cc3e..d25e56a 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.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -12,104 +13,118 @@ import org.xml.sax.SAXException; public class ReqIFDocument { - - - private DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - private DocumentBuilder builder; + + private Document reqifDocument; - + protected String filePath; private String fileName; - + private ReqIFHeader header; private ReqIFCoreContent content; - - - public String getFilePath() { - return this.filePath; - } - - public String getFileName() { - return this.fileName; - } - + + + public String getFilePath() { + return this.filePath; + } + + public String getFileName() { + return this.fileName; + } + public ReqIFHeader getHeader() { - return this.header; - } - - public ReqIFCoreContent getCoreContent() { - return this.content; - } - - - - + return this.header; + } + + public ReqIFCoreContent getCoreContent() { + return this.content; + } + + public ReqIFDocument(String filePath) throws FileNotFoundException { - + this.filePath = filePath; - this.fileName = this.filePath.substring(filePath.lastIndexOf(System.getProperty("file.separator"))+1); - + this.fileName = extractFileName(filePath); + try { - - this.builder = factory.newDocumentBuilder(); - this.reqifDocument = this.builder.parse(this.filePath); - - if(this.reqifDocument.getElementsByTagName(ReqIFConst.THE_HEADER).getLength() > 0 && this.reqifDocument.getElementsByTagName(ReqIFConst.THE_HEADER).item(0).hasChildNodes()) { - header = new ReqIFHeader((Element)this.reqifDocument.getElementsByTagName(ReqIFConst.THE_HEADER).item(0)); - } - content = new ReqIFCoreContent((Element)this.reqifDocument.getElementsByTagName(ReqIFConst.CORE_CONTENT).item(0)); - - this.builder = null; - this.factory = null; - + this.reqifDocument = newDocumentBuilder().parse(this.filePath); + readDocument(); + } catch (SAXException | IOException | ParserConfigurationException e) { - e.printStackTrace(); - System.exit(1); + throw new ReqIFParseException("Failed to parse ReqIF document " + filePath, e); } } - + public ReqIFDocument(InputStream is, String filePath) throws FileNotFoundException { - + this.filePath = filePath; - this.fileName = this.filePath.substring(filePath.lastIndexOf(System.getProperty("file.separator"))+1); - + this.fileName = extractFileName(filePath); + try { - - this.builder = factory.newDocumentBuilder(); - this.reqifDocument = this.builder.parse(is); - - if(this.reqifDocument.getElementsByTagName(ReqIFConst.THE_HEADER).getLength() > 0 && this.reqifDocument.getElementsByTagName(ReqIFConst.THE_HEADER).item(0).hasChildNodes()) { - header = new ReqIFHeader((Element)this.reqifDocument.getElementsByTagName(ReqIFConst.THE_HEADER).item(0)); - } - content = new ReqIFCoreContent((Element)this.reqifDocument.getElementsByTagName(ReqIFConst.CORE_CONTENT).item(0)); - + this.reqifDocument = newDocumentBuilder().parse(is); + readDocument(); + } catch (SAXException | IOException | ParserConfigurationException e) { - e.printStackTrace(); - System.exit(1); + throw new ReqIFParseException("Failed to parse ReqIF document " + filePath, e); } } - + public ReqIFDocument(InputStream is, String zipFilePath, String fileName) { - - this.filePath = zipFilePath; // TODO + + this.filePath = zipFilePath; this.fileName = fileName; - + try { - - this.builder = factory.newDocumentBuilder(); - - this.reqifDocument = this.builder.parse(is); - - if(this.reqifDocument.getElementsByTagName(ReqIFConst.THE_HEADER).getLength() > 0 && this.reqifDocument.getElementsByTagName(ReqIFConst.THE_HEADER).item(0).hasChildNodes()) { - header = new ReqIFHeader((Element)this.reqifDocument.getElementsByTagName(ReqIFConst.THE_HEADER).item(0)); - } - content = new ReqIFCoreContent((Element)this.reqifDocument.getElementsByTagName(ReqIFConst.CORE_CONTENT).item(0)); - - + this.reqifDocument = newDocumentBuilder().parse(is); + readDocument(); + } catch (SAXException | IOException | ParserConfigurationException e) { - e.printStackTrace(); - System.exit(1); + throw new ReqIFParseException("Failed to parse ReqIF document " + fileName + " in " + zipFilePath, e); } } - + + + /** + * Creates a namespace-aware, XXE-hardened document builder. Namespace + * awareness is required so XHTML content with namespace prefixes + * (e.g. {@code xhtml:div}) can be matched by local name. + */ + private static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException { + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + + // Harden against XXE / entity expansion attacks + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + try { + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + } catch (IllegalArgumentException ignored) { + // parser implementation does not support these attributes + } + + return factory.newDocumentBuilder(); + } + + private void readDocument() { + + if (this.reqifDocument.getElementsByTagName(ReqIFConst.THE_HEADER).getLength() > 0 + && this.reqifDocument.getElementsByTagName(ReqIFConst.THE_HEADER).item(0).hasChildNodes()) { + this.header = new ReqIFHeader((Element) this.reqifDocument.getElementsByTagName(ReqIFConst.THE_HEADER).item(0)); + } + 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)); + } + + private static String extractFileName(String path) { + int lastSeparator = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); + return path.substring(lastSeparator + 1); + } + } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFParseException.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFParseException.java new file mode 100644 index 0000000..06e9818 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFParseException.java @@ -0,0 +1,17 @@ +package de.uni_stuttgart.ils.reqif4j.reqif; + +/** + * Thrown when a ReqIF document cannot be parsed. Replaces the former + * {@code System.exit(1)} behavior, which terminated the host JVM on any + * malformed input. + */ +public class ReqIFParseException extends RuntimeException { + + public ReqIFParseException(String message, Throwable cause) { + super(message, cause); + } + + public ReqIFParseException(String message) { + super(message); + } +} 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 9b1d927..53895dc 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 @@ -10,6 +10,7 @@ import org.w3c.dom.NodeList; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; public class SpecObject { @@ -138,8 +139,9 @@ public SpecObject(Node specObject, SpecType specType) { Node attribute = attributeValues.item(attval); String attValNodeName = attribute.getNodeName(); if(!attValNodeName.equals(ReqIFConst._TEXT)) { - - String attributeDefinitionRef = ((Element)attribute).getElementsByTagName(ReqIFConst.DEFINITION).item(0).getChildNodes().item(1).getTextContent(); + + 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); 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 638a78a..9dbfb2e 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 @@ -10,6 +10,7 @@ import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeEnumeration; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; public class SpecType { @@ -100,9 +101,10 @@ public SpecType(Node specType, Map dataTypes) { this.type = ReqIFConst.UNDEFINED; //Doors relationship definitionen habe keine ChildNodes - if(specType.getChildNodes().getLength() > 0 && !(specType.getChildNodes().item(1) == null)) { + Node specAttributes = XmlUtils.firstChildElementByLocalName(specType, ReqIFConst.SPEC_ATTRIBUTES); + if(specAttributes != null) { - NodeList attributeDefinitions = specType.getChildNodes().item(1).getChildNodes(); + NodeList attributeDefinitions = specAttributes.getChildNodes(); for(int specatt = 0; specatt < attributeDefinitions.getLength(); specatt++) { 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 a8ebf8e..052b49c 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 @@ -18,6 +18,7 @@ import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueString; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; public class Specification { @@ -110,8 +111,9 @@ public Specification(Node specification, SpecType specType, Map= 0 ? nodeName.substring(colon + 1) : nodeName; + } + + /** + * @return all direct child nodes of type ELEMENT_NODE. + */ + public static List childElements(Node parent) { + List elements = new ArrayList(); + if (parent == null) { + return elements; + } + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + if (children.item(i).getNodeType() == Node.ELEMENT_NODE) { + elements.add((Element) children.item(i)); + } + } + return elements; + } + + /** + * @return the first direct child of type ELEMENT_NODE, or null. + */ + public static Element firstChildElement(Node parent) { + if (parent == null) { + return null; + } + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + if (children.item(i).getNodeType() == Node.ELEMENT_NODE) { + return (Element) children.item(i); + } + } + return null; + } + + /** + * @return the first direct child element with the given local name, or null. + */ + public static Element firstChildElementByLocalName(Node parent, String localName) { + if (parent == null) { + return null; + } + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE && localName(child).equals(localName)) { + return (Element) child; + } + } + return null; + } + + /** + * @return all descendant elements (document order) with the given local name. + */ + public static List descendantsByLocalName(Node root, String localName) { + List result = new ArrayList(); + collectDescendants(root, localName, result); + return result; + } + + /** + * @return the first descendant element (document order) with the given local + * name, or null. + */ + public static Element firstDescendantByLocalName(Node root, String localName) { + List descendants = descendantsByLocalName(root, localName); + return descendants.isEmpty() ? null : descendants.get(0); + } + + private static void collectDescendants(Node node, String localName, List result) { + if (node == null) { + return; + } + NodeList children = node.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + if (localName(child).equals(localName)) { + result.add((Element) child); + } + collectDescendants(child, localName, result); + } + } + } + + /** + * @return the text content of the named attribute, or null if the node has no + * such attribute. + */ + public static String attribute(Node node, String attributeName) { + if (node == null || node.getAttributes() == null) { + return null; + } + Node attribute = node.getAttributes().getNamedItem(attributeName); + return attribute == null ? null : attribute.getTextContent(); + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java index 9356fb0..83b256e 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java @@ -7,6 +7,7 @@ import org.w3c.dom.NodeList; import de.uni_stuttgart.ils.reqif4j.attributes.XHTML; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; public class XHTMLElement extends XHTMLNode { @@ -47,8 +48,10 @@ private void addChildren(Node xhtmlElement) { for(int child = 0; child < children.getLength(); child++) { Node childNode = children.item(child); - String nodeName = childNode.getNodeName(); - + // Match by local name so namespace-prefixed XHTML (xhtml:p, + // reqif-xhtml:object, ...) is recognized; strip digits to map h1-h6 to h. + String nodeName = XmlUtils.localName(childNode); + switch (nodeName.replaceAll("[0-9]", "")) { case XHTML.BR: this.children.add(new XHTMLElementBr(childNode, this)); diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java index af59547..610ec0d 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java @@ -2,6 +2,8 @@ import org.w3c.dom.Node; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; + import java.util.List; @@ -10,10 +12,10 @@ public class XHTMLNode { protected String tagName; protected XHTMLNode parent = null; protected Node node; - - + + /** - * @return the node name of this xhtml node + * @return the tag name of this xhtml node without any namespace prefix */ public String getTagName() { return this.tagName; @@ -31,12 +33,12 @@ public XHTMLNode getParent() { public XHTMLNode(Node xhtmlElement) { this.node = xhtmlElement; - this.tagName = xhtmlElement.getNodeName(); + this.tagName = XmlUtils.localName(xhtmlElement); } - + public XHTMLNode(Node xhtmlElement, XHTMLNode parent) { this.node = xhtmlElement; - this.tagName = xhtmlElement.getNodeName(); + this.tagName = XmlUtils.localName(xhtmlElement); this.parent = parent; } diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/ParserHardeningTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/ParserHardeningTest.java new file mode 100644 index 0000000..eeae78c --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/ParserHardeningTest.java @@ -0,0 +1,55 @@ +package de.uni_stuttgart.ils.reqif4j; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFParseException; + +/** + * Bug: parse failures called System.exit(1), killing the host JVM instead of + * reporting an error. The parser was also vulnerable to XXE because external + * entities and DOCTYPEs were allowed. + */ +class ParserHardeningTest { + + @Test + void malformedXmlThrowsInsteadOfKillingTheJvm(@TempDir Path tempDir) throws Exception { + Path file = TestFixtures.write(tempDir, "broken.reqif", ""); + + // If System.exit were still called, the test JVM would die here. + assertThrows(ReqIFParseException.class, () -> new ReqIF(file.toString())); + } + + @Test + void documentWithoutCoreContentThrows(@TempDir Path tempDir) throws Exception { + Path file = TestFixtures.write(tempDir, "empty.reqif", ""); + + assertThrows(ReqIFParseException.class, () -> new ReqIF(file.toString())); + } + + @Test + void doctypeIsRejectedToPreventXxe(@TempDir Path tempDir) throws Exception { + String xxe = """ + + ]> + &xxe; + """; + Path file = TestFixtures.write(tempDir, "xxe.reqif", xxe); + + assertThrows(ReqIFParseException.class, () -> new ReqIF(file.toString())); + } + + @Test + void validFixtureStillParses(@TempDir Path tempDir) throws Exception { + ReqIF reqif = new ReqIF(TestFixtures.writeDefaultFixture(tempDir).toString()); + + assertNotNull(reqif.getReqIFHeader()); + assertNotNull(reqif.getReqIFCoreContent()); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java new file mode 100644 index 0000000..81275af --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java @@ -0,0 +1,164 @@ +package de.uni_stuttgart.ils.reqif4j; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Shared ReqIF test fixtures. The main fixture uses a namespace-prefixed + * XHTML block (as produced by real ReqIF tools such as DOORS or Polarion), + * a multi-valued enumeration and an enumeration default value. + */ +public final class TestFixtures { + + private TestFixtures() { + } + + public static final String REQIF_FIXTURE = """ + + + + + Created by: Tester + 2026-07-23T10:00:00Z + reqif4j + 1.0 + test + TestDoc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dt-string + + + dt-int + + + dt-enum + + + ad-color + ev-blue + + + + + dt-xhtml + + + + + + + + + + st-req + + + ad-title + + + ad-prio + + + ad-color + + ev-red + ev-green + + + + ad-desc + + Intro Text with span contentalternative text + + + + + + st-req + + + ad-title + + + + + + + + st-spec + + + so-1 + + + so-2 + + + + + + + + + + """; + + /** + * Writes the given content to {@code dir/name} and returns the path. + */ + public static Path write(Path dir, String name, String content) throws IOException { + Path file = dir.resolve(name); + Files.writeString(file, content, StandardCharsets.UTF_8); + return file; + } + + /** + * Writes the default fixture to {@code dir/test.reqif} and returns the path. + */ + public static Path writeDefaultFixture(Path dir) throws IOException { + return write(dir, "test.reqif", REQIF_FIXTURE); + } + + /** + * @return the fixture with all inter-tag whitespace removed (minified XML, + * i.e. without the whitespace text nodes the parser used to rely on). + */ + public static String minified(String xml) { + return xml.replaceAll(">\\s+<", "><").trim(); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/XHTMLNamespaceTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/XHTMLNamespaceTest.java new file mode 100644 index 0000000..15585eb --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/XHTMLNamespaceTest.java @@ -0,0 +1,68 @@ +package de.uni_stuttgart.ils.reqif4j; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.specification.SpecHierarchy; +import de.uni_stuttgart.ils.reqif4j.specification.SpecObject; +import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLElementObject; +import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLElementP; +import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLNode; + +/** + * Bug: XHTML content with namespace prefixes (xhtml:div, xhtml:p, ...) was not + * recognized because elements were matched against unprefixed tag names. All + * content, including images, was silently dropped or caused an NPE when the + * div could not be found. + */ +class XHTMLNamespaceTest { + + private ReqIF reqif; + + @BeforeEach + void parseFixture(@TempDir Path tempDir) throws Exception { + reqif = new ReqIF(TestFixtures.writeDefaultFixture(tempDir).toString()); + } + + @Test + void xhtmlDivWithNamespacePrefixIsFound() { + SpecObject so1 = reqif.getReqIFCoreContent().getSpecObject("so-1"); + String description = (String) so1.getAttribute("Description"); + + assertNotNull(description); + assertFalse(description.isEmpty(), "XHTML content must not be dropped"); + assertTrue(description.contains("Text with"), "text content must be preserved, got: " + description); + assertTrue(description.contains("span content"), "nested span content must be preserved, got: " + description); + } + + @Test + void prefixedElementsAreMappedToTypedNodes() { + SpecHierarchy sh1 = reqif.getReqIFCoreContent().getSpecificationsList().get(0).getAllSpecHierarchies().get(0); + List divContent = sh1.getXHTMLDivContent(); + + assertNotNull(divContent, "div content must be found despite namespace prefix"); + assertTrue(divContent.stream().anyMatch(n -> n instanceof XHTMLElementP), + "xhtml:p must be parsed as XHTMLElementP"); + assertTrue(divContent.stream().anyMatch(n -> n instanceof XHTMLElementObject), + "xhtml:object must be parsed as XHTMLElementObject"); + } + + @Test + void tagNamesAreExposedWithoutNamespacePrefix() { + SpecHierarchy sh1 = reqif.getReqIFCoreContent().getSpecificationsList().get(0).getAllSpecHierarchies().get(0); + + for (XHTMLNode node : sh1.getXHTMLDivContent()) { + assertFalse(node.getTagName().contains(":"), + "tag name must not contain a namespace prefix: " + node.getTagName()); + } + } +} From 0b28e9d3edc4e66b1d67247d0abf2d8c950a0d7b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 18:10:54 +0000 Subject: [PATCH 4/7] Fix multiselect enumerations, enum defaults and enum null-safety - Read ALL ENUM-VALUE-REF children of an enumeration attribute value; previously only the first selected value survived parsing - AttributeValueEnumeration now carries the full value list (getValues/ getValueRefs); getValue() returns the joined string for compatibility - AttributeDefinitionEnumeration resolves DEFAULT-VALUE enum refs (they are elements, not a THE-VALUE attribute) and exposes MULTI-VALUED - Rewrite DatatypeEnumeration parsing: element-based navigation instead of fixed child indices, optional LONG-NAME/KEY/OTHER-CONTENT handled - Null-safe enum lookups in DatatypeEnumeration and SpecType (unknown ids no longer throw NPE) - Tests: MultiselectEnumTest Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011mat2d7AJkouKhXWUYzHxs --- .../AttributeDefinitionEnumeration.java | 70 ++++++++++++- .../attributes/AttributeValueEnumeration.java | 64 +++++++++++- .../datatypes/DatatypeEnumeration.java | 99 ++++++++++--------- .../ils/reqif4j/reqif/ReqIFConst.java | 6 ++ .../ils/reqif4j/specification/SpecObject.java | 22 ++++- .../ils/reqif4j/specification/SpecType.java | 24 +++-- .../reqif4j/specification/Specification.java | 22 ++++- .../ils/reqif4j/MultiselectEnumTest.java | 92 +++++++++++++++++ 8 files changed, 325 insertions(+), 74 deletions(-) create mode 100644 src/test/java/de/uni_stuttgart/ils/reqif4j/MultiselectEnumTest.java diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java index 7910496..c555a0d 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java @@ -1,18 +1,80 @@ package de.uni_stuttgart.ils.reqif4j.attributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; +import org.w3c.dom.Element; import org.w3c.dom.Node; import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; +import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeEnumeration; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; public class AttributeDefinitionEnumeration extends AttributeDefinition { - - + + private final List defaultValueRefs = new ArrayList(); + private final List defaultValues = new ArrayList(); + private final boolean multiValued; + public AttributeDefinitionEnumeration(Node attributeDefinition, Map dataTypes) { super(attributeDefinition, dataTypes); - - + + this.multiValued = "true".equalsIgnoreCase(XmlUtils.attribute(attributeDefinition, ReqIFConst.MULTI_VALUED)); + + // Enumeration defaults are ENUM-VALUE-REF elements, not a THE-VALUE + // attribute, so the base class cannot read them. + Element defaultValue = XmlUtils.firstChildElementByLocalName(attributeDefinition, ReqIFConst.DEFAULT_VALUE); + if (defaultValue != null) { + for (Element ref : XmlUtils.descendantsByLocalName(defaultValue, ReqIFConst.ENUM_VALUE_REF)) { + String refID = ref.getTextContent().trim(); + this.defaultValueRefs.add(refID); + this.defaultValues.add(resolveName(refID)); + } + } + } + + private String resolveName(String enumValueID) { + if (getDataType() instanceof DatatypeEnumeration) { + String name = ((DatatypeEnumeration) getDataType()).getEnumValueName(enumValueID); + if (name != null) { + return name; + } + } + return enumValueID; + } + + /** + * @return true if this attribute may carry more than one enum value + * (MULTI-VALUED="true") + */ + public boolean isMultiValued() { + return this.multiValued; + } + + /** + * @return resolved names of the default enum values (empty if none) + */ + public List getDefaultValues() { + return Collections.unmodifiableList(this.defaultValues); + } + + /** + * @return IDENTIFIERs of the default enum values (empty if none) + */ + public List getDefaultValueRefs() { + return Collections.unmodifiableList(this.defaultValueRefs); + } + + /** + * @return the default enum value names joined with ", ", or null if the + * definition declares no default + */ + @Override + public String getDefaultValue() { + return this.defaultValues.isEmpty() ? null : String.join(", ", this.defaultValues); } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java index d9271da..28df016 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java @@ -1,12 +1,68 @@ package de.uni_stuttgart.ils.reqif4j.attributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Value of an enumeration attribute. ReqIF enumeration attributes may be + * multi-valued (multiselect), so the value is a list of enum value names. + * + * {@link #getValue()} returns all selected names joined with {@code ", "} for + * backwards compatibility with consumers that expect a single string. + */ public class AttributeValueEnumeration extends AttributeValue { - - + + private final List values; + private final List valueRefs; + + /** + * @param values resolved LONG-NAMEs of all selected enum values + * @param valueRefs IDENTIFIERs of all selected enum values + */ + public AttributeValueEnumeration(List values, List valueRefs, AttributeDefinition type) { + super(type); + + this.values = values == null ? new ArrayList() : new ArrayList(values); + this.valueRefs = valueRefs == null ? new ArrayList() : new ArrayList(valueRefs); + this.value = String.join(", ", this.values); + } + + /** + * Single-value convenience constructor. + */ public AttributeValueEnumeration(String value, AttributeDefinition type) { super(value, type); - - + + this.values = new ArrayList(); + if (value != null && !value.isEmpty()) { + this.values.add(value); + } + this.valueRefs = new ArrayList(); + this.value = String.join(", ", this.values); + } + + /** + * @return the resolved names of all selected enum values (empty list if the + * attribute has no value) + */ + public List getValues() { + return Collections.unmodifiableList(this.values); + } + + /** + * @return the IDENTIFIERs of all selected enum values + */ + public List getValueRefs() { + return Collections.unmodifiableList(this.valueRefs); + } + + /** + * @return all selected enum value names joined with ", " (never null) + */ + @Override + public Object getValue() { + return String.join(", ", this.values); } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java index b31e665..e63ea60 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java @@ -3,70 +3,75 @@ import java.util.LinkedHashMap; 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.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; + public class DatatypeEnumeration extends Datatype { - - + + private Map enumValues = new LinkedHashMap(); - private String id; - private String name; - - - - - public String getID() { - return this.id; - } - - public String getName() { - return this.name; + + + /** + * @return all enum values of this datatype, keyed by IDENTIFIER + */ + public Map getEnumValues() { + return this.enumValues; } - + + /** + * @return the LONG-NAME of the enum value with the given id, or null if + * unknown + */ public String getEnumValueName(String id) { - if(this.enumValues.get(id) == null) { - return null; - } - return this.enumValues.get(id).getName(); + DatatypeEnumerationValue value = this.enumValues.get(id); + return value == null ? null : value.getName(); } - + + /** + * @return the KEY of the enum value with the given id, or null if unknown + */ public String getEnumValueKey(String id) { - return this.enumValues.get(id).getKey(); + DatatypeEnumerationValue value = this.enumValues.get(id); + return value == null ? null : value.getKey(); } - + + /** + * @return the OTHER-CONTENT of the enum value with the given id, or null if + * unknown + */ public String getEnumValueOtherContent(String id) { - return this.enumValues.get(id).getOtherContent(); + DatatypeEnumerationValue value = this.enumValues.get(id); + return value == null ? null : value.getOtherContent(); } - - - + public DatatypeEnumeration(String id, String name, Node enumeration) { super(id, name, ReqIFConst.ENUMERATION); - - this.id = id; - this.name = name; - - NodeList values = enumeration.getChildNodes().item(1).getChildNodes(); - for(int value = 0; value < values.getLength(); value++) { - - if(!values.item(value).getNodeName().equals(ReqIFConst._TEXT)) { - - String identifier = values.item(value).getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent(); - String longName = values.item(value).getAttributes().getNamedItem(ReqIFConst.LONG_NAME).getTextContent(); - String key = values.item(value).getChildNodes().item(1).getChildNodes().item(1).getAttributes().getNamedItem(ReqIFConst.KEY).getTextContent(); - String otherContent; - if (values.item(value).getChildNodes().item(1).getChildNodes().item(1).getAttributes().getNamedItem(ReqIFConst.OTHER_CONTENT) != null) { - otherContent = values.item(value).getChildNodes().item(1).getChildNodes().item(1).getAttributes().getNamedItem(ReqIFConst.OTHER_CONTENT).getTextContent(); - }else{ - otherContent = ""; - } - - enumValues.put(identifier, new DatatypeEnumerationValue(identifier, longName, key, otherContent)); + + // Iterate ENUM-VALUE elements independent of whitespace text nodes, + // namespace prefixes and optional attributes. + for (Element enumValue : XmlUtils.descendantsByLocalName(enumeration, ReqIFConst.ENUM_VALUE)) { + + String identifier = XmlUtils.attribute(enumValue, ReqIFConst.IDENTIFIER); + if (identifier == null) { + continue; } + String longName = XmlUtils.attribute(enumValue, ReqIFConst.LONG_NAME); + + Element embeddedValue = XmlUtils.firstDescendantByLocalName(enumValue, ReqIFConst.EMBEDDED_VALUE); + String key = embeddedValue == null ? "" : orEmpty(XmlUtils.attribute(embeddedValue, ReqIFConst.KEY)); + String otherContent = embeddedValue == null ? "" : orEmpty(XmlUtils.attribute(embeddedValue, ReqIFConst.OTHER_CONTENT)); + + enumValues.put(identifier, new DatatypeEnumerationValue(identifier, orEmpty(longName), key, otherContent)); } } + private static String orEmpty(String value) { + return value == null ? "" : value; + } + } 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 eb4c4f4..f9cc869 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 @@ -12,6 +12,12 @@ public class ReqIFConst { public final static String MAX_LENGTH = "MAX-LENGTH"; public final static String _TEXT = "#text"; + public final static String ENUM_VALUE = "ENUM-VALUE"; + public final static String ENUM_VALUE_REF = "ENUM-VALUE-REF"; + public final static String SPECIFIED_VALUES = "SPECIFIED-VALUES"; + public final static String EMBEDDED_VALUE = "EMBEDDED-VALUE"; + public final static String PROPERTIES = "PROPERTIES"; + public final static String MULTI_VALUED = "MULTI-VALUED"; public final static String TYPE = "TYPE"; public final static String VALUES = "VALUES"; public final static String DATATYPES = "DATATYPES"; 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 53895dc..74c5123 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,6 +1,8 @@ package de.uni_stuttgart.ils.reqif4j.specification; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import de.uni_stuttgart.ils.reqif4j.attributes.*; @@ -172,10 +174,15 @@ public SpecObject(Node specObject, SpecType specType) { this.attributeValues.put(attributeDefinitionName, new AttributeValueString(attributeValue, attributeDefinition)); break; - case ReqIFConst.ENUMERATION: String enumValueRef = ((Element)attribute).getElementsByTagName(ReqIFConst.VALUES).item(0).getChildNodes().item(1).getTextContent(); - // TODO check how it is behaving if more than one enum value exits. Right now the attribute value is only a string. - attributeValue = specType.getEnumValueName(enumValueRef); - this.attributeValues.put(attributeDefinitionName, new AttributeValueEnumeration(attributeValue, attributeDefinition)); + 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)); @@ -226,7 +233,12 @@ public SpecObject(Node specObject, SpecType specType) { case ReqIFConst.STRING: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueString(attributeDefinition.getDefaultValue(), attributeDefinition)); break; - case ReqIFConst.ENUMERATION: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueEnumeration(attributeDefinition.getDefaultValue(), attributeDefinition)); + 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)); 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 9dbfb2e..a63d504 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 @@ -62,28 +62,34 @@ public String getEnumValueName(String id) { } public String getEnumValueKey(String id) { - + for(AttributeDefinition attributeDefinition: this.attributeDefinitions.values()) { - + //Falls noch keine Klasse für diese Attributsdefinition definiert ist + if(attributeDefinition.getDataType() == null){ + continue; + } if(attributeDefinition.getDataType().getClass().equals(DatatypeEnumeration.class)) { - + if(((DatatypeEnumeration)attributeDefinition.getDataType()).getEnumValueName(id) != null) { - + return ((DatatypeEnumeration)attributeDefinition.getDataType()).getEnumValueKey(id); } } } return ""; } - + public String getEnumValueOtherContent(String id) { - + for(AttributeDefinition attributeDefinition: this.attributeDefinitions.values()) { - + //Falls noch keine Klasse für diese Attributsdefinition definiert ist + if(attributeDefinition.getDataType() == null){ + continue; + } if(attributeDefinition.getDataType().getClass().equals(DatatypeEnumeration.class)) { - + if(((DatatypeEnumeration)attributeDefinition.getDataType()).getEnumValueName(id) != null) { - + return ((DatatypeEnumeration)attributeDefinition.getDataType()).getEnumValueOtherContent(id); } } 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 052b49c..15ddce1 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 @@ -11,6 +11,7 @@ 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.AttributeValueEnumeration; @@ -144,9 +145,15 @@ public Specification(Node specification, SpecType specType, Map 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)); @@ -175,9 +182,14 @@ public Specification(Node specification, SpecType specType, Map Date: Thu, 23 Jul 2026 18:13:48 +0000 Subject: [PATCH 5/7] Fix image/object conversion pipeline - XHTMLElementObject renders a valid data="..." attribute (plus type), keeps URI forward slashes on every platform and no longer NPEs when the data attribute is missing; adds getMimeType/getDataAsFilePath - Restore the dead XHTML deconstruction mapping so P/TBL/L/H/OBJ tokens (including image paths) are extracted again; expose getElementList() - Fix picture association: consistent map keys (full document name) with extension-less fallback, lookup by object data URI or plain file name - ReqIFz: guard against zip-slip, close streams while extracting, drop unused swing import; ReqIF/ReqIFz path handling no longer breaks on dots or mixed separators in paths - ReqIFFile implements Closeable so picture streams can be released - Fix var extraction in list items reading the wrong node - Tests: ImagePipelineTest Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011mat2d7AJkouKhXWUYzHxs --- .../attributes/AttributeValueXHTML.java | 58 ++++--- .../ils/reqif4j/reqif/ReqIF.java | 51 ++++--- .../ils/reqif4j/reqif/ReqIFFile.java | 121 ++++++++++++--- .../ils/reqif4j/reqif/ReqIFz.java | 46 ++++-- .../ils/reqif4j/xhtml/XHTMLElementObject.java | 62 ++++++-- .../ils/reqif4j/ImagePipelineTest.java | 143 ++++++++++++++++++ 6 files changed, 395 insertions(+), 86 deletions(-) create mode 100644 src/test/java/de/uni_stuttgart/ils/reqif4j/ImagePipelineTest.java diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java index 90ce579..da46d33 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java @@ -41,6 +41,17 @@ public AttributeValueXHTML(String value, AttributeDefinition type) { public XHTMLElementDiv getDivValue() { return this.divValue; } + + /** + * @return the deconstructed element list (P/TBL/L/H/OBJ tokens with their + * content), never null + */ + public AttributeValueXHTMLElementList getElementList() { + if(this.value instanceof AttributeValueXHTMLElementList) { + return (AttributeValueXHTMLElementList) this.value; + } + return new AttributeValueXHTMLElementList(); + } @@ -56,24 +67,28 @@ private AttributeValueXHTMLElementList deconstructXHTML(Node xhtmlContent) { for(int e=0; e < div.getChildNodes().getLength(); e++) { Node xhtmlElement = div.getChildNodes().item(e); - String elementName = div.getChildNodes().item(e).getNodeName(); - xhtmlElementList.add(elementName, decostructXHTMLElement(elementName, xhtmlElement)); - /*if(elementName.contains("xhtml:")) { - if(elementName.endsWith("p")) { - xhtmlElementList.add("P",decostructXHTMLElement("P", xhtmlElement)); - }else if(elementName.endsWith("table")) { - xhtmlElementList.add("TBL",decostructXHTMLElement("TBL", xhtmlElement)); - }else if(elementName.endsWith(":ul")) { - xhtmlElementList.add("L", decostructXHTMLElement("L", xhtmlElement)); - }else if(elementName.contains(":h")) { - xhtmlElementList.add("H",decostructXHTMLElement("H", xhtmlElement)); - }else if(elementName.contains("object")) { - xhtmlElementList.add("OBJ",decostructXHTMLElement("OBJ", xhtmlElement)); - } - }*/ + // Map the element's local name to the content token expected by + // decostructXHTMLElement. The former implementation passed the raw + // (prefixed) node name, which never matched, so all content lists + // stayed empty. + String token = elementToken(XmlUtils.localName(xhtmlElement)); + if(token != null) { + xhtmlElementList.add(token, decostructXHTMLElement(token, xhtmlElement)); + } } return xhtmlElementList; } + + private static String elementToken(String localName) { + switch(localName.replaceAll("[0-9]", "")) { + case "p": return "P"; + case "table": return "TBL"; + case "ul": return "L"; + case "h": return "H"; + case "object": return "OBJ"; + default: return null; + } + } private List decostructXHTMLElement(String elementType, Node xhtmlElement) { @@ -146,9 +161,12 @@ private List decostructXHTMLElement(String elementType, Node xhtmlElemen } }else if(elementType.equals("OBJ")) { - //System.out.println("\n"+xhtmlElement.getAttributes().getNamedItem("data").getTextContent()); - String path = xhtmlElement.getAttributes().getNamedItem("data").getTextContent().trim(); - content.add(path.replace("/", System.getProperty("file.separator"))); + // Keep the original URI (forward slashes) so it matches the picture + // entries of the .reqifz archive on every platform. + String path = XmlUtils.attribute(xhtmlElement, "data"); + if(path != null) { + content.add(path.trim()); + } } /*// @@ -185,10 +203,10 @@ private List list(Node listNode) { }else if(leName.endsWith("var")) { list.add("VAR"); - if(child.getTextContent().trim().equals("")) { + if(listChild.getTextContent().trim().equals("")) { list.add("VARIABLE_NAME_MISSING"); }else{ - list.add(child.getTextContent().trim()); + list.add(listChild.getTextContent().trim()); } /// diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIF.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIF.java index 819ab25..c1bd8a4 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIF.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIF.java @@ -8,44 +8,49 @@ import java.util.HashMap; public class ReqIF extends ReqIFFile { - - + + public ReqIFDocument getReqIFDocument() { return this.reqifDocuments.get(this.name); } - + public ReqIFHeader getReqIFHeader() { return this.reqifDocuments.get(this.name).getHeader(); } - + public ReqIFCoreContent getReqIFCoreContent() { return this.reqifDocuments.get(this.name).getCoreContent(); } - - - - + + + + public ReqIF(String filePath) throws FileNotFoundException { - + this.path = filePath; - this.name = this.path.substring(this.path.lastIndexOf(System.getProperty("file.separator"))+1); - + this.name = extractFileName(filePath); + this.reqifDocuments.put(this.name, new ReqIFDocument(filePath)); this.numberOfReqIFDocuments = 1; - + this.picturesIS = new HashMap(); - File picturesFolder = new File(this.path.split("\\.")[0]); - if(picturesFolder.exists()) { - + // Pictures conventionally live in a sibling folder named after the file + // (extension stripped). Only the extension is removed; dots elsewhere in + // the path no longer truncate it. + File picturesFolder = new File(removeExtension(this.path)); + if(picturesFolder.exists() && picturesFolder.isDirectory()) { + File[] pictures = picturesFolder.listFiles(); - for(int picture = 0; picture < pictures.length; picture++) { - - if( pictures[picture].getName().endsWith("png") || - pictures[picture].getName().endsWith("jpg") || - pictures[picture].getName().endsWith("jpeg") ) { - - InputStream pictureIS = new BufferedInputStream(new FileInputStream(pictures[picture])); - this.picturesIS.put(pictures[picture].getName(), pictureIS); + if(pictures != null) { + for(int picture = 0; picture < pictures.length; picture++) { + + if( pictures[picture].getName().endsWith("png") || + pictures[picture].getName().endsWith("jpg") || + pictures[picture].getName().endsWith("jpeg") ) { + + InputStream pictureIS = new BufferedInputStream(new FileInputStream(pictures[picture])); + this.picturesIS.put(pictures[picture].getName(), pictureIS); + } } } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java index 68d8b0c..ce047ed 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java @@ -1,53 +1,140 @@ package de.uni_stuttgart.ils.reqif4j.reqif; +import java.io.Closeable; +import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -public class ReqIFFile { - - +public class ReqIFFile implements Closeable { + + protected String path; protected String name; protected int numberOfReqIFDocuments; protected Map picturesIS; protected Map> picturesInReqIFDocument = new LinkedHashMap>(); protected Map reqifDocuments = new LinkedHashMap(); - - - - + + + + public String getPath() { return this.path; } - + public String getName() { return this.name; } - + public int getReqIFDocumentsCount() { return this.numberOfReqIFDocuments; } - + public Map getReqIFDocuments() { return this.reqifDocuments; } - + public List getReqIFDocumentsList() { List reqIFDocuments= new ArrayList(); reqIFDocuments.addAll(this.reqifDocuments.values()); return reqIFDocuments; } - + + /** + * Returns the pictures belonging to the given ReqIF document. The document + * name may be passed with or without its file extension. + */ public Map getPicturesInputStreams(String reqifDocumentName) { - return this.picturesInReqIFDocument.get(reqifDocumentName.split("\\.")[0]); + + Map pictures = this.picturesInReqIFDocument.get(reqifDocumentName); + if(pictures != null) { + return pictures; + } + // Fallback: match ignoring the file extension (legacy behavior of this + // method stripped the extension while the map was keyed with it). + for(Map.Entry> entry: this.picturesInReqIFDocument.entrySet()) { + if(removeExtension(entry.getKey()).equals(removeExtension(reqifDocumentName))) { + return entry.getValue(); + } + } + return null; } - + + /** + * Returns the input stream of a picture referenced by a ReqIF document. + * The picture name is matched against the archive entry path (with forward + * slashes, as referenced by xhtml object data attributes) and, as a + * fallback, against the plain file name. + */ public InputStream getPictureInputStream(String reqifDocumentName, String pictureFileName) { - return this.picturesInReqIFDocument.get(reqifDocumentName).get(pictureFileName); + + Map pictures = getPicturesInputStreams(reqifDocumentName); + if(pictures == null || pictureFileName == null) { + return null; + } + InputStream picture = pictures.get(pictureFileName); + if(picture != null) { + return picture; + } + String normalized = pictureFileName.replace('\\', '/'); + picture = pictures.get(normalized); + if(picture != null) { + return picture; + } + String baseName = normalized.substring(normalized.lastIndexOf('/') + 1); + for(Map.Entry entry: pictures.entrySet()) { + String entryBaseName = entry.getKey().substring(entry.getKey().replace('\\', '/').lastIndexOf('/') + 1); + if(entryBaseName.equals(baseName)) { + return entry.getValue(); + } + } + return null; + } + + /** + * Closes all picture input streams held by this file. + */ + @Override + public void close() throws IOException { + IOException firstFailure = null; + for(Map pictures: this.picturesInReqIFDocument.values()) { + for(InputStream picture: pictures.values()) { + try { + picture.close(); + } catch (IOException e) { + if(firstFailure == null) { + firstFailure = e; + } + } + } + } + if(firstFailure != null) { + throw firstFailure; + } } - - + + /** + * Removes the last file extension of a name or path without mangling dots + * in directory names. + */ + protected static String removeExtension(String fileName) { + if(fileName == null) { + return null; + } + int lastSeparator = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\')); + int lastDot = fileName.lastIndexOf('.'); + return lastDot > lastSeparator ? fileName.substring(0, lastDot) : fileName; + } + + /** + * @return the file name portion of a path, handling both separator styles. + */ + protected static String extractFileName(String path) { + int lastSeparator = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); + return path.substring(lastSeparator + 1); + } + } 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 74871c0..83e2415 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 @@ -1,26 +1,31 @@ package de.uni_stuttgart.ils.reqif4j.reqif; -import javax.swing.filechooser.FileNameExtensionFilter; -import java.io.*; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; public class ReqIFz extends ReqIFFile { - private String extendFilename = "_unzipped"; + + private static final String EXTRACTION_SUFFIX = "_unzipped"; public ReqIFz(String filePath) throws IOException { - File destDir = new File(filePath); - String pathWithoutFileExtension = removeFileExtension(destDir.getAbsolutePath(), true); - destDir = new File(pathWithoutFileExtension + extendFilename); + + this.path = filePath; + this.name = extractFileName(filePath); + + 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); } + String canonicalDestDir = destDir.getCanonicalPath(); - byte[] buffer = new byte[1024]; + byte[] buffer = new byte[8192]; Map picturesIS = new HashMap<>(); try (ZipInputStream zis = new ZipInputStream(new FileInputStream(filePath))) { @@ -28,6 +33,13 @@ public ReqIFz(String filePath) throws IOException { while (zipEntry != null) { File newFile = new File(destDir, zipEntry.getName()); + + // 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 (zipEntry.isDirectory()) { if (!newFile.isDirectory() && !newFile.mkdirs()) { throw new IOException("Failed to create directory " + newFile); @@ -50,9 +62,12 @@ public ReqIFz(String filePath) throws IOException { // Process reqif files and associated images if (zipEntry.getName().endsWith("reqif")) { this.numberOfReqIFDocuments++; - String reqifBaseName = zipEntry.getName().split("\\.")[0]; - this.reqifDocuments.put(zipEntry.getName(), new ReqIFDocument(new FileInputStream(newFile), filePath, zipEntry.getName())); + try (InputStream reqifIS = new FileInputStream(newFile)) { + this.reqifDocuments.put(zipEntry.getName(), new ReqIFDocument(reqifIS, filePath, zipEntry.getName())); + } } 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. picturesIS.put(zipEntry.getName(), new FileInputStream(newFile)); } } @@ -60,9 +75,10 @@ public ReqIFz(String filePath) throws IOException { } } - // Assuming you want to associate pictures with ReqIF documents - for (String reqifBaseName : this.reqifDocuments.keySet()) { - this.picturesInReqIFDocument.put(reqifBaseName, picturesIS); + // Associate the archive's pictures with each contained ReqIF document, + // using the same keys as reqifDocuments (full entry name). + for (String reqifDocumentName : this.reqifDocuments.keySet()) { + this.picturesInReqIFDocument.put(reqifDocumentName, picturesIS); } } @@ -74,4 +90,4 @@ public static String removeFileExtension(String filename, boolean removeAllExten String extPattern = "(?'); - sb.append((!children.isEmpty() ? "\t" + this.listToString(children) : "")); + sb.append('<').append(tagName); + if (data != null) { + sb.append(" data=\"").append(data).append('"'); + } + if (mimeType != null) { + sb.append(" type=\"").append(mimeType).append('"'); + } + sb.append('>'); + sb.append((!children.isEmpty() ? this.listToString(children) : "")); sb.append("'); return sb.toString(); } diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/ImagePipelineTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/ImagePipelineTest.java new file mode 100644 index 0000000..b4224f6 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/ImagePipelineTest.java @@ -0,0 +1,143 @@ +package de.uni_stuttgart.ils.reqif4j; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTMLElementList; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFz; +import de.uni_stuttgart.ils.reqif4j.specification.SpecObject; +import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLElementObject; +import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLNode; + +/** + * Bugs: xhtml object elements were rendered without a data attribute name + * (invalid markup), URI slashes were replaced by the platform separator, a + * missing data attribute caused an NPE, the picture lookup keys never matched, + * and zip extraction was vulnerable to zip-slip. + */ +class ImagePipelineTest { + + private XHTMLElementObject parseObjectFromFixture(Path tempDir, String fixture) throws Exception { + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "test.reqif", fixture).toString()); + SpecObject so1 = reqif.getReqIFCoreContent().getSpecObject("so-1"); + AttributeValueXHTML description = (AttributeValueXHTML) so1.getAttributes().get("Description"); + for (XHTMLNode node : description.getDivValue().getChildren()) { + if (node instanceof XHTMLElementObject) { + return (XHTMLElementObject) node; + } + } + return null; + } + + @Test + void objectToStringProducesValidDataAttribute(@TempDir Path tempDir) throws Exception { + XHTMLElementObject object = parseObjectFromFixture(tempDir, TestFixtures.REQIF_FIXTURE); + + assertNotNull(object); + String html = object.toString(); + assertTrue(html.contains("data=\"files/image.png\""), + "object output must contain a valid data attribute, got: " + html); + assertTrue(html.contains("type=\"image/png\""), + "object output must keep the type attribute, got: " + html); + assertFalse(html.contains("\\"), "URI must keep forward slashes on every platform"); + } + + @Test + void objectDataKeepsUriSlashes(@TempDir Path tempDir) throws Exception { + XHTMLElementObject object = parseObjectFromFixture(tempDir, TestFixtures.REQIF_FIXTURE); + + assertEquals("files/image.png", object.getData(), + "getData must return the original URI with forward slashes"); + } + + @Test + void objectWithoutDataAttributeDoesNotThrow(@TempDir Path tempDir) throws Exception { + String fixture = TestFixtures.REQIF_FIXTURE.replace( + " data=\"files/image.png\" type=\"image/png\"", ""); + + XHTMLElementObject object = parseObjectFromFixture(tempDir, fixture); + + assertNotNull(object); + assertNull(object.getData()); + assertNotNull(object.toString()); + } + + @Test + void deconstructedElementListContainsImagePath(@TempDir Path tempDir) throws Exception { + ReqIF reqif = new ReqIF(TestFixtures.writeDefaultFixture(tempDir).toString()); + SpecObject so1 = reqif.getReqIFCoreContent().getSpecObject("so-1"); + AttributeValueXHTML description = (AttributeValueXHTML) so1.getAttributes().get("Description"); + + AttributeValueXHTMLElementList elements = description.getElementList(); + boolean foundImage = false; + for (int i = 0; i < elements.size(); i++) { + if (elements.getElementType(i).equals("OBJ")) { + assertEquals("files/image.png", elements.getElementContentList(i).get(0)); + foundImage = true; + } + } + assertTrue(foundImage, "element list must contain the OBJ entry with the image path"); + } + + @Test + void reqifzPicturesAreAssociatedWithTheirDocument(@TempDir Path tempDir) throws Exception { + Path zipFile = createReqIFz(tempDir); + + try (ReqIFz reqifz = new ReqIFz(zipFile.toString())) { + assertEquals(1, reqifz.getReqIFDocumentsCount()); + + Map pictures = reqifz.getPicturesInputStreams("test.reqif"); + assertNotNull(pictures, "picture lookup by document name must work"); + assertTrue(pictures.containsKey("files/image.png"), "pictures must be keyed like the object data URI"); + + // lookup exactly as an xhtml object references the image + assertNotNull(reqifz.getPictureInputStream("test.reqif", "files/image.png")); + // legacy lookup without extension must also work + assertNotNull(reqifz.getPicturesInputStreams("test")); + } + } + + @Test + void zipSlipEntriesAreRejected(@TempDir Path tempDir) throws Exception { + Path zipFile = tempDir.resolve("evil.reqifz"); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile.toFile()))) { + zos.putNextEntry(new ZipEntry("../evil.png")); + zos.write(new byte[] {1, 2, 3}); + zos.closeEntry(); + } + + assertThrows(IOException.class, () -> new ReqIFz(zipFile.toString()), + "entries escaping the extraction directory must be rejected"); + } + + private static Path createReqIFz(Path dir) throws IOException { + Path zipFile = dir.resolve("archive.reqifz"); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile.toFile()))) { + zos.putNextEntry(new ZipEntry("test.reqif")); + zos.write(TestFixtures.REQIF_FIXTURE.getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + zos.putNextEntry(new ZipEntry("files/image.png")); + zos.write(new byte[] {(byte) 0x89, 'P', 'N', 'G'}); + zos.closeEntry(); + } + return zipFile; + } +} From 32376adecf2f876e07e1d13d314f7ebfe4f40f0a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 18:17:46 +0000 Subject: [PATCH 6/7] Fix crash bugs and robustness issues - ReqIFCoreContent: register unknown datatypes under their ID instead of a null key; treat MIN/MAX/MAX-LENGTH as optional - DatatypeInteger: long-range bounds with BigInteger clamping (DOORS exports exceed int); DatatypeString: missing MAX-LENGTH = unbounded - AttributeValueInteger/Double: empty THE-VALUE parses as 0 instead of NumberFormatException - SpecHierarchy.getXHTMLContent: return the element list via its getter (the former cast threw ClassCastException on every call) - Specification: parse DATE and REAL attribute values (formerly dropped), fill DATE/DOUBLE defaults, fail with ExceptionSpecObject instead of NPE on unresolvable datatypes; null-safe getAttribute/getDescription - SpecObject: null-safe getAttribute, consistent empty REAL handling - ReqIFHeader: no crash on comments without 'Created by:' or unexpected CREATION-TIME formats - XHTMLElement.hasChildren: fix inverted logic - Remove unused javax.xml.crypto.Data import - Tests: RobustnessTest Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011mat2d7AJkouKhXWUYzHxs --- .../attributes/AttributeValueDouble.java | 6 +- .../attributes/AttributeValueInteger.java | 6 +- .../ils/reqif4j/datatypes/DatatypeDate.java | 2 - .../reqif4j/datatypes/DatatypeInteger.java | 55 +++++--- .../ils/reqif4j/datatypes/DatatypeString.java | 34 +++-- .../ils/reqif4j/reqif/ReqIFCoreContent.java | 13 +- .../ils/reqif4j/reqif/ReqIFHeader.java | 19 ++- .../reqif4j/specification/SpecHierarchy.java | 7 +- .../ils/reqif4j/specification/SpecObject.java | 11 +- .../reqif4j/specification/Specification.java | 43 +++++- .../ils/reqif4j/xhtml/XHTMLElement.java | 2 +- .../ils/reqif4j/RobustnessTest.java | 126 ++++++++++++++++++ .../ils/reqif4j/TestFixtures.java | 16 ++- 13 files changed, 279 insertions(+), 61 deletions(-) create mode 100644 src/test/java/de/uni_stuttgart/ils/reqif4j/RobustnessTest.java diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java index 9bcdb41..12178d5 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java @@ -4,10 +4,12 @@ public class AttributeValueDouble extends AttributeValue{ public AttributeValueDouble(String value, AttributeDefinition type) { super(value, type); - if(value == null){ + // Missing THE-VALUE reaches this constructor as null or "", both of + // which must not crash the parser. + if(value == null || value.isBlank()){ this.value = 0.0; }else { - this.value = Double.parseDouble(value); + this.value = Double.parseDouble(value.trim()); } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java index d97952b..b7b24ee 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java @@ -5,10 +5,12 @@ public class AttributeValueInteger extends AttributeValue { public AttributeValueInteger(String value, AttributeDefinition type) { super(value, type); - if(value == null){ + // Missing THE-VALUE reaches this constructor as null or "", both of + // which must not crash the parser. + if(value == null || value.isBlank()){ this.value = 0; }else { - this.value = Integer.parseInt(value); + this.value = Integer.parseInt(value.trim()); } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDate.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDate.java index 32d56e0..f6155a8 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDate.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDate.java @@ -2,8 +2,6 @@ import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; -import javax.xml.crypto.Data; - public class DatatypeDate extends Datatype { public DatatypeDate(String id, String name) { super(id, name, ReqIFConst.DATE); diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java index 7e3f143..9c2e380 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java @@ -1,32 +1,51 @@ package de.uni_stuttgart.ils.reqif4j.datatypes; +import java.math.BigInteger; + import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; public class DatatypeInteger extends Datatype { - - - private int max; - private int min; - - - - - public int getMax() { + + + private final long max; + private final long min; + + + public long getMax() { return this.max; } - - public int getMin() { + + public long getMin() { return this.min; } - - - - + + + /** + * MIN/MAX are optional in ReqIF and may exceed the int range (DOORS + * regularly exports long-range bounds). Missing or unparseable values fall + * back to the long range; larger values are clamped. + */ public DatatypeInteger(String id, String name, String min, String max) { super(id, name, ReqIFConst.INTEGER); - - this.min = Integer.parseInt(min); - this.max = Integer.parseInt(max); + + this.min = parseBound(min, Long.MIN_VALUE); + this.max = parseBound(max, Long.MAX_VALUE); + } + + private static long parseBound(String value, long fallback) { + if (value == null || value.isBlank()) { + return fallback; + } + try { + return Long.parseLong(value.trim()); + } catch (NumberFormatException outOfLongRange) { + try { + BigInteger bound = new BigInteger(value.trim()); + return bound.signum() < 0 ? Long.MIN_VALUE : Long.MAX_VALUE; + } catch (NumberFormatException notANumber) { + return fallback; + } + } } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java index 9408f03..6f648ed 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java @@ -3,24 +3,32 @@ import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; public class DatatypeString extends Datatype { - - - private int maxLength; - - - - + + + private final int maxLength; + + public int getMaxLength() { return this.maxLength; } - - - - + + + /** + * MAX-LENGTH is optional in ReqIF; a missing or unparseable value means + * "unbounded" and maps to {@link Integer#MAX_VALUE}. + */ public DatatypeString(String id, String name, String maxLength) { super(id, name, ReqIFConst.STRING); - - this.maxLength = Integer.parseInt(maxLength); + + int parsedMaxLength = Integer.MAX_VALUE; + if (maxLength != null && !maxLength.isBlank()) { + try { + parsedMaxLength = Integer.parseInt(maxLength.trim()); + } catch (NumberFormatException ignored) { + // keep unbounded + } + } + this.maxLength = parsedMaxLength; } } 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 d26a571..0baad28 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 @@ -7,6 +7,7 @@ import de.uni_stuttgart.ils.reqif4j.datatypes.*; 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; @@ -102,13 +103,15 @@ public ReqIFCoreContent(Element coreContent) { break; case ReqIFConst.INTEGER: - String min = dataType.getAttributes().getNamedItem(ReqIFConst.MIN).getTextContent(); - String max = dataType.getAttributes().getNamedItem(ReqIFConst.MAX).getTextContent(); + // 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: - String maxLength = dataType.getAttributes().getNamedItem(ReqIFConst.MAX_LENGTH).getTextContent(); + // MAX-LENGTH is optional in ReqIF + String maxLength = XmlUtils.attribute(dataType, ReqIFConst.MAX_LENGTH); this.dataTypes.put(dataTypeID, new DatatypeString(dataTypeID, dataTypeName, maxLength)); break; @@ -130,7 +133,9 @@ public ReqIFCoreContent(Element coreContent) { break; default: - this.dataTypes.put(null, new Datatype(dataTypeID, dataTypeName, ReqIFConst.UNDEFINED)); + // Register unknown datatypes under their ID (formerly the + // key was null, making them unresolvable). + this.dataTypes.put(dataTypeID, new Datatype(dataTypeID, dataTypeName, ReqIFConst.UNDEFINED)); break; } } 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 3a8f174..60502dc 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 @@ -64,12 +64,23 @@ public ReqIFHeader(Element theHeader) { this.reqifVersion = theHeader.getElementsByTagName(ReqIFConst.REQ_IF_VERSION).item(0).getTextContent(); } if(theHeader.getElementsByTagName(ReqIFConst.COMMENT).getLength() > 0) { - //this.comment = theHeader.getElementsByTagName(ReqIFConst.COMMENT).item(0).getTextContent(); - this.author = theHeader.getElementsByTagName(ReqIFConst.COMMENT).item(0).getTextContent().split("Created by: ")[1]; + // The "Created by: " convention is tool-specific; a comment without + // it must not crash the parser. + String comment = theHeader.getElementsByTagName(ReqIFConst.COMMENT).item(0).getTextContent(); + int createdBy = comment.indexOf("Created by: "); + if(createdBy >= 0) { + this.author = comment.substring(createdBy + "Created by: ".length()).trim(); + } } if(theHeader.getElementsByTagName(ReqIFConst.CREATION_TIME).getLength() > 0) { - String[] date = theHeader.getElementsByTagName(ReqIFConst.CREATION_TIME).item(0).getTextContent().split("T")[0].split("-"); - this.creationDate = date[2] + "." + date[1] + "." + date[0]; + String creationTime = theHeader.getElementsByTagName(ReqIFConst.CREATION_TIME).item(0).getTextContent(); + String[] date = creationTime.split("T")[0].split("-"); + if(date.length >= 3) { + this.creationDate = date[2] + "." + date[1] + "." + date[0]; + }else{ + // unexpected format: keep the raw value instead of crashing + this.creationDate = creationTime; + } } if(theHeader.getElementsByTagName(ReqIFConst.TITLE).getLength() > 0) { this.title = theHeader.getElementsByTagName(ReqIFConst.TITLE).item(0).getTextContent().replace("_Template", ""); 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 cf5ceef..610998f 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 @@ -58,8 +58,11 @@ public Object getAttribute(String attributeName) { public AttributeValueXHTMLElementList getXHTMLContent() { for(AttributeValue attributeValue: this.specObject.getAttributes().values()) { - if(attributeValue.getDatatype().equals(ReqIFConst.XHTML)) { - return (AttributeValueXHTMLElementList)attributeValue.getValue(); + // 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(); } } return null; 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 74c5123..4ec4a1a 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 @@ -46,7 +46,8 @@ public Map getAttributes() { } public Object getAttribute(String attributeName) { - return this.attributeValues.get(attributeName).getValue(); + AttributeValue attributeValue = this.attributeValues.get(attributeName); + return attributeValue == null ? null : attributeValue.getValue(); } public boolean isReq() { @@ -196,11 +197,11 @@ public SpecObject(Node specObject, SpecType specType) { 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(); + case ReqIFConst.REAL: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { + attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); }else{ - attributeValue = "0.0"; - } + attributeValue = ""; + } this.attributeValues.put(attributeDefinitionName, new AttributeValueDouble(attributeValue, attributeDefinition)); break; 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 15ddce1..0d0c860 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 @@ -14,6 +14,8 @@ 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; @@ -43,7 +45,8 @@ public String getID() { /// public String getDescription() { - return (String) this.attributeValues.get("Description").getValue(); + AttributeValue description = this.attributeValues.get("Description"); + return description == null ? null : (String) description.getValue(); } //*/ @@ -64,7 +67,8 @@ public Map getAttributes() { } public Object getAttribute(String attributeName) { - return this.attributeValues.get(attributeName).getValue(); + AttributeValue attributeValue = this.attributeValues.get(attributeName); + return attributeValue == null ? null : attributeValue.getValue(); } public int getNumberOfSpecObjects() { @@ -158,7 +162,24 @@ public Specification(Node specification, SpecType specType, Map children = null; public boolean hasChildren() { - return children == null ? false : children.isEmpty(); + return children != null && !children.isEmpty(); } public List getChildren() { diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/RobustnessTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/RobustnessTest.java new file mode 100644 index 0000000..450a674 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/RobustnessTest.java @@ -0,0 +1,126 @@ +package de.uni_stuttgart.ils.reqif4j; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueEnumeration; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTMLElementList; +import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeInteger; +import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeString; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.specification.SpecHierarchy; +import de.uni_stuttgart.ils.reqif4j.specification.SpecObject; +import de.uni_stuttgart.ils.reqif4j.specification.Specification; + +/** + * Bugs: crashes on valid ReqIF files — unknown datatypes were stored under a + * null key, optional MIN/MAX/MAX-LENGTH attributes caused NPEs, long-range + * bounds and empty numeric THE-VALUEs threw NumberFormatException, minified + * XML broke fixed child-index navigation, getXHTMLContent always threw + * ClassCastException, header parsing crashed on unexpected comment/date + * formats, and DATE values on specifications were silently dropped. + */ +class RobustnessTest { + + private ReqIF parse(Path dir, String content) throws Exception { + return new ReqIF(TestFixtures.write(dir, "test.reqif", content).toString()); + } + + @Test + void minifiedXmlParsesLikePrettyPrintedXml(@TempDir Path tempDir) throws Exception { + ReqIF reqif = parse(tempDir, TestFixtures.minified(TestFixtures.REQIF_FIXTURE)); + + SpecObject so1 = reqif.getReqIFCoreContent().getSpecObject("so-1"); + assertEquals("First requirement", so1.getAttribute("Title")); + assertEquals(5, so1.getAttribute("Priority")); + AttributeValueEnumeration colors = (AttributeValueEnumeration) so1.getAttributes().get("Colors"); + assertEquals(List.of("Red", "Green"), colors.getValues()); + assertTrue(((String) so1.getAttribute("Description")).contains("span content")); + } + + @Test + void unknownDatatypeIsRegisteredUnderItsId(@TempDir Path tempDir) throws Exception { + ReqIF reqif = parse(tempDir, TestFixtures.REQIF_FIXTURE); + + assertNotNull(reqif.getReqIFCoreContent().getDatatype("dt-custom"), + "unknown datatypes must be stored under their ID, not under null"); + assertEquals(ReqIFConst.UNDEFINED, reqif.getReqIFCoreContent().getDatatype("dt-custom").getType()); + } + + @Test + void longRangeAndMissingIntegerBoundsAreAccepted(@TempDir Path tempDir) throws Exception { + ReqIF reqif = parse(tempDir, TestFixtures.REQIF_FIXTURE); + + DatatypeInteger bounded = (DatatypeInteger) reqif.getReqIFCoreContent().getDatatype("dt-int"); + assertEquals(0L, bounded.getMin()); + assertEquals(Long.MAX_VALUE, bounded.getMax(), "long-range MAX must not overflow"); + + DatatypeInteger unbounded = (DatatypeInteger) reqif.getReqIFCoreContent().getDatatype("dt-int-unbounded"); + assertEquals(Long.MIN_VALUE, unbounded.getMin(), "missing MIN must not throw"); + assertEquals(Long.MAX_VALUE, unbounded.getMax(), "missing MAX must not throw"); + + DatatypeString stringType = (DatatypeString) reqif.getReqIFCoreContent().getDatatype("dt-string-unbounded"); + assertEquals(Integer.MAX_VALUE, stringType.getMaxLength(), "missing MAX-LENGTH must not throw"); + } + + @Test + void emptyNumericValuesDefaultToZero(@TempDir Path tempDir) throws Exception { + String fixture = TestFixtures.REQIF_FIXTURE.replace( + "", ""); + + ReqIF reqif = parse(tempDir, fixture); + SpecObject so1 = reqif.getReqIFCoreContent().getSpecObject("so-1"); + + assertEquals(0, so1.getAttribute("Priority"), + "a missing THE-VALUE on an integer attribute must parse as 0, not crash"); + } + + @Test + void getXHTMLContentReturnsElementListWithoutClassCastException(@TempDir Path tempDir) throws Exception { + ReqIF reqif = parse(tempDir, TestFixtures.REQIF_FIXTURE); + SpecHierarchy sh1 = reqif.getReqIFCoreContent().getSpecificationsList().get(0).getAllSpecHierarchies().get(0); + + AttributeValueXHTMLElementList content = sh1.getXHTMLContent(); + assertNotNull(content); + assertTrue(content.size() > 0, "element list must contain the deconstructed XHTML content"); + } + + @Test + void headerWithoutCreatedByCommentDoesNotCrash(@TempDir Path tempDir) throws Exception { + String fixture = TestFixtures.REQIF_FIXTURE.replace( + "Created by: Tester", "just a plain comment"); + + ReqIF reqif = parse(tempDir, fixture); + + assertEquals("", reqif.getReqIFHeader().getAuthor(), + "a comment without 'Created by:' must leave the author empty instead of crashing"); + assertEquals("23.07.2026", reqif.getReqIFHeader().getCreationDate()); + assertEquals("Tester", parse(tempDir, TestFixtures.REQIF_FIXTURE).getReqIFHeader().getAuthor()); + } + + @Test + void dateValuesOnSpecificationsAreParsed(@TempDir Path tempDir) throws Exception { + ReqIF reqif = parse(tempDir, TestFixtures.REQIF_FIXTURE); + Specification spec = reqif.getReqIFCoreContent().getSpecification("spec-1"); + + assertEquals("2026-01-01", spec.getAttribute("ReviewDate"), + "DATE attribute values on specifications were formerly dropped"); + } + + @Test + void unknownAttributeNamesReturnNullInsteadOfThrowing(@TempDir Path tempDir) throws Exception { + ReqIF reqif = parse(tempDir, TestFixtures.REQIF_FIXTURE); + + assertNull(reqif.getReqIFCoreContent().getSpecObject("so-1").getAttribute("DoesNotExist")); + assertNull(reqif.getReqIFCoreContent().getSpecification("spec-1").getAttribute("DoesNotExist")); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java index 81275af..0287d2e 100644 --- a/src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java @@ -33,7 +33,10 @@ private TestFixtures() { - + + + + @@ -80,7 +83,11 @@ private TestFixtures() { - + + + dt-date + + @@ -121,6 +128,11 @@ private TestFixtures() { st-spec + + + ad-review + + so-1 From e1d51de820b30f8464d53fb6fd479efe2c9b4f7d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 18:18:22 +0000 Subject: [PATCH 7/7] Document fix status in FEHLERANALYSE.md and add build/test section to README Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_011mat2d7AJkouKhXWUYzHxs --- FEHLERANALYSE.md | 20 +++++++++++++++++++- README.md | 13 +++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/FEHLERANALYSE.md b/FEHLERANALYSE.md index fafdfb7..9780530 100644 --- a/FEHLERANALYSE.md +++ b/FEHLERANALYSE.md @@ -2,7 +2,25 @@ Stand: 2026-07-23 · Analysierte Dateien: alle 62 Java-Klassen unter `de/uni_stuttgart/ils/reqif4j/` -Die Fehler sind nach Schweregrad gruppiert. Datei- und Zeilenangaben beziehen sich auf den aktuellen Stand des Branches. +## Behebungsstand + +Die Fehler der Abschnitte 1–4 sind auf diesem Branch **behoben** und durch Tests abgesichert, +die bei jedem Push in der GitHub-Actions-Pipeline (`.github/workflows/ci.yml`, `mvn verify`) laufen: + +| Bereich | Fix-Commit | Tests | +|---|---|---| +| Namespace-Handling, XXE, `System.exit`, `item(1)`-Navigation | "Fix XHTML namespace handling and harden the XML parser" | `XHTMLNamespaceTest`, `ParserHardeningTest` | +| 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` | + +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), +HTML-Escaping/Attribut-Erhalt in `toString()` (4.8) sowie die kosmetischen Punkte aus Abschnitt 5 +(bis auf den entfernten `javax.xml.crypto.Data`-Import). + +Die ursprüngliche Analyse folgt unverändert. Datei- und Zeilenangaben beziehen sich auf den +Stand **vor** den Fixes (Quellen liegen inzwischen unter `src/main/java/`). --- diff --git a/README.md b/README.md index 414e960..4d87994 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,16 @@ This fork is based on https://github.com/bfriebel/requirements-interchange-forma # Supported Formats ReqIF file extensions .reqif and .reqifz (compressed). + +# Build & Test +The project builds with Maven (Java 17+): + +``` +mvn verify +``` + +Every push and pull request runs the test suite via GitHub Actions +(`.github/workflows/ci.yml`). The tests in `src/test/java` cover the +fixed parser defects documented in `FEHLERANALYSE.md` (namespace-prefixed +XHTML, multiselect enumerations, image/object conversion, picture lookup +in .reqifz archives, and crash robustness).