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/FEHLERANALYSE.md b/FEHLERANALYSE.md new file mode 100644 index 0000000..9780530 --- /dev/null +++ b/FEHLERANALYSE.md @@ -0,0 +1,135 @@ +# Fehleranalyse ReqIF-Parser (reqif4j) + +Stand: 2026-07-23 · Analysierte Dateien: alle 62 Java-Klassen unter `de/uni_stuttgart/ils/reqif4j/` + +## 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/`). + +--- + +## 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. 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). diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java b/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java deleted file mode 100644 index 7910496..0000000 --- a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.uni_stuttgart.ils.reqif4j.attributes; - -import java.util.Map; - -import org.w3c.dom.Node; - -import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; - -public class AttributeDefinitionEnumeration extends AttributeDefinition { - - - public AttributeDefinitionEnumeration(Node attributeDefinition, Map dataTypes) { - super(attributeDefinition, dataTypes); - - - } - -} diff --git a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java b/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java deleted file mode 100644 index d9271da..0000000 --- a/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.uni_stuttgart.ils.reqif4j.attributes; - -public class AttributeValueEnumeration extends AttributeValue { - - - public AttributeValueEnumeration(String value, AttributeDefinition type) { - super(value, type); - - - } - -} diff --git a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java b/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java deleted file mode 100644 index b31e665..0000000 --- a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java +++ /dev/null @@ -1,72 +0,0 @@ -package de.uni_stuttgart.ils.reqif4j.datatypes; - -import java.util.LinkedHashMap; -import java.util.Map; - -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; -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; - } - - public String getEnumValueName(String id) { - if(this.enumValues.get(id) == null) { - return null; - } - return this.enumValues.get(id).getName(); - } - - public String getEnumValueKey(String id) { - return this.enumValues.get(id).getKey(); - } - - public String getEnumValueOtherContent(String id) { - return this.enumValues.get(id).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)); - } - } - } - -} diff --git a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java b/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java deleted file mode 100644 index 7e3f143..0000000 --- a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.uni_stuttgart.ils.reqif4j.datatypes; - -import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; - -public class DatatypeInteger extends Datatype { - - - private int max; - private int min; - - - - - public int getMax() { - return this.max; - } - - public int getMin() { - return this.min; - } - - - - - 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); - } - -} diff --git a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java b/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java deleted file mode 100644 index 9408f03..0000000 --- a/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.uni_stuttgart.ils.reqif4j.datatypes; - -import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; - -public class DatatypeString extends Datatype { - - - private int maxLength; - - - - - public int getMaxLength() { - return this.maxLength; - } - - - - - public DatatypeString(String id, String name, String maxLength) { - super(id, name, ReqIFConst.STRING); - - this.maxLength = Integer.parseInt(maxLength); - } - -} diff --git a/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java b/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java deleted file mode 100644 index 212cc3e..0000000 --- a/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java +++ /dev/null @@ -1,115 +0,0 @@ -package de.uni_stuttgart.ils.reqif4j.reqif; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -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 ReqIFHeader getHeader() { - 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); - - 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; - - } catch (SAXException | IOException | ParserConfigurationException e) { - e.printStackTrace(); - System.exit(1); - } - } - - public ReqIFDocument(InputStream is, String filePath) throws FileNotFoundException { - - this.filePath = filePath; - this.fileName = this.filePath.substring(filePath.lastIndexOf(System.getProperty("file.separator"))+1); - - 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)); - - } catch (SAXException | IOException | ParserConfigurationException e) { - e.printStackTrace(); - System.exit(1); - } - } - - public ReqIFDocument(InputStream is, String zipFilePath, String fileName) { - - this.filePath = zipFilePath; // TODO - 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)); - - - } catch (SAXException | IOException | ParserConfigurationException e) { - e.printStackTrace(); - System.exit(1); - } - } - -} diff --git a/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java b/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java deleted file mode 100644 index 68d8b0c..0000000 --- a/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java +++ /dev/null @@ -1,53 +0,0 @@ -package de.uni_stuttgart.ils.reqif4j.reqif; - -import java.io.InputStream; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -public class ReqIFFile { - - - 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; - } - - public Map getPicturesInputStreams(String reqifDocumentName) { - return this.picturesInReqIFDocument.get(reqifDocumentName.split("\\.")[0]); - } - - public InputStream getPictureInputStream(String reqifDocumentName, String pictureFileName) { - return this.picturesInReqIFDocument.get(reqifDocumentName).get(pictureFileName); - } - - -} diff --git a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementObject.java b/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementObject.java deleted file mode 100644 index 7ad7d8c..0000000 --- a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementObject.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.uni_stuttgart.ils.reqif4j.xhtml; - -import org.w3c.dom.Node; - -public class XHTMLElementObject extends XHTMLElement { - - private String data; - - public String getData() { - return this.data; - } - - - - public XHTMLElementObject (Node xhtmlElement) { - super(xhtmlElement); - - this.data = xhtmlElement.getAttributes().getNamedItem("data").getTextContent().trim().replace("/", System.getProperty("file.separator")); - } - - public XHTMLElementObject(Node xhtmlElement, XHTMLNode parent) { - super(xhtmlElement, parent); - - this.data = xhtmlElement.getAttributes().getNamedItem("data").getTextContent().trim().replace("/", System.getProperty("file.separator")); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append('<').append(tagName).append(" ").append(data).append('>'); - sb.append((!children.isEmpty() ? "\t" + this.listToString(children) : "")); - sb.append("'); - return sb.toString(); - } - -} 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 66% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java index a7bfab8..433f586 100644 --- a/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/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/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java new file mode 100644 index 0000000..c555a0d --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinitionEnumeration.java @@ -0,0 +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/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 61% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java index 9bcdb41..12178d5 100644 --- a/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/AttributeValueEnumeration.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java new file mode 100644 index 0000000..28df016 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueEnumeration.java @@ -0,0 +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/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java similarity index 61% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java index d97952b..b7b24ee 100644 --- a/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/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 77% rename from de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java index 4c77424..da46d33 100644 --- a/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); } @@ -37,36 +41,54 @@ 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(); + } 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(); - 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) { @@ -139,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()); + } } /*// @@ -178,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/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 88% rename from de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDate.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDate.java index 32d56e0..f6155a8 100644 --- a/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/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/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java new file mode 100644 index 0000000..e63ea60 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java @@ -0,0 +1,77 @@ +package de.uni_stuttgart.ils.reqif4j.datatypes; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +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(); + + + /** + * @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) { + 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) { + 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) { + 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); + + // 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/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/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java new file mode 100644 index 0000000..9c2e380 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeInteger.java @@ -0,0 +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 final long max; + private final long min; + + + public long getMax() { + return this.max; + } + + 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 = 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 new file mode 100644 index 0000000..6f648ed --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeString.java @@ -0,0 +1,34 @@ +package de.uni_stuttgart.ils.reqif4j.datatypes; + +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; + +public class DatatypeString extends Datatype { + + + 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); + + 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/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 54% rename from de/uni_stuttgart/ils/reqif4j/reqif/ReqIF.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIF.java index 819ab25..c1bd8a4 100644 --- a/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/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java similarity index 88% rename from de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java index 02e39c9..f9cc869 100644 --- a/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java @@ -12,12 +12,19 @@ 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"; 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/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java similarity index 92% rename from de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java index d26a571..0baad28 100644 --- a/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/ReqIFDocument.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java new file mode 100644 index 0000000..d25e56a --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java @@ -0,0 +1,130 @@ +package de.uni_stuttgart.ils.reqif4j.reqif; + +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; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +public class ReqIFDocument { + + + 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 ReqIFHeader getHeader() { + return this.header; + } + + public ReqIFCoreContent getCoreContent() { + return this.content; + } + + + public ReqIFDocument(String filePath) throws FileNotFoundException { + + this.filePath = filePath; + this.fileName = extractFileName(filePath); + + try { + this.reqifDocument = newDocumentBuilder().parse(this.filePath); + readDocument(); + + } catch (SAXException | IOException | ParserConfigurationException e) { + throw new ReqIFParseException("Failed to parse ReqIF document " + filePath, e); + } + } + + public ReqIFDocument(InputStream is, String filePath) throws FileNotFoundException { + + this.filePath = filePath; + this.fileName = extractFileName(filePath); + + try { + this.reqifDocument = newDocumentBuilder().parse(is); + readDocument(); + + } catch (SAXException | IOException | ParserConfigurationException e) { + throw new ReqIFParseException("Failed to parse ReqIF document " + filePath, e); + } + } + + public ReqIFDocument(InputStream is, String zipFilePath, String fileName) { + + this.filePath = zipFilePath; + this.fileName = fileName; + + try { + this.reqifDocument = newDocumentBuilder().parse(is); + readDocument(); + + } catch (SAXException | IOException | ParserConfigurationException e) { + 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/ReqIFFile.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java new file mode 100644 index 0000000..ce047ed --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFFile.java @@ -0,0 +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 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) { + + 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) { + + 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/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java similarity index 72% rename from de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java index 3a8f174..60502dc 100644 --- a/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/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/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java similarity index 61% rename from de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java index 74871c0..83e2415 100644 --- a/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 = "(? 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() { @@ -138,8 +142,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); @@ -170,10 +175,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)); @@ -187,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; @@ -224,7 +234,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/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 89% rename from de/uni_stuttgart/ils/reqif4j/specification/SpecType.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java index 638a78a..a63d504 100644 --- a/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 { @@ -61,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); } } @@ -100,9 +107,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/de/uni_stuttgart/ils/reqif4j/specification/Specification.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java similarity index 68% rename from de/uni_stuttgart/ils/reqif4j/specification/Specification.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java index a8ebf8e..0d0c860 100644 --- a/de/uni_stuttgart/ils/reqif4j/specification/Specification.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java @@ -11,13 +11,17 @@ import org.w3c.dom.NodeList; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinitionEnumeration; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueBoolean; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueDate; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueDouble; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueEnumeration; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueInteger; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueString; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; public class Specification { @@ -41,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(); } //*/ @@ -62,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() { @@ -110,8 +116,9 @@ 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)); break; - + + // DATE and REAL values were formerly ignored on specifications + case ReqIFConst.DATE: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { + attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); + }else{ + attributeValue = ""; + } + this.attributeValues.put(attributeDefinitionName, new AttributeValueDate(attributeValue, attributeDefinition)); + break; + + case ReqIFConst.REAL: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { + attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); + }else{ + attributeValue = ""; + } + this.attributeValues.put(attributeDefinitionName, new AttributeValueDouble(attributeValue, attributeDefinition)); + break; + default: break; } } @@ -160,8 +190,12 @@ 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/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java similarity index 90% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java index 9356fb0..608f3c2 100644 --- a/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java @@ -7,13 +7,14 @@ 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 { protected List children = null; public boolean hasChildren() { - return children == null ? false : children.isEmpty(); + return children != null && !children.isEmpty(); } public List getChildren() { @@ -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/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/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementObject.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementObject.java new file mode 100644 index 0000000..c8e32cc --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElementObject.java @@ -0,0 +1,76 @@ +package de.uni_stuttgart.ils.reqif4j.xhtml; + +import java.io.File; + +import org.w3c.dom.Node; + +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; + +/** + * An embedded object, typically an image. The {@code data} attribute is kept + * as the original URI (with forward slashes) so it stays a valid reference in + * generated markup and matches the picture entries of a .reqifz archive. + */ +public class XHTMLElementObject extends XHTMLElement { + + private String data; + private String mimeType; + + /** + * @return the object URI as written in the data attribute (forward slashes), + * or null if the object has no data attribute + */ + public String getData() { + return this.data; + } + + /** + * @return the data URI converted to a platform-specific file path, or null + */ + public String getDataAsFilePath() { + return this.data == null ? null : this.data.replace("/", File.separator); + } + + /** + * @return the MIME type of the object (type attribute), or null + */ + public String getMimeType() { + return this.mimeType; + } + + + public XHTMLElementObject (Node xhtmlElement) { + super(xhtmlElement); + + readAttributes(xhtmlElement); + } + + public XHTMLElementObject(Node xhtmlElement, XHTMLNode parent) { + super(xhtmlElement, parent); + + readAttributes(xhtmlElement); + } + + private void readAttributes(Node xhtmlElement) { + String rawData = XmlUtils.attribute(xhtmlElement, "data"); + this.data = rawData == null ? null : rawData.trim(); + this.mimeType = XmlUtils.attribute(xhtmlElement, "type"); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + 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/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 81% rename from de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java rename to src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java index af59547..610ec0d 100644 --- a/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/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; + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/MultiselectEnumTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/MultiselectEnumTest.java new file mode 100644 index 0000000..26864c1 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/MultiselectEnumTest.java @@ -0,0 +1,92 @@ +package de.uni_stuttgart.ils.reqif4j; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.BeforeEach; +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.datatypes.DatatypeEnumeration; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.specification.SpecObject; +import de.uni_stuttgart.ils.reqif4j.specification.SpecType; + +/** + * Bug: only the first ENUM-VALUE-REF of a multi-valued (multiselect) + * enumeration attribute was read; all further selected values were silently + * dropped. Enum default values were lost entirely, and unknown enum ids + * caused NPEs in the key/other-content lookups. + */ +class MultiselectEnumTest { + + private ReqIF reqif; + + @BeforeEach + void parseFixture(@TempDir Path tempDir) throws Exception { + reqif = new ReqIF(TestFixtures.writeDefaultFixture(tempDir).toString()); + } + + @Test + void allSelectedEnumValuesAreParsed() { + SpecObject so1 = reqif.getReqIFCoreContent().getSpecObject("so-1"); + AttributeValueEnumeration colors = (AttributeValueEnumeration) so1.getAttributes().get("Colors"); + + assertEquals(List.of("Red", "Green"), colors.getValues(), + "both selected enum values must be parsed, not just the first"); + assertEquals(List.of("ev-red", "ev-green"), colors.getValueRefs()); + } + + @Test + void joinedStringValueContainsAllSelections() { + SpecObject so1 = reqif.getReqIFCoreContent().getSpecObject("so-1"); + + assertEquals("Red, Green", so1.getAttribute("Colors")); + } + + @Test + void enumDefaultValueIsAppliedWhenAttributeIsMissing() { + SpecObject so2 = reqif.getReqIFCoreContent().getSpecObject("so-2"); + AttributeValueEnumeration colors = (AttributeValueEnumeration) so2.getAttributes().get("Colors"); + + assertEquals(List.of("Blue"), colors.getValues(), + "the DEFAULT-VALUE enum ref must be resolved when the spec object has no value"); + } + + @Test + void enumValueKeyAndOtherContentAreResolved() { + DatatypeEnumeration colorType = (DatatypeEnumeration) reqif.getReqIFCoreContent().getDatatype("dt-enum"); + + assertEquals("Red", colorType.getEnumValueName("ev-red")); + assertEquals("1", colorType.getEnumValueKey("ev-red")); + assertEquals("#ff0000", colorType.getEnumValueOtherContent("ev-red")); + assertEquals("", colorType.getEnumValueOtherContent("ev-green"), + "missing OTHER-CONTENT must map to empty string"); + } + + @Test + void unknownEnumIdsDoNotThrow() { + DatatypeEnumeration colorType = (DatatypeEnumeration) reqif.getReqIFCoreContent().getDatatype("dt-enum"); + SpecType specType = reqif.getReqIFCoreContent().getSpecType("st-req"); + + assertNull(colorType.getEnumValueName("does-not-exist")); + assertNull(colorType.getEnumValueKey("does-not-exist")); + assertNull(colorType.getEnumValueOtherContent("does-not-exist")); + assertEquals("", specType.getEnumValueKey("does-not-exist")); + assertEquals("", specType.getEnumValueOtherContent("does-not-exist")); + } + + @Test + void enumValuesKeepDocumentOrder() { + DatatypeEnumeration colorType = (DatatypeEnumeration) reqif.getReqIFCoreContent().getDatatype("dt-enum"); + + assertEquals(3, colorType.getEnumValues().size()); + assertTrue(colorType.getEnumValues().keySet().stream().toList() + .equals(List.of("ev-red", "ev-green", "ev-blue"))); + } +} 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/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 new file mode 100644 index 0000000..0287d2e --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java @@ -0,0 +1,176 @@ +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 + + + + + + + dt-date + + + + + + + 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 + + + ad-review + + + + + 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()); + } + } +}