diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e2d63a..3fad4c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,17 @@ jobs: java-version: '17' cache: maven + # The schema conformance tests download the official OMG ReqIF XSD and its + # imports on first use; the schemas are frozen, so the cache key never changes. + - name: Cache ReqIF schema + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/reqif-schema + key: reqif-schema-20110401 + - name: Build and run tests + env: + REQIF_SCHEMA_DIR: ${{ runner.temp }}/reqif-schema run: mvn -B --no-transfer-progress verify - name: Upload test reports diff --git a/IST-SOLL-ANALYSE.md b/IST-SOLL-ANALYSE.md new file mode 100644 index 0000000..b8e0384 --- /dev/null +++ b/IST-SOLL-ANALYSE.md @@ -0,0 +1,397 @@ +# Ist-/Soll-Analyse reqif4j: Weg zum vollständigen ReqIF-Import und -Export + +Stand: 2026-10-11 · Basis: Branch `claude/dreamy-goldberg-muhi8j` (= `master` @ `972ae90`, Version `1.1.2-SNAPSHOT`) + +## 0. Umsetzungsstand + +| Phase | Stand | Ergebnis | +|---|---|---| +| **1.1** Schemaprüfung | ✅ erledigt | `validateAgainstSchema` löst die http-Importe des offiziellen XSD lokal auf und geht nie online. Fehlende Importe werden mit Namen und URL gemeldet. Neue Variante für Dateien auf der Platte mit Zeilennummern. Die Tests laden das Schema einmalig herunter (in der CI gecacht) und prüfen gegen das echte OMG-XSD (`OfficialReqIFSchema.assertSchemaValid`). | +| **1.2** `Identifiable` | ✅ erledigt | Gemeinsame Basisklasse für alle 9 Elementarten. `LAST-CHANGE`, `DESC`, `LONG-NAME` (auch an Spec-Objekten, Relationen und Hierarchieknoten) und `ALTERNATIVE-ID` werden gelesen und geschrieben. Fehlender `LONG-NAME` führt nicht mehr zur NPE, fehlender `IDENTIFIER` zu einer klaren `ReqIFParseException`. Das Lesen der Attributwerte liegt jetzt in einer Klasse (`AttributeValueReader`). | + +Beim Umsetzen von Phase 1 fielen vier weitere Writer-Fehler auf. Sie wurden behoben, weil sie einen +schemagültigen Round-Trip verhinderten: + +- **`ALTERNATIVE-ID` hatte die falsche Form.** Das Schema verschachtelt das Element + (``). reqif4j las und schrieb die flache + Form, las also die Alternative-IDs echter Dateien nicht und schrieb ungültiges XML. Jetzt wird die + verschachtelte Form geschrieben, gelesen werden beide. +- **`SPEC-RELATION-GROUPS` stand vor `SPECIFICATIONS`.** `REQ-IF-CONTENT` ist eine `xsd:sequence`. + Jede Datei mit Relationsgruppen war deshalb ungültig. +- **Leeres `` für XHTML-Attribute ohne Inhalt.** Das ist ungültig, solche Werte werden jetzt ausgelassen. +- **Werte unbenannter Attributdefinitionen überschrieben sich.** Ohne `LONG-NAME` dient jetzt die ID als + Schlüssel. Die eigentliche Umstellung auf IDs bleibt Arbeitspaket 3.1. + +| Phase | Stand | Ergebnis | +|---|---|---| +| **5.1** Testkorpus | ✅ erledigt (ohne Zielwerkzeuge) | 25 echte Exporte (IBM DOORS, Siemens Polarion, ReqIF Studio, Enterprise Architect, Capella, StrictDoc) und die Referenzdateien des ReqIF Implementor Forum aus dem Apache-2.0-lizenzierten Projekt *reqif* (`src/test/resources/corpus`, Herkunft im README dort). `CorpusTest` prüft für jede Datei: Lesen ohne Abbruch, Zurückschreiben wie gelesen (sonst mit dokumentiertem Grund), zweites Schreiben ohne Änderung, Validator und offizielles Schema stimmen überein. Archive werden mit allen Einträgen neu gepackt. Gefunden und behoben: Werte von Elementen mit fehlendem Typ, von unbekannten Definitionen und von unpassender Art (Polarion schreibt XHTML-`ReqIF.Text` als `ATTRIBUTE-VALUE-STRING`) gingen verloren oder wurden umgedeutet. Sie bleiben jetzt als `UninterpretedAttributeValue` unverändert erhalten. Ebenso Attributdefinitionen ohne Datentyp. `xsi:schemaLocation` ging verloren. Relationen dürfen Objekte eines anderen Dokuments im Archiv referenzieren (`GLOBAL-REF`, Testfall TC1700), dafür gibt es `ReqIFValidator.validate(ReqIFz)`. Offen bleibt der Import in ReqIF Studio und Zielwerkzeuge: Das braucht die Werkzeuge und ist ein manueller Schritt. | +| **5.2** Implementation Guide | ✅ erledigt | `ImplementationGuideCheck` prüft Datentypen der Standardattribute (aus den Exporten abgeleitet: Datumsattribute DATE, `ReqIF.ForeignDeleted` BOOLEAN, `ReqIF.Text` XHTML, Namen STRING oder XHTML), eingebettete Objekte (MIME-Typ, relativer Pfad, PNG-Vorschau für OLE und andere Formate, auch in XHTML-Defaults), `IS-SIMPLIFIED` zusammen mit `THE-ORIGINAL-VALUE` sowie bei Archiven, ob referenzierte Dateien vorhanden sind (relativ zum Dokument, Percent-Encoding aufgelöst). Die Referenzdateien des Implementor Forum ergeben keinen Befund. | +| **5.3** Große Dateien | ✅ gemessen und beschleunigt, StAX zurückgestellt | Messung mit der 19-MB-Referenzdatei TC4000 (10.000 Objekte): Lesen 2,5 s → 0,5 s, Schreiben 0,3 s, Validieren 0,2 s, 99 MB Heap (läuft mit `-Xmx160m`). Behoben wurden quadratische Stellen: `NodeList.item(i)` in verschachtelten Schleifen, Löschen mit Nachrücken, Neunummerierung beim Anhängen, Kopieren der ID-Menge im Builder, Suche nach Hierarchieknoten (jetzt ein inkrementell gepflegter Index), dazu eine Factory pro XHTML-Wert. `LargeDocumentTest` sichert das mit 20.000 Objekten ab. Ein StAX-Reader/-Writer würde den Speicher etwa halbieren (kein zweites DOM beim Schreiben), lohnt den Aufwand bei linear ~10 KB Heap pro Objekt aber erst für Exporte mit deutlich über 100.000 Objekten. | + +Ergebnis von Phase 5: Die Bibliothek ist gegen reale Exporte und die Referenzdateien des Implementor Forum +getestet, prüft die Konventionen des Implementation Guide und verarbeitet große Dateien in linearer Zeit. +Der Interop-Nachweis im Zielwerkzeug (Abnahmekriterium 6) braucht die Werkzeuge selbst. Er bleibt ein +manueller Schritt: Die von `CorpusTest` geschriebenen Dateien in ReqIF Studio, DOORS oder Polarion +importieren. + +| Phase | Stand | Ergebnis | +|---|---|---| +| **4.1** Mutations-API | ✅ erledigt | Werte per Definitions-ID setzen (`set`, `setXhtml`, `setEnum`, `removeValue`), Hierarchieknoten einfügen, verschieben und löschen (`addChild`, `addSpecHierarchy`, `moveSpecHierarchy`, `removeSpecHierarchy`), Relationen und Relationsgruppen umhängen. Löschen hält Referenzen konsistent: Ein entferntes Spec-Objekt nimmt seine Relationen mit, die Kinder seiner Knoten rücken nach. Entfernte Relationen verlassen ihre Gruppen, entfernte Spezifikationen nehmen ihre Gruppen mit. Typen und Datentypen in Verwendung werden nicht gelöscht. IDs müssen dokumentweit frei sein. Neue und geänderte Elemente bekommen beim Schreiben den Schreibzeitpunkt als `LAST-CHANGE`, der nach erfolgreicher Ausgabe ins Modell übernommen wird. | +| **4.2** Builder | ✅ erledigt | Relationsgruppen und ihre Typen, `with(id, …)` für `LONG-NAME`/`DESC`/`LAST-CHANGE`/`ALTERNATIVE-ID` jedes Elements, `lang`, Tool-Extensions (nur Elemente eines eigenen Namespace, wie das Schema verlangt), `IS-EDITABLE` an Attributdefinitionen und Knoten, `IS-TABLE-INTERNAL`, `EDITABLE-ATTS`, XHTML mit `THE-ORIGINAL-VALUE`/`IS-SIMPLIFIED`. Der Validator prüft jetzt auch `EDITABLE-ATTS`. | + +Ergebnis von Phase 4: „Import → Änderung → Export“ funktioniert über das Modell, ohne dass Aufrufer +Referenzen selbst nachziehen müssen. Alles, was der Parser liest, lässt sich auch erzeugen. + +API-Änderungen in Phase 4: Die Maps von `ReqIFCoreContent` (`getSpecObjects()` …) sind schreibgeschützt, +geändert wird über `add…`/`remove…`. `add…` lehnt vergebene IDs ab, statt still zu ersetzen. +`getAllSpecHierarchies()` liefert eine schreibgeschützte Liste. Ein Knoten lässt sich nur an einer Stelle +einhängen (verschieben mit `moveSpecHierarchy`). Aus Werten erzeugte Elemente gelten als geändert, bis +der Builder (oder `clearModified()`) das zurücksetzt. `ReqIFBuildException` ist eine +`IllegalArgumentException`. + +| Phase | Stand | Ergebnis | +|---|---|---| +| **3.1/3.2** Werte | ✅ erledigt | Werte liegen nach Definitions-ID in Dokumentreihenfolge vor (`getValues()`), Defaults werden nicht mehr als Werte geschrieben. `getAttributes()` bleibt als schreibgeschützte Sicht mit Defaults nach `LONG-NAME`, `setValue`/`removeValue` ändern Werte. Gemeinsame Basisklasse `SpecElementWithAttributes`. | +| **3.3** Zahlen | ✅ erledigt | Integer ohne Größenbeschränkung (`getBigInteger()`), REAL-Grenzen und XHTML-Defaults schon in Phase 2. | +| **3.4** Metadaten | ✅ erledigt | `xml:lang`, `REPOSITORY-ID`, `IS-EDITABLE`, `IS-TABLE-INTERNAL`, `EDITABLE-ATTS`, `THE-ORIGINAL-VALUE`, `IS-SIMPLIFIED`, `p`-Inhalt statt `div`. Laut Schemaabgleich fehlt dem Code kein Element und kein Attribut des XSD mehr. | +| **3.5** `.reqifz` | ✅ erledigt | Alle Anhänge bleiben erhalten (OLE, PDF, GIF …), Neupacken ohne Speicherladen, `InputStream`-Konstruktor, Entpacken in ein Temp-Verzeichnis, das `close()` löscht. | +| **3.6** toleranter Import | ✅ erledigt | Statt Abstürzen gibt es `ReqIFDocument.getParseIssues()`. Unpassende Werte bleiben als Rohtext erhalten, nicht auflösbare Referenzen werden mitgeschrieben, nur Unverwendbares wird übersprungen. Der Validator meldet das als Warnungen. | +| **3.7** Round-Trip-Test | ✅ erledigt | `LosslessRoundTripTest` mit einer schemagültigen Datei, die alle Elemente und Attribute nutzt. Der kanonische XML-Vergleich ignoriert nur, was das Schema offenlässt. | + +Ergebnis von Phase 3: Import und Export sind im Sinne des Zielbilds vollständig. Was eine schemagültige +Datei enthält, wird gelesen und inhaltlich unverändert geschrieben. Lexikalisch normalisiert werden nur +typisierte Werte (`1` → `true`, `1e3` → `1000.0`). Kaputte Dateien werden gelesen, soweit es geht, und die +Probleme gemeldet. + +API-Änderungen in Phase 3: `getAttributes()` ist schreibgeschützt (Änderungen über `setValue`/`removeValue`), +`ReqIFz` entpackt nicht mehr neben die Quelldatei, und ein XHTML-Default gilt jetzt als effektiver Wert. Bei +attributbasierter Klassifizierung kann sich dadurch die Kategorie von Objekten ohne eigenen Wert ändern. + +Eine API-Änderung: `getName()` liefert für Elemente ohne `LONG-NAME` jetzt `null` (vorher Absturz, +bei Enum-Werten `""`). `DatatypeEnumeration.getEnumValueName()` liefert weiterhin `""`. + +| Phase | Stand | Ergebnis | +|---|---|---| +| **2.1** `LAST-CHANGE`-Default | ✅ erledigt | Der Writer setzt fehlendes `LAST-CHANGE` auf die `CREATION-TIME` des Headers bzw. den Schreibzeitpunkt (Clock injizierbar). Das Modell bleibt unverändert. | +| **2.2** Pflichtattribute | ✅ erledigt | `MULTI-VALUED` und `OTHER-CONTENT` werden immer geschrieben, REAL behält `ACCURACY`/`MIN`/`MAX` (Defaults 15 / voller double-Bereich), Header-Pflichtfelder bekommen Defaults, der Builder vergibt eine freie Header-ID. Unendlich wird als `INF`/`-INF` geschrieben und gelesen, XHTML-Defaults werden als Markup geschrieben. | +| **2.3** Modellvalidator | ✅ erledigt | Fehler bei allem, was die Ausgabe schemaungültig machen würde: ungültige `xsd:ID`, kein `xsd:dateTime` (`LAST-CHANGE`, `CREATION-TIME`, DATE-Werte), `REQ-IF-VERSION` ≠ `1.0`, nicht ganzzahliger Enum-`KEY`, Defaults nicht in Schema-Notation, fehlender Header. Warnungen bei Verstößen gegen `MAX-LENGTH` und `MIN`/`MAX`. Außerdem Fehler bei Zeichen, die XML nicht aufnehmen kann, Enum-Defaults als Text und Relationsgruppen ohne Typ/Quelle/Ziel. Vertrag: Ein Dokument ohne Fehler wird schemagültig geschrieben, mit Ausnahme des XHTML-Vokabulars (z. B. `font`, `img` sind in ReqIF nicht erlaubt), das nur `validateAgainstSchema` prüft (siehe 5.2). | + +Ergebnis von Phase 2: Mit dem Builder erzeugte Dokumente (das README-Beispiel) und der Round-Trip einer +schemagültigen Datei mit REAL-Grenzen, `MULTI-VALUED="false"` und leerem `OTHER-CONTENT` bestehen das +offizielle OMG-Schema. Die Tests prüfen das in der CI gegen das echte XSD. + +Weitere Funde der Review-Runden in Phase 2: Der Parser übernahm auch verschachtelte `TOOL-EXTENSIONS` +aus Vendor-Erweiterungen (jetzt nur direkte Kinder von `REQ-IF`). Unendliche REAL-Werte konnte die +Bibliothek nicht mehr einlesen, nachdem sie als `INF` geschrieben wurden (jetzt in beide Richtungen +xsd-Notation). XHTML-Defaults wurden als ungültiges Attribut geschrieben und nicht gelesen (jetzt beides, +das Lesen ist aus 3.3 vorgezogen). Ein Boolean-Wert `1` (gültiges xsd:boolean) wurde als `false` gelesen. +Offen für 3.3 bleibt: Integer-Werte außerhalb des int-Bereichs. + +Bekannte Grenzen: IDs mit Nicht-ASCII-Zeichen prüft der Validator mit dem Schemavalidator des JDK, damit +beide übereinstimmen. Ein reines Datum (`2026-01-01`) in DATE-Werten liest der Parser weiterhin tolerant +ein, der Validator meldet es als Fehler. + +## 1. Zusammenfassung + +**Zielbild:** Jede gültige ReqIF-1.2-Datei (`.reqif`/`.reqifz`) wird verlustfrei eingelesen, lässt +sich über eine API bearbeiten und wird wieder als **schemakonforme** Datei ausgegeben, die +gängige Werkzeuge (DOORS, Polarion, ReqIF Studio, Enterprise Architect …) wieder importieren können. + +**Ist-Stand in einem Satz:** Der *Import* deckt die fachlichen Kerninhalte gut ab und ist +weitgehend robust. Der *Export* ist strukturell vollständig, erzeugt aber **in keinem Fall eine +XSD-gültige Datei** und verliert beim Round-Trip eine Reihe von Informationen. + +| Bereich | Reifegrad | Kernaussage | +|---|---|---| +| Import `.reqif` | 🟡 gut | Alle Datentypen, Spec-Typen, Objekte, Relationen, Relationsgruppen, Hierarchien, XHTML, Tool-Extensions. Es fehlen Metadaten (`LAST-CHANGE`, `DESC`, `LONG-NAME` von Objekten …). Bei einigen gültigen Dateien bricht der Import ab. | +| Import `.reqifz` | 🟡 eingeschränkt | Nur Anhänge vom Typ `png`/`jpg`/`jpeg`. OLE-, GIF-, SVG- und PDF-Anhänge gehen verloren. | +| Export `.reqif` | 🔴 nicht schemakonform | `LAST-CHANGE` und weitere Pflichtattribute fehlen. Jede geschriebene Datei verletzt das OMG-XSD. | +| Export `.reqifz` | 🟡 eingeschränkt | Funktioniert, übernimmt aber nur die Bildanhänge, die der Import kennt. | +| Erzeugen (`ReqIFBuilder`) | 🟡 gut | Komfortabel, prüft Referenzen. Relationsgruppen, Metadaten und Pflicht-Header-Felder werden nicht unterstützt bzw. nicht erzwungen. | +| Validierung | 🟡 Lücke | Die Modellprüfung meldet „gültig“, obwohl das XSD 17 bis 23 Fehler findet. Die Schemaprüfung kann das offizielle XSD nicht laden. | +| Bearbeiten geparster Dokumente | 🔴 fehlt | Es gibt keine Mutations-API (Werte, Hierarchie, Relationen). | +| Tests/CI | 🟢 solide Basis | 154 Tests, alle grün (`mvn verify`). Es fehlen XSD-Konformitätstests und echte Tool-Exporte als Testdaten. | + +## 2. Vorgehen + +1. Code-Review der Kernklassen unter `src/main/java/de/uni_stuttgart/ils/reqif4j` (83 Dateien) + mit Fokus auf Parser, Writer, Builder und Validator. +2. `mvn verify` ausgeführt: **154 Tests, 0 Fehler**. +3. Das offizielle OMG-Schema `http://www.omg.org/spec/ReqIF/20110401/reqif.xsd` samt XHTML-Modulen + heruntergeladen und lokal aufgelöst. +4. Experimente gegen den aktuellen Build: + - **Builder → Writer → XSD:** das Beispiel aus dem README, mit dem Schema geprüft. + - **Round-Trip:** eine Datei, die alle optionalen ReqIF-Merkmale nutzt, einlesen, schreiben, + die Ausgabe vergleichen und gegen das XSD prüfen. + - **Robustheit:** fehlender `LONG-NAME`, doppelte Attributnamen, unbekannte Typreferenz, + `.reqifz` mit OLE-, GIF- und PNG-Anhängen. + +Alle unten als „nachgewiesen“ markierten Befunde stammen aus diesen Experimenten. + +## 3. Ist-Stand + +### 3.1 Architektur + +| Paket | Aufgabe | Bemerkung | +|---|---|---| +| `reqif` | Einstieg: `ReqIF` (.reqif), `ReqIFz` (.reqifz), `ReqIFDocument`, `ReqIFHeader`, `ReqIFCoreContent` | DOM-basiert, liest die ganze Datei in den Speicher | +| `datatypes` | `Datatype*` (Boolean, Integer, String, Real→„Double“, Date, XHTML, Enumeration) | Unbekannte Datentypen werden generisch erhalten | +| `attributes` | `AttributeDefinition*`, `AttributeValue*`, XHTML-Werte | Werte sind nach `LONG-NAME` der Definition abgelegt, nicht nach ID | +| `specification` | `SpecType`, `SpecObject`, `SpecRelation`, `Specification`, `SpecHierarchy`, `RelationGroup`, Klassifizierer | Inhaltskategorien (REQ/HEADLINE/…) sind parsereigene Erweiterungen | +| `xhtml` | Typisierter XHTML-Knotenbaum, Token-Liste, Rendering | Gut getestet | +| `write` | `ReqIFWriter`, `ReqIFzWriter` | Schreibt aus dem Objektmodell, nicht aus dem Quell-DOM | +| `build` | `ReqIFBuilder`, `SpecTypeBuilder`, `ValuesBuilder` | Fluent-API zum Neuerzeugen von Dokumenten | +| `validate` | `ReqIFValidator` (Modell + optional XSD) | | +| `util` | `SecureXml` (XXE-Schutz), `XmlUtils`, `XhtmlParser` | Zip-Slip-Schutz in `ReqIFz` | + +Positiv: Prefix-unabhängiges Lesen (`rif:`, `xhtml:`, `reqif-xhtml:`), sichere XML-Konfiguration, +Erhalt unbekannter Datentyp-, Attribut- und Spec-Typ-Arten, Tool-Extensions werden 1:1 +durchgereicht, Multiselect-Enums, XHTML-Rendering mit korrektem Escaping, deterministische Ausgabe. + +### 3.2 Abdeckungsmatrix (ReqIF 1.2 gegen reqif4j) + +Legende: ✅ unterstützt · ⚠️ teilweise · ❌ fehlt · — nicht anwendbar + +| ReqIF-Element / -Attribut | Import | Export | Builder | +|---|---|---|---| +| `REQ-IF` `xml:lang` | ✅ *(3.4)* | ✅ *(3.4)* | ⚠️ per Setter | +| Header: `COMMENT`, `CREATION-TIME`, `REQ-IF-TOOL-ID`, `REQ-IF-VERSION`, `SOURCE-TOOL-ID`, `TITLE` | ✅ | ✅ Pflichtfelder mit Default *(2.2)* | ✅ freie Header-ID als Default *(2.2)* | +| Header: `REPOSITORY-ID` | ✅ *(3.4)* | ✅ *(3.4)* | ✅ *(3.4)* | +| `TOOL-EXTENSIONS` | ✅ (roh) | ✅ (roh) | ❌ | +| Identifiable: `IDENTIFIER` | ✅ | ✅ | ✅ | +| Identifiable: `LAST-CHANGE` (**Pflicht**) | ✅ *(1.2)* | ✅ mit Default *(2.1)* | ⚠️ nur per Setter, sonst Default | +| Identifiable: `DESC` | ✅ *(1.2)* | ✅ *(1.2)* | ⚠️ nur per Setter | +| Identifiable: `LONG-NAME` | ✅ alle Elemente, optional *(1.2)* | ✅ *(1.2)* | ⚠️ nicht für Spec-Objekte | +| `ALTERNATIVE-ID` | ✅ verschachtelte und flache Form *(1.2)* | ✅ verschachtelt *(1.2)* | ⚠️ nur per Setter | +| `DATATYPE-DEFINITION-BOOLEAN` / `-DATE` / `-XHTML` | ✅ | ✅ | ✅ | +| `DATATYPE-DEFINITION-STRING` `MAX-LENGTH` | ✅ | ✅ | ✅ | +| `DATATYPE-DEFINITION-INTEGER` `MIN`/`MAX` | ✅ (long) | ✅ | ✅ | +| `DATATYPE-DEFINITION-REAL` `ACCURACY`/`MIN`/`MAX` (**Pflicht**) | ✅ *(2.2)* | ✅ *(2.2)* | ✅ *(2.2)* | +| `DATATYPE-DEFINITION-ENUMERATION`, `ENUM-VALUE`, `KEY` | ✅ | ✅ | ✅ | +| `EMBEDDED-VALUE` `OTHER-CONTENT` (**Pflicht**) | ✅ | ✅ auch leer *(2.2)* | ✅ | +| `SPEC-OBJECT-TYPE`, `SPECIFICATION-TYPE`, `SPEC-RELATION-TYPE` | ✅ | ✅ | ✅ | +| `RELATION-GROUP-TYPE` | ✅ | ✅ | ✅ *(4.2)* | +| `ATTRIBUTE-DEFINITION-*` | ✅ | ✅ | ✅ | +| `ATTRIBUTE-DEFINITION-*` `IS-EDITABLE` | ✅ *(3.4)* | ✅ *(3.4)* | ✅ *(4.2)* | +| `ATTRIBUTE-DEFINITION-ENUMERATION` `MULTI-VALUED` (**Pflicht**) | ✅ | ✅ auch `false` *(2.2)* | ✅ | +| `DEFAULT-VALUE` (einfache Typen, Enum) | ✅ | ✅ | ✅ | +| `DEFAULT-VALUE` (XHTML) | ✅ *(2.2, aus 3.3 vorgezogen)* | ✅ als Markup *(2.2)* | ✅ über `attribute(...)` | +| `ATTRIBUTE-VALUE-*` (alle 7 Arten) | ✅ | ✅ | ✅ | +| `ATTRIBUTE-VALUE-INTEGER` außerhalb des int-Bereichs | ✅ unbegrenzt *(3.3)* | ✅ *(3.3)* | ✅ *(3.3)* | +| `ATTRIBUTE-VALUE-XHTML` `THE-ORIGINAL-VALUE` / `IS-SIMPLIFIED` | ✅ *(3.4)* | ✅ *(3.4)* | ✅ *(4.2)* | +| `SPEC-OBJECT` | ✅ | ✅ | ✅ | +| `SPEC-RELATION` (+ Attribute) | ✅ | ✅ | ✅ | +| `SPECIFICATION` (+ Attribute) | ✅ | ✅ | ✅ | +| `SPEC-HIERARCHY` | ✅ | ✅ | ✅ | +| `SPEC-HIERARCHY` `IS-TABLE-INTERNAL`, `IS-EDITABLE`, `EDITABLE-ATTS` | ✅ *(3.4)* | ✅ *(3.4)* | ✅ *(4.2)* | +| `RELATION-GROUP` | ✅ | ✅ | ✅ *(4.2)* | +| `.reqifz` mit mehreren `.reqif` | ✅ | ✅ | — | +| `.reqifz`-Anhänge `png`/`jpg`/`jpeg` | ✅ | ✅ | ✅ | +| `.reqifz`-Anhänge OLE, GIF, SVG, PDF, RTF … | ✅ *(3.5)* | ✅ *(3.5)* | ✅ über `Map` | + +## 4. Befunde: Was zum Zielstand fehlt + +Die Befunde sind nach Auswirkung auf das Zielbild priorisiert: +**P0** blockiert jeden produktiven Export, **P1** verursacht Datenverlust oder Abbrüche bei realen Dateien, +**P2** betrifft Vollständigkeit, API und Betrieb. + +### P0: Export ist nicht schemakonform + +**4.1 `LAST-CHANGE` fehlt vollständig (nachgewiesen).** +Das XSD verlangt `LAST-CHANGE` (`xsd:dateTime`, `use="required"`) auf **jedem** Identifiable: +Datentypen, Enum-Werte, Spec-Typen, Attributdefinitionen, Spec-Objekte, Relationen, +Spezifikationen, Hierarchieknoten und Relationsgruppen. Der Parser liest das Attribut nicht, +das Modell hat kein Feld dafür und der Writer schreibt es nicht. Ergebnis: **Jede** exportierte +Datei ist ungültig. Bei einem Werkzeug wie DOORS geht damit außerdem die Änderungsverfolgung +beim Re-Import verloren. + +**4.2 Weitere Pflichtattribute werden nicht geschrieben (nachgewiesen).** +- `MULTI-VALUED` auf `ATTRIBUTE-DEFINITION-ENUMERATION` wird nur bei `true` gesetzt (`ReqIFWriter.attributeDefinition`). +- `OTHER-CONTENT` auf `EMBEDDED-VALUE` entfällt, wenn der Wert leer ist. +- `ACCURACY`, `MIN` und `MAX` auf `DATATYPE-DEFINITION-REAL` werden weder gelesen noch geschrieben + (`DatatypeDouble` hat keine Felder). +- Die Pflichtelemente des Headers (`CREATION-TIME`, `REQ-IF-TOOL-ID`, `REQ-IF-VERSION`, + `SOURCE-TOOL-ID`, `TITLE`) werden bei leerem Wert weggelassen. Der Builder setzt keine Defaults. + Das README-Beispiel erzeugt so einen ungültigen Header. + +XSD-Prüfung des README-Beispiels: **17 unterschiedliche Fehlermeldungen**. +Round-Trip der Testdatei: **23 unterschiedliche Fehlermeldungen**. + +**4.3 Die Modellvalidierung gibt falsche Sicherheit (nachgewiesen).** +`ReqIFValidator.validate()` meldet für dasselbe Builder-Dokument `isValid() == true`. +Geprüft werden Referenzen und Eindeutigkeit, aber keine Pflichtattribute und Wertebereiche +(`MAX-LENGTH`, `MIN`/`MAX`, `ACCURACY`). + +**4.4 `validateAgainstSchema` kann das offizielle XSD nicht laden (nachgewiesen).** +`reqif.xsd` importiert `xml.xsd` und `driver.xsd` (XHTML-Module) per HTTP. `SecureXml` erlaubt +nur `file:`-Zugriffe. Die Prüfung endet daher mit +`Failed to read schema document 'xml.xsd', because 'http' access is not allowed`. +Nötig sind ein `LSResourceResolver` oder ein XML-Katalog mit lokal abgelegten Schemata +(im Test-Classpath oder als Download-Anleitung). Erst dann kann die Schemakonformität in der CI +geprüft werden. + +### P1: Datenverlust und Abbrüche beim Import und im Round-Trip + +**4.5 Metadaten gehen verloren (nachgewiesen).** Folgende Angaben werden nicht modelliert und +fehlen nach `parse → write`: +`DESC` (alle Elemente), `LONG-NAME` von `SPEC-OBJECT`/`SPEC-RELATION`/`SPEC-HIERARCHY`, +`REPOSITORY-ID`, `xml:lang`, `IS-EDITABLE`, `IS-TABLE-INTERNAL`, `EDITABLE-ATTS`, +`ACCURACY`/`MIN`/`MAX` (Real) sowie XHTML-Defaultwerte. + +**4.6 XHTML: `THE-ORIGINAL-VALUE` und `IS-SIMPLIFIED` fehlen (nachgewiesen).** +Werkzeuge wie DOORS legen den vollständigen Inhalt (z. B. eingebettete OLE-Objekte mit +Vorschaubild) in `THE-ORIGINAL-VALUE` ab und eine vereinfachte Fassung in `THE-VALUE`. +reqif4j liest nur `THE-VALUE`. **Eingebettete OLE-Objekte verschwinden** beim Round-Trip. + +**4.7 Nicht gesetzte Werte werden zu explizit gesetzten Werten (nachgewiesen).** +Beim Import füllt `SpecObject`/`Specification` fehlende Werte mit dem Default auf. Fehlt auch +der Default, bleibt Boolean `false`, Integer `0` und Real `0.0`. Der Writer schreibt diese +Werte als echte `ATTRIBUTE-VALUE-*`. In der Testdatei hatte das Objekt kein `Flag` und kein `Real`. +Nach dem Export trägt es `THE-VALUE="false"` und `THE-VALUE="0.0"`. +Das ändert die Semantik: „nicht gesetzt“ ist nicht dasselbe wie „false“. Das Modell muss +*explizite Werte* und *effektive Werte (inkl. Default)* trennen. + +**4.8 Werte sind nach `LONG-NAME` statt nach Definitions-ID abgelegt (nachgewiesen).** +`attributeValues` ist eine `HashMap`. Tragen zwei Attributdefinitionen eines Typs +denselben Namen (bei DOORS-Exporten mit Modul- und Systemattributen keine Seltenheit), überschreibt +der zweite Wert den ersten. Im Test kam von `A` und `B` nur `B` an, und es wurde nur ein Wert +geschrieben. Die Reihenfolge der Werte geht ebenfalls verloren. + +**4.9 Integer-Werte außerhalb des `int`-Bereichs brechen den Import ab (nachgewiesen).** +`DatatypeInteger` arbeitet mit `long`, `AttributeValueInteger` aber mit `Integer.parseInt`. +`THE-VALUE="99999999999"` führt zur `NumberFormatException`, und das ganze Dokument wird +nicht geladen. ReqIF verwendet `xsd:integer` (unbeschränkt). Richtig wäre `long` mit Fallback +auf `BigInteger`. + +**4.10 Optionale Angaben führen zu `NullPointerException` (nachgewiesen).** +- `LONG-NAME` ist im XSD optional. `Datatype`-, `SpecType`-, `AttributeDefinition`- und + `Specification`-Konstruktoren rufen aber `getNamedItem(LONG_NAME).getTextContent()` ohne + Null-Prüfung auf. +- Ein `SPEC-OBJECT` mit unbekannter Typreferenz bricht in `readAttributeValues` ab (`specType` ist `null`). +- `SpecHierarchy` mit nicht auflösbarem `OBJECT`: `getSpecObjectID()` wirft eine NPE, und der Writer + lässt das Pflichtelement `OBJECT` weg. + +Bei einem fehlertoleranten Import gehören solche Fälle in `ValidationResult`-Meldungen, nicht in Abstürze. + +**4.11 `.reqifz`: Anhänge außer `png`/`jpg`/`jpeg` gehen verloren (nachgewiesen).** +`ReqIFz` registriert nur diese drei Endungen, `ReqIFzWriter.write(ReqIFz, …)` packt nur sie +wieder ein. Aus einem Archiv mit `obj.ole`, `a.gif` und `b.png` blieb nach dem Neupacken nur +`b.png` übrig. Für einen vollständigen Export müssen **alle** Nicht-ReqIF-Einträge erhalten +bleiben, unabhängig von der Endung. Dasselbe gilt für den Bilderordner neben einer `.reqif`-Datei. + +**4.12 Doppelte Identifier werden stillschweigend überschrieben.** +Das ist im README dokumentiert, aber ungelöst. Der Parser sollte Duplikate beim Einlesen erkennen und melden. + +### P2: Vollständigkeit, API und Betrieb + +**4.13 Keine API zum Bearbeiten geparster Dokumente.** *(behoben in 4.1)* +`AttributeValue` hat keine Setter, Hierarchien und Relationen lassen sich nicht umhängen, +Objekte nicht konsistent löschen (inklusive Referenzen in Hierarchien und Relationen). +Der Builder erzeugt nur *neue* Dokumente. Für „Import → Änderung → Export“ fehlt eine +Bearbeitungsschicht, die auch `LAST-CHANGE` automatisch fortschreibt. + +**4.14 Builder unvollständig.** *(behoben in 2.2, 3.4 und 4.2)* +Es fehlen `RELATION-GROUP` und `RELATION-GROUP-TYPE`, `ALTERNATIVE-ID`, `DESC`, `LAST-CHANGE`, +`LONG-NAME` für Objekte, `REPOSITORY-ID`, `IS-EDITABLE`, Real-Attribute (`ACCURACY` …), +Tool-Extensions und Defaults für Pflicht-Header-Felder. + +**4.15 Doppelte Parse-Logik.** +Das Einlesen der Attributwerte ist in `SpecObject.readAttributeValues` und im Konstruktor von +`Specification` doppelt vorhanden (ca. 100 Zeilen) und schon jetzt uneinheitlich, +z. B. `REAL` im Wert-Switch gegenüber `DOUBLE` im Default-Switch. Jede Erweiterung aus 4.5 bis 4.9 +müsste doppelt umgesetzt werden. Vorher sollte eine gemeinsame Basisklasse +`Identifiable`/`SpecElementWithAttributes` (wie im ReqIF-Metamodell) eingeführt werden. + +**4.16 Speicher und Dateisystem.** *(Dateisystem in 3.5, Laufzeit in 5.3; StAX zurückgestellt)* +- Rein DOM-basiert: Große Exporte (z. B. 100.000 Objekte mit XHTML) liegen vollständig im Speicher, + der Writer baut zusätzlich ein zweites DOM auf. Für große Dateien wären StAX zum Lesen und + Streaming zum Schreiben sinnvoll. +- `ReqIFz` entpackt ungefragt in ein Verzeichnis `_unzipped` neben der Quelldatei (nachgewiesen). + Das scheitert auf schreibgeschützten Laufwerken und wird nie aufgeräumt. + Eine `InputStream`-Variante für `.reqifz` fehlt. +- Bild-`InputStream`s sind nur einmal lesbar. Hier wäre `byte[]` bzw. `Path`-Zugriff besser. + +**4.17 Interoperabilität und Profile.** *(Prüfungen in 5.2)* +Die Klassifizierung nach Implementation Guide existiert. Es fehlen aber Prüfungen und Hilfen für die +Regeln des *ReqIF Implementation Guide* (ProSTEP iViP), z. B. die erlaubte XHTML-Teilmenge, +OLE-Konventionen (`object` mit Vorschaubild), `ReqIF.ForeignID`/`ReqIF.ChapterName` und den +Umgang mit `IS-SIMPLIFIED`. + +**4.18 Tests und Dokumentation.** +- Es gibt keinen Test, der Writer-Ausgaben gegen das XSD prüft. Die 154 Tests vergleichen das Modell + nach Round-Trip, nicht die XML-Vollständigkeit. Deshalb fielen 4.1 bis 4.11 bisher nicht auf. +- Es fehlen echte, anonymisierte Exporte (DOORS Classic/Next, Polarion, Jama, EA, ReqIF Studio) + als Testdaten. +- Das README nennt Version `1.1.0`, die `pom.xml` steht auf `1.1.2-SNAPSHOT`. Der Writer ist + noch als *experimental* gekennzeichnet. + +## 5. Soll-Bild: Abnahmekriterien „vollständiger Import und Export“ + +1. **Schemakonformität:** Jede vom Writer erzeugte Datei besteht die Validierung gegen das + OMG-XSD (ReqIF 1.2, `20110401`). Das wird in der CI geprüft. +2. **Verlustfreier Round-Trip:** `parse → write → parse` ergibt ein inhaltlich identisches Modell, und + `parse → write` ergibt ein XML, das bis auf Formatierung und Attributreihenfolge dem + Original entspricht (kanonischer XML-Vergleich). Getestet wird das mit einer „Alle-Merkmale“-Datei + und mit echten Tool-Exporten. +3. **Fehlertoleranter Import:** Gültige, aber ungewöhnliche Dateien (optionale Attribute fehlen, + große Zahlen, unbekannte Referenzen) werden geladen. Probleme landen als `ValidationIssue` + statt als Exception. +4. **Vollständige Archive:** `.reqifz` behält alle Einträge (Dokumente und beliebige Anhänge), + auch beim Neupacken. +5. **Bearbeitbarkeit:** Geparste Dokumente lassen sich über eine API ändern. `LAST-CHANGE` wird dabei + automatisch gepflegt, und Referenzen bleiben konsistent. +6. **Interop-Nachweis:** Exportierte Dateien lassen sich in mindestens zwei Zielwerkzeugen + (z. B. ReqIF Studio und DOORS oder Polarion) importieren. + +## 6. Empfohlene Umsetzungsschritte + +Aufwand: S ≈ bis 1 Tag, M ≈ 2–4 Tage, L ≈ 1–2 Wochen (eine Person, inklusive Tests). + +| # | Arbeitspaket | Befunde | Aufwand | +|---|---|---|---| +| **Phase 1: Fundament und Messbarkeit** |||| +| 1.1 | XSD und XHTML-Module lokal einbinden (Resolver/Katalog), Testhelfer `assertSchemaValid(doc)`, CI-Test „Builder-Beispiel ist XSD-gültig“ (zunächst rot) | 4.4, 4.18 | S | +| 1.2 | Gemeinsame Basisklasse `Identifiable` (ID, LONG-NAME optional, DESC, LAST-CHANGE, ALTERNATIVE-ID) für alle Elemente. Wert-Parsing in einer Klasse zusammenführen | 4.5, 4.10, 4.15 | M | +| **Phase 2: Schemakonformer Export** |||| +| 2.1 | `LAST-CHANGE` lesen, modellieren und schreiben (Default: Zeitpunkt des Exports bzw. Erzeugungszeit) | 4.1 | S | +| 2.2 | Pflichtattribute: `MULTI-VALUED` immer, `OTHER-CONTENT` immer, Real `ACCURACY`/`MIN`/`MAX`, Header-Pflichtfelder mit sinnvollen Defaults | 4.2 | S | +| 2.3 | `ReqIFValidator` um Pflichtfelder und Wertebereiche erweitern | 4.3 | S–M | +| **Phase 3: Verlustfreier Import und Round-Trip** |||| +| 3.1 | Werte nach Definitions-ID statt nach LONG-NAME ablegen (Namenszugriff als Komfortmethode beibehalten), Reihenfolge erhalten | 4.8 | M (API-Bruch → Major-Version) | +| 3.2 | Explizite und effektive Werte trennen, keine Default-Werte mehr materialisieren | 4.7 | M | +| 3.3 | Integer als `long`/`BigInteger`, Real-Grenzen, XHTML-Defaults | 4.9, 4.5 | S | +| 3.4 | `THE-ORIGINAL-VALUE`/`IS-SIMPLIFIED`, `IS-EDITABLE`, `IS-TABLE-INTERNAL`, `EDITABLE-ATTS`, `REPOSITORY-ID`, `xml:lang` | 4.5, 4.6 | M | +| 3.5 | `.reqifz`: alle Einträge erhalten, `InputStream`-Konstruktor, Entpacken ins Temp-Verzeichnis oder in den Speicher | 4.11, 4.16 | M | +| 3.6 | Fehlertoleranter Import: NPE-Stellen in `ValidationIssue`s umwandeln, Duplikate melden | 4.10, 4.12 | M | +| 3.7 | Round-Trip-Test mit „Alle-Merkmale“-Datei und kanonischem XML-Vergleich | 4.18 | S | +| **Phase 4: Bearbeitung und Komfort** |||| +| 4.1 | Mutations-API (Werte setzen und entfernen, Objekte und Hierarchieknoten einfügen, verschieben und löschen, Relationen pflegen) mit automatischem `LAST-CHANGE` | 4.13 | L | +| 4.2 | Builder vervollständigen (Relationsgruppen, Metadaten, Real-Attribute, Tool-Extensions) | 4.14 | M | +| **Phase 5: Interoperabilität und Skalierung** |||| +| 5.1 | Echte Tool-Exporte als Testkorpus, Import-Tests in ReqIF Studio und Zielwerkzeugen | 4.17, 4.18 | M | +| 5.2 | Implementation-Guide-Prüfungen (XHTML-Teilmenge, OLE-Konventionen, Standardattribute) | 4.17 | M | +| 5.3 | Optional: StAX-basierter Reader/Writer für sehr große Dateien | 4.16 | L | + +**Schnellster Weg zu einem brauchbaren Export:** Phase 1 und Phase 2 (ca. 1–1,5 Wochen) machen den +Export schemakonform. Mit Phase 3 (ca. 3–4 Wochen) wird der Round-Trip verlustfrei. Danach ist +das Zielbild „vollständiger Import und Export“ im engeren Sinn erreicht. Phase 4 und 5 machen die +Bibliothek bearbeitungsfähig und belegen die Interoperabilität mit realen Werkzeugen. + +## Anhang: Reproduktion der Experimente + +Das XSD ist nicht im Repository enthalten (Lizenz). Zum Nachstellen: + +1. `reqif.xsd` von `https://www.omg.org/spec/ReqIF/20110401/reqif.xsd` laden und die + `schemaLocation`-Importe (`xml.xsd`, `driver.xsd` und die XHTML-Module) lokal auflösen. +2. Das README-Beispiel (`ReqIFBuilder` … `build()`) mit + `new ReqIFValidator().validateAgainstSchema(doc, lokalesXsd)` prüfen. Ergebnis: + Fehler zu `LAST-CHANGE`, `MULTI-VALUED`, `OTHER-CONTENT` und zum Header. +3. Eine Datei mit `LAST-CHANGE`, `DESC`, `LONG-NAME` am `SPEC-OBJECT`, `REPOSITORY-ID`, `xml:lang`, + `IS-EDITABLE`, `ACCURACY`, `THE-ORIGINAL-VALUE`, `IS-TABLE-INTERNAL`, `EDITABLE-ATTS` und + ungesetzten Boolean- und Real-Attributen einlesen und mit `ReqIFWriter` schreiben. Die Ausgabe + enthält keines dieser Merkmale, dafür aber `THE-VALUE="false"` und `THE-VALUE="0.0"`. +4. `THE-VALUE="99999999999"` in einem Integer-Attribut führt zur `NumberFormatException`. + Ein fehlender `LONG-NAME` an einem Datentyp oder eine unbekannte `SPEC-OBJECT-TYPE-REF` + führt zur `NullPointerException`. +5. Ein `.reqifz` mit `files/obj.ole`, `files/a.gif` und `files/b.png` einlesen und mit + `ReqIFzWriter.write(ReqIFz, …)` neu packen: Das Ergebnis enthält nur `doc.reqif` und `files/b.png`. diff --git a/README.md b/README.md index 0ea34b2..40af7cd 100644 --- a/README.md +++ b/README.md @@ -279,18 +279,151 @@ new ReqIFWriter().write(reqif.getReqIFDocument(), Path.of("out.reqif")); String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); ``` -Round-tripping (parse -> write -> parse) preserves the header, all -datatypes including enumerations, spec types with attribute definitions -and defaults, spec objects with all attribute value kinds (multiselect -enumerations and XHTML included), spec relations with their attributes, -and the specification hierarchy. Attribute values are written sorted by -definition id so the output is reproducible. +Round-tripping (parse -> write) is lossless: every element and attribute +of the ReqIF schema is read into the model and written back, including +`xml:lang`, the header's `REPOSITORY-ID`, `IS-EDITABLE` of attribute +definitions and hierarchy nodes, `IS-TABLE-INTERNAL` and `EDITABLE-ATTS`, +and XHTML values with `THE-ORIGINAL-VALUE` / `IS-SIMPLIFIED` (e.g. an +embedded OLE object of which `THE-VALUE` only shows the preview). +`LosslessRoundTripTest` checks this with a document that uses all of +them; the comparison ignores only what the schema leaves open (namespace +prefixes, attribute order, the order of xsd:all children, formatting). +Typed values are written in canonical notation, so the lexical form of +a value may change while its meaning does not: `1` becomes `true` for a +boolean, `1e3` becomes `1000.0` for a real, `+5` becomes `5`. +Attribute values are written in document order. Integer values have no +size limit (`AttributeValueInteger.getBigInteger()`). + +## Attribute values + +An element's values are available in two views: + +```java +SpecObject specObject = reqif.getReqIFCoreContent().getSpecObject("so-1"); + +// what the element itself carries - this is what is written back - +// keyed by attribute definition IDENTIFIER, in document order +Map own = specObject.getValues(); +AttributeValue text = specObject.getValue("ad-text"); + +// effective values by LONG-NAME, including the defaults of attribute +// definitions the element has no value for (read-only) +Object priority = specObject.getAttribute("Priority"); + +// changing values, by attribute definition IDENTIFIER; the value class +// follows the datatype, unknown definitions and enum values are refused +specObject.set("ad-prio", 7) + .setXhtml("ad-text", "

The system shall boot.

") + .setEnum("ad-status", "ev-draft", "ev-approved"); +specObject.removeValue("ad-text"); // the default applies again +``` + +Defaults are never turned into values: an attribute the document leaves +unset stays unset when the document is written, and the default keeps +applying through its attribute definition. Prefer `getValues()` when +names may repeat - LONG-NAMEs need not be unique within a spec type. + +## Identity and metadata of elements + +Every identifiable element - datatypes, enum values, spec types, attribute +definitions, spec objects, spec relations, specifications, spec hierarchy +nodes and relation groups - extends `Identifiable` and exposes the +properties the ReqIF schema defines for it: + +```java +SpecObject specObject = reqif.getReqIFCoreContent().getSpecObject("so-1"); + +specObject.getID(); // IDENTIFIER +specObject.getName(); // LONG-NAME, null if the document declares none +specObject.getDesc(); // DESC +specObject.getLastChange(); // LAST-CHANGE, as written (xsd:dateTime) +specObject.getAlternativeID(); // ALTERNATIVE-ID + +specObject.setDesc("reviewed").setLastChange("2026-10-11T12:00:00Z"); +``` + +They are written back unchanged; properties a document does not declare +stay absent instead of being written empty. + +Elements created from plain values, and elements changed through the API +(values, properties, structure), are *modified* (`isModified()`): the +writer gives them the time of writing as `LAST-CHANGE`, so a tool that +imports the document sees what changed, and once the output succeeded +takes that time into the model - writing again reports no new change. +`setLastChange(...)` after the last change keeps the given value; +`ReqIFCoreContent.clearModified()` keeps every element's `LAST-CHANGE`. +`buildDocument` and the validator leave the model alone. `ALTERNATIVE-ID` is written +in the nested form the schema prescribes +(``); +the flat form older versions of this library wrote is still read. + +## Schema-valid output + +The writer emits every value the official ReqIF schema requires, also when +the model does not carry it - typically for a document built with +`ReqIFBuilder`. The model itself is not changed: + +| missing in the model | written as | +|---|---| +| `LAST-CHANGE` of a modified element | the time of writing | +| `LAST-CHANGE` of an element | the header's `CREATION-TIME`, or the time of writing | +| header `CREATION-TIME` | the time of writing | +| header `REQ-IF-TOOL-ID`, `SOURCE-TOOL-ID` | `reqif4j` | +| header `REQ-IF-VERSION` | `1.0` (the only value the schema allows) | +| header `TITLE` | empty | +| `ACCURACY` / `MIN` / `MAX` of a REAL datatype | 15 / full double range | + +`MULTI-VALUED` and `OTHER-CONTENT` are always written, also when `false` +or empty, and infinite REAL values use the xsd notation `INF` / `-INF`. +The time of writing comes from a `java.time.Clock`; pass a fixed one for +reproducible output: + +```java +new ReqIFWriter().setClock(Clock.fixed(Instant.parse("2026-10-11T12:00:00Z"), ZoneOffset.UTC)) + .write(document, Path.of("out.reqif")); +``` + +What no default can fix - a missing header, missing or invalid +identifiers, malformed dates - is reported by `ReqIFValidator` (see +below). A document the validator accepts without errors is written +schema-valid, with one exception: which XHTML elements a value may use +(ReqIF allows a subset, e.g. no `font` or `img`) is only checked by +`validateAgainstSchema`, against the official schema. Indenting is **off by default**: the XML indenter inserts whitespace into mixed content, which would change XHTML attribute values. Enable it with `new ReqIFWriter().setIndent(true)` only when readable output matters more than exact XHTML content. +## Changing documents + +The content is changed through its methods, which keep the references +between elements consistent; the maps its getters return are read-only. + +```java +ReqIFCoreContent content = reqif.getReqIFCoreContent(); + +// hierarchy: insert, move (to any specification or node) and remove nodes +content.addSpecHierarchy("spec-1", 0, new SpecHierarchy("sh-new", 0, 0, specObject, null)); +content.moveSpecHierarchy("sh-new", "sh-1", 2); // below sh-1, third child +content.removeSpecHierarchy("sh-new"); // with everything below it + +// elements: identifiers must be unused anywhere in the document +content.addSpecObject(new SpecObject("so-3", content.getSpecType("st-req"), List.of())); +content.removeSpecObject("so-1"); // its nodes' children move up, its relations go +content.removeSpecRelation("sr-1"); // also from its relation groups +content.removeSpecification("spec-2"); // with the relation groups between it and others +content.removeSpecType("st-old"); // refused while an element has this type +content.removeDatatype("dt-old"); // refused while an attribute uses it + +// relations and relation groups +content.getSpecRelation("sr-2").setSourceObjID("so-3").setTargetObjID("so-2"); +content.getRelationGroup("rg-1").addSpecRelationRef("sr-2"); +``` + +Levels and section numbers of hierarchy nodes follow every change; a node +is in one place only (`getParent()`) and never below itself. + ## Creating documents from scratch `ReqIFBuilder` assembles the same object model the parser produces, so a @@ -328,25 +461,62 @@ ReqIFDocument document = ReqIFBuilder.create() new ReqIFWriter().write(document, Path.of("out.reqif")); ``` +Everything the parser reads can be built as well: + +```java +builder + .relationGroupType("st-group", "Trace", t -> {}) + .specObjectType("st-req", "Requirement Type", t -> t + .xhtmlAttribute("ad-text", "ReqIF.Text", "dt-xhtml") + .editable("ad-text", true)) // IS-EDITABLE + .specObject("so-ole", "st-req", o -> o // IS-SIMPLIFIED + .setXhtml("ad-text", "

preview

", "

complete content

")) + .specification("spec-1", "System", "st-spec", s -> s + .child("sh-1", "so-1", h -> h.tableInternal(false).editable(true) + .editableAttributes("ad-text"))) // EDITABLE-ATTS + .relationGroup("rg-1", "System to software", "st-group", "spec-1", "spec-2", "sr-1") + .with("so-1", e -> e.setDesc("Boot").setAlternativeID("REQ-1")) // any element + .lang("de") // xml:lang + .toolExtension(""); +``` + +Tool extensions must be elements of a namespace of their own, as the +schema requires. The builder's elements count as unchanged: they get the +header's `CREATION-TIME` as `LAST-CHANGE` unless one is set with `with`. + XHTML values may be passed with or without a surrounding `div`; the markup is parsed, so the node tree and the token list work on generated values too. The content category of generated spec objects is derived by the same `TypeClassifier` used when reading (`typeClassifier(...)` to override). -## Writing .reqifz archives +## Reading and writing .reqifz archives + +A .reqifz archive holds one or more ReqIF documents plus the files they +reference - images, embedded OLE objects, PDFs or anything else. Reading +it keeps every entry: the documents in `getReqIFDocuments()`, all other +files in `getAttachmentFiles()` (pictures additionally in +`getPictureFiles()`). The archive is extracted into a temporary +directory that `close()` deletes again, so use try-with-resources and +read the files before closing. Archives can also be read from a stream: + +```java +try (ReqIFz reqifz = new ReqIFz(uploadStream, "upload.reqifz")) { + File ole = reqifz.getAttachmentFiles().get("files/diagram.ole"); +} +``` -Images travel with the document, stored under the path the XHTML +Attachments travel with the document, stored under the path the XHTML `object` elements reference: ```java -Map pictures = Map.of("files/diagram.png", pngBytes); -new ReqIFzWriter().write(document, "spec.reqif", pictures, Path.of("spec.reqifz")); +Map attachments = Map.of("files/diagram.png", pngBytes); +new ReqIFzWriter().write(document, "spec.reqif", attachments, Path.of("spec.reqifz")); ``` An archive that was read can be re-packed - the documents are serialized -from the model (so modifications are included) while the images are -copied from the extracted files, without consuming the one-shot streams: +from the model (so modifications are included) while every attachment is +copied unchanged from the extracted files: ```java try (ReqIFz source = new ReqIFz("in.reqifz")) { @@ -357,11 +527,21 @@ try (ReqIFz source = new ReqIFz("in.reqifz")) { # Validation `ReqIFValidator` checks a document before it is written: identifiers -present and globally unique, resolvable datatype and spec type -references, attribute values matching their spec type, enum value -references, relation endpoints, spec hierarchy targets and relation +present, valid `xsd:ID`s and globally unique, resolvable datatype and +spec type references, attribute values matching their spec type, enum +value references, relation endpoints, spec hierarchy targets and relation group references. +Values the schema constrains are errors when they do not fit: +`LAST-CHANGE`, `CREATION-TIME` and DATE values must be `xsd:dateTime` +(a date alone is not enough), enum `KEY`s integers, `REQ-IF-VERSION` +`1.0`, and default values - written as they are - must already be in +schema notation (`true`/`false`, `1.5`, `INF`). Values outside the +limits of their datatype (`MAX-LENGTH`, `MIN`/`MAX`) are warnings: the +schema cannot express those limits, but importing tools may reject them. +Text with characters XML cannot hold (control characters) is an error, +as are relation groups without type, source or target specification. + ```java new ReqIFValidator().validate(document).throwIfInvalid(); new ReqIFWriter().write(document, Path.of("out.reqif")); @@ -369,16 +549,123 @@ new ReqIFWriter().write(document, Path.of("out.reqif")); This is a model-level check. The OMG ReqIF XSD is **not bundled** with this library for licensing reasons; if you have it, run a full XML -Schema validation on top: +Schema validation on top - either of the XML the writer produces for a +document, or of a file on disk (findings then carry line numbers): ```java ValidationResult schemaIssues = - new ReqIFValidator().validateAgainstSchema(document, Path.of("reqif.xsd")); + new ReqIFValidator().validateAgainstSchema(document, Path.of("schema/reqif.xsd")); +ValidationResult fileIssues = + new ReqIFValidator().validateAgainstSchema(Path.of("in.reqif"), Path.of("schema/reqif.xsd")); +``` + +`reqif.xsd` imports further schemas by http URL: `xml.xsd`, +[`driver.xsd`](http://www.omg.org/spec/ReqIF/20110402/driver.xsd) and the +XHTML modules it references (`xhtml-*-1.xsd`). Validation never goes +online; these imports are looked up by file name in the directory of +`reqif.xsd`, so download them into the same folder. A missing import is +reported by name together with its original URL. + +The test suite validates against the official schema. It downloads the +schema files on first use into `target/reqif-schema` (or the directory +in the `REQIF_SCHEMA_DIR` environment variable); without network access +these tests are skipped locally and fail on CI. + +Relations may connect spec objects, and relation groups specifications, +of another document of the same .reqifz archive (the schema types these +references `GLOBAL-REF`). Validate the documents of an archive together +to resolve them across documents: + +```java +try (ReqIFz archive = new ReqIFz("export.reqifz")) { + new ReqIFValidator().validate(archive).throwIfInvalid(); +} +``` + +Identifiers duplicated within one category cannot be seen in the model +(maps are keyed by identifier, so the later element has replaced the +earlier); the parser reports them while reading, see below. + +## Conventions of the ReqIF Implementation Guide + +`ImplementationGuideCheck` checks what tools exchanging ReqIF rely on +beyond the schema, following the *ReqIF Implementation Guide* of the +ReqIF Implementor Forum: + +- the standard attributes have the datatype tools expect: dates + (`ReqIF.ForeignCreatedOn`, `ReqIF.ForeignModifiedOn`) are DATE, + `ReqIF.ForeignDeleted` is BOOLEAN, `ReqIF.Text` is XHTML, names and + descriptions are STRING or XHTML; +- embedded objects state their MIME type and reference a file of the + archive by a relative path; an object that is not a PNG image, such as + an OLE object, contains a PNG image as preview; +- a simplified XHTML value (`IS-SIMPLIFIED`) carries + `THE-ORIGINAL-VALUE`, and the other way round; +- for an archive, every file an object references is in it. + +```java +ValidationResult conventions = new ImplementationGuideCheck().check(document); +ValidationResult archiveConventions = new ImplementationGuideCheck().check(reqifz); +``` + +Findings are warnings - the document is valid ReqIF, but importing tools +may show it differently than intended - except for files missing from an +archive, which are errors. + +# Tested against real exports + +`CorpusTest` reads exports of IBM DOORS, Siemens Polarion, ReqIF Studio, +Enterprise Architect, Capella and StrictDoc as well as the reference files +of the ReqIF Implementor Forum (`src/test/resources/corpus`, with their +origin and license). Each document is written back as read - or differs +for a stated reason, such as a document in the namespace of a ReqIF draft +- and written the same way a second time; the validator raises no error +for a schema-valid file, and a document it finds no error in is written +schema-valid. Archives are re-packed with all their entries. + +# Large documents + +Reading, changing and writing take time in proportion to the size of a +document. The 19 MB reference file of the ReqIF Implementor Forum with +10,000 objects is read in about half a second and written in less; +the model needs about 100 MB of heap for it (roughly 10 KB per object). +`LargeDocumentTest` builds, writes, reads and changes 20,000 objects to +keep it that way. + +# Reading imperfect documents + +Real exports are not always schema-valid. A document with broken parts is +still read: what cannot be used is skipped, everything else is kept, and +each problem is recorded instead of stopping the reading with an +exception: + +```java +ReqIF reqif = new ReqIF("export.reqif"); +for (ParseIssue issue : reqif.getReqIFDocument().getParseIssues()) { + System.out.println(issue); // e.g. "[so-1] Value references attribute definition ad-x, ..." +} ``` -Not covered yet: identifiers duplicated within one category (the parser -keys its maps by identifier, so a duplicate has already replaced its -predecessor by the time the model exists). +- elements without `IDENTIFIER` are skipped; +- an unresolvable type reference keeps the element and its reference (it + is written back); its values cannot be interpreted and are kept as + `UninterpretedAttributeValue`s - the XML element as read, written back + unchanged - as are values of unknown attribute definitions and values + whose kind does not match their definition (Polarion, for example, + writes `ReqIF.Text` values as `ATTRIBUTE-VALUE-STRING`); +- an attribute definition whose datatype is missing is kept as read, too; +- values that do not fit their datatype (`THE-VALUE="abc"` for an integer) + are kept as written and written back unchanged; +- identifiers used twice within one kind are reported, the later element + wins. + +`ReqIFValidator.validate` reports these issues as warnings - they say +what happened while reading - next to its own findings, which report as +errors what is still wrong with the model (an unresolved reference, a +value that does not fit its datatype) until it is fixed. The model +classes' constructors that take no `ParseIssues` read just as tolerantly +and discard the issues; only an element without IDENTIFIER makes them +throw a `ReqIFParseException`. # Unmodelled ReqIF kinds diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java index 5bd2ba2..3dc642d 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeDefinition.java @@ -7,30 +7,23 @@ import org.w3c.dom.NodeList; import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; +import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; -public class AttributeDefinition { +public class AttributeDefinition extends Identifiable { - private String id; - private String name; private Datatype type; private String defaultValue; - private String alternativeID; private String sourceElementName; + private Boolean editable; + private AttributeValueXHTML defaultXhtml; + private Element unresolvedElement; - public String getID() { - return this.id; - } - - public String getName() { - return this.name; - } - public Datatype getDataType() { return this.type; } @@ -40,10 +33,26 @@ public String getDefaultValue() { } /** - * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null + * @return the default of an XHTML attribute as read from the document, or + * null if there is none or the definition was not read from a + * document (then {@link #getDefaultValue()} holds the markup) + */ + public AttributeValueXHTML getDefaultXhtmlValue() { + return this.defaultXhtml; + } + + /** + * @return IS-EDITABLE: whether values of this attribute may be edited in + * the receiving tool, or null if the document does not say */ - public String getAlternativeID() { - return this.alternativeID; + public Boolean getEditable() { + return this.editable; + } + + public AttributeDefinition setEditable(Boolean editable) { + this.editable = editable; + markModified(); + return this; } /** @@ -53,6 +62,27 @@ public String getAlternativeID() { public String getSourceElementName() { return this.sourceElementName; } + + /** + * @return the element this definition was read from if its datatype is not + * declared - it is then written back as read - otherwise null + */ + public Element getUnresolvedElement() { + return this.type == null ? this.unresolvedElement : null; + } + + /** + * @return the element name this definition is written with, e.g. + * ATTRIBUTE-DEFINITION-STRING; for datatype kinds the parser does + * not model the name it was read with; null if neither is known + */ + public String getElementName() { + String suffix = ReqIFConst.datatypeSuffix(this.type == null ? null : this.type.getType()); + if(suffix != null) { + return "ATTRIBUTE-DEFINITION-" + suffix; + } + return this.sourceElementName; + } @@ -62,19 +92,17 @@ public String getSourceElementName() { * generated instead of parsed. */ public AttributeDefinition(String id, String name, Datatype type, String defaultValue) { + super(id, name); - this.id = id; - this.name = name; this.type = type; this.defaultValue = defaultValue; } public AttributeDefinition(Node attributeDefinition, Map dataTypes) { - - this.id = attributeDefinition.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent(); - this.name = attributeDefinition.getAttributes().getNamedItem(ReqIFConst.LONG_NAME).getTextContent(); - this.alternativeID = XmlUtils.alternativeID(attributeDefinition); + super(attributeDefinition); + this.sourceElementName = XmlUtils.localName(attributeDefinition); + this.editable = XmlUtils.booleanAttribute(attributeDefinition, ReqIFConst.IS_EDITABLE); // Navigate by element (not by fixed child index) so both pretty-printed // and minified ReqIF files are handled. @@ -83,13 +111,28 @@ public AttributeDefinition(Node attributeDefinition, Map dataT if(typeRef != null) { this.type = dataTypes.get(typeRef.getTextContent().trim()); } + if(this.type == null && attributeDefinition instanceof Element) { + // without its datatype the definition cannot be interpreted; it is + // written back as read + this.unresolvedElement = (Element) attributeDefinition; + } 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(); + Element attDefVal = XmlUtils.firstChildElement(defVal); + String theValue = XmlUtils.attribute(attDefVal, ReqIFConst.THE_VALUE); + if(theValue != null) { + this.defaultValue = theValue; + + } else if(XmlUtils.firstChildElementByLocalName(attDefVal, ReqIFConst.THE_VALUE) != null) { + // an XHTML default carries its markup in a THE-VALUE element. The + // parsed value is kept, so it is written back as it was (a p + // instead of a div, THE-ORIGINAL-VALUE); getDefaultValue() offers + // it as rendered XHTML. + this.defaultXhtml = new AttributeValueXHTML(attDefVal, this); + String markup = (String) this.defaultXhtml.getValue(); + this.defaultValue = markup.isEmpty() ? null : markup; } } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValue.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValue.java index 5a30cf4..3cba54b 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValue.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValue.java @@ -23,8 +23,12 @@ public AttributeDefinition getAttributeDefinitionType() { return this.type; } + /** + * @return the category of the definition's datatype, or null if the + * definition has no datatype + */ public String getDatatype() { - return this.type.getDataType().getType(); + return this.type.getDataType() == null ? null : this.type.getDataType().getType(); } /** diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueBoolean.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueBoolean.java index 1fa40d9..d5b0eed 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueBoolean.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueBoolean.java @@ -6,7 +6,10 @@ public class AttributeValueBoolean extends AttributeValue { public AttributeValueBoolean(String value, AttributeDefinition type) { super(value, type); - if(value != null && value.toLowerCase().equals("true")) { + // xsd:boolean: "true" or "1" (lower case "true" only, but older versions + // accepted any case) + String trimmed = value == null ? "" : value.trim(); + if(trimmed.equalsIgnoreCase("true") || trimmed.equals("1")) { this.value = true; }else{ this.value = false; diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java index 12178d5..dfba172 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueDouble.java @@ -1,5 +1,7 @@ package de.uni_stuttgart.ils.reqif4j.attributes; +import de.uni_stuttgart.ils.reqif4j.util.XsdTypes; + public class AttributeValueDouble extends AttributeValue{ public AttributeValueDouble(String value, AttributeDefinition type) { @@ -9,7 +11,8 @@ public AttributeValueDouble(String value, AttributeDefinition type) { if(value == null || value.isBlank()){ this.value = 0.0; }else { - this.value = Double.parseDouble(value.trim()); + // xsd:double notation: INF / -INF for infinity + this.value = XsdTypes.parseDouble(value); } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueFactory.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueFactory.java new file mode 100644 index 0000000..8f2fc82 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueFactory.java @@ -0,0 +1,116 @@ +package de.uni_stuttgart.ils.reqif4j.attributes; + +import java.util.ArrayList; +import java.util.List; + +import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeEnumeration; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; + +/** + * Creates attribute values of the class that fits the attribute's datatype, + * so callers need not know AttributeValueBoolean, AttributeValueInteger, ... + * Used by the builder and by the setters of the model elements. + */ +public final class AttributeValueFactory { + + private AttributeValueFactory() { + } + + /** + * @param value a Boolean, Number, String or anything whose toString() is + * the value's text; null for an empty value + * @throws IllegalArgumentException for enumeration and XHTML attributes, + * which have their own factory methods + */ + public static AttributeValue of(AttributeDefinition definition, Object value) { + + String category = categoryOf(definition); + String text = value == null ? null : String.valueOf(value); + + if (ReqIFConst.ENUMERATION.equals(category)) { + throw new IllegalArgumentException("Use setEnum(...) for the enumeration attribute " + definition.getID()); + } + if (ReqIFConst.XHTML.equals(category)) { + throw new IllegalArgumentException("Use setXhtml(...) for the XHTML attribute " + definition.getID()); + } + if (category == null) { + return new AttributeValue(text, definition); + } + try { + switch (category) { + case ReqIFConst.BOOLEAN: return new AttributeValueBoolean(text, definition); + case ReqIFConst.INTEGER: return new AttributeValueInteger(text, definition); + case ReqIFConst.STRING: return new AttributeValueString(text, definition); + case ReqIFConst.DATE: return new AttributeValueDate(text, definition); + case ReqIFConst.DOUBLE: + case ReqIFConst.REAL: return new AttributeValueDouble(text, definition); + default: return new AttributeValue(text, definition); + } + } catch (IllegalArgumentException e) { + // e.g. a NumberFormatException, which does not say which attribute + throw new IllegalArgumentException("'" + text + "' is not a valid " + category + + " value for attribute " + definition.getID(), e); + } + } + + /** + * @param markup XHTML, with or without a surrounding div + * @throws IllegalArgumentException if the attribute is not an XHTML one or + * the markup is not well-formed + */ + public static AttributeValueXHTML xhtml(AttributeDefinition definition, String markup) { + requireCategory(definition, ReqIFConst.XHTML); + return new AttributeValueXHTML(markup, definition); + } + + /** + * @param markup the simplified XHTML (THE-VALUE) + * @param originalMarkup the complete XHTML (THE-ORIGINAL-VALUE); the value + * is marked IS-SIMPLIFIED + * @throws IllegalArgumentException if the attribute is not an XHTML one or + * the markup is not well-formed + */ + public static AttributeValueXHTML xhtml(AttributeDefinition definition, String markup, String originalMarkup) { + requireCategory(definition, ReqIFConst.XHTML); + return new AttributeValueXHTML(markup, originalMarkup, definition); + } + + /** + * @param enumValueIDs the IDENTIFIERs of the selected enum values; none for + * an empty selection + * @throws IllegalArgumentException if the attribute is not an enumeration + * one or an enum value does not exist + */ + public static AttributeValueEnumeration enumeration(AttributeDefinition definition, List enumValueIDs) { + + requireCategory(definition, ReqIFConst.ENUMERATION); + if (enumValueIDs == null || enumValueIDs.contains(null)) { + throw new IllegalArgumentException("The enum values for attribute " + definition.getID() + + " must not be null; pass none for an empty selection"); + } + DatatypeEnumeration datatype = (DatatypeEnumeration) definition.getDataType(); + List names = new ArrayList(); + for (String enumValueID : enumValueIDs) { + String name = datatype.getEnumValueName(enumValueID); + if (name == null) { + throw new IllegalArgumentException("Unknown enum value " + enumValueID + + " for attribute " + definition.getID()); + } + names.add(name); + } + return new AttributeValueEnumeration(names, new ArrayList(enumValueIDs), definition); + } + + private static String categoryOf(AttributeDefinition definition) { + return definition.getDataType() == null ? null : definition.getDataType().getType(); + } + + private static void requireCategory(AttributeDefinition definition, String expected) { + String category = categoryOf(definition); + if (!expected.equals(category) + || ReqIFConst.ENUMERATION.equals(expected) && !(definition.getDataType() instanceof DatatypeEnumeration)) { + throw new IllegalArgumentException("Attribute " + definition.getID() + " is of kind " + category + + ", but " + expected + " was expected"); + } + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java index b7b24ee..612854a 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueInteger.java @@ -1,22 +1,53 @@ package de.uni_stuttgart.ils.reqif4j.attributes; +import java.math.BigInteger; + +/** + * Value of an INTEGER attribute. ReqIF declares it as {@code xsd:integer}, + * which has no size limit, and DOORS regularly uses values beyond the int + * range. + * + * {@link #getValue()} returns an {@link Integer} when the value fits, so + * existing code comparing with int values keeps working, a {@link Long} when + * it fits that, and a {@link BigInteger} otherwise. {@link #getBigInteger()} + * always returns the exact value. + */ public class AttributeValueInteger extends AttributeValue { - - + + private final BigInteger integer; + public AttributeValueInteger(String value, AttributeDefinition type) { super(value, type); // 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.trim()); - } + this.integer = value == null || value.isBlank() ? BigInteger.ZERO : new BigInteger(value.trim()); + this.value = narrowest(this.integer); } + /** + * @return the value as Integer, Long or BigInteger, whichever is the + * smallest type that holds it + */ @Override public Object getValue() { - return (int)this.value; + return this.value; + } + + /** + * @return the exact value + */ + public BigInteger getBigInteger() { + return this.integer; + } + + private static Number narrowest(BigInteger value) { + // bitLength excludes the sign bit + if (value.bitLength() < Integer.SIZE) { + return value.intValue(); + } + if (value.bitLength() < Long.SIZE) { + return value.longValue(); + } + return value; } - } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java index 85b5c93..328a8dd 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/AttributeValueXHTML.java @@ -4,8 +4,10 @@ import java.util.Collections; import java.util.List; +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.XhtmlParser; import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLElement; @@ -59,6 +61,8 @@ public class AttributeValueXHTML extends AttributeValue { private static final String VARIABLE_NAME_MISSING = "VARIABLE_NAME_MISSING"; XHTMLElementDiv divValue; + private XHTMLElementDiv originalValue; + private Boolean simplified; @Override public Object getValue() { @@ -68,16 +72,48 @@ public Object getValue() { return divValue.toString(); } + /** + * @param xhtmlContent the ATTRIBUTE-VALUE-XHTML element; for compatibility a + * node that merely contains the XHTML div is accepted too + */ public AttributeValueXHTML(Node xhtmlContent, AttributeDefinition type) { super(type); - // 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); + Element theValue = XmlUtils.firstChildElementByLocalName(xhtmlContent, ReqIFConst.THE_VALUE); + Node content; + if(theValue != null) { + // THE-VALUE holds a div or, as the schema allows, a single p + content = XmlUtils.firstChildElement(theValue); + Element original = XmlUtils.firstChildElement( + XmlUtils.firstChildElementByLocalName(xhtmlContent, ReqIFConst.THE_ORIGINAL_VALUE)); + this.originalValue = original == null ? null : new XHTMLElementDiv(original); + this.simplified = XmlUtils.booleanAttribute(xhtmlContent, ReqIFConst.IS_SIMPLIFIED); + } else { + // The div may carry a namespace prefix (xhtml:div, reqif-xhtml:div, ...), + // so it has to be located by local name. + content = XmlUtils.firstDescendantByLocalName(xhtmlContent, XHTML.DIV); + } + this.divValue = content == null ? null : new XHTMLElementDiv(content); this.value = deconstructXHTML(this.divValue); } + /** + * @return THE-ORIGINAL-VALUE: the complete content when THE-VALUE holds a + * simplified version of it, e.g. an embedded OLE object of which + * THE-VALUE only shows the preview image; null if there is none + */ + public XHTMLElementDiv getOriginalValue() { + return this.originalValue; + } + + /** + * @return IS-SIMPLIFIED: whether THE-VALUE is a simplified version of + * THE-ORIGINAL-VALUE, or null if the document does not say + */ + public Boolean getSimplified() { + return this.simplified; + } + /** * Creates an XHTML value from markup, for documents that are generated * instead of parsed. The markup is parsed so the node tree and the token @@ -92,10 +128,44 @@ public AttributeValueXHTML(String value, AttributeDefinition type) { this.value = deconstructXHTML(this.divValue); } + /** + * Creates an XHTML value whose THE-VALUE is a simplified version of the + * complete content, e.g. the preview image of an embedded OLE object. + * + * @param value the simplified markup + * @param originalValue the complete markup (THE-ORIGINAL-VALUE) + * @throws IllegalArgumentException if the markup is not well-formed + */ + public AttributeValueXHTML(String value, String originalValue, AttributeDefinition type) { + this(value, type); + + if(value == null || value.isBlank() || originalValue == null || originalValue.isBlank()) { + throw new IllegalArgumentException("A simplified XHTML value of attribute " + + (type == null ? null : type.getID()) + " needs both the value and the original value"); + } + this.originalValue = new XHTMLElementDiv(XhtmlParser.parseDiv(originalValue)); + this.simplified = Boolean.TRUE; + } + public XHTMLElementDiv getDivValue() { return this.divValue; } + /** + * @return the block elements of THE-VALUE (paragraphs, tables, lists ...), + * empty if the value has no content + */ + public List getBlocks() { + return this.divValue == null ? Collections.emptyList() : blocksOf(this.divValue); + } + + /** + * THE-VALUE holds a div or, as the schema allows, a single p. + */ + private static List blocksOf(XHTMLElementDiv value) { + return XHTML.P.equals(value.getTagName()) ? List.of(value) : childrenOf(value); + } + /** * @return the deconstructed element list (P/TBL/L/H/OBJ tokens with their * content), never null @@ -121,7 +191,8 @@ private AttributeValueXHTMLElementList deconstructXHTML(XHTMLElementDiv div) { return xhtmlElementList; } - for(XHTMLNode child: childrenOf(div)) { + List blocks = blocksOf(div); + for(XHTMLNode child: blocks) { String elementType = elementToken(child.getTagName()); if(elementType != null) { xhtmlElementList.add(elementType, deconstructElement(elementType, child)); diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/UninterpretedAttributeValue.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/UninterpretedAttributeValue.java new file mode 100644 index 0000000..c2b58b2 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/attributes/UninterpretedAttributeValue.java @@ -0,0 +1,70 @@ +package de.uni_stuttgart.ils.reqif4j.attributes; + +import org.w3c.dom.Element; + +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; + +/** + * A value the parser could not interpret, kept as the ATTRIBUTE-VALUE-* + * element it was read from and written back unchanged: a value of an element + * whose spec type is not declared, a value whose attribute definition is not + * declared, or a value whose kind does not match its definition (e.g. an + * ATTRIBUTE-VALUE-STRING for an XHTML attribute). + * + * Exports cut out of a larger document often reference types they do not + * contain; their values are kept this way instead of being lost. + */ +public class UninterpretedAttributeValue extends AttributeValue { + + private final Element element; + private final String reason; + + /** + * @param element the ATTRIBUTE-VALUE-* element + * @param definition the attribute definition, or one standing in for an + * undeclared definition (IDENTIFIER only) + * @param reason why the value could not be interpreted + */ + public UninterpretedAttributeValue(Element element, AttributeDefinition definition, String reason) { + super(theValue(element), definition); + this.element = element; + this.reason = reason; + setSourceElementName(XmlUtils.localName(element)); + } + + /** + * @return the ATTRIBUTE-VALUE-* element exactly as read + */ + public Element getElement() { + return this.element; + } + + /** + * @return why the value could not be interpreted + */ + public String getReason() { + return this.reason; + } + + /** + * @return the THE-VALUE attribute; for an XHTML value the text of its + * THE-VALUE element, for an enumeration value the referenced enum + * value IDENTIFIERs separated by ", "; "" if there is none + */ + private static String theValue(Element element) { + String theValue = XmlUtils.attribute(element, ReqIFConst.THE_VALUE); + if (theValue != null) { + return theValue; + } + Element content = XmlUtils.firstChildElementByLocalName(element, ReqIFConst.THE_VALUE); + if (content != null) { + return content.getTextContent().trim(); + } + java.util.List refs = new java.util.ArrayList(); + for (Element ref : XmlUtils.descendantsByLocalName(element, ReqIFConst.ENUM_VALUE_REF)) { + refs.add(ref.getTextContent().trim()); + } + return String.join(", ", refs); + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/build/ReqIFBuildException.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/build/ReqIFBuildException.java index e1eb8c8..05c5618 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/build/ReqIFBuildException.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/build/ReqIFBuildException.java @@ -2,9 +2,10 @@ /** * Thrown when a ReqIF document cannot be assembled, e.g. because an unknown - * identifier is referenced. + * identifier is referenced. An {@link IllegalArgumentException}: the builder + * was called with arguments that do not fit together. */ -public class ReqIFBuildException extends RuntimeException { +public class ReqIFBuildException extends IllegalArgumentException { public ReqIFBuildException(String message) { super(message); diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/build/ReqIFBuilder.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/build/ReqIFBuilder.java index e820184..f55416c 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/build/ReqIFBuilder.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/build/ReqIFBuilder.java @@ -1,12 +1,25 @@ package de.uni_stuttgart.ils.reqif4j.build; +import java.io.IOException; +import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeBoolean; import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeDate; @@ -16,9 +29,14 @@ import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeInteger; import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeString; import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeXHTML; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition; +import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFHeader; +import de.uni_stuttgart.ils.reqif4j.specification.RelationGroup; +import de.uni_stuttgart.ils.reqif4j.specification.RelationGroupType; import de.uni_stuttgart.ils.reqif4j.specification.SpecHierarchy; import de.uni_stuttgart.ils.reqif4j.specification.SpecObject; import de.uni_stuttgart.ils.reqif4j.specification.SpecObjectType; @@ -28,6 +46,9 @@ import de.uni_stuttgart.ils.reqif4j.specification.Specification; import de.uni_stuttgart.ils.reqif4j.specification.SpecificationType; import de.uni_stuttgart.ils.reqif4j.specification.TypeClassifier; +import de.uni_stuttgart.ils.reqif4j.util.SecureXml; +import de.uni_stuttgart.ils.reqif4j.util.XsdTypes; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; /** * Fluent builder for creating ReqIF documents from scratch. The result is the @@ -59,13 +80,23 @@ */ public class ReqIFBuilder { + /** IDENTIFIER of the header if {@link HeaderBuilder#id(String)} is not set. */ + public static final String DEFAULT_HEADER_ID = "reqif4j-header"; + private final Map datatypes = new LinkedHashMap(); private final Map specTypes = new LinkedHashMap(); private final Map specObjects = new LinkedHashMap(); - private final List specRelations = new ArrayList(); - private final List specifications = new ArrayList(); + private final Map specRelations = new LinkedHashMap(); + private final Map specifications = new LinkedHashMap(); + private final List relationGroups = new ArrayList(); + private final List toolExtensions = new ArrayList(); + private String lang; private final Map identifiers = new LinkedHashMap(); + /** the identifiers claimed during the running call, to undo it if it fails */ + private List claimed; + /** every element by IDENTIFIER, for with(); rebuilt after elements were added */ + private Map elements; private final HeaderBuilder headerBuilder = new HeaderBuilder(); private TypeClassifier typeClassifier = TypeClassifier.defaultClassifier(); @@ -88,14 +119,92 @@ public ReqIFBuilder header(Consumer header) { return this; } + /** + * @param lang the document language (xml:lang), e.g. "de" or "en-US" + */ + public ReqIFBuilder lang(String lang) { + this.lang = lang; + return this; + } + + /** + * Adds a tool extension (REQ-IF-TOOL-EXTENSION): vendor-specific XML the + * document carries along without interpreting it. + * + * @param xml the content, one or more elements, e.g. + * {@code } + * @throws ReqIFBuildException if the content is not well-formed XML, or + * holds text or elements without a namespace of their own, which + * the schema does not allow there + */ + public ReqIFBuilder toolExtension(String xml) { + this.toolExtensions.add(toolExtensionElement(xml == null ? "" : xml)); + return this; + } + + /** + * Sets the properties every element has - LONG-NAME, DESC, LAST-CHANGE, + * ALTERNATIVE-ID - on an element added before, e.g. + * {@code .with("so-1", e -> e.setDesc("Boot").setAlternativeID("REQ-1"))}. + * + * @param id any element: datatype, enum value, spec type, attribute + * definition, spec object, relation, specification, spec + * hierarchy or relation group + * @throws ReqIFBuildException if no element has this IDENTIFIER + */ + public ReqIFBuilder with(String id, Consumer properties) { + if (this.elements == null) { + this.elements = new LinkedHashMap(); + for (Identifiable element : allElements()) { + this.elements.putIfAbsent(element.getID(), element); + } + } + Identifiable element = this.elements.get(id); + if (element == null) { + throw new ReqIFBuildException("Unknown element: " + id); + } + String previous = element.getLastChange(); + properties.accept(element); + String lastChange = element.getLastChange(); + if (lastChange != null && !XsdTypes.isDateTime(lastChange)) { + element.setLastChange(previous); + throw new ReqIFBuildException("LAST-CHANGE of " + id + " is not an xsd:dateTime: " + lastChange); + } + return this; + } + + private List allElements() { + + List all = new ArrayList(); + for (Datatype datatype : this.datatypes.values()) { + all.add(datatype); + if (datatype instanceof DatatypeEnumeration) { + all.addAll(((DatatypeEnumeration) datatype).getEnumValues().values()); + } + } + for (SpecType specType : this.specTypes.values()) { + all.add(specType); + all.addAll(specType.getAttributeDefinitions().values()); + } + all.addAll(this.specObjects.values()); + all.addAll(this.specRelations.values()); + for (Specification specification : this.specifications.values()) { + all.add(specification); + all.addAll(specification.getAllSpecHierarchies()); + } + all.addAll(this.relationGroups); + return all; + } + // ---------------------------------------------------------------- datatypes /** Adds a datatype the builder has no shorthand for. */ public ReqIFBuilder datatype(Datatype datatype) { - claim(datatype.getID(), "datatype"); - this.datatypes.put(datatype.getID(), datatype); - return this; + return atomically(() -> { + claim(datatype.getID(), "datatype"); + this.datatypes.put(datatype.getID(), datatype); + }); } public ReqIFBuilder stringDatatype(String id, String name, int maxLength) { @@ -114,11 +223,31 @@ public ReqIFBuilder dateDatatype(String id, String name) { return datatype(new DatatypeDate(id, name)); } - /** Written as DATATYPE-DEFINITION-REAL. */ + /** + * Written as DATATYPE-DEFINITION-REAL, with the default accuracy and the + * full double range ({@link DatatypeDouble#DEFAULT_ACCURACY}). + */ public ReqIFBuilder realDatatype(String id, String name) { return datatype(new DatatypeDouble(id, name)); } + /** + * Written as DATATYPE-DEFINITION-REAL. + * + * @param accuracy the maximum precision of the values, not negative + * @param min lower bound, may be {@link Double#NEGATIVE_INFINITY} + * @param max upper bound, not below {@code min} + */ + public ReqIFBuilder realDatatype(String id, String name, int accuracy, double min, double max) { + if (accuracy < 0) { + throw new ReqIFBuildException("Accuracy of real datatype '" + id + "' must not be negative: " + accuracy); + } + if (Double.isNaN(min) || Double.isNaN(max) || min > max) { + throw new ReqIFBuildException("Real datatype '" + id + "' needs min <= max, got " + min + " and " + max); + } + return datatype(new DatatypeDouble(id, name, accuracy, min, max)); + } + public ReqIFBuilder xhtmlDatatype(String id, String name) { return datatype(new DatatypeXHTML(id, name)); } @@ -145,26 +274,30 @@ public ReqIFBuilder specRelationType(String id, String name, Consumer attributes) { + public ReqIFBuilder relationGroupType(String id, String name, Consumer attributes) { + return specType(new RelationGroupType(id, name), attributes); + } - claim(specType.getID(), "spec type"); - attributes.accept(new SpecTypeBuilder(specType, this.datatypes, this::claim)); - this.specTypes.put(specType.getID(), specType); - return this; + private ReqIFBuilder specType(SpecType specType, Consumer attributes) { + return atomically(() -> { + claim(specType.getID(), "spec type"); + attributes.accept(new SpecTypeBuilder(specType, this.datatypes, this::claim)); + this.specTypes.put(specType.getID(), specType); + }); } // ------------------------------------------------------------- spec objects public ReqIFBuilder specObject(String id, String specTypeID, Consumer values) { - - claim(id, "spec object"); - SpecType specType = requireSpecType(specTypeID); - ValuesBuilder builder = new ValuesBuilder(specType); - values.accept(builder); - - this.specObjects.put(id, new SpecObject(id, specType, builder.build(), this.typeClassifier)); - return this; + return atomically(() -> { + claim(id, "spec object"); + SpecType specType = requireSpecType(specTypeID, ReqIFConst.SPEC_OBJECT_TYPE); + ValuesBuilder builder = new ValuesBuilder(specType); + values.accept(builder); + + this.specObjects.put(id, new SpecObject(id, specType, builder.build(), this.typeClassifier)); + }); } public ReqIFBuilder specObject(String id, String specTypeID) { @@ -177,16 +310,17 @@ public ReqIFBuilder specObject(String id, String specTypeID) { public ReqIFBuilder specRelation(String id, String specTypeID, String sourceID, String targetID, Consumer values) { - claim(id, "spec relation"); - SpecType specType = requireSpecType(specTypeID); - requireSpecObject(sourceID); - requireSpecObject(targetID); + return atomically(() -> { + claim(id, "spec relation"); + SpecType specType = requireSpecType(specTypeID, ReqIFConst.SPEC_RELATION_TYPE); + requireSpecObject(sourceID); + requireSpecObject(targetID); - ValuesBuilder builder = new ValuesBuilder(specType); - values.accept(builder); + ValuesBuilder builder = new ValuesBuilder(specType); + values.accept(builder); - this.specRelations.add(new SpecRelation(id, specType, sourceID, targetID, builder.build())); - return this; + this.specRelations.put(id, new SpecRelation(id, specType, sourceID, targetID, builder.build())); + }); } public ReqIFBuilder specRelation(String id, String specTypeID, String sourceID, String targetID) { @@ -199,14 +333,49 @@ public ReqIFBuilder specRelation(String id, String specTypeID, String sourceID, public ReqIFBuilder specification(String id, String name, String specTypeID, Consumer content) { - claim(id, "specification"); - SpecType specType = requireSpecType(specTypeID); - SpecificationBuilder builder = new SpecificationBuilder(specType); - content.accept(builder); + return atomically(() -> { + claim(id, "specification"); + SpecType specType = requireSpecType(specTypeID, ReqIFConst.SPECIFICATION_TYPE); + SpecificationBuilder builder = new SpecificationBuilder(specType); + content.accept(builder); - this.specifications.add(new Specification(id, name, specType, builder.values.build(), - builder.children(1, 0))); - return this; + this.specifications.put(id, new Specification(id, name, specType, builder.values.build(), + builder.children(1, 0))); + }); + } + + + // ----------------------------------------------------------- relation groups + + /** + * Adds a relation group: relations between a source and a target + * specification, e.g. a traceability view between two of them. + * + * @param specRelationIDs the relations that belong to the group + */ + public ReqIFBuilder relationGroup(String id, String name, String relationGroupTypeID, + String sourceSpecificationID, String targetSpecificationID, String... specRelationIDs) { + + return atomically(() -> { + claim(id, "relation group"); + requireSpecType(relationGroupTypeID, ReqIFConst.RELATION_GROUP_TYPE); + requireSpecification(sourceSpecificationID); + requireSpecification(targetSpecificationID); + List relations = specRelationIDs == null ? List.of() : Arrays.asList(specRelationIDs); + for (String relationID : relations) { + if (!this.specRelations.containsKey(relationID)) { + throw new ReqIFBuildException("Unknown spec relation: " + relationID); + } + } + this.relationGroups.add(new RelationGroup(id, name, relationGroupTypeID, sourceSpecificationID, + targetSpecificationID, relations)); + }); + } + + private void requireSpecification(String specificationID) { + if (!this.specifications.containsKey(specificationID)) { + throw new ReqIFBuildException("Unknown specification: " + specificationID); + } } @@ -225,14 +394,102 @@ public ReqIFDocument build() { for (SpecObject specObject : this.specObjects.values()) { content.addSpecObject(specObject); } - for (SpecRelation relation : this.specRelations) { + for (SpecRelation relation : this.specRelations.values()) { content.addSpecRelation(relation); } - for (Specification specification : this.specifications) { + for (Specification specification : this.specifications.values()) { content.addSpecification(specification); } + for (RelationGroup relationGroup : this.relationGroups) { + content.addRelationGroup(relationGroup); + } - return new ReqIFDocument(this.headerBuilder.build(), content); + // the elements are new, not changed: they get the default LAST-CHANGE + content.clearModified(); + ReqIFDocument document = new ReqIFDocument(this.headerBuilder.build(defaultHeaderID()), content) + .setLang(this.lang); + if (!this.toolExtensions.isEmpty()) { + document.getToolExtensions().add(toolExtensionsElement(this.toolExtensions)); + } + return document; + } + + /** + * @return REQ-IF-TOOL-EXTENSION holding the content + * @throws ReqIFBuildException if the content is not well-formed XML or + * not what the schema allows there: elements of a namespace other + * than ReqIF's + */ + private static Element toolExtensionElement(String xml) { + + String wrapped = "" + xml + ""; + Element extension; + try { + DocumentBuilder parser = SecureXml.newDocumentBuilder(); + // the exception says what is wrong; nothing goes to stderr + parser.setErrorHandler(new org.xml.sax.helpers.DefaultHandler()); + extension = parser.parse(new InputSource(new StringReader(wrapped))).getDocumentElement(); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw new ReqIFBuildException("Tool extension is not well-formed XML: " + e.getMessage(), e); + } + for (Node child = extension.getFirstChild(); child != null; child = child.getNextSibling()) { + if ((child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE) + && !child.getNodeValue().isBlank()) { + throw new ReqIFBuildException("A tool extension holds elements only, not text: " + xml); + } + if (child.getNodeType() == Node.ELEMENT_NODE) { + String namespace = child.getNamespaceURI(); + if (namespace == null || namespace.isEmpty() || ReqIFWriter.REQIF_NAMESPACE.equals(namespace)) { + throw new ReqIFBuildException("The elements of a tool extension need a namespace of their own, " + + "not ReqIF's and not none: " + child.getNodeName()); + } + } + } + return extension; + } + + /** + * @return TOOL-EXTENSIONS holding the extensions; the schema allows one + */ + private static Element toolExtensionsElement(List extensions) { + try { + Document document = SecureXml.newDocumentBuilder().newDocument(); + Element toolExtensions = document.createElementNS(ReqIFWriter.REQIF_NAMESPACE, ReqIFConst.TOOL_EXTENSIONS); + for (Element extension : extensions) { + Element imported = (Element) document.importNode(extension, true); + // the prefix only served the parsing of the content + imported.setPrefix(null); + imported.removeAttributeNS(javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "reqif"); + toolExtensions.appendChild(imported); + } + return toolExtensions; + } catch (ParserConfigurationException e) { + throw new ReqIFBuildException("Cannot create the tool extensions: " + e.getMessage(), e); + } + } + + + /** + * The schema requires a header IDENTIFIER that is unique in the document; + * if none was set, the first free one of reqif4j-header, reqif4j-header-2, ... + */ + private String defaultHeaderID() { + + Set used = new HashSet(this.identifiers.keySet()); + for (SpecType specType : this.specTypes.values()) { + used.addAll(specType.getAttributeDefinitions().keySet()); + } + for (Datatype datatype : this.datatypes.values()) { + if (datatype instanceof DatatypeEnumeration) { + used.addAll(((DatatypeEnumeration) datatype).getEnumValues().keySet()); + } + } + String id = DEFAULT_HEADER_ID; + for (int suffix = 2; used.contains(id); suffix++) { + id = DEFAULT_HEADER_ID + "-" + suffix; + } + return id; } @@ -245,19 +502,57 @@ void claim(String id, String kind) { if (id == null || id.isBlank()) { throw new ReqIFBuildException("A " + kind + " needs an identifier"); } - String previousKind = this.identifiers.put(id, kind); + String previousKind = this.identifiers.get(id); if (previousKind != null) { throw new ReqIFBuildException("Identifier '" + id + "' is already used by a " + previousKind + "; identifiers must be unique across the document"); } + this.identifiers.put(id, kind); + if (this.claimed != null) { + this.claimed.add(id); + } + this.elements = null; } - private SpecType requireSpecType(String specTypeID) { + /** + * Adds an element and the elements it contains; if that fails, the + * identifiers claimed on the way are free again, so the call can be + * repeated with corrected arguments. + */ + private ReqIFBuilder atomically(Runnable add) { + List outer = this.claimed; + this.claimed = new ArrayList(); + try { + add.run(); + if (outer != null) { + outer.addAll(this.claimed); + } + } catch (RuntimeException e) { + for (String id : this.claimed) { + this.identifiers.remove(id); + } + throw e; + } finally { + this.claimed = outer; + this.elements = null; + } + return this; + } + + /** + * @param kind SPEC-OBJECT-TYPE, SPEC-RELATION-TYPE, SPECIFICATION-TYPE or + * RELATION-GROUP-TYPE + */ + private SpecType requireSpecType(String specTypeID, String kind) { SpecType specType = this.specTypes.get(specTypeID); if (specType == null) { throw new ReqIFBuildException("Unknown spec type: " + specTypeID); } + if (!kind.equals(specType.getType())) { + throw new ReqIFBuildException("Spec type " + specTypeID + " is a " + specType.getType() + + ", but a " + kind + " is needed here"); + } return specType; } @@ -307,6 +602,27 @@ public SpecificationBuilder set(String attributeDefinitionID, Object value) { return this; } + /** Sets an XHTML value of the specification itself. */ + public SpecificationBuilder setXhtml(String attributeDefinitionID, String markup) { + this.values.setXhtml(attributeDefinitionID, markup); + return this; + } + + /** + * Sets a simplified XHTML value of the specification itself, see + * {@link ValuesBuilder#setXhtml(String, String, String)}. + */ + public SpecificationBuilder setXhtml(String attributeDefinitionID, String markup, String originalMarkup) { + this.values.setXhtml(attributeDefinitionID, markup, originalMarkup); + return this; + } + + /** Selects enum values of the specification itself. */ + public SpecificationBuilder setEnum(String attributeDefinitionID, String... enumValueIDs) { + this.values.setEnum(attributeDefinitionID, enumValueIDs); + return this; + } + public SpecificationBuilder child(String hierarchyID, String specObjectID) { return child(hierarchyID, specObjectID, child -> { }); } @@ -338,12 +654,37 @@ public class HierarchyBuilder { private final String hierarchyID; private final String specObjectID; private final List children = new ArrayList(); + private Boolean tableInternal; + private Boolean editable; + private final List editableAttributeIDs = new ArrayList(); private HierarchyBuilder(String hierarchyID, String specObjectID) { this.hierarchyID = hierarchyID; this.specObjectID = specObjectID; } + /** IS-TABLE-INTERNAL: whether the node is part of a table in the tool's view */ + public HierarchyBuilder tableInternal(boolean tableInternal) { + this.tableInternal = tableInternal; + return this; + } + + /** IS-EDITABLE: whether the object may be edited through this node */ + public HierarchyBuilder editable(boolean editable) { + this.editable = editable; + return this; + } + + /** + * EDITABLE-ATTS: the attributes of the object's spec type that may be + * edited through this node. + */ + public HierarchyBuilder editableAttributes(String... attributeDefinitionIDs) { + this.editableAttributeIDs.clear(); + this.editableAttributeIDs.addAll(Arrays.asList(attributeDefinitionIDs)); + return this; + } + public HierarchyBuilder child(String hierarchyID, String specObjectID) { return child(hierarchyID, specObjectID, child -> { }); } @@ -366,8 +707,22 @@ private SpecHierarchy build(int level, int section) { for (HierarchyBuilder child : this.children) { nested.add(child.build(level + 1, section)); } - return new SpecHierarchy(this.hierarchyID, level, section, - ReqIFBuilder.this.specObjects.get(this.specObjectID), nested); + SpecObject specObject = ReqIFBuilder.this.specObjects.get(this.specObjectID); + SpecHierarchy hierarchy = new SpecHierarchy(this.hierarchyID, level, section, specObject, nested) + .setTableInternal(this.tableInternal).setEditable(this.editable); + if (!this.editableAttributeIDs.isEmpty()) { + List definitions = new ArrayList(); + for (String id : this.editableAttributeIDs) { + AttributeDefinition definition = specObject.getResolvedSpecType().getAttributeDefinition(id); + if (definition == null) { + throw new ReqIFBuildException("Spec hierarchy " + this.hierarchyID + ": the type of spec object " + + this.specObjectID + " has no attribute definition " + id); + } + definitions.add(definition); + } + hierarchy.setEditableAttributes(definitions); + } + return hierarchy; } } @@ -381,6 +736,7 @@ public static class HeaderBuilder { private String toolID = ""; private String sourceToolID = ""; private String reqifVersion = "1.0"; + private String repositoryID; public HeaderBuilder id(String id) { this.id = id; @@ -413,14 +769,20 @@ public HeaderBuilder sourceToolID(String sourceToolID) { return this; } + public HeaderBuilder repositoryID(String repositoryID) { + this.repositoryID = repositoryID; + return this; + } + public HeaderBuilder reqifVersion(String reqifVersion) { this.reqifVersion = reqifVersion; return this; } - private ReqIFHeader build() { - return new ReqIFHeader(this.id, this.title, this.comment, this.creationTime, - this.toolID, this.sourceToolID, this.reqifVersion); + private ReqIFHeader build(String defaultID) { + return new ReqIFHeader(this.id == null || this.id.isBlank() ? defaultID : this.id, this.title, + this.comment, this.creationTime, this.toolID, this.sourceToolID, this.reqifVersion) + .setRepositoryID(this.repositoryID); } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/build/SpecTypeBuilder.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/build/SpecTypeBuilder.java index 64f8e31..0b5a654 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/build/SpecTypeBuilder.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/build/SpecTypeBuilder.java @@ -76,6 +76,21 @@ public SpecTypeBuilder enumerationAttribute(String id, String name, String datat return enumerationAttribute(id, name, datatypeID, multiValued, null); } + /** + * Sets IS-EDITABLE of an attribute definition added before: whether its + * values may be edited in the receiving tool. + */ + public SpecTypeBuilder editable(String attributeDefinitionID, boolean editable) { + + AttributeDefinition definition = this.specType.getAttributeDefinition(attributeDefinitionID); + if (definition == null) { + throw new ReqIFBuildException("Spec type " + this.specType.getID() + + " has no attribute definition " + attributeDefinitionID); + } + definition.setEditable(editable); + return this; + } + /** Adds an attribute definition for a datatype without a shorthand. */ public SpecTypeBuilder attribute(String id, String name, String datatypeID, String defaultValue) { diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/build/ValuesBuilder.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/build/ValuesBuilder.java index 69159d4..690190a 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/build/ValuesBuilder.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/build/ValuesBuilder.java @@ -4,18 +4,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.function.Supplier; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition; 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.datatypes.DatatypeEnumeration; -import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueFactory; import de.uni_stuttgart.ils.reqif4j.specification.SpecType; /** @@ -38,83 +31,57 @@ public class ValuesBuilder { * datatype. */ public ValuesBuilder set(String attributeDefinitionID, Object value) { - AttributeDefinition definition = requireDefinition(attributeDefinitionID); - String category = definition.getDataType() == null ? null : definition.getDataType().getType(); - String text = value == null ? null : String.valueOf(value); - - if (ReqIFConst.ENUMERATION.equals(category)) { - throw new ReqIFBuildException("Use setEnum(...) for the enumeration attribute " + attributeDefinitionID); - } - if (ReqIFConst.XHTML.equals(category)) { - throw new ReqIFBuildException("Use setXhtml(...) for the XHTML attribute " + attributeDefinitionID); - } - - this.values.add(scalarValue(category, text, definition)); - return this; + return add(() -> AttributeValueFactory.of(definition, value)); } /** * Sets an XHTML value from markup, with or without a surrounding div. */ public ValuesBuilder setXhtml(String attributeDefinitionID, String markup) { - AttributeDefinition definition = requireDefinition(attributeDefinitionID); - requireCategory(definition, ReqIFConst.XHTML, attributeDefinitionID); + return add(() -> AttributeValueFactory.xhtml(definition, markup)); + } - this.values.add(new AttributeValueXHTML(markup, definition)); - return this; + /** + * Sets an XHTML value that is a simplified version of the complete + * content, e.g. the preview image of an embedded OLE object; the value is + * marked IS-SIMPLIFIED. + * + * @param originalMarkup the complete content (THE-ORIGINAL-VALUE) + */ + public ValuesBuilder setXhtml(String attributeDefinitionID, String markup, String originalMarkup) { + AttributeDefinition definition = requireDefinition(attributeDefinitionID); + return add(() -> AttributeValueFactory.xhtml(definition, markup, originalMarkup)); } /** * Sets one or more enum values by their IDENTIFIER (multiselect). */ public ValuesBuilder setEnum(String attributeDefinitionID, String... enumValueIDs) { - return setEnum(attributeDefinitionID, Arrays.asList(enumValueIDs)); + return setEnum(attributeDefinitionID, enumValueIDs == null ? null : Arrays.asList(enumValueIDs)); } public ValuesBuilder setEnum(String attributeDefinitionID, List enumValueIDs) { - AttributeDefinition definition = requireDefinition(attributeDefinitionID); - requireCategory(definition, ReqIFConst.ENUMERATION, attributeDefinitionID); - - DatatypeEnumeration datatype = (DatatypeEnumeration) definition.getDataType(); - List names = new ArrayList(); - for (String enumValueID : enumValueIDs) { - String name = datatype.getEnumValueName(enumValueID); - if (name == null) { - throw new ReqIFBuildException("Unknown enum value " + enumValueID - + " for attribute " + attributeDefinitionID); - } - names.add(name); - } + return add(() -> AttributeValueFactory.enumeration(definition, enumValueIDs)); + } + - this.values.add(new AttributeValueEnumeration(names, new ArrayList(enumValueIDs), definition)); + private ValuesBuilder add(Supplier value) { + try { + this.values.add(value.get()); + } catch (IllegalArgumentException e) { + throw new ReqIFBuildException(e.getMessage(), e); + } return this; } - Collection build() { return this.values; } - private AttributeValue scalarValue(String category, String text, AttributeDefinition definition) { - - if (category == null) { - return new AttributeValue(text, definition); - } - switch (category) { - case ReqIFConst.BOOLEAN: return new AttributeValueBoolean(text, definition); - case ReqIFConst.INTEGER: return new AttributeValueInteger(text, definition); - case ReqIFConst.STRING: return new AttributeValueString(text, definition); - case ReqIFConst.DATE: return new AttributeValueDate(text, definition); - case ReqIFConst.DOUBLE: - case ReqIFConst.REAL: return new AttributeValueDouble(text, definition); - default: return new AttributeValue(text, definition); - } - } - private AttributeDefinition requireDefinition(String attributeDefinitionID) { AttributeDefinition definition = this.specType.getAttributeDefinition(attributeDefinitionID); @@ -124,13 +91,4 @@ private AttributeDefinition requireDefinition(String attributeDefinitionID) { } return definition; } - - private static void requireCategory(AttributeDefinition definition, String expected, String attributeDefinitionID) { - - String category = definition.getDataType() == null ? null : definition.getDataType().getType(); - if (!expected.equals(category)) { - throw new ReqIFBuildException("Attribute " + attributeDefinitionID + " is of kind " + category - + ", but " + expected + " was expected"); - } - } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/Datatype.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/Datatype.java index 0af27de..d964bc9 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/Datatype.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/Datatype.java @@ -1,24 +1,15 @@ package de.uni_stuttgart.ils.reqif4j.datatypes; -public class Datatype { +import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable; + +public class Datatype extends Identifiable { - private String id; - private String name; private String type; private String sourceElementName; - private String alternativeID; - - public String getID() { - return this.id; - } - - public String getName() { - return this.name; - } public String getType() { return this.type; @@ -33,15 +24,9 @@ public String getSourceElementName() { return this.sourceElementName; } - /** - * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null - */ - public String getAlternativeID() { - return this.alternativeID; - } - + @Override public Datatype setAlternativeID(String alternativeID) { - this.alternativeID = alternativeID; + super.setAlternativeID(alternativeID); return this; } @@ -49,9 +34,8 @@ public Datatype setAlternativeID(String alternativeID) { public Datatype(String id, String name, String type) { + super(id, name); - this.id = id; - this.name = name; this.type = type; } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDouble.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDouble.java index 06af942..41a0828 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDouble.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeDouble.java @@ -1,9 +1,90 @@ package de.uni_stuttgart.ils.reqif4j.datatypes; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XsdTypes; +/** + * A DATATYPE-DEFINITION-REAL. The parser normalizes the ReqIF kind REAL to the + * category {@link ReqIFConst#DOUBLE}; it is written as REAL again. + * + * ACCURACY, MIN and MAX are required by the ReqIF schema. A document that + * omits them, or a datatype created without them, gets the defaults below, + * so it is always written schema-valid. + */ public class DatatypeDouble extends Datatype { - public DatatypeDouble(String id, String name) { - super(id, name, ReqIFConst.DOUBLE); - } + + /** Significant decimal digits a double can hold. */ + public static final int DEFAULT_ACCURACY = 15; + public static final double DEFAULT_MIN = -Double.MAX_VALUE; + public static final double DEFAULT_MAX = Double.MAX_VALUE; + + private final int accuracy; + private final double min; + private final double max; + + + /** + * Creates a datatype with the default accuracy and the full double range. + */ + public DatatypeDouble(String id, String name) { + this(id, name, DEFAULT_ACCURACY, DEFAULT_MIN, DEFAULT_MAX); + } + + public DatatypeDouble(String id, String name, int accuracy, double min, double max) { + super(id, name, ReqIFConst.DOUBLE); + + this.accuracy = accuracy; + this.min = min; + this.max = max; + } + + /** + * Reads the attributes as written in a document; missing or unparseable + * values fall back to the defaults. + */ + public DatatypeDouble(String id, String name, String accuracy, String min, String max) { + this(id, name, parseAccuracy(accuracy), parseBound(min, DEFAULT_MIN), parseBound(max, DEFAULT_MAX)); + } + + + /** + * @return the ACCURACY, the maximum precision of the values + */ + public int getAccuracy() { + return this.accuracy; + } + + public double getMin() { + return this.min; + } + + public double getMax() { + return this.max; + } + + + private static int parseAccuracy(String value) { + if (value == null || value.isBlank()) { + return DEFAULT_ACCURACY; + } + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + return DEFAULT_ACCURACY; + } + } + + /** + * Accepts the xsd:double spellings of infinity (INF, -INF) as well. + */ + private static double parseBound(String value, double fallback) { + if (value == null || value.isBlank()) { + return fallback; + } + try { + return XsdTypes.parseDouble(value); + } catch (NumberFormatException e) { + return fallback; + } + } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java index 76fed6a..9d6b124 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumeration.java @@ -6,6 +6,7 @@ import org.w3c.dom.Element; import org.w3c.dom.Node; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; @@ -23,12 +24,15 @@ public Map getEnumValues() { } /** - * @return the LONG-NAME of the enum value with the given id, or null if - * unknown + * @return the LONG-NAME of the enum value with the given id, "" if the + * value has no LONG-NAME, or null if the id is unknown */ public String getEnumValueName(String id) { DatatypeEnumerationValue value = this.enumValues.get(id); - return value == null ? null : value.getName(); + if (value == null) { + return null; + } + return value.getName() == null ? "" : value.getName(); } /** @@ -64,28 +68,23 @@ public DatatypeEnumeration(String id, String name, java.util.List new DatatypeEnumerationValue(enumValue)); + issues.put(enumValues, value, "Enum value"); } } - private static String orEmpty(String value) { - return value == null ? "" : value; - } - } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java index 919d52b..c0be4f7 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/datatypes/DatatypeEnumerationValue.java @@ -1,24 +1,20 @@ package de.uni_stuttgart.ils.reqif4j.datatypes; -public class DatatypeEnumerationValue { +import org.w3c.dom.Element; + +import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; + +public class DatatypeEnumerationValue extends Identifiable { - private java.lang.String id; - private java.lang.String name; private java.lang.String key; private java.lang.String otherContent; - public String getID() { - return this.id; - } - - public String getName() { - return this.name; - } - public String getKey() { return this.key; } @@ -31,11 +27,21 @@ public String getOtherContent() { public DatatypeEnumerationValue(String id, String name, String key, String otherContent) { - - this.id = id; - this.name = name; + super(id, name); + this.key = key; this.otherContent = otherContent; } + /** + * Reads an ENUM-VALUE element. A missing KEY or OTHER-CONTENT is read as "". + */ + DatatypeEnumerationValue(Element enumValue) { + super(enumValue); + + Element embeddedValue = XmlUtils.firstDescendantByLocalName(enumValue, ReqIFConst.EMBEDDED_VALUE); + this.key = XmlUtils.attributeOrEmpty(embeddedValue, ReqIFConst.KEY); + this.otherContent = XmlUtils.attributeOrEmpty(embeddedValue, ReqIFConst.OTHER_CONTENT); + } + } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/Identifiable.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/Identifiable.java new file mode 100644 index 0000000..b6668da --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/Identifiable.java @@ -0,0 +1,172 @@ +package de.uni_stuttgart.ils.reqif4j.reqif; + +import org.w3c.dom.Node; + +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; + +/** + * Common base of every ReqIF element that has an identity: datatypes, enum + * values, spec types, attribute definitions, spec objects, spec relations, + * specifications, spec hierarchy nodes and relation groups. + * + * Mirrors the abstract {@code Identifiable} of the ReqIF metamodel: + * + *
+ * IDENTIFIER     required  the element's id, unique within the document
+ * LAST-CHANGE    required  xsd:dateTime of the last modification
+ * LONG-NAME      optional  human readable name
+ * DESC           optional  description
+ * ALTERNATIVE-ID optional  a second, tool specific id
+ * 
+ * + * Values are kept exactly as written in the document; an attribute the + * document does not declare is {@code null}, so it is not invented when the + * element is written back. + * + * Creating an element from plain values or changing one through the API + * marks it {@link #isModified() modified}; the writer then gives it the time of writing as LAST-CHANGE, + * so tools that import the document see what changed, and takes that time + * into the model, so writing again gives the same result. Setting LAST-CHANGE + * explicitly after the last change takes precedence. + */ +public abstract class Identifiable { + + private String id; + private String name; + private String desc; + private String lastChange; + private String alternativeID; + private boolean modified; + + + /** + * For elements created from plain values instead of a document. A new + * element counts as modified, so it is written with the time of writing + * as LAST-CHANGE unless one is set; the builder clears that for the + * documents it assembles. + */ + protected Identifiable(String id, String name) { + this.id = id; + this.name = name; + this.modified = true; + } + + /** + * Reads IDENTIFIER, LONG-NAME, DESC, LAST-CHANGE and ALTERNATIVE-ID of a + * parsed element. + */ + protected Identifiable(Node element) { + readIdentifiable(element); + } + + /** + * Fills the properties from an element; for elements that are created from + * values while a document is read (datatypes). + * + * @throws ReqIFParseException if the element has no IDENTIFIER, which the + * schema requires and every reference relies on + */ + protected final void readIdentifiable(Node element) { + this.id = XmlUtils.attribute(element, ReqIFConst.IDENTIFIER); + if (this.id == null) { + throw new ReqIFParseException(XmlUtils.localName(element) + " without " + ReqIFConst.IDENTIFIER); + } + this.name = XmlUtils.attribute(element, ReqIFConst.LONG_NAME); + this.desc = XmlUtils.attribute(element, ReqIFConst.DESC); + this.lastChange = XmlUtils.attribute(element, ReqIFConst.LAST_CHANGE); + this.alternativeID = XmlUtils.alternativeID(element); + // as read, not changed + this.modified = false; + } + + + /** + * @return the IDENTIFIER + */ + public String getID() { + return this.id; + } + + /** + * @return the LONG-NAME, or null if the element declares none + */ + public String getName() { + return this.name; + } + + public Identifiable setName(String name) { + this.name = name; + markModified(); + return this; + } + + /** + * @return the DESC, or null if the element declares none + */ + public String getDesc() { + return this.desc; + } + + public Identifiable setDesc(String desc) { + this.desc = desc; + markModified(); + return this; + } + + /** + * @return the LAST-CHANGE exactly as written (xsd:dateTime), or null if the + * element declares none + */ + public String getLastChange() { + return this.lastChange; + } + + /** + * Sets LAST-CHANGE explicitly; the element is no longer considered + * modified, so the writer keeps this value - unless the element is + * changed again afterwards. + * + * @param lastChange xsd:dateTime, e.g. 2026-07-23T10:00:00Z + */ + public Identifiable setLastChange(String lastChange) { + this.lastChange = lastChange; + this.modified = false; + return this; + } + + /** + * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null + */ + public String getAlternativeID() { + return this.alternativeID; + } + + public Identifiable setAlternativeID(String alternativeID) { + this.alternativeID = alternativeID; + markModified(); + return this; + } + + /** + * @return true if the element was changed through the API since it was + * read or built (and LAST-CHANGE was not set since) + */ + public boolean isModified() { + return this.modified; + } + + /** + * Marks the element changed; done by every setter and structural change. + */ + public void markModified() { + this.modified = true; + } + + /** + * Forgets that the element was changed; the writer then keeps the + * LAST-CHANGE it has. + */ + public void clearModified() { + this.modified = false; + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ParseIssue.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ParseIssue.java new file mode 100644 index 0000000..7149e71 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ParseIssue.java @@ -0,0 +1,33 @@ +package de.uni_stuttgart.ils.reqif4j.reqif; + +/** + * A problem found while reading a document that did not stop the reading, + * e.g. a value referencing an attribute definition the document does not + * declare. See {@link ReqIFDocument#getParseIssues()}. + */ +public final class ParseIssue { + + private final String elementID; + private final String message; + + public ParseIssue(String elementID, String message) { + this.elementID = elementID; + this.message = message; + } + + /** + * @return the IDENTIFIER of the element the issue was found on, or null + */ + public String getElementID() { + return this.elementID; + } + + public String getMessage() { + return this.message; + } + + @Override + public String toString() { + return (this.elementID == null ? "" : "[" + this.elementID + "] ") + this.message; + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ParseIssues.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ParseIssues.java new file mode 100644 index 0000000..727f39e --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ParseIssues.java @@ -0,0 +1,93 @@ +package de.uni_stuttgart.ils.reqif4j.reqif; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Receives the problems found while reading a document. + * + * A {@link #collecting() collecting} instance lets the reader skip what it + * cannot use and carry on, so one broken element does not make a whole + * document unreadable; the constructors of the model classes that take no + * issues use one and discard it. A {@link #strict() strict} instance stops + * at the first problem with a {@link ReqIFParseException}. + */ +public final class ParseIssues { + + private final boolean strict; + private final List issues = new ArrayList(); + + private ParseIssues(boolean strict) { + this.strict = strict; + } + + public static ParseIssues collecting() { + return new ParseIssues(false); + } + + public static ParseIssues strict() { + return new ParseIssues(true); + } + + /** + * @throws ReqIFParseException if this instance is strict + */ + public void report(String elementID, String message) { + ParseIssue issue = new ParseIssue(elementID, message); + if (this.strict) { + throw new ReqIFParseException(issue.toString()); + } + this.issues.add(issue); + } + + /** + * @return the issues in the order they were found (read-only) + */ + public List getIssues() { + return Collections.unmodifiableList(this.issues); + } + + /** + * Runs a reader for one element; if the element cannot be read at all (it + * has no IDENTIFIER), it is reported and skipped. + * + * @return what the reader returned, or null if the element was skipped + */ + public T readOrSkip(String kind, java.util.function.Supplier reader) { + try { + return reader.get(); + } catch (ReqIFParseException e) { + if (this.strict) { + throw e; + } + report(null, e.getMessage() + "; the " + kind + " is skipped"); + return null; + } + } + + /** + * Adds an element read from the document to the map that holds its kind, + * keyed by identifier. A second element with the same identifier replaces + * the first, which is reported. + * + * @param element may be null (skipped element), then nothing happens + */ + public void put(java.util.Map elements, T element, String kind) { + if (element == null) { + return; + } + if (elements.containsKey(element.getID())) { + reportDuplicate(kind, element.getID()); + } + elements.put(element.getID(), element); + } + + /** + * Reports a second element with the same identifier, which replaces the + * first. + */ + public void reportDuplicate(String kind, String id) { + report(id, kind + " " + id + " is declared twice; the later one replaces the earlier"); + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java index ed585db..d2c62b1 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFConst.java @@ -4,12 +4,15 @@ public class ReqIFConst { public final static String IDENTIFIER = "IDENTIFIER"; public final static String LONG_NAME = "LONG-NAME"; + public final static String DESC = "DESC"; + public final static String LAST_CHANGE = "LAST-CHANGE"; public final static String THE_VALUE = "THE-VALUE"; public final static String KEY = "KEY"; public final static String OTHER_CONTENT = "OTHER-CONTENT"; public final static String MIN = "MIN"; public final static String MAX = "MAX"; public final static String MAX_LENGTH = "MAX-LENGTH"; + public final static String ACCURACY = "ACCURACY"; public final static String _TEXT = "#text"; public final static String ENUM_VALUE = "ENUM-VALUE"; @@ -27,6 +30,9 @@ public class ReqIFConst { public final static String SPEC_ATTRIBUTES = "SPEC-ATTRIBUTES"; public final static String ALTERNATIVE_ID = "ALTERNATIVE-ID"; public final static String TOOL_EXTENSIONS = "TOOL-EXTENSIONS"; + public final static String REQ_IF_TOOL_EXTENSION = "REQ-IF-TOOL-EXTENSION"; + + public final static String SPEC_RELATION_GROUPS = "SPEC-RELATION-GROUPS"; public final static String RELATION_GROUP = "RELATION-GROUP"; public final static String RELATION_GROUP_TYPE = "RELATION-GROUP-TYPE"; @@ -55,6 +61,12 @@ public class ReqIFConst { public final static String REQ_IF_VERSION = "REQ-IF-VERSION"; public final static String COMMENT = "COMMENT"; public final static String CREATION_TIME = "CREATION-TIME"; + public final static String REPOSITORY_ID = "REPOSITORY-ID"; + public final static String IS_EDITABLE = "IS-EDITABLE"; + public final static String IS_TABLE_INTERNAL = "IS-TABLE-INTERNAL"; + public final static String EDITABLE_ATTS = "EDITABLE-ATTS"; + public final static String THE_ORIGINAL_VALUE = "THE-ORIGINAL-VALUE"; + public final static String IS_SIMPLIFIED = "IS-SIMPLIFIED"; public final static String SPECIFICATION_TYPE = "SPECIFICATION-TYPE"; public final static String SPEC_OBJECT_TYPE = "SPEC-OBJECT-TYPE"; @@ -83,4 +95,33 @@ public class ReqIFConst { public final static String EOR = "EOR"; public final static String EOD = "EOD"; -} + + /** + * @return the kind an element name ends with, e.g. STRING for + * ATTRIBUTE-VALUE-STRING or REAL for ATTRIBUTE-DEFINITION-REAL + */ + public static String kindOf(String elementName) { + return elementName == null ? null : elementName.substring(elementName.lastIndexOf('-') + 1); + } + + /** + * @return the suffix of the element names belonging to a datatype + * category, e.g. "REAL" for DOUBLE as in DATATYPE-DEFINITION-REAL, + * or null for a category the parser does not model + */ + public static String datatypeSuffix(String datatypeCategory) { + if (datatypeCategory == null) { + return null; + } + switch (datatypeCategory) { + case DOUBLE: + case REAL: return REAL; + case BOOLEAN: + case INTEGER: + case STRING: + case DATE: + case XHTML: + case ENUMERATION: return datatypeCategory; + default: return null; + } + }} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java index f8ce6bb..a1feb58 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFCoreContent.java @@ -9,8 +9,6 @@ import de.uni_stuttgart.ils.reqif4j.specification.*; import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; public class ReqIFCoreContent { @@ -21,61 +19,81 @@ public class ReqIFCoreContent { private Map specRelation = new LinkedHashMap<>(); private Map specifications = new LinkedHashMap(); private Map relationGroups = new LinkedHashMap(); + private ParseIssues parseIssues = ParseIssues.collecting(); + /** + * @return the dataTypes by IDENTIFIER, in document order (read-only; change + * them with the add and remove methods) + */ public Map getDatatypes() { - return this.dataTypes; + return java.util.Collections.unmodifiableMap(this.dataTypes); } public Datatype getDatatype(String id) { return this.dataTypes.get(id); } + /** + * @return the specTypes by IDENTIFIER, in document order (read-only; change + * them with the add and remove methods) + */ public Map getSpecTypes() { - return this.specTypes; + return java.util.Collections.unmodifiableMap(this.specTypes); } public SpecType getSpecType(String id) { return this.specTypes.get(id); } + /** + * @return the specObjects by IDENTIFIER, in document order (read-only; change + * them with the add and remove methods) + */ public Map getSpecObjects() { - return this.specObjects; + return java.util.Collections.unmodifiableMap(this.specObjects); } public SpecObject getSpecObject(String id) { return this.specObjects.get(id); } + /** + * @return the relations by IDENTIFIER, in document order (read-only; change + * them with the add and remove methods) + */ public Map getSpecRelation() { - return this.specRelation; + return java.util.Collections.unmodifiableMap(this.specRelation); } public SpecRelation getSpecRelation(String id) { return this.specRelation.get(id); } + /** + * @return the specifications by IDENTIFIER, in document order (read-only; change + * them with the add and remove methods) + */ public Map getSpecifications() { - return this.specifications; + return java.util.Collections.unmodifiableMap(this.specifications); } public Specification getSpecification(String id) { return this.specifications.get(id); } + /** + * @return the relationGroups by IDENTIFIER, in document order (read-only; change + * them with the add and remove methods) + */ public Map getRelationGroups() { - return this.relationGroups; + return java.util.Collections.unmodifiableMap(this.relationGroups); } public RelationGroup getRelationGroup(String id) { return this.relationGroups.get(id); } - public ReqIFCoreContent addRelationGroup(RelationGroup relationGroup) { - this.relationGroups.put(relationGroup.getID(), relationGroup); - return this; - } - public List getSpecificationsList() { List specifications = new ArrayList(); for (Specification specification : this.specifications.values()) { @@ -102,180 +120,578 @@ public List getOrderedSpecHierarchyList() { public ReqIFCoreContent() { } + /* + * Adding and removing elements. The add methods refuse an element whose + * IDENTIFIER a datatype, spec type, spec object, relation, specification, + * hierarchy node or relation group already has (attribute definitions and + * enum values are not checked; the validator reports any clash); the + * remove methods keep the references between the elements consistent. + */ + + /** + * @throws IllegalArgumentException if the IDENTIFIER is missing or taken + */ public ReqIFCoreContent addDatatype(Datatype datatype) { - this.dataTypes.put(datatype.getID(), datatype); - return this; + return add(this.dataTypes, datatype); } + /** + * @throws IllegalArgumentException if the IDENTIFIER is missing or taken + */ public ReqIFCoreContent addSpecType(SpecType specType) { - this.specTypes.put(specType.getID(), specType); - return this; + return add(this.specTypes, specType); } + /** + * @throws IllegalArgumentException if the IDENTIFIER is missing or taken + */ public ReqIFCoreContent addSpecObject(SpecObject specObject) { - this.specObjects.put(specObject.getID(), specObject); - return this; + return add(this.specObjects, specObject); } + /** + * @throws IllegalArgumentException if the IDENTIFIER is missing or taken + */ public ReqIFCoreContent addSpecRelation(SpecRelation relation) { - this.specRelation.put(relation.getID(), relation); - return this; + return add(this.specRelation, relation); } + /** + * @throws IllegalArgumentException if the IDENTIFIER is missing or taken + */ public ReqIFCoreContent addSpecification(Specification specification) { - this.specifications.put(specification.getID(), specification); + if (specification != null && this.specifications.get(specification.getID()) != specification) { + for (SpecHierarchy node : specification.getAllSpecHierarchies()) { + checkUnused(node.getID()); + } + } + return add(this.specifications, specification); + } + + /** + * @throws IllegalArgumentException if the IDENTIFIER is missing or taken + */ + public ReqIFCoreContent addRelationGroup(RelationGroup relationGroup) { + return add(this.relationGroups, relationGroup); + } + + private ReqIFCoreContent add(Map elements, T element) { + + if (element == null) { + throw new IllegalArgumentException("Cannot add null"); + } + if (elements.get(element.getID()) == element) { + return this; + } + checkUnused(element.getID()); + elements.put(element.getID(), element); return this; } + /** + * @throws IllegalArgumentException if the identifier is blank or an + * element of this content has it + */ + private void checkUnused(String id) { - public ReqIFCoreContent(Element coreContent) { - this(coreContent, TypeClassifier.defaultClassifier()); + if (id == null || id.isBlank()) { + throw new IllegalArgumentException("An element needs an identifier"); + } + for (Map map : List.of(this.dataTypes, this.specTypes, this.specObjects, + this.specRelation, this.specifications, this.relationGroups)) { + if (map.containsKey(id)) { + throw new IllegalArgumentException("Identifier " + id + " is already in use"); + } + } + if (find(id) != null) { + throw new IllegalArgumentException("Identifier " + id + " is already in use by a spec hierarchy"); + } } - public ReqIFCoreContent(Element coreContent, TypeClassifier typeClassifier) { + /** + * Inserts a new hierarchy node, with any nodes below it. Unlike + * {@link HierarchyParent#addChild(int, SpecHierarchy)}, this checks that + * no element of the document has the identifiers already. + * + * @param parentID the specification or hierarchy node to insert it below + * @param index the position among the parent's children, 0 for first + * @throws IllegalArgumentException if the parent does not exist, an + * identifier is taken, the node is already placed somewhere, or + * the position is out of range + */ + public SpecHierarchy addSpecHierarchy(String parentID, int index, SpecHierarchy node) { - if (typeClassifier == null) { - typeClassifier = TypeClassifier.defaultClassifier(); + HierarchyParent parent = parent(parentID); + if (node == null) { + throw new IllegalArgumentException("Cannot add null"); + } + checkUnused(node.getID()); + for (SpecHierarchy below : node.getAllChildren()) { + checkUnused(below.getID()); } + parent.addChild(index, node); + return node; + } + /** + * @throws IllegalArgumentException if there is no such specification or + * hierarchy node + */ + private HierarchyParent parent(String parentID) { + HierarchyParent parent = this.specifications.get(parentID); + if (parent == null) { + Placement placement = find(parentID); + if (placement == null) { + throw new IllegalArgumentException("There is no specification or spec hierarchy " + parentID); + } + parent = placement.node; + } + return parent; + } - // Every element is matched by local name, so documents that put the - // ReqIF elements into a prefixed namespace are read as well. - Element datatypesElement = XmlUtils.firstDescendantByLocalName(coreContent, ReqIFConst.DATATYPES); - if (datatypesElement != null) { - - for (Element dataType : XmlUtils.childElements(datatypesElement)) { - - String dataTypeNodeName = XmlUtils.localName(dataType); - { - - String dataTypeID = dataType.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent(); - String dataTypeName = dataType.getAttributes().getNamedItem(ReqIFConst.LONG_NAME).getTextContent(); - String dataTypeAlternativeID = XmlUtils.alternativeID(dataType); - - switch (dataTypeNodeName.substring(dataTypeNodeName.lastIndexOf("-") + 1)) { - - case ReqIFConst.BOOLEAN: - this.dataTypes.put(dataTypeID, new DatatypeBoolean(dataTypeID, dataTypeName)); - break; - - case ReqIFConst.INTEGER: - // MIN/MAX are optional in ReqIF - String min = XmlUtils.attribute(dataType, ReqIFConst.MIN); - String max = XmlUtils.attribute(dataType, ReqIFConst.MAX); - this.dataTypes.put(dataTypeID, new DatatypeInteger(dataTypeID, dataTypeName, min, max)); - break; - - case ReqIFConst.STRING: - // MAX-LENGTH is optional in ReqIF - String maxLength = XmlUtils.attribute(dataType, ReqIFConst.MAX_LENGTH); - this.dataTypes.put(dataTypeID, new DatatypeString(dataTypeID, dataTypeName, maxLength)); - break; - - case ReqIFConst.ENUMERATION: - Node enumeration = dataType; - this.dataTypes.put(dataTypeID, new DatatypeEnumeration(dataTypeID, dataTypeName, enumeration)); - break; - - case ReqIFConst.XHTML: - this.dataTypes.put(dataTypeID, new DatatypeXHTML(dataTypeID, dataTypeName)); - break; - - case ReqIFConst.DATE: - this.dataTypes.put(dataTypeID, new DatatypeDate(dataTypeID, dataTypeName)); - break; - - case ReqIFConst.DOUBLE, ReqIFConst.REAL: - this.dataTypes.put(dataTypeID, new DatatypeDouble(dataTypeID, dataTypeName)); - break; - - default: - // Register unknown datatypes under their ID (formerly the - // key was null, making them unresolvable). - this.dataTypes.put(dataTypeID, new Datatype(dataTypeID, dataTypeName, ReqIFConst.UNDEFINED, dataTypeNodeName)); - break; + /** + * Removes a spec object, the hierarchy nodes that show it - their children + * move up into their place - and the relations from or to it. + * + * @return the removed object, or null if there is no such object + */ + public SpecObject removeSpecObject(String id) { + + if (!this.specObjects.containsKey(id)) { + return null; + } + // only the parents of the nodes that show the object change: the + // nearest ancestor that does not show it itself + List parents = new ArrayList(); + java.util.Set seen = java.util.Collections.newSetFromMap( + new java.util.IdentityHashMap()); + for (Specification specification : this.specifications.values()) { + for (SpecHierarchy node : specification.getAllSpecHierarchies()) { + if (id.equals(node.getSpecObjectID())) { + HierarchyParent parent = node.getParent(); + while (parent instanceof SpecHierarchy && id.equals(((SpecHierarchy) parent).getSpecObjectID())) { + parent = ((SpecHierarchy) parent).getParent(); } - if (this.dataTypes.get(dataTypeID) != null) { - this.dataTypes.get(dataTypeID).setAlternativeID(dataTypeAlternativeID); + if (seen.add(parent)) { + parents.add(parent); } } } } + // checked before anything changes, so a refusal leaves all as it was + List> lifted = new ArrayList>(); + for (HierarchyParent parent : parents) { + lifted.add(checkedLifted(parent, id)); + } + SpecObject removed = this.specObjects.remove(id); + for (int i = 0; i < parents.size(); i++) { + for (SpecHierarchy node : lifted.get(i)) { + if (node.getParent() != parents.get(i)) { + // below a node that shows the object, which goes + node.getParent().removeChild(node.getID()); + } + } + // in one step, so the siblings are renumbered once + parents.get(i).setChildren(lifted.get(i)); + } + for (SpecRelation relation : new ArrayList(this.specRelation.values())) { + if (id.equals(relation.getSourceObjID()) || id.equals(relation.getTargetObjID())) { + removeSpecRelation(relation.getID()); + } + } + return removed; + } + /** + * @return the children {@code parent} has once the nodes showing the + * object are replaced by their children + * @throws IllegalStateException if that would put two nodes with the same + * IDENTIFIER side by side (only in documents that already repeat + * one) + */ + private static List checkedLifted(HierarchyParent parent, String specObjectID) { + List lifted = liftedChildren(parent, specObjectID); + java.util.Set ids = new java.util.HashSet(); + for (SpecHierarchy child : lifted) { + if (!ids.add(child.getID())) { + throw new IllegalStateException("Removing spec object " + specObjectID + " would put spec hierarchy " + + child.getID() + " twice below " + parent.getID()); + } + } + return lifted; + } - Element specTypesElement = XmlUtils.firstDescendantByLocalName(coreContent, ReqIFConst.SPEC_TYPES); - if (specTypesElement != null) { + /** + * @return the children {@code parent} has once the nodes showing the + * object are replaced by their children + */ + private static List liftedChildren(HierarchyParent parent, String specObjectID) { + List lifted = new ArrayList(); + for (SpecHierarchy child : parent.getChildren()) { + if (specObjectID.equals(child.getSpecObjectID())) { + lifted.addAll(liftedChildren(child, specObjectID)); + } else { + lifted.add(child); + } + } + return lifted; + } - for (Element specType : XmlUtils.childElements(specTypesElement)) { + /** + * Removes a relation, also from the relation groups it belongs to. + * + * @return the removed relation, or null if there is no such relation + */ + public SpecRelation removeSpecRelation(String id) { - String specTypeNodeName = XmlUtils.localName(specType); - { + SpecRelation removed = this.specRelation.remove(id); + if (removed != null) { + for (RelationGroup group : this.relationGroups.values()) { + group.removeSpecRelationRef(id); + } + } + return removed; + } - String specTypeID = specType.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent(); + /** + * Removes a specification with its hierarchy, and the relation groups + * between it and another specification. The spec objects stay. + * + * @return the removed specification, or null if there is no such one + */ + public Specification removeSpecification(String id) { - switch (specTypeNodeName) { + Specification removed = this.specifications.remove(id); + if (removed != null) { + this.relationGroups.values().removeIf(group -> id.equals(group.getSourceSpecificationRef()) + || id.equals(group.getTargetSpecificationRef())); + } + return removed; + } - case ReqIFConst.SPECIFICATION_TYPE: - this.specTypes.put(specTypeID, new SpecificationType(specType, this.dataTypes)); - break; + /** + * Removes a relation group; its relations stay. + * + * @return the removed group, or null if there is no such group + */ + public RelationGroup removeRelationGroup(String id) { + return this.relationGroups.remove(id); + } - case ReqIFConst.SPEC_OBJECT_TYPE: - this.specTypes.put(specTypeID, new SpecObjectType(specType, this.dataTypes)); - break; + /** + * Removes a spec type that no element uses. + * + * @return the removed type, or null if there is no such type + * @throws IllegalStateException if a spec object, relation, specification + * or relation group still has this type + */ + public SpecType removeSpecType(String id) { - case ReqIFConst.SPEC_RELATION_TYPE: - this.specTypes.put(specTypeID, new SpecRelationType(specType, this.dataTypes)); - break; + if (!this.specTypes.containsKey(id)) { + return null; + } + List users = new ArrayList(); + for (Map elements : List.of(this.specObjects, + this.specRelation, this.specifications)) { + for (SpecElementWithAttributes element : elements.values()) { + if (id.equals(typeID(element))) { + users.add(element.getID()); + } + } + } + for (RelationGroup group : this.relationGroups.values()) { + if (id.equals(group.getRelationGroupTypeRef())) { + users.add(group.getID()); + } + } + if (!users.isEmpty()) { + throw new IllegalStateException("Spec type " + id + " is still used by " + String.join(", ", users)); + } + return this.specTypes.remove(id); + } - case ReqIFConst.RELATION_GROUP_TYPE: - this.specTypes.put(specTypeID, new RelationGroupType(specType, this.dataTypes)); - break; + private static String typeID(SpecElementWithAttributes element) { + if (element instanceof SpecObject) { + return ((SpecObject) element).getSpecTypeID(); + } + return element instanceof Specification ? ((Specification) element).getSpecTypeID() : null; + } - default: - this.specTypes.put(specTypeID, new SpecType(specType, this.dataTypes)); - break; - } + /** + * Removes a datatype that no attribute definition uses. + * + * @return the removed datatype, or null if there is no such datatype + * @throws IllegalStateException if an attribute definition still has + * this datatype + */ + public Datatype removeDatatype(String id) { + + Datatype datatype = this.dataTypes.get(id); + if (datatype == null) { + return null; + } + List users = new ArrayList(); + for (SpecType specType : this.specTypes.values()) { + for (de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition definition + : specType.getAttributeDefinitions().values()) { + if (definition.getDataType() == datatype) { + users.add(definition.getID()); } } } + if (!users.isEmpty()) { + throw new IllegalStateException("Datatype " + id + " is still used by " + String.join(", ", users)); + } + return this.dataTypes.remove(id); + } + /** + * @return the hierarchy node with this IDENTIFIER in any specification, or + * null if there is none + */ + public SpecHierarchy getSpecHierarchy(String id) { + Placement placement = find(id); + return placement == null ? null : placement.node; + } - for (Element specObj : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.SPEC_OBJECT)) { + /** + * Removes a hierarchy node with everything below it. The spec objects + * stay. + * + * @return the removed node, or null if there is no such node + */ + public SpecHierarchy removeSpecHierarchy(String id) { + Placement placement = find(id); + return placement == null ? null : placement.parent.removeChild(id); + } - String specObjID = XmlUtils.attribute(specObj, ReqIFConst.IDENTIFIER); - Element typeRef = XmlUtils.firstDescendantByLocalName(specObj, ReqIFConst.SPEC_OBJECT_TYPE_REF); - String specObjTypeRef = typeRef == null ? null : typeRef.getTextContent().trim(); + /** + * Moves a hierarchy node, with everything below it, to another place, in + * the same or another specification. + * + * @param newParentID the specification or hierarchy node to move it to + * @param index the position among the new parent's children after + * the move, 0 for first + * @throws IllegalArgumentException if the node or the new parent does not + * exist, the new parent is the node itself or below it, or the + * position is out of range + */ + public SpecHierarchy moveSpecHierarchy(String id, String newParentID, int index) { - this.specObjects.put(specObjID, new SpecObject(specObj, this.specTypes.get(specObjTypeRef), typeClassifier)); + Placement placement = find(id); + if (placement == null) { + throw new IllegalArgumentException("There is no spec hierarchy " + id); + } + HierarchyParent newParent = parent(newParentID); + // the new parent must not be the node or below it + for (HierarchyParent above = newParent; above instanceof SpecHierarchy; + above = ((SpecHierarchy) above).getParent()) { + if (above == placement.node) { + throw new IllegalArgumentException("Spec hierarchy " + id + " cannot be moved below itself"); + } + } + boolean sameParent = newParent == placement.parent; + int size = newParent.getChildCount() - (sameParent ? 1 : 0); + if (index < 0 || index > size) { + throw new IllegalArgumentException("Position " + index + " is outside 0.." + size); + } + if (!sameParent && newParent.getChild(id) != null) { + throw new IllegalArgumentException("Spec hierarchy " + id + " is already a child of " + newParentID); } + placement.parent.removeChild(id); + newParent.addChild(index, placement.node); + return placement.node; + } + private static final class Placement { + final HierarchyParent parent; + final SpecHierarchy node; - for (Element specRelation : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.SPEC_RELATION)) { + Placement(HierarchyParent parent, SpecHierarchy node) { + this.parent = parent; + this.node = node; + } + } - String specRelID = XmlUtils.attribute(specRelation, ReqIFConst.IDENTIFIER); - Element typeRef = XmlUtils.firstDescendantByLocalName(specRelation, ReqIFConst.SPEC_RELATION_TYPE_REF); - String specRelTypeRef = typeRef == null ? null : typeRef.getTextContent().trim(); + private Placement find(String id) { + if (id == null) { + return null; + } + for (Specification specification : this.specifications.values()) { + SpecHierarchy node = specification.getSpecHierarchy(id); + if (node != null) { + return new Placement(node.getParent(), node); + } + } + return null; + } + + /** + * Marks every element as unchanged, so that the writer keeps their + * LAST-CHANGE instead of giving them the time of writing. + */ + public void clearModified() { - this.specRelation.put(specRelID, new SpecRelation(specRelation, this.specTypes.get(specRelTypeRef))); + List all = new ArrayList(); + for (Datatype datatype : this.dataTypes.values()) { + all.add(datatype); + if (datatype instanceof DatatypeEnumeration) { + all.addAll(((DatatypeEnumeration) datatype).getEnumValues().values()); + } + } + for (SpecType specType : this.specTypes.values()) { + all.add(specType); + all.addAll(specType.getAttributeDefinitions().values()); + } + all.addAll(this.specObjects.values()); + all.addAll(this.specRelation.values()); + for (Specification specification : this.specifications.values()) { + all.add(specification); + all.addAll(specification.getAllSpecHierarchies()); + } + all.addAll(this.relationGroups.values()); + for (Identifiable identifiable : all) { + identifiable.clearModified(); } + } - for (Element relationGroup : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.RELATION_GROUP)) { - RelationGroup group = new RelationGroup(relationGroup); - this.relationGroups.put(group.getID(), group); + public ReqIFCoreContent(Element coreContent) { + this(coreContent, TypeClassifier.defaultClassifier()); + } + + public ReqIFCoreContent(Element coreContent, TypeClassifier typeClassifier) { + this(coreContent, typeClassifier, ParseIssues.collecting()); + } + + /** + * Reads the content of a document. Elements that cannot be read are + * skipped and, like every other problem found, reported to {@code issues} + * instead of making the whole document unreadable. + */ + public ReqIFCoreContent(Element coreContent, TypeClassifier typeClassifier, ParseIssues issues) { + + TypeClassifier classifier = typeClassifier == null ? TypeClassifier.defaultClassifier() : typeClassifier; + this.parseIssues = issues; + + // Every element is matched by local name, so documents that put the + // ReqIF elements into a prefixed namespace are read as well. + Element datatypesElement = XmlUtils.firstDescendantByLocalName(coreContent, ReqIFConst.DATATYPES); + for (Element dataType : XmlUtils.childElements(datatypesElement)) { + issues.put(this.dataTypes, issues.readOrSkip("datatype", () -> readDatatype(dataType, issues)), "Datatype"); + } + + Element specTypesElement = XmlUtils.firstDescendantByLocalName(coreContent, ReqIFConst.SPEC_TYPES); + for (Element specType : XmlUtils.childElements(specTypesElement)) { + issues.put(this.specTypes, issues.readOrSkip("spec type", () -> readSpecType(specType, issues)), "Spec type"); + } + + for (Element specObj : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.SPEC_OBJECT)) { + SpecType type = resolveType(specObj, ReqIFConst.SPEC_OBJECT_TYPE_REF, issues); + issues.put(this.specObjects, issues.readOrSkip("spec object", + () -> new SpecObject(specObj, type, classifier, issues)), "Spec object"); } + for (Element specRelation : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.SPEC_RELATION)) { + SpecType type = resolveType(specRelation, ReqIFConst.SPEC_RELATION_TYPE_REF, issues); + issues.put(this.specRelation, issues.readOrSkip("spec relation", + () -> new SpecRelation(specRelation, type, issues)), "Spec relation"); + } + + for (Element relationGroup : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.RELATION_GROUP)) { + issues.put(this.relationGroups, issues.readOrSkip("relation group", + () -> new RelationGroup(relationGroup)), "Relation group"); + } for (Element specification : XmlUtils.descendantsByLocalName(coreContent, ReqIFConst.SPECIFICATION)) { + SpecType type = resolveType(specification, ReqIFConst.SPEC_TYPE_REF, issues); + issues.put(this.specifications, issues.readOrSkip("specification", + () -> new Specification(specification, type, this.specObjects, issues)), "Specification"); + } + } + + /** + * @return the problems found while reading this content; empty for content + * that was not read from a document + */ + public java.util.List getParseIssues() { + return this.parseIssues.getIssues(); + } + + + private static Datatype readDatatype(Element dataType, ParseIssues issues) { + + String dataTypeNodeName = XmlUtils.localName(dataType); + String dataTypeID = XmlUtils.attribute(dataType, ReqIFConst.IDENTIFIER); + // LONG-NAME is optional in ReqIF + String dataTypeName = XmlUtils.attribute(dataType, ReqIFConst.LONG_NAME); + + Datatype datatype; + switch (ReqIFConst.kindOf(dataTypeNodeName)) { + + case ReqIFConst.BOOLEAN: + datatype = new DatatypeBoolean(dataTypeID, dataTypeName); + break; - String specID = XmlUtils.attribute(specification, ReqIFConst.IDENTIFIER); - Element typeRef = XmlUtils.firstDescendantByLocalName(specification, ReqIFConst.SPEC_TYPE_REF); - String specTypeRef = typeRef == null ? null : typeRef.getTextContent().trim(); + case ReqIFConst.INTEGER: + // MIN/MAX are optional in ReqIF + datatype = new DatatypeInteger(dataTypeID, dataTypeName, + XmlUtils.attribute(dataType, ReqIFConst.MIN), XmlUtils.attribute(dataType, ReqIFConst.MAX)); + break; - this.specifications.put(specID, new Specification(specification, this.specTypes.get(specTypeRef), this.specObjects)); + case ReqIFConst.STRING: + // MAX-LENGTH is optional in ReqIF + datatype = new DatatypeString(dataTypeID, dataTypeName, XmlUtils.attribute(dataType, ReqIFConst.MAX_LENGTH)); + break; + + case ReqIFConst.ENUMERATION: + datatype = new DatatypeEnumeration(dataTypeID, dataTypeName, dataType, issues); + break; + + case ReqIFConst.XHTML: + datatype = new DatatypeXHTML(dataTypeID, dataTypeName); + break; + + case ReqIFConst.DATE: + datatype = new DatatypeDate(dataTypeID, dataTypeName); + break; + + case ReqIFConst.DOUBLE, ReqIFConst.REAL: + datatype = new DatatypeDouble(dataTypeID, dataTypeName, + XmlUtils.attribute(dataType, ReqIFConst.ACCURACY), + XmlUtils.attribute(dataType, ReqIFConst.MIN), + XmlUtils.attribute(dataType, ReqIFConst.MAX)); + break; + + default: + // kinds the parser does not model are kept under their element name + datatype = new Datatype(dataTypeID, dataTypeName, ReqIFConst.UNDEFINED, dataTypeNodeName); + break; + } + // IDENTIFIER (required, checked here), DESC, LAST-CHANGE and ALTERNATIVE-ID + datatype.readIdentifiable(dataType); + return datatype; + } + + private SpecType readSpecType(Element specType, ParseIssues issues) { + + switch (XmlUtils.localName(specType)) { + case ReqIFConst.SPECIFICATION_TYPE: return new SpecificationType(specType, this.dataTypes, issues); + case ReqIFConst.SPEC_OBJECT_TYPE: return new SpecObjectType(specType, this.dataTypes, issues); + case ReqIFConst.SPEC_RELATION_TYPE: return new SpecRelationType(specType, this.dataTypes, issues); + case ReqIFConst.RELATION_GROUP_TYPE: return new RelationGroupType(specType, this.dataTypes, issues); + default: return new SpecType(specType, this.dataTypes, issues); } + } + /** + * @return the spec type the element's TYPE refers to, or null if the + * reference cannot be resolved (the element reports that) + */ + private SpecType resolveType(Element element, String refName, ParseIssues issues) { + Element typeRef = XmlUtils.firstDescendantByLocalName(element, refName); + return typeRef == null ? null : this.specTypes.get(typeRef.getTextContent().trim()); } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java index 5af0731..46da5cb 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFDocument.java @@ -3,6 +3,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import javax.xml.XMLConstants; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; @@ -25,6 +26,8 @@ public class ReqIFDocument { private ReqIFHeader header; private ReqIFCoreContent content; private final java.util.List toolExtensions = new java.util.ArrayList<>(); + private String lang; + private String schemaLocation; public String getFilePath() { @@ -43,6 +46,42 @@ public ReqIFCoreContent getCoreContent() { return this.content; } + /** + * @return the problems found while reading the document - skipped + * elements, unresolved references, identifiers used twice - in the + * order they were found; empty for a document that was not read + * from a file. {@code ReqIFValidator} reports them as warnings. + */ + public java.util.List getParseIssues() { + return this.content == null ? java.util.List.of() : this.content.getParseIssues(); + } + + /** + * @return the document language (xml:lang of REQ-IF), e.g. "de", or null + * if the document declares none + */ + public String getLang() { + return this.lang; + } + + /** + * @return xsi:schemaLocation of REQ-IF: pairs of namespace and schema file + * that tell a validator where to find the schemas, or null + */ + public String getSchemaLocation() { + return this.schemaLocation; + } + + public ReqIFDocument setSchemaLocation(String schemaLocation) { + this.schemaLocation = schemaLocation; + return this; + } + + public ReqIFDocument setLang(String lang) { + this.lang = lang; + return this; + } + /** * Tool-specific extensions of the document. They are not interpreted, only * kept as their original nodes so they survive a round trip. @@ -143,8 +182,22 @@ private void readDocument() { } this.content = new ReqIFCoreContent(coreContent, this.typeClassifier); + Element root = this.reqifDocument.getDocumentElement(); + if (root.hasAttributeNS(XMLConstants.XML_NS_URI, "lang")) { + this.lang = root.getAttributeNS(XMLConstants.XML_NS_URI, "lang"); + } + if (root.hasAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "schemaLocation")) { + this.schemaLocation = root.getAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "schemaLocation"); + } + // Tool extensions are kept verbatim; the parser does not interpret them. - this.toolExtensions.addAll(XmlUtils.descendantsByLocalName(this.reqifDocument, ReqIFConst.TOOL_EXTENSIONS)); + // Only the TOOL-EXTENSIONS of REQ-IF itself count: a vendor extension may + // well contain an element of that name, and the schema allows one only. + for (Element child : XmlUtils.childElements(root)) { + if (ReqIFConst.TOOL_EXTENSIONS.equals(XmlUtils.localName(child))) { + this.toolExtensions.add(child); + } + } } private static String extractFileName(String path) { diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java index 59b7e59..663c5c9 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFHeader.java @@ -16,6 +16,7 @@ public class ReqIFHeader { private String comment = ""; private String creationDate = ""; private String creationTime = ""; + private String repositoryID; @@ -64,6 +65,19 @@ public String getCreationDate() { public String getCreationTime() { return this.creationTime; } + + /** + * @return the optional REPOSITORY-ID, the repository the document was + * exported from, or null if the document declares none + */ + public String getRepositoryID() { + return this.repositoryID; + } + + public ReqIFHeader setRepositoryID(String repositoryID) { + this.repositoryID = repositoryID; + return this; + } @@ -92,6 +106,7 @@ public ReqIFHeader(Element theHeader) { this.creationTime = creationTime; this.creationDate = creationDateOf(creationTime); } + this.repositoryID = textOf(theHeader, ReqIFConst.REPOSITORY_ID); String title = textOf(theHeader, ReqIFConst.TITLE); if(title != null) { // The title is returned as written in the document; stripping a diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java index 18d6f62..ad65039 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/reqif/ReqIFz.java @@ -2,35 +2,66 @@ import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import de.uni_stuttgart.ils.reqif4j.specification.TypeClassifier; +/** + * A .reqifz archive: one or more ReqIF documents plus the files they + * reference - images, embedded OLE objects, PDFs or anything else. + * + * The archive is extracted into a temporary directory, which {@link #close()} + * deletes again; use try-with-resources. Every entry that is not a ReqIF + * document is kept as an attachment, so re-packing the archive with + * {@code ReqIFzWriter} loses nothing. + */ public class ReqIFz extends ReqIFFile { - private static final String EXTRACTION_SUFFIX = "_unzipped"; + private static final String[] PICTURE_EXTENSIONS = { + ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".tif", ".tiff", ".webp", ".emf", ".wmf" }; - private final Map pictureFiles = new java.util.LinkedHashMap<>(); - private final Map reqifFiles = new java.util.LinkedHashMap<>(); + private Path extractionDirectory; + private final Map attachmentFiles = new LinkedHashMap<>(); + private final Map pictureFiles = new LinkedHashMap<>(); + private final Map reqifFiles = new LinkedHashMap<>(); /** - * @return the extracted picture files, keyed by their archive entry name - * (forward slashes, as referenced by xhtml object data attributes). - * Unlike the input streams these can be read repeatedly, which is - * what re-packing an archive needs. + * @return every entry that is not a ReqIF document, keyed by its archive + * entry name (forward slashes, as referenced by xhtml object data + * attributes), in archive order. Valid until {@link #close()}. + */ + public Map getAttachmentFiles() { + return this.attachmentFiles; + } + + /** + * @return the attachments that are pictures (png, jpg, gif, svg, ... by + * file extension), keyed like + * {@link #getAttachmentFiles()}. Unlike the input streams these can + * be read repeatedly. Valid until {@link #close()}. */ public Map getPictureFiles() { return this.pictureFiles; } /** - * @return the extracted ReqIF files, keyed by their archive entry name + * @return the extracted ReqIF files, keyed by their archive entry name. + * Valid until {@link #close()}. */ public Map getReqIFFiles() { return this.reqifFiles; @@ -46,73 +77,149 @@ public ReqIFz(String filePath) throws IOException { * LONG-NAME heuristic */ public ReqIFz(String filePath, TypeClassifier typeClassifier) throws IOException { + this(new FileInputStream(filePath), filePath, typeClassifier); + } + + /** + * Reads an archive from a stream, e.g. an upload or a classpath resource. + * The stream is closed. + * + * @param name the archive's name or path, used in messages and as + * {@link #getPath()} + */ + public ReqIFz(InputStream archive, String name) throws IOException { + this(archive, name, TypeClassifier.defaultClassifier()); + } + + public ReqIFz(InputStream archive, String name, TypeClassifier typeClassifier) throws IOException { - this.path = filePath; - this.name = extractFileName(filePath); + this.path = name; + this.name = extractFileName(name); - File destDir = new File(removeExtension(new File(filePath).getAbsolutePath()) + EXTRACTION_SUFFIX); - if (!destDir.exists() && !destDir.mkdirs()) { - throw new IOException("Failed to create extraction directory " + destDir); + try (InputStream in = archive) { + this.extractionDirectory = Files.createTempDirectory("reqifz-"); + extract(in, typeClassifier); + } catch (IOException | RuntimeException e) { + try { + close(); + } catch (IOException cleanup) { + e.addSuppressed(cleanup); + } + throw e; } - String canonicalDestDir = destDir.getCanonicalPath(); + } - byte[] buffer = new byte[8192]; - Map picturesIS = new HashMap<>(); + private void extract(InputStream archive, TypeClassifier typeClassifier) throws IOException { - try (ZipInputStream zis = new ZipInputStream(new FileInputStream(filePath))) { - ZipEntry zipEntry = zis.getNextEntry(); + Path destination = this.extractionDirectory.toRealPath(); - while (zipEntry != null) { - File newFile = new File(destDir, zipEntry.getName()); + try (ZipInputStream zis = new ZipInputStream(archive)) { + for (ZipEntry zipEntry = zis.getNextEntry(); zipEntry != null; zipEntry = zis.getNextEntry()) { + // zip entries use forward slashes; archives made on Windows + // sometimes contain backslashes instead + String entryName = zipEntry.getName().replace('\\', '/'); + Path target = destination.resolve(entryName).normalize(); // Guard against zip-slip: entries must not escape the // extraction directory (e.g. via "../" path segments). - if (!newFile.getCanonicalPath().startsWith(canonicalDestDir + File.separator)) { - throw new IOException("Zip entry outside of extraction directory: " + zipEntry.getName()); + if (!target.startsWith(destination) || target.equals(destination)) { + throw new IOException("Zip entry outside of extraction directory: " + entryName); } - if (zipEntry.isDirectory()) { - if (!newFile.isDirectory() && !newFile.mkdirs()) { - throw new IOException("Failed to create directory " + newFile); + Files.createDirectories(target); + continue; + } + // also for archives that list no directory entries + Files.createDirectories(target.getParent()); + // a zip may hold an entry twice (tools append updated + // entries); the later one wins, as when unzipping + Files.copy(zis, target, StandardCopyOption.REPLACE_EXISTING); + + File file = target.toFile(); + if (entryName.toLowerCase(Locale.ROOT).endsWith(".reqif")) { + this.numberOfReqIFDocuments++; + this.reqifFiles.put(entryName, file); + try (InputStream reqifIS = new FileInputStream(file)) { + this.reqifDocuments.put(entryName, new ReqIFDocument(reqifIS, this.path, entryName, typeClassifier)); } } else { - // Fix for Windows-created archives - File parent = newFile.getParentFile(); - if (!parent.isDirectory() && !parent.mkdirs()) { - throw new IOException("Failed to create directory " + parent); - } - - // Write file content - try (FileOutputStream fos = new FileOutputStream(newFile)) { - int len; - while ((len = zis.read(buffer)) > 0) { - fos.write(buffer, 0, len); - } - } - - // Process reqif files and associated images - if (zipEntry.getName().endsWith("reqif")) { - this.numberOfReqIFDocuments++; - this.reqifFiles.put(zipEntry.getName(), newFile); - try (InputStream reqifIS = new FileInputStream(newFile)) { - this.reqifDocuments.put(zipEntry.getName(), new ReqIFDocument(reqifIS, filePath, zipEntry.getName(), typeClassifier)); - } - } else if (zipEntry.getName().endsWith("png") || zipEntry.getName().endsWith("jpeg") || zipEntry.getName().endsWith("jpg")) { - // Keyed by the archive entry path (forward slashes), which is - // what xhtml object data attributes reference. - this.pictureFiles.put(zipEntry.getName(), newFile); - picturesIS.put(zipEntry.getName(), new FileInputStream(newFile)); + this.attachmentFiles.put(entryName, file); + if (isPicture(entryName)) { + this.pictureFiles.put(entryName, file); } } - zipEntry = zis.getNextEntry(); } } - // Associate the archive's pictures with each contained ReqIF document, - // using the same keys as reqifDocuments (full entry name). + // Opened only now, so a failure while extracting leaves no stream open + // that would keep the files from being deleted. Associated with each + // contained ReqIF document, using the same keys as reqifDocuments. + // The map is registered before the streams are opened, so close() + // reaches every stream also if opening one of them fails. + Map picturesIS = new HashMap<>(); for (String reqifDocumentName : this.reqifDocuments.keySet()) { this.picturesInReqIFDocument.put(reqifDocumentName, picturesIS); } + if (!this.reqifDocuments.isEmpty()) { + for (Map.Entry picture : this.pictureFiles.entrySet()) { + picturesIS.put(picture.getKey(), new FileInputStream(picture.getValue())); + } + } + } + + /** + * Closes the picture streams and deletes the extracted files. + */ + @Override + public void close() throws IOException { + + IOException failure = null; + try { + super.close(); + } catch (IOException e) { + failure = e; + } + if (this.extractionDirectory != null && Files.exists(this.extractionDirectory)) { + List files; + try (Stream walk = Files.walk(this.extractionDirectory)) { + // children before their directories + files = walk.sorted(Comparator.reverseOrder()).collect(Collectors.toList()); + } catch (IOException | UncheckedIOException e) { + files = List.of(); + failure = addFailure(failure, e); + } + // a file that cannot be deleted must not keep the others + for (Path file : files) { + try { + Files.deleteIfExists(file); + } catch (IOException e) { + failure = addFailure(failure, e); + } + } + } + if (failure != null) { + throw failure; + } + } + + private static boolean isPicture(String entryName) { + String lower = entryName.toLowerCase(Locale.ROOT); + for (String extension : PICTURE_EXTENSIONS) { + if (lower.endsWith(extension)) { + return true; + } + } + return false; + } + + private static IOException addFailure(IOException failure, Exception next) { + IOException asIO = next instanceof UncheckedIOException ? ((UncheckedIOException) next).getCause() + : next instanceof IOException ? (IOException) next : new IOException(next); + if (failure == null) { + return asIO; + } + failure.addSuppressed(asIO); + return failure; } public static String removeFileExtension(String filename, boolean removeAllExtensions) { diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/AttributeValueReader.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/AttributeValueReader.java new file mode 100644 index 0000000..627e921 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/AttributeValueReader.java @@ -0,0 +1,283 @@ +package de.uni_stuttgart.ils.reqif4j.specification; + +import java.util.ArrayList; +import java.util.List; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinitionEnumeration; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueBoolean; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueDate; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueDouble; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueEnumeration; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueInteger; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueString; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML; +import de.uni_stuttgart.ils.reqif4j.attributes.UninterpretedAttributeValue; +import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; +import de.uni_stuttgart.ils.reqif4j.util.XsdTypes; + +/** + * Reads the VALUES of an element with attributes - spec objects, spec + * relations and specifications share the same structure - and creates the + * values that stand in for absent ones from the attribute definition defaults. + */ +final class AttributeValueReader { + + private AttributeValueReader() { + } + + /** + * Reads the explicit values, the ones the element itself carries. + * + * A value whose definition cannot be found, whose definition has no + * datatype, or whose kind does not match its definition is reported and + * kept as an {@link UninterpretedAttributeValue}, which is written back + * unchanged. A value that does not fit its datatype (e.g. + * THE-VALUE="abc" for an integer) is kept as written, as a plain + * {@link AttributeValue}, so it is written back unchanged and the + * validator can report it. + * + * @param element the SPEC-OBJECT, SPEC-RELATION or SPECIFICATION + * @param specType the element's resolved spec type + * @param values receives the values + * @param issues receives the problems found + */ + static void read(Node element, SpecType specType, AttributeValues values, ParseIssues issues) { + + // VALUES is located by local name so prefixed ReqIF namespaces work too; + // only this element's own VALUES is taken, not a nested one. + Element valuesElement = XmlUtils.firstChildElementByLocalName(element, ReqIFConst.VALUES); + if(valuesElement == null) { + return; + } + String ownerID = XmlUtils.attribute(element, ReqIFConst.IDENTIFIER); + + for(Element attribute: XmlUtils.childElements(valuesElement)) { + + String attValNodeName = XmlUtils.localName(attribute); + Element definitionRef = XmlUtils.firstChildElement( + XmlUtils.firstChildElementByLocalName(attribute, ReqIFConst.DEFINITION)); + if(definitionRef == null) { + issues.report(ownerID, attValNodeName + " has no DEFINITION; the value is skipped"); + continue; + } + String attributeDefinitionRef = definitionRef.getTextContent().trim(); + if(values.explicit().containsKey(attributeDefinitionRef)) { + issues.report(ownerID, "Attribute definition " + attributeDefinitionRef + + " has two values; the later one replaces the earlier"); + } + AttributeDefinition attributeDefinition = specType.getAttributeDefinition(attributeDefinitionRef); + String kind = ReqIFConst.kindOf(attValNodeName); + String definitionKind = attributeDefinition == null || attributeDefinition.getDataType() == null ? null + : ReqIFConst.datatypeSuffix(attributeDefinition.getDataType().getType()); + String reason = null; + if(attributeDefinition == null) { + reason = "Value references attribute definition " + attributeDefinitionRef + + ", which spec type " + specType.getID() + " does not declare"; + } else if(attributeDefinition.getDataType() == null) { + reason = "Value of attribute definition " + attributeDefinitionRef + ", which has no datatype"; + } else if(definitionKind != null && !kind.equals(definitionKind)) { + reason = attValNodeName + " for attribute definition " + attributeDefinitionRef + ", which is of kind " + + definitionKind; + } + if(reason != null) { + // a definition without datatype is reported once, with its values + if(attributeDefinition == null || attributeDefinition.getDataType() != null) { + issues.report(ownerID, reason + "; the value is kept as written"); + } + values.put(new UninterpretedAttributeValue(attribute, + attributeDefinition != null ? attributeDefinition : standIn(attributeDefinitionRef), reason)); + continue; + } + + String theValue = theValue(attribute); + if(THE_VALUE_KINDS.contains(kind) && XmlUtils.attribute(attribute, ReqIFConst.THE_VALUE) == null) { + // read as the neutral value (false, 0, ""), as earlier versions did + issues.report(ownerID, attValNodeName + " for " + attributeDefinitionRef + + " has no THE-VALUE; it is read as empty"); + } + switch(kind) { + + case ReqIFConst.BOOLEAN: + // any case is accepted, as earlier versions did; other text is kept + values.put(theValue.isEmpty() || BOOLEAN_TEXT.contains(theValue.trim().toLowerCase(java.util.Locale.ROOT)) + ? new AttributeValueBoolean(theValue, attributeDefinition) + : asWritten(theValue, attributeDefinition, attValNodeName)); + break; + + case ReqIFConst.INTEGER: + values.put(theValue.isEmpty() || XsdTypes.isInteger(theValue) + ? new AttributeValueInteger(theValue, attributeDefinition) + : asWritten(theValue, attributeDefinition, attValNodeName)); + break; + + case ReqIFConst.STRING: + values.put(new AttributeValueString(theValue, attributeDefinition)); + break; + + case ReqIFConst.ENUMERATION: + // Multiselect: read ALL ENUM-VALUE-REF children, not just the first + List enumRefs = new ArrayList(); + List enumNames = new ArrayList(); + for(Element ref : XmlUtils.descendantsByLocalName(attribute, ReqIFConst.ENUM_VALUE_REF)) { + String refID = ref.getTextContent().trim(); + enumRefs.add(refID); + enumNames.add(specType.getEnumValueName(refID)); + } + values.put(new AttributeValueEnumeration(enumNames, enumRefs, attributeDefinition)); + break; + + case ReqIFConst.XHTML: + values.put(new AttributeValueXHTML(attribute, attributeDefinition)); + break; + + case ReqIFConst.DATE: + values.put(new AttributeValueDate(theValue, attributeDefinition)); + break; + + case ReqIFConst.REAL: + values.put(theValue.isEmpty() || XsdTypes.isDouble(theValue) + ? new AttributeValueDouble(theValue, attributeDefinition) + : asWritten(theValue, attributeDefinition, attValNodeName)); + break; + + // Values of datatype kinds the parser does not model are kept + // generically instead of being dropped, so they survive a + // round trip. + default: + values.put(asWritten(theValue, attributeDefinition, attValNodeName)); + break; + } + } + } + + /** + * Keeps the values of an element whose spec type is not declared as + * written: without the type, they cannot be interpreted. + * + * @return the number of values kept + */ + static int readUninterpreted(Node element, String reason, AttributeValues values, ParseIssues issues) { + + int count = 0; + String ownerID = XmlUtils.attribute(element, ReqIFConst.IDENTIFIER); + for(Element attribute: XmlUtils.childElements(XmlUtils.firstChildElementByLocalName(element, ReqIFConst.VALUES))) { + Element definitionRef = XmlUtils.firstChildElement( + XmlUtils.firstChildElementByLocalName(attribute, ReqIFConst.DEFINITION)); + if(definitionRef == null) { + issues.report(ownerID, XmlUtils.localName(attribute) + " has no DEFINITION; the value is skipped"); + continue; + } + String attributeDefinitionRef = definitionRef.getTextContent().trim(); + if(values.explicit().containsKey(attributeDefinitionRef)) { + issues.report(ownerID, "Attribute definition " + attributeDefinitionRef + + " has two values; the later one replaces the earlier"); + } else { + count++; + } + values.put(new UninterpretedAttributeValue(attribute, standIn(attributeDefinitionRef), reason)); + } + return count; + } + + /** + * @return an attribute definition that stands in for an undeclared one: + * only its IDENTIFIER is known + */ + private static AttributeDefinition standIn(String attributeDefinitionID) { + return new AttributeDefinition(attributeDefinitionID, null, null, null); + } + + private static final java.util.Set BOOLEAN_TEXT = java.util.Set.of("true", "false", "1", "0"); + + /** the value kinds that carry their value in a required THE-VALUE attribute */ + private static final java.util.Set THE_VALUE_KINDS = java.util.Set.of( + ReqIFConst.BOOLEAN, ReqIFConst.INTEGER, ReqIFConst.STRING, ReqIFConst.DATE, ReqIFConst.REAL); + + /** + * @return the value exactly as written, for kinds the parser does not model + * and for text that does not fit the datatype + */ + private static AttributeValue asWritten(String text, AttributeDefinition definition, String elementName) { + return new AttributeValue(text, definition).setSourceElementName(elementName); + } + + /** + * @return the value that stands in for an absent one: the definition's + * default, or the neutral value of its datatype (false, 0, ...) as + * earlier versions reported it; null for datatypes without one + */ + static AttributeValue defaultValue(AttributeDefinition attributeDefinition) { + + Datatype datatype = attributeDefinition.getDataType(); + if(datatype == null) { + return null; + } + String defaultValue = attributeDefinition.getDefaultValue(); + switch(datatype.getType()) { + + // a default that does not fit its datatype is kept as written, like a + // value; the validator reports it + case ReqIFConst.BOOLEAN: + return defaultValue == null || BOOLEAN_TEXT.contains(defaultValue.trim().toLowerCase(java.util.Locale.ROOT)) + ? new AttributeValueBoolean(defaultValue, attributeDefinition) + : new AttributeValue(defaultValue, attributeDefinition); + + case ReqIFConst.INTEGER: + return defaultValue == null || XsdTypes.isInteger(defaultValue) + ? new AttributeValueInteger(defaultValue, attributeDefinition) + : new AttributeValue(defaultValue, attributeDefinition); + + case ReqIFConst.STRING: + return new AttributeValueString(defaultValue, attributeDefinition); + + case ReqIFConst.ENUMERATION: + if(attributeDefinition instanceof AttributeDefinitionEnumeration) { + AttributeDefinitionEnumeration enumDefinition = (AttributeDefinitionEnumeration) attributeDefinition; + return new AttributeValueEnumeration(enumDefinition.getDefaultValues(), enumDefinition.getDefaultValueRefs(), attributeDefinition); + } + return new AttributeValueEnumeration(defaultValue, attributeDefinition); + + case ReqIFConst.XHTML: + return attributeDefinition.getDefaultXhtmlValue() != null + ? attributeDefinition.getDefaultXhtmlValue() + : new AttributeValueXHTML(defaultValue, attributeDefinition); + + case ReqIFConst.DATE: + return new AttributeValueDate(defaultValue, attributeDefinition); + + // the parser normalizes REAL datatypes to DOUBLE + case ReqIFConst.DOUBLE: + case ReqIFConst.REAL: + return defaultValue == null || XsdTypes.isDouble(defaultValue) + ? new AttributeValueDouble(defaultValue, attributeDefinition) + : new AttributeValue(defaultValue, attributeDefinition); + + default: + return null; + } + } + + /** + * Attribute values are kept by the LONG-NAME of their definition. LONG-NAME + * is optional, though; definitions without one are keyed by their + * IDENTIFIER instead, so their values do not overwrite each other. + */ + static String key(AttributeDefinition attributeDefinition) { + return attributeDefinition.getName() != null ? attributeDefinition.getName() : attributeDefinition.getID(); + } + + /** + * @return the THE-VALUE attribute, or "" if the value element has none + */ + private static String theValue(Element attribute) { + return XmlUtils.attributeOrEmpty(attribute, ReqIFConst.THE_VALUE); + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/AttributeValues.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/AttributeValues.java new file mode 100644 index 0000000..3817351 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/AttributeValues.java @@ -0,0 +1,93 @@ +package de.uni_stuttgart.ils.reqif4j.specification; + +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue; + +/** + * The attribute values of a spec object, spec relation or specification. + * + * Two views are kept apart: + * + *
    + *
  • explicit values are the ones the element carries itself - + * exactly what the document contains and what is written back - keyed by the + * IDENTIFIER of their attribute definition, in document order;
  • + *
  • effective values additionally fill in the default value of + * every attribute definition the element has no value for. They are keyed by + * LONG-NAME for convenient lookup and only ever derived, never written.
  • + *
+ */ +final class AttributeValues { + + private final SpecType specType; + private final Map explicit = new LinkedHashMap(); + private final Map explicitView = Collections.unmodifiableMap(this.explicit); + + AttributeValues(SpecType specType) { + this.specType = specType; + } + + SpecType specType() { + return this.specType; + } + + /** + * Adds an explicit value; a second value for the same definition replaces + * the first. + */ + void put(AttributeValue value) { + this.explicit.put(value.getAttributeDefinitionType().getID(), value); + } + + AttributeValue remove(String attributeDefinitionID) { + return this.explicit.remove(attributeDefinitionID); + } + + /** + * @return the explicit values by attribute definition IDENTIFIER, in + * document order (read-only) + */ + Map explicit() { + return this.explicitView; + } + + /** + * @return the effective values by LONG-NAME (IDENTIFIER for definitions + * without one), in spec type order: explicit values plus the + * defaults of all other definitions. An explicit value always wins + * over a default of the same name. Derived on every call, so it + * follows later changes of the values or the spec type. + */ + Map effectiveByName() { + + Map effective = new LinkedHashMap(); + Map undeclared = new LinkedHashMap(this.explicit); + Set explicitNames = new HashSet(); + for (AttributeValue value : this.explicit.values()) { + explicitNames.add(AttributeValueReader.key(value.getAttributeDefinitionType())); + } + + if (this.specType != null) { + for (AttributeDefinition definition : this.specType.getAttributeDefinitions().values()) { + String name = AttributeValueReader.key(definition); + AttributeValue value = undeclared.remove(definition.getID()); + if (value == null && !explicitNames.contains(name)) { + value = AttributeValueReader.defaultValue(definition); + } + if (value != null) { + effective.putIfAbsent(name, value); + } + } + } + for (AttributeValue value : undeclared.values()) { + effective.putIfAbsent(AttributeValueReader.key(value.getAttributeDefinitionType()), value); + } + return Collections.unmodifiableMap(effective); + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/HierarchyChildren.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/HierarchyChildren.java new file mode 100644 index 0000000..9785d47 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/HierarchyChildren.java @@ -0,0 +1,311 @@ +package de.uni_stuttgart.ils.reqif4j.specification; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The ordered children of a specification or a spec hierarchy node. Each + * child knows its parent, so a node is in one place only and cannot be put + * below itself. + * + * Two views of all nodes below are kept, as callers ask for them repeatedly: + * the flattened list, rebuilt after the structure below changed, and an + * index by IDENTIFIER, kept up to date as nodes come and go, so that looking + * nodes up while changing a large hierarchy stays cheap. + */ +final class HierarchyChildren { + + private final HierarchyParent owner; + private final List ordered = new ArrayList(); + private final Map byID = new HashMap(); + private List descendants; + private Map index; + /** whether two nodes below have the same IDENTIFIER (only read documents) */ + private boolean duplicates; + + HierarchyChildren(HierarchyParent owner) { + this.owner = owner; + } + + /** + * Adds the children of an owner that is being created; a child with an + * IDENTIFIER that is already there replaces the earlier one. + * + * @throws IllegalArgumentException if one of them is placed elsewhere; + * then none is added + */ + void putAll(List children) { + for (SpecHierarchy child : children) { + checkPlaceable(child, true); + } + for (SpecHierarchy child : children) { + put(child); + } + } + + /** + * Adds a child read from a document; one with an IDENTIFIER that is + * already there replaces the earlier one, which is reported. + * + * @param child may be null (a skipped node), then nothing happens + */ + void put(SpecHierarchy child, de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues issues) { + if (child == null) { + return; + } + if (this.byID.containsKey(child.getID())) { + issues.reportDuplicate("Spec hierarchy", child.getID()); + } + put(child); + } + + private void put(SpecHierarchy child) { + SpecHierarchy previous = this.byID.put(child.getID(), child); + if (previous == child) { + return; + } + if (previous != null) { + // in the place of the earlier one, as a map would keep it + this.ordered.set(position(previous), child); + previous.setParent(null); + removed(previous); + } else { + this.ordered.add(child); + } + child.setParent(this.owner); + added(child); + } + + /** + * @return the children in order (a copy) + */ + List list() { + return new ArrayList(this.ordered); + } + + /** + * @return the children in order (a live view, read-only) + */ + List values() { + return Collections.unmodifiableList(this.ordered); + } + + /** + * @param index position among the children, 0 for first; the number of + * children appends + * @throws IllegalArgumentException if the node is already placed somewhere, + * a child with the same IDENTIFIER exists, the node is the owner or + * above it, or the index is out of range + */ + void add(int index, SpecHierarchy child) { + + checkPlaceable(child, false); + if (this.byID.containsKey(child.getID())) { + throw new IllegalArgumentException("Spec hierarchy " + child.getID() + " is already a child here"); + } + if (index < 0 || index > this.ordered.size()) { + throw new IllegalArgumentException("Position " + index + " is outside 0.." + this.ordered.size()); + } + this.ordered.add(index, child); + this.byID.put(child.getID(), child); + child.setParent(this.owner); + added(child); + } + + /** + * Replaces the children by these, in this order, in one step. Former + * children that are not among them are removed with everything below them. + * + * @throws IllegalArgumentException if two have the same IDENTIFIER, one is + * placed elsewhere, or one is the owner or above it; then nothing + * changes + */ + void replace(List children) { + + Set ids = new HashSet(); + for (SpecHierarchy child : children) { + checkPlaceable(child, true); + if (!ids.add(child.getID())) { + throw new IllegalArgumentException("Spec hierarchy " + child.getID() + " is given twice"); + } + } + Set kept = Collections.newSetFromMap(new IdentityHashMap()); + kept.addAll(children); + for (SpecHierarchy former : this.ordered) { + if (!kept.contains(former)) { + former.setParent(null); + } + } + this.ordered.clear(); + this.byID.clear(); + for (SpecHierarchy child : children) { + this.ordered.add(child); + this.byID.put(child.getID(), child); + child.setParent(this.owner); + } + // many nodes at once: the views are rebuilt when needed + for (HierarchyChildren level = this; level != null; level = level.above()) { + level.descendants = null; + level.index = null; + } + } + + /** + * @param ownChildToo whether a node that is already a child here may be + * placed again (in a replacement of all children) + * @throws IllegalArgumentException if the node has no IDENTIFIER, is + * placed elsewhere, or is the owner or above it + */ + private void checkPlaceable(SpecHierarchy child, boolean ownChildToo) { + if (child == null || child.getID() == null) { + throw new IllegalArgumentException("A spec hierarchy node needs an identifier"); + } + if (child.getParent() != null && !(ownChildToo && child.getParent() == this.owner)) { + throw new IllegalArgumentException("Spec hierarchy " + child.getID() + " is already placed below " + + child.getParent().getID() + "; remove or move it instead"); + } + for (HierarchyParent ancestor = this.owner; ancestor != null; ancestor = parentOf(ancestor)) { + if (ancestor == child) { + throw new IllegalArgumentException("Spec hierarchy " + child.getID() + " cannot be placed below itself"); + } + } + } + + private static HierarchyParent parentOf(HierarchyParent node) { + return node instanceof SpecHierarchy ? ((SpecHierarchy) node).getParent() : null; + } + + SpecHierarchy remove(String id) { + SpecHierarchy removed = this.byID.remove(id); + if (removed != null) { + this.ordered.remove(position(removed)); + removed.setParent(null); + removed(removed); + } + return removed; + } + + /** + * @return the position of the child, or -1 + */ + int indexOf(String id) { + SpecHierarchy child = this.byID.get(id); + return child == null ? -1 : position(child); + } + + private int position(SpecHierarchy child) { + for (int i = 0; i < this.ordered.size(); i++) { + if (this.ordered.get(i) == child) { + return i; + } + } + return -1; + } + + int size() { + return this.ordered.size(); + } + + SpecHierarchy get(String id) { + return this.byID.get(id); + } + + /** + * @return all nodes below, depth-first in document order (read-only) + */ + List descendants() { + if (this.descendants == null) { + List all = new ArrayList(); + for (SpecHierarchy child : this.ordered) { + all.add(child); + all.addAll(child.getAllChildren()); + } + this.descendants = Collections.unmodifiableList(all); + } + return this.descendants; + } + + /** + * @return the node below with this IDENTIFIER; for an identifier used + * twice the first in document order + */ + SpecHierarchy descendant(String id) { + if (this.index == null) { + this.index = new HashMap(); + this.duplicates = false; + for (SpecHierarchy node : descendants()) { + if (this.index.putIfAbsent(node.getID(), node) != null) { + this.duplicates = true; + } + } + } + return this.index.get(id); + } + + /** + * Updates the views here and above after a node, with everything below + * it, was added. + */ + private void added(SpecHierarchy node) { + List subtree = null; + for (HierarchyChildren level = this; level != null; level = level.above()) { + level.descendants = null; + if (level.index != null) { + subtree = subtree != null ? subtree : subtree(node); + for (SpecHierarchy added : subtree) { + if (level.index.containsKey(added.getID())) { + // which of the two comes first in document order is not + // known here: the index starts over + level.index = null; + break; + } + level.index.put(added.getID(), added); + } + } + } + } + + /** + * Updates the views here and above after a node, with everything below + * it, was removed. + */ + private void removed(SpecHierarchy node) { + List subtree = null; + for (HierarchyChildren level = this; level != null; level = level.above()) { + level.descendants = null; + if (level.index != null && level.duplicates) { + // another node may have the identifier: start over + level.index = null; + } else if (level.index != null) { + subtree = subtree != null ? subtree : subtree(node); + for (SpecHierarchy removed : subtree) { + level.index.remove(removed.getID(), removed); + } + } + } + } + + private static List subtree(SpecHierarchy node) { + List subtree = new ArrayList(); + subtree.add(node); + subtree.addAll(node.getAllChildren()); + return subtree; + } + + /** + * @return the children of the owner's parent, or null at the top + */ + private HierarchyChildren above() { + HierarchyParent parent = parentOf(this.owner); + if (parent instanceof SpecHierarchy) { + return ((SpecHierarchy) parent).hierarchyChildren(); + } + return parent instanceof Specification ? ((Specification) parent).hierarchyChildren() : null; + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/HierarchyParent.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/HierarchyParent.java new file mode 100644 index 0000000..245f90f --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/HierarchyParent.java @@ -0,0 +1,64 @@ +package de.uni_stuttgart.ils.reqif4j.specification; + +import java.util.List; + +/** + * What holds spec hierarchy nodes: a specification (the top level) or a + * hierarchy node (the levels below). + */ +public interface HierarchyParent { + + /** + * @return the IDENTIFIER of this specification or hierarchy node + */ + String getID(); + + /** + * @return the direct children in document order (a copy) + */ + List getChildren(); + + /** + * @return the number of direct children + */ + default int getChildCount() { + return getChildren().size(); + } + + /** + * @return the direct child with this IDENTIFIER, or null + */ + default SpecHierarchy getChild(String specHierarchyID) { + for (SpecHierarchy child : getChildren()) { + if (child.getID() != null && child.getID().equals(specHierarchyID)) { + return child; + } + } + return null; + } + + /** + * Inserts a child node at a position, 0 for first. + * + * @throws IllegalArgumentException if a child with the same IDENTIFIER + * exists or the position is out of range + */ + HierarchyParent addChild(int index, SpecHierarchy child); + + /** + * Replaces the children by these, in this order, in one step: to reorder + * them, or to add and remove several at once. Former children that are + * not among them are removed with everything below them. + * + * @throws IllegalArgumentException if two have the same IDENTIFIER, one is + * placed elsewhere, or one is this node or above it + */ + HierarchyParent setChildren(List children); + + /** + * Removes a direct child node, with everything below it. + * + * @return the removed node, or null if there is no such child + */ + SpecHierarchy removeChild(String specHierarchyID); +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/LongNameTypeClassifier.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/LongNameTypeClassifier.java index fa012f0..c52063c 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/LongNameTypeClassifier.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/LongNameTypeClassifier.java @@ -45,7 +45,7 @@ public boolean isRequirement(SpecObject specObject) { for (AttributeValue attributeValue : specObject.getAttributes().values()) { if (isBoolean(attributeValue) - && attributeValue.getName().toLowerCase().contains(ReqIFConst.REQ.toLowerCase())) { + && lowerCaseName(attributeValue).contains(ReqIFConst.REQ.toLowerCase())) { return Boolean.TRUE.equals(attributeValue.getValue()); } } @@ -61,8 +61,8 @@ public boolean isSubRequirement(SpecObject specObject) { for (AttributeValue attributeValue : specObject.getAttributes().values()) { if (isBoolean(attributeValue) - && attributeValue.getName().toLowerCase().contains(ReqIFConst.SUB.toLowerCase()) - && attributeValue.getName().toLowerCase().contains(ReqIFConst.REQ.toLowerCase())) { + && lowerCaseName(attributeValue).contains(ReqIFConst.SUB.toLowerCase()) + && lowerCaseName(attributeValue).contains(ReqIFConst.REQ.toLowerCase())) { return Boolean.TRUE.equals(attributeValue.getValue()); } } @@ -74,4 +74,11 @@ private static boolean isBoolean(AttributeValue attributeValue) { && attributeValue.getAttributeDefinitionType().getDataType() != null && ReqIFConst.BOOLEAN.equals(attributeValue.getAttributeDefinitionType().getDataType().getType()); } + + /** + * LONG-NAME is optional in ReqIF, so an attribute may have no name. + */ + private static String lowerCaseName(AttributeValue attributeValue) { + return attributeValue.getName() == null ? "" : attributeValue.getName().toLowerCase(); + } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroup.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroup.java index 777fc1b..b9303f6 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroup.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroup.java @@ -7,6 +7,7 @@ import org.w3c.dom.Element; import org.w3c.dom.Node; +import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; @@ -18,32 +19,14 @@ * between a source and a target specification, for example a traceability view * between a system and a software specification. */ -public class RelationGroup { +public class RelationGroup extends Identifiable { - private String id; - private String name; - private String alternativeID; private String relationGroupTypeRef; private String sourceSpecificationRef; private String targetSpecificationRef; private final List specRelationRefs = new ArrayList(); - public String getID() { - return this.id; - } - - public String getName() { - return this.name; - } - - /** - * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null - */ - public String getAlternativeID() { - return this.alternativeID; - } - /** * @return the IDENTIFIER of the RELATION-GROUP-TYPE, or null */ @@ -72,6 +55,40 @@ public List getSpecRelationRefs() { return Collections.unmodifiableList(this.specRelationRefs); } + public RelationGroup setSourceSpecificationRef(String sourceSpecificationRef) { + this.sourceSpecificationRef = sourceSpecificationRef; + markModified(); + return this; + } + + public RelationGroup setTargetSpecificationRef(String targetSpecificationRef) { + this.targetSpecificationRef = targetSpecificationRef; + markModified(); + return this; + } + + /** + * Adds a relation to the group, unless it already belongs to it. + */ + public RelationGroup addSpecRelationRef(String specRelationID) { + if (!this.specRelationRefs.contains(specRelationID)) { + this.specRelationRefs.add(specRelationID); + markModified(); + } + return this; + } + + /** + * @return whether the relation belonged to the group + */ + public boolean removeSpecRelationRef(String specRelationID) { + boolean removed = this.specRelationRefs.remove(specRelationID); + if (removed) { + markModified(); + } + return removed; + } + @@ -81,9 +98,8 @@ public List getSpecRelationRefs() { */ public RelationGroup(String id, String name, String relationGroupTypeRef, String sourceSpecificationRef, String targetSpecificationRef, List specRelationRefs) { + super(id, name); - this.id = id; - this.name = name; this.relationGroupTypeRef = relationGroupTypeRef; this.sourceSpecificationRef = sourceSpecificationRef; this.targetSpecificationRef = targetSpecificationRef; @@ -93,10 +109,8 @@ public RelationGroup(String id, String name, String relationGroupTypeRef, } public RelationGroup(Node relationGroup) { + super(relationGroup); - this.id = XmlUtils.attribute(relationGroup, ReqIFConst.IDENTIFIER); - this.name = XmlUtils.attribute(relationGroup, ReqIFConst.LONG_NAME); - this.alternativeID = XmlUtils.alternativeID(relationGroup); this.relationGroupTypeRef = refIn(relationGroup, ReqIFConst.TYPE, ReqIFConst.RELATION_GROUP_TYPE_REF); this.sourceSpecificationRef = refIn(relationGroup, ReqIFConst.SOURCE_SPECIFICATION, ReqIFConst.SPECIFICATION_REF); diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroupType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroupType.java index a87a529..9095d16 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroupType.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/RelationGroupType.java @@ -5,13 +5,18 @@ import org.w3c.dom.Node; import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; public class RelationGroupType extends SpecType { public RelationGroupType(Node specType, Map dataTypes) { - super(specType, dataTypes); + this(specType, dataTypes, ParseIssues.collecting()); + } + + public RelationGroupType(Node specType, Map dataTypes, ParseIssues issues) { + super(specType, dataTypes, issues); this.type = ReqIFConst.RELATION_GROUP_TYPE; } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecElementWithAttributes.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecElementWithAttributes.java new file mode 100644 index 0000000..38bdcb6 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecElementWithAttributes.java @@ -0,0 +1,207 @@ +package de.uni_stuttgart.ils.reqif4j.specification; + +import java.util.List; +import java.util.Map; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueFactory; +import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; + +/** + * An element that carries attribute values: spec objects, spec relations and + * specifications, as in the ReqIF metamodel. + * + * Values are available in two views. {@link #getValues()} holds what the + * element itself carries - exactly what the document contains and what is + * written back. {@link #getAttributes()} adds the defaults of all attribute + * definitions the element has no value for, keyed by LONG-NAME for + * convenient lookup. + */ +public abstract class SpecElementWithAttributes extends Identifiable { + + private AttributeValues values = new AttributeValues(null); + + + protected SpecElementWithAttributes(String id, String name) { + super(id, name); + } + + protected SpecElementWithAttributes(Node element) { + super(element); + } + + + /** + * @return the values this element carries itself, by IDENTIFIER of their + * attribute definition, in document order (read-only). These are + * the values that are written; definition defaults are not + * included. + */ + public Map getValues() { + return this.values.explicit(); + } + + /** + * @return the value this element carries for the attribute definition, or + * null if it has none (the definition's default then applies) + */ + public AttributeValue getValue(String attributeDefinitionID) { + return this.values.explicit().get(attributeDefinitionID); + } + + /** + * Sets a value of this element, replacing the one it has for the same + * attribute definition. + */ + public SpecElementWithAttributes setValue(AttributeValue value) { + if (value == null || value.getAttributeDefinitionType() == null) { + throw new IllegalArgumentException("A value needs an attribute definition"); + } + this.values.put(value); + markModified(); + valuesChanged(); + return this; + } + + /** + * Sets a scalar value - boolean, integer, real, string or date - of the + * attribute definition with this IDENTIFIER, from e.g. a Boolean, a + * Number or a String. + * + * @throws IllegalArgumentException if the element's spec type has no such + * attribute definition, or it is an enumeration or XHTML one + */ + public SpecElementWithAttributes set(String attributeDefinitionID, Object value) { + return setValue(AttributeValueFactory.of(definition(attributeDefinitionID), value)); + } + + /** + * Sets an XHTML value from markup, with or without a surrounding div. + */ + public SpecElementWithAttributes setXhtml(String attributeDefinitionID, String markup) { + return setValue(AttributeValueFactory.xhtml(definition(attributeDefinitionID), markup)); + } + + /** + * Sets an XHTML value that is a simplified version of the complete + * content, e.g. the preview image of an embedded OLE object. + * + * @param originalMarkup the complete content (THE-ORIGINAL-VALUE) + */ + public SpecElementWithAttributes setXhtml(String attributeDefinitionID, String markup, String originalMarkup) { + return setValue(AttributeValueFactory.xhtml(definition(attributeDefinitionID), markup, originalMarkup)); + } + + /** + * Selects enum values by their IDENTIFIER; none for an empty selection. + */ + public SpecElementWithAttributes setEnum(String attributeDefinitionID, String... enumValueIDs) { + return setValue(AttributeValueFactory.enumeration(definition(attributeDefinitionID), + enumValueIDs == null ? null : java.util.Arrays.asList(enumValueIDs))); + } + + /** + * Removes the element's own value for the attribute definition, so its + * default applies again. + * + * @return the removed value, or null if the element had none + */ + public AttributeValue removeValue(String attributeDefinitionID) { + AttributeValue removed = this.values.remove(attributeDefinitionID); + if (removed != null) { + markModified(); + valuesChanged(); + } + return removed; + } + + /** + * @return the element's spec type, or null if its type reference could + * not be resolved + */ + public SpecType getResolvedSpecType() { + return this.values.specType(); + } + + private AttributeDefinition definition(String attributeDefinitionID) { + SpecType specType = this.values.specType(); + AttributeDefinition definition = specType == null ? null : specType.getAttributeDefinition(attributeDefinitionID); + if (definition == null) { + throw new IllegalArgumentException((specType == null ? "The type of " + getID() + " is unknown" + : "Spec type " + specType.getID() + " has no attribute definition " + attributeDefinitionID)); + } + return definition; + } + + /** + * @return the effective values by attribute LONG-NAME: the element's own + * values plus the defaults of all other attribute definitions of + * its type (read-only). A convenience view: LONG-NAMEs need not be + * unique, so prefer {@link #getValues()} to see what the document + * contains. + */ + public Map getAttributes() { + return this.values.effectiveByName(); + } + + /** + * @return the effective value of the attribute with this LONG-NAME, or null + */ + public Object getAttribute(String attributeName) { + AttributeValue attributeValue = getAttributes().get(attributeName); + return attributeValue == null ? null : attributeValue.getValue(); + } + + + /** + * Starts a new set of values for the given spec type, e.g. before reading + * them from a document. + */ + AttributeValues resetValues(SpecType specType) { + this.values = new AttributeValues(specType); + return this.values; + } + + /** + * Called after a value was set or removed. + */ + protected void valuesChanged() { + } + + /** + * Reads the element's values. Without a resolved spec type there are no + * attribute definitions to interpret them by; that is reported once, + * together with the number of values skipped. + * + * @param typeRef the unresolved type reference, for the report + */ + void readValues(Node element, SpecType specType, String typeRef, ParseIssues issues) { + + AttributeValues values = resetValues(specType); + if (specType != null) { + AttributeValueReader.read(element, specType, values, issues); + return; + } + String problem = XmlUtils.localName(element) + + (typeRef == null ? " has no type reference" : " references type " + typeRef + ", which is not declared"); + int count = AttributeValueReader.readUninterpreted(element, problem, values, issues); + issues.report(getID(), problem + + (count == 0 ? "" : "; its " + count + " value" + (count == 1 ? " is" : "s are") + " kept as written")); + } + + /** + * @return the IDENTIFIER in TYPE/{refName}, or null + */ + static String typeRef(Node element, String refName) { + Element type = XmlUtils.firstChildElementByLocalName(element, ReqIFConst.TYPE); + Element ref = XmlUtils.firstChildElementByLocalName(type, refName); + return ref == null ? null : ref.getTextContent().trim(); + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java index 220ed87..5fa71fa 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecHierarchy.java @@ -1,7 +1,6 @@ package de.uni_stuttgart.ils.reqif4j.specification; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -9,40 +8,124 @@ import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML; +import de.uni_stuttgart.ils.reqif4j.attributes.XHTML; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTMLElementList; +import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLNode; import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLElementDiv; -public class SpecHierarchy { +public class SpecHierarchy extends Identifiable implements HierarchyParent { - private String specHierarchyID; - private String alternativeID; private int hierarchyLvl; private int section; private SpecObject specObject; - private Map children = new LinkedHashMap(); + private String specObjectRef; + private Boolean tableInternal; + private Boolean editable; + private final List> editableAttributes = new ArrayList>(); + private final HierarchyChildren children = new HierarchyChildren(this); + private HierarchyParent parent; + /** + * @return the IDENTIFIER of this hierarchy node, same as {@link #getID()} + */ public String getSpecHierarchyID() { - return this.specHierarchyID; + return getID(); + } + + /** + * @return the IDENTIFIER of the referenced spec object; also when the + * reference could not be resolved, null if there is none + */ + public String getSpecObjectID(){ + return this.specObject != null ? this.specObject.getID() : this.specObjectRef; } /** - * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null + * @return IS-TABLE-INTERNAL: whether this node is part of a table in the + * tool's view, or null if the document does not say */ - public String getAlternativeID() { - return this.alternativeID; + public Boolean getTableInternal() { + return this.tableInternal; } - - public String getSpecObjectID(){ - return this.specObject.getID(); + + public SpecHierarchy setTableInternal(Boolean tableInternal) { + this.tableInternal = tableInternal; + markModified(); + return this; + } + + /** + * @return IS-EDITABLE: whether the referenced object may be edited, or + * null if the document does not say + */ + public Boolean getEditable() { + return this.editable; + } + + public SpecHierarchy setEditable(Boolean editable) { + this.editable = editable; + markModified(); + return this; + } + + /** + * @return the IDENTIFIERs of the attribute definitions listed in + * EDITABLE-ATTS (editable on this node), in document order + */ + public List getEditableAttributeIDs() { + List ids = new ArrayList(); + for(Map.Entry ref: this.editableAttributes) { + ids.add(ref.getKey()); + } + return ids; + } + + /** + * @return the EDITABLE-ATTS references as read, in document order: + * attribute definition IDENTIFIER and the reference element name, + * e.g. ATTRIBUTE-DEFINITION-STRING-REF (read-only) + */ + public List> getEditableAttributeRefs() { + return java.util.Collections.unmodifiableList(this.editableAttributes); + } + + /** + * Sets the attributes of the node's object that are editable on this node + * (EDITABLE-ATTS); none or null to remove the list. + * + * @throws IllegalArgumentException if a definition is null, of a datatype + * kind that cannot be written, or not an attribute of the spec + * type of the node's object + */ + public SpecHierarchy setEditableAttributes(List attributeDefinitions) { + List> refs = new ArrayList>(); + SpecType objectType = this.specObject == null ? null : this.specObject.getResolvedSpecType(); + for(AttributeDefinition definition: attributeDefinitions == null ? List.of() : attributeDefinitions) { + if(definition == null || definition.getElementName() == null) { + throw new IllegalArgumentException("Editable attributes need attribute definitions of a known kind"); + } + if(objectType != null && objectType.getAttributeDefinition(definition.getID()) != definition) { + throw new IllegalArgumentException("Attribute definition " + definition.getID() + + " is not an attribute of spec type " + objectType.getID() + " of the node's object"); + } + refs.add(new java.util.AbstractMap.SimpleImmutableEntry( + definition.getID(), definition.getElementName() + "-REF")); + } + this.editableAttributes.clear(); + this.editableAttributes.addAll(refs); + markModified(); + return this; } public int getHierarchyLvl() { @@ -65,30 +148,76 @@ public Object getAttribute(String attributeName) { return (Object)this.specObject.getAttribute(attributeName); } + /** + * @return the token list of the object's first XHTML value - its own values + * before defaults, so another attribute's default never stands in + * for the object's text - or null if it has none + */ public AttributeValueXHTMLElementList getXHTMLContent() { - for(AttributeValue attributeValue: this.specObject.getAttributes().values()) { - // getValue() returns the rendered string, so the element list has - // to be fetched via its dedicated getter (the former cast threw - // ClassCastException on every call). - if(attributeValue instanceof AttributeValueXHTML) { - return ((AttributeValueXHTML)attributeValue).getElementList(); + AttributeValueXHTML xhtml = firstXhtmlValue(); + return xhtml == null ? null : xhtml.getElementList(); + } + + /** + * @return the block elements of the object's first XHTML value (see + * {@link #getXHTMLContent()}), or null if it has none or no content + */ + public List getXHTMLDivContent() { + AttributeValueXHTML xhtml = firstXhtmlValue(); + return xhtml == null || xhtml.getDivValue() == null ? null : xhtml.getBlocks(); + } + + /** + * @return the object's first XHTML value, in the order of its spec type's + * attribute definitions: one of its own values if it has any - one + * with content first - otherwise a default; null if there is none + * or the node's object is unknown + */ + private AttributeValueXHTML firstXhtmlValue() { + if(this.specObject == null) { + return null; + } + Map own = this.specObject.getValues(); + List ordered = new ArrayList(); + SpecType type = this.specObject.getResolvedSpecType(); + if(type != null) { + for(String definitionID: type.getAttributeDefinitions().keySet()) { + if(own.containsKey(definitionID)) { + ordered.add(own.get(definitionID)); + } + } + } + for(AttributeValue value: own.values()) { + if(!ordered.contains(value)) { + ordered.add(value); } } - return null; + AttributeValueXHTML first = firstXhtml(ordered); + // the defaults only matter if the object has no XHTML value of its own + return first != null ? first : firstXhtml(this.specObject.getAttributes().values()); } - - public List getXHTMLDivContent() { - for(AttributeValue attributeValue: this.specObject.getAttributes().values()) { - if(attributeValue instanceof AttributeValueXHTML) { - XHTMLElementDiv value = ((AttributeValueXHTML)attributeValue).getDivValue(); - if(value != null) { - return value.getChildren(); + + /** + * @return the first XHTML value with content, failing that the first + * without, as earlier versions returned an empty token list rather + * than null for it + */ + private static AttributeValueXHTML firstXhtml(java.util.Collection values) { + AttributeValueXHTML withoutContent = null; + for(AttributeValue value: values) { + if(value instanceof AttributeValueXHTML) { + AttributeValueXHTML xhtml = (AttributeValueXHTML) value; + if(xhtml.getDivValue() != null) { + return xhtml; + } + if(withoutContent == null) { + withoutContent = xhtml; } } } - return null; + return withoutContent; } - + public String getSpecType() { return this.specObject.getSpecType(); } @@ -141,21 +270,116 @@ public boolean isText() { } } + /** + * @return the direct children in document order (a copy) + */ + @Override public List getChildren() { - return new ArrayList(this.children.values()); + return this.children.list(); } - + + /** + * @return all nodes below this one, depth-first in document order + */ public List getAllChildren() { - - List allChildren = new ArrayList(); - for(SpecHierarchy specHierarchy: this.children.values()) { - allChildren.add(specHierarchy); - for(SpecHierarchy child: specHierarchy.getAllChildren()) { - allChildren.add(child); - } + return this.children.descendants(); + } + + /** + * Appends a child node. + * + * @throws IllegalArgumentException see {@link #addChild(int, SpecHierarchy)} + */ + public SpecHierarchy addChild(SpecHierarchy child) { + return addChild(this.children.size(), child); + } + + /** + * Inserts a child node at a position, 0 for first. + * + * @throws IllegalArgumentException if the node is already placed somewhere + * (move it with {@link de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent#moveSpecHierarchy}), + * a child with the same IDENTIFIER exists, the node is this one or + * above it, or the position is out of range + */ + @Override + public SpecHierarchy addChild(int index, SpecHierarchy child) { + this.children.add(index, child); + child.place(this.hierarchyLvl + 1, this.section); + markModified(); + return this; + } + + @Override + public int getChildCount() { + return this.children.size(); + } + + @Override + public SpecHierarchy getChild(String specHierarchyID) { + return this.children.get(specHierarchyID); + } + + @Override + public SpecHierarchy setChildren(List children) { + this.children.replace(children); + for(SpecHierarchy child: children) { + child.place(this.hierarchyLvl + 1, this.section); + } + markModified(); + return this; + } + + /** + * Removes a direct child node, with everything below it. + * + * @return the removed node, or null if there is no such child + */ + @Override + public SpecHierarchy removeChild(String specHierarchyID) { + SpecHierarchy removed = this.children.remove(specHierarchyID); + if(removed != null) { + markModified(); + } + return removed; + } + + /** + * @return the specification or hierarchy node this node is placed below, + * or null if it is not placed + */ + public HierarchyParent getParent() { + return this.parent; + } + + void setParent(HierarchyParent parent) { + this.parent = parent; + } + + HierarchyChildren hierarchyChildren() { + return this.children; + } + + /** + * Points this node at another spec object. + */ + public SpecHierarchy setSpecObject(SpecObject specObject) { + this.specObject = specObject; + this.specObjectRef = specObject == null ? null : specObject.getID(); + markModified(); + return this; + } + + /** + * Sets level and section of this node and everything below it, after it + * was inserted somewhere. + */ + void place(int hierarchyLvl, int section) { + this.hierarchyLvl = hierarchyLvl; + this.section = section; + for(SpecHierarchy child: this.children.values()) { + child.place(hierarchyLvl + 1, section); } - - return allChildren; } @@ -167,42 +391,54 @@ public List getAllChildren() { */ public SpecHierarchy(String specHierarchyID, int hierarchyLvl, int section, SpecObject specObject, List children) { + super(specHierarchyID, null); - this.specHierarchyID = specHierarchyID; this.hierarchyLvl = hierarchyLvl; this.section = section; this.specObject = specObject; if(children != null) { - for(SpecHierarchy child: children) { - this.children.put(child.getSpecHierarchyID(), child); - } + this.children.putAll(children); } } public SpecHierarchy(int hierarchyLvl, int section, Node specHierarchy, Map specObjects) { + this(hierarchyLvl, section, specHierarchy, specObjects, ParseIssues.collecting()); + } + + /** + * @param issues receives nested hierarchy nodes that cannot be read or are + * declared twice + */ + public SpecHierarchy(int hierarchyLvl, int section, Node specHierarchy, Map specObjects, + ParseIssues issues) { + super(specHierarchy); this.hierarchyLvl = hierarchyLvl; this.section = section; - this.specHierarchyID = specHierarchy.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent(); - this.alternativeID = XmlUtils.alternativeID(specHierarchy); Element object = XmlUtils.firstChildElementByLocalName(specHierarchy, ReqIFConst.OBJECT); Element specObjectRef = XmlUtils.firstDescendantByLocalName(object, ReqIFConst.SPEC_OBJECT_REF); if(specObjectRef != null) { - this.specObject = specObjects.get(specObjectRef.getTextContent().trim()); + // the reference is kept even if it cannot be resolved, so it is not + // lost on writing and the validator can report it + this.specObjectRef = specObjectRef.getTextContent().trim(); + this.specObject = specObjects.get(this.specObjectRef); } - Element childrenElement = XmlUtils.firstChildElementByLocalName(specHierarchy, ReqIFConst.CHILDREN); - if(childrenElement != null) { - - for(Element newSpecHierarchy: XmlUtils.childElements(childrenElement)) { - - String specHierarchyID = XmlUtils.attribute(newSpecHierarchy, ReqIFConst.IDENTIFIER); + this.tableInternal = XmlUtils.booleanAttribute(specHierarchy, ReqIFConst.IS_TABLE_INTERNAL); + this.editable = XmlUtils.booleanAttribute(specHierarchy, ReqIFConst.IS_EDITABLE); + Element editableAtts = XmlUtils.firstChildElementByLocalName(specHierarchy, ReqIFConst.EDITABLE_ATTS); + for(Element ref: XmlUtils.childElements(editableAtts)) { + this.editableAttributes.add(new java.util.AbstractMap.SimpleImmutableEntry( + ref.getTextContent().trim(), XmlUtils.localName(ref))); + } - this.children.put(specHierarchyID, new SpecHierarchy(this.hierarchyLvl+1, section, newSpecHierarchy, specObjects)); - } + Element childrenElement = XmlUtils.firstChildElementByLocalName(specHierarchy, ReqIFConst.CHILDREN); + for(Element newSpecHierarchy: XmlUtils.childElements(childrenElement)) { + SpecHierarchy child = issues.readOrSkip("spec hierarchy", + () -> new SpecHierarchy(this.hierarchyLvl+1, section, newSpecHierarchy, specObjects, issues)); + this.children.put(child, issues); } } - } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java index 0273e0a..670cfab 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObject.java @@ -1,72 +1,52 @@ package de.uni_stuttgart.ils.reqif4j.specification; -import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import de.uni_stuttgart.ils.reqif4j.attributes.*; -import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; -import org.w3c.dom.Element; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue; import org.w3c.dom.Node; -import org.w3c.dom.NodeList; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; -import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; -public class SpecObject { +public class SpecObject extends SpecElementWithAttributes { - protected String id; protected SpecType specType; + private String specTypeRef; protected String type; - protected String alternativeID; protected TypeClassifier typeClassifier = TypeClassifier.defaultClassifier(); - protected Map attributeValues = new HashMap(); - public String getID() { - return this.id; - } - - /** - * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null - */ - public String getAlternativeID() { - return this.alternativeID; - } - public String getType() { return this.type; } + /** + * @return the kind of the spec type (e.g. SPEC-OBJECT-TYPE), or null if + * the type reference could not be resolved + */ public String getSpecType() { - return this.specType.getType(); + return this.specType == null ? null : this.specType.getType(); } + /** + * @return the LONG-NAME of the spec type, or null if the type reference + * could not be resolved + */ public String getSpecTypeName() { - return this.specType.getName(); + return this.specType == null ? null : this.specType.getName(); } /** - * @return the IDENTIFIER of the spec type this object refers to, or null if - * the reference could not be resolved + * @return the IDENTIFIER of the spec type this object refers to - also if + * the reference could not be resolved - or null if it has none */ public String getSpecTypeID() { - return this.specType == null ? null : this.specType.getID(); + return this.specType != null ? this.specType.getID() : this.specTypeRef; } - public Map getAttributes() { - return this.attributeValues; - } - - public Object getAttribute(String attributeName) { - AttributeValue attributeValue = this.attributeValues.get(attributeName); - return attributeValue == null ? null : attributeValue.getValue(); - } public boolean isReq() { return this.typeClassifier.isRequirement(this); @@ -96,8 +76,7 @@ public boolean isText() { public SpecObject(Node specObject){ - this.id = specObject.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent(); - this.alternativeID = XmlUtils.alternativeID(specObject); + super(specObject); } /** @@ -107,14 +86,15 @@ public SpecObject(Node specObject){ */ public SpecObject(String id, SpecType specType, Collection attributeValues, TypeClassifier typeClassifier) { + super(id, null); - this.id = id; this.specType = specType; this.typeClassifier = typeClassifier == null ? TypeClassifier.defaultClassifier() : typeClassifier; + AttributeValues values = resetValues(specType); if(attributeValues != null) { for(AttributeValue attributeValue: attributeValues) { - this.attributeValues.put(attributeValue.getName(), attributeValue); + values.put(attributeValue); } } this.type = this.typeClassifier.classify(this); @@ -129,13 +109,23 @@ public SpecObject(Node specObject, SpecType specType) { } public SpecObject(Node specObject, SpecType specType, TypeClassifier typeClassifier) { + this(specObject, specType, typeClassifier, ParseIssues.collecting()); + } + + /** + * @param specType the resolved spec type; if null, the object is kept with + * its unresolved type reference, but its values cannot be + * interpreted and are reported and skipped + * @param issues receives the problems found + */ + public SpecObject(Node specObject, SpecType specType, TypeClassifier typeClassifier, ParseIssues issues) { + super(specObject); - this.id = specObject.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent(); - this.alternativeID = XmlUtils.alternativeID(specObject); this.specType = specType; + this.specTypeRef = typeRef(specObject, ReqIFConst.SPEC_OBJECT_TYPE_REF); this.typeClassifier = typeClassifier == null ? TypeClassifier.defaultClassifier() : typeClassifier; - readAttributeValues(specObject, specType); + readAttributeValues(specObject, specType, issues); // Classify only after all attribute values are available, so an // attribute-based classifier (e.g. the ReqIF Implementation Guide @@ -144,146 +134,28 @@ public SpecObject(Node specObject, SpecType specType, TypeClassifier typeClassif } /** - * Reads all attribute values of the given node and fills in default values - * for attribute definitions that carry no explicit value. Shared by + * The content category may depend on the values (attribute-based + * classifiers), so it is derived again. + */ + @Override + protected void valuesChanged() { + this.type = this.typeClassifier.classify(this); + } + + /** + * Reads the attribute values of the given node. Shared by * {@link SpecObject} and {@link SpecRelation}. */ protected void readAttributeValues(Node specObject, SpecType specType) { + readAttributeValues(specObject, specType, ParseIssues.collecting()); + } - // VALUES is located by local name so prefixed ReqIF namespaces work too; - // only this object's own VALUES is taken, not a nested one. - Element valuesElement = XmlUtils.firstChildElementByLocalName(specObject, ReqIFConst.VALUES); - if(valuesElement != null) { - - for(Element attribute: XmlUtils.childElements(valuesElement)) { - - String attValNodeName = XmlUtils.localName(attribute); - { - - String attributeDefinitionRef = XmlUtils.firstChildElement( - XmlUtils.firstChildElementByLocalName(attribute, ReqIFConst.DEFINITION)).getTextContent().trim(); - String attributeDefinitionName = specType.getAttributeDefinitions().get(attributeDefinitionRef).getName(); - String attributeValue; - AttributeDefinition attributeDefinition = specType.getAttributeDefinition(attributeDefinitionRef); - - switch(attValNodeName.substring(attValNodeName.lastIndexOf("-")+1)) { - - case ReqIFConst.BOOLEAN: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { - attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); - }else{ - attributeValue = ""; - } - this.attributeValues.put(attributeDefinitionName, new AttributeValueBoolean(attributeValue, attributeDefinition)); - break; - - case ReqIFConst.INTEGER: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { - attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); - }else{ - attributeValue = ""; - } - this.attributeValues.put(attributeDefinitionName, new AttributeValueInteger(attributeValue, attributeDefinition)); - break; - - case ReqIFConst.STRING: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { - attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); - }else{ - attributeValue = ""; - } - this.attributeValues.put(attributeDefinitionName, new AttributeValueString(attributeValue, attributeDefinition)); - break; - - case ReqIFConst.ENUMERATION: // Multiselect: read ALL ENUM-VALUE-REF children, not just the first - List enumRefs = new ArrayList(); - List enumNames = new ArrayList(); - for(Element ref : XmlUtils.descendantsByLocalName(attribute, ReqIFConst.ENUM_VALUE_REF)) { - String refID = ref.getTextContent().trim(); - enumRefs.add(refID); - enumNames.add(specType.getEnumValueName(refID)); - } - this.attributeValues.put(attributeDefinitionName, new AttributeValueEnumeration(enumNames, enumRefs, attributeDefinition)); - break; - - case ReqIFConst.XHTML: this.attributeValues.put(attributeDefinitionName, new AttributeValueXHTML(attribute, attributeDefinition)); - break; - - case ReqIFConst.DATE: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { - attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); - }else{ - attributeValue = ""; - } - this.attributeValues.put(attributeDefinitionName, new AttributeValueDate(attributeValue, attributeDefinition)); - break; - - case ReqIFConst.REAL: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { - attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); - }else{ - attributeValue = ""; - } - this.attributeValues.put(attributeDefinitionName, new AttributeValueDouble(attributeValue, attributeDefinition)); - break; - - // Values of datatype kinds the parser does not model are - // kept generically instead of being dropped, so they - // survive a round trip. - default: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { - attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); - }else{ - attributeValue = ""; - } - this.attributeValues.put(attributeDefinitionName, - new AttributeValue(attributeValue, attributeDefinition) - .setSourceElementName(XmlUtils.localName(attribute))); - break; - } - } - } - } - - if(this.attributeValues.size() < specType.getAttributeDefinitions().size()) { - - for(AttributeDefinition attributeDefinition: specType.getAttributeDefinitions().values()) { - - if(!this.attributeValues.containsKey(attributeDefinition.getName())) { - - Datatype datatype = attributeDefinition.getDataType(); - - if(datatype == null) { - throw new ExceptionSpecObject("Attribute definition not existing in ReqIF parser\n", attributeDefinition); - } - - switch(attributeDefinition.getDataType().getType()) { - - case ReqIFConst.BOOLEAN: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueBoolean(attributeDefinition.getDefaultValue(), attributeDefinition)); - break; - - case ReqIFConst.INTEGER: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueInteger(attributeDefinition.getDefaultValue(), attributeDefinition)); - break; - - case ReqIFConst.STRING: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueString(attributeDefinition.getDefaultValue(), attributeDefinition)); - break; - - case ReqIFConst.ENUMERATION: if(attributeDefinition instanceof AttributeDefinitionEnumeration) { - AttributeDefinitionEnumeration enumDefinition = (AttributeDefinitionEnumeration) attributeDefinition; - this.attributeValues.put(attributeDefinition.getName(), new AttributeValueEnumeration(enumDefinition.getDefaultValues(), enumDefinition.getDefaultValueRefs(), attributeDefinition)); - }else{ - this.attributeValues.put(attributeDefinition.getName(), new AttributeValueEnumeration(attributeDefinition.getDefaultValue(), attributeDefinition)); - } - break; - - case ReqIFConst.XHTML: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueXHTML(attributeDefinition.getDefaultValue(), attributeDefinition)); - break; - - case ReqIFConst.DATE: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueDate(attributeDefinition.getDefaultValue(), attributeDefinition)); - break; - - case ReqIFConst.DOUBLE: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueDouble(attributeDefinition.getDefaultValue(), attributeDefinition)); - break; - - default: break; - } - } - } - } + /** + * Reads the attribute values of the given node; without a spec type they + * cannot be interpreted and are reported and skipped. + */ + protected void readAttributeValues(Node specObject, SpecType specType, ParseIssues issues) { + readValues(specObject, specType, getSpecTypeID(), issues); } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java index 39daf5d..28759a2 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecObjectType.java @@ -5,13 +5,18 @@ import org.w3c.dom.Node; import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; public class SpecObjectType extends SpecType { public SpecObjectType(Node specType, Map dataTypes) { - super(specType, dataTypes); + this(specType, dataTypes, ParseIssues.collecting()); + } + + public SpecObjectType(Node specType, Map dataTypes, ParseIssues issues) { + super(specType, dataTypes, issues); this.type = ReqIFConst.SPEC_OBJECT_TYPE; } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java index 3a2a053..b79c10e 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelation.java @@ -3,6 +3,7 @@ import java.util.Collection; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; import org.w3c.dom.Element; @@ -40,6 +41,16 @@ public SpecRelation(String id, SpecType specType, String sourceObjID, String tar } public SpecRelation(Node specRelation, SpecType specType) { + this(specRelation, specType, ParseIssues.collecting()); + } + + /** + * @param specType the resolved relation type; if null, the relation is + * kept with its unresolved type reference, but its values + * cannot be interpreted and are reported and skipped + * @param issues receives the problems found + */ + public SpecRelation(Node specRelation, SpecType specType, ParseIssues issues) { super(specRelation); this.specType = specType; @@ -58,9 +69,16 @@ public SpecRelation(Node specRelation, SpecType specType) { // relationTypeRef, not in the inherited type field. this.type = ReqIFConst.UNDEFINED; - if (specType != null) { - readAttributeValues(specRelation, specType); - } + readAttributeValues(specRelation, specType, issues); + } + + /** + * @return the IDENTIFIER of the relation type - also if the reference + * could not be resolved - or null if it has none + */ + @Override + public String getSpecTypeID() { + return this.specType != null ? this.specType.getID() : this.relationTypeRef; } public String getSourceObjID() { @@ -71,6 +89,24 @@ public String getTargetObjID() { return this.targetObjID; } + /** + * Points the relation at another source object. + */ + public SpecRelation setSourceObjID(String sourceObjID) { + this.sourceObjID = sourceObjID; + markModified(); + return this; + } + + /** + * Points the relation at another target object. + */ + public SpecRelation setTargetObjID(String targetObjID) { + this.targetObjID = targetObjID; + markModified(); + return this; + } + /** * @return the IDENTIFIER of the SPEC-RELATION-TYPE this relation refers to * (SPEC-RELATION-TYPE-REF), or null if the document declares none @@ -87,6 +123,11 @@ public String getRelationTypeName() { return this.specType == null ? null : this.specType.getName(); } + // A relation has no content category, whatever its values. + @Override + protected void valuesChanged() { + } + // A relation is none of the content categories. @Override diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java index f1a3445..10014de 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecRelationType.java @@ -5,13 +5,18 @@ import org.w3c.dom.Node; import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; public class SpecRelationType extends SpecType { public SpecRelationType(Node specType, Map dataTypes) { - super(specType, dataTypes); + this(specType, dataTypes, ParseIssues.collecting()); + } + + public SpecRelationType(Node specType, Map dataTypes, ParseIssues issues) { + super(specType, dataTypes, issues); this.type = ReqIFConst.SPEC_RELATION_TYPE; } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java index b0c456c..095732e 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecType.java @@ -9,33 +9,21 @@ import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeEnumeration; +import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; -public class SpecType { +public class SpecType extends Identifiable { private Map attributeDefinitions = new LinkedHashMap(); - private String id; - protected String name; protected String type; - protected String alternativeID; protected String sourceElementName; - public String getID() { - return this.id; - } - - /** - * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null - */ - public String getAlternativeID() { - return this.alternativeID; - } - /** * @return the SPEC-TYPE element name this type was read from, or null when * it was not created from a document. Needed to write back spec type @@ -46,10 +34,6 @@ public String getSourceElementName() { return this.sourceElementName; } - public String getName() { - return this.name; - } - public String getType() { return this.type; } @@ -126,9 +110,8 @@ public String getEnumValueOtherContent(String id) { * @param kind SPEC-OBJECT-TYPE, SPECIFICATION-TYPE or SPEC-RELATION-TYPE */ public SpecType(String id, String name, String kind) { + super(id, name); - this.id = id; - this.name = name; this.type = kind; } @@ -138,60 +121,56 @@ public SpecType(String id, String name, String kind) { public SpecType addAttributeDefinition(AttributeDefinition attributeDefinition) { this.attributeDefinitions.put(attributeDefinition.getID(), attributeDefinition); + markModified(); return this; } public SpecType(Node specType, Map dataTypes) { - - this.id = specType.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent(); - this.name = specType.getAttributes().getNamedItem(ReqIFConst.LONG_NAME).getTextContent(); - this.alternativeID = XmlUtils.alternativeID(specType); + this(specType, dataTypes, ParseIssues.collecting()); + } + + /** + * @param issues receives attribute definitions that cannot be read or are + * declared twice + */ + public SpecType(Node specType, Map dataTypes, ParseIssues issues) { + super(specType); + this.sourceElementName = XmlUtils.localName(specType); this.type = ReqIFConst.UNDEFINED; //Doors relationship definitionen habe keine ChildNodes Node specAttributes = XmlUtils.firstChildElementByLocalName(specType, ReqIFConst.SPEC_ATTRIBUTES); - if(specAttributes != null) { - - for(Node attributeDefinition: XmlUtils.childElements(specAttributes)) { - - String attDefNodeName = XmlUtils.localName(attributeDefinition); - { - - String attDefID = attributeDefinition.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent(); - - switch(attDefNodeName.substring(attDefNodeName.lastIndexOf("-")+1)) { - - case ReqIFConst.BOOLEAN: this.attributeDefinitions.put(attDefID, new AttributeDefinitionBoolean(attributeDefinition, dataTypes)); - break; - - case ReqIFConst.INTEGER: this.attributeDefinitions.put(attDefID, new AttributeDefinitionInteger(attributeDefinition, dataTypes)); - break; - - case ReqIFConst.STRING: this.attributeDefinitions.put(attDefID, new AttributeDefinitionString(attributeDefinition, dataTypes)); - break; - - case ReqIFConst.ENUMERATION: this.attributeDefinitions.put(attDefID, new AttributeDefinitionEnumeration(attributeDefinition, dataTypes)); - break; - - case ReqIFConst.XHTML: this.attributeDefinitions.put(attDefID, new AttributeDefinitionXHTML(attributeDefinition, dataTypes)); - break; - - case ReqIFConst.DATE: this.attributeDefinitions.put(attDefID, new AttributeDefinitionDate(attributeDefinition, dataTypes)); - break; - - case ReqIFConst.DOUBLE: this.attributeDefinitions.put(attDefID, new AttributeDefinitionDouble(attributeDefinition, dataTypes)); - break; - //Convert REAL to DOUBLE - case ReqIFConst.REAL: this.attributeDefinitions.put(attDefID, new AttributeDefinitionDouble(attributeDefinition, dataTypes)); - break; - - default: this.attributeDefinitions.put(attDefID, new AttributeDefinition(attributeDefinition, dataTypes)); - break; - } + for(Node attributeDefinition: XmlUtils.childElements(specAttributes)) { + + AttributeDefinition definition = issues.readOrSkip("attribute definition", + () -> readAttributeDefinition(attributeDefinition, dataTypes)); + if(definition != null) { + if(definition.getDataType() == null) { + issues.report(definition.getID(), "Attribute definition references a datatype that is not declared " + + "in DATATYPES; it and its values are kept as written"); } + issues.put(this.attributeDefinitions, definition, "Attribute definition"); } } } + private static AttributeDefinition readAttributeDefinition(Node attributeDefinition, Map dataTypes) { + + String attDefNodeName = XmlUtils.localName(attributeDefinition); + switch(ReqIFConst.kindOf(attDefNodeName)) { + + case ReqIFConst.BOOLEAN: return new AttributeDefinitionBoolean(attributeDefinition, dataTypes); + case ReqIFConst.INTEGER: return new AttributeDefinitionInteger(attributeDefinition, dataTypes); + case ReqIFConst.STRING: return new AttributeDefinitionString(attributeDefinition, dataTypes); + case ReqIFConst.ENUMERATION: return new AttributeDefinitionEnumeration(attributeDefinition, dataTypes); + case ReqIFConst.XHTML: return new AttributeDefinitionXHTML(attributeDefinition, dataTypes); + case ReqIFConst.DATE: return new AttributeDefinitionDate(attributeDefinition, dataTypes); + // REAL is read as DOUBLE + case ReqIFConst.DOUBLE: + case ReqIFConst.REAL: return new AttributeDefinitionDouble(attributeDefinition, dataTypes); + default: return new AttributeDefinition(attributeDefinition, dataTypes); + } + } + } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java index a99c65d..4c82be3 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/Specification.java @@ -1,118 +1,184 @@ package de.uni_stuttgart.ils.reqif4j.specification; import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.w3c.dom.Element; import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition; -import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinitionEnumeration; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue; -import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueBoolean; -import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueDate; -import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueDouble; -import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueEnumeration; -import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueInteger; -import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueString; -import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; -public class Specification { +public class Specification extends SpecElementWithAttributes implements HierarchyParent { - private String id; - private String name; - private String alternativeID; private SpecType type; - private int numberOfSpecObjects; - private int sectionCounter = 0; - private Map attributeValues = new HashMap(); - private Map children = new LinkedHashMap(); - private List allSpecHierarchies = new ArrayList(); + private String specTypeRef; + private final HierarchyChildren children = new HierarchyChildren(this); - public String getID() { - return this.id; - } - - /** - * @return the IDENTIFIER of the optional ALTERNATIVE-ID, or null - */ - public String getAlternativeID() { - return this.alternativeID; - } /** * @return the value of an attribute named "Description", or null if the * specification has no such attribute */ public String getDescription() { - AttributeValue description = this.attributeValues.get("Description"); + AttributeValue description = getAttributes().get("Description"); return description == null ? null : (String) description.getValue(); } - public String getName() { - return this.name; - } public String getSpecType() { - return this.type.getType(); + return this.type == null ? null : this.type.getType(); } public String getSpecTypeName() { - return this.type.getName(); + return this.type == null ? null : this.type.getName(); } /** - * @return the IDENTIFIER of the specification type, or null if the - * reference could not be resolved + * @return the IDENTIFIER of the specification type - also if the + * reference could not be resolved - or null if it has none */ public String getSpecTypeID() { - return this.type == null ? null : this.type.getID(); + return this.type != null ? this.type.getID() : this.specTypeRef; } - public Map getAttributes() { - return this.attributeValues; - } - - public Object getAttribute(String attributeName) { - AttributeValue attributeValue = this.attributeValues.get(attributeName); - return attributeValue == null ? null : attributeValue.getValue(); - } + /** + * @return the number of hierarchy nodes, at all levels + */ public int getNumberOfSpecObjects() { - return this.numberOfSpecObjects; + return this.children.descendants().size(); } - + + /** + * @return the number of top-level hierarchy nodes (sections) + */ public int getNumberOfSections() { - return this.sectionCounter; + return this.children.size(); } + /** + * @return the top-level hierarchy nodes in document order (a copy) + */ + @Override public List getChildren() { - - return new ArrayList(this.children.values()); + return this.children.list(); } public List getLvlOneSpecHierarchies() { List children = new ArrayList(); - for(SpecHierarchy specHierarchy: this.children.values()) { + for(SpecHierarchy specHierarchy: this.children.list()) { children.add(specHierarchy.getSpecObject()); } return children; } - + + /** + * @return all hierarchy nodes, depth-first in document order (read-only) + */ public List getAllSpecHierarchies() { - return this.allSpecHierarchies; + return this.children.descendants(); + } + + /** + * @return the hierarchy node with this IDENTIFIER at any level, or null + */ + public SpecHierarchy getSpecHierarchy(String specHierarchyID) { + return specHierarchyID == null ? null : this.children.descendant(specHierarchyID); + } + + /** + * Appends a top-level hierarchy node. + * + * @throws IllegalArgumentException see {@link #addChild(int, SpecHierarchy)} + */ + public Specification addChild(SpecHierarchy child) { + return addChild(this.children.size(), child); + } + + /** + * Inserts a top-level hierarchy node at a position, 0 for first; the + * sections of the following nodes shift. + * + * @throws IllegalArgumentException if the node is already placed somewhere + * (move it with {@link de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent#moveSpecHierarchy}), + * a node with the same IDENTIFIER is already at the top level, or + * the position is out of range + */ + @Override + public Specification addChild(int index, SpecHierarchy child) { + this.children.add(index, child); + if(index == this.children.size() - 1) { + // appended: nothing else moves + child.place(1, index + 1); + } else { + renumber(index); + } + markModified(); + return this; + } + + /** + * Removes a top-level hierarchy node, with everything below it. + * + * @return the removed node, or null if there is no such node + */ + @Override + public SpecHierarchy removeChild(String specHierarchyID) { + int position = this.children.indexOf(specHierarchyID); + SpecHierarchy removed = this.children.remove(specHierarchyID); + if(removed != null) { + // the sections before it stay as they are + renumber(position); + markModified(); + } + return removed; + } + + @Override + public int getChildCount() { + return this.children.size(); + } + + @Override + public SpecHierarchy getChild(String specHierarchyID) { + return this.children.get(specHierarchyID); + } + + @Override + public Specification setChildren(List children) { + this.children.replace(children); + renumber(0); + markModified(); + return this; + } + + /** + * Top-level nodes are the sections, numbered from 1. + * + * @param from the first position whose section may have changed + */ + private void renumber(int from) { + int index = 0; + for(SpecHierarchy child: this.children.values()) { + if(index >= from) { + child.place(1, index + 1); + } + index++; + } + } + + HierarchyChildren hierarchyChildren() { + return this.children; } @@ -124,191 +190,52 @@ public List getAllSpecHierarchies() { */ public Specification(String id, String name, SpecType specType, java.util.Collection attributeValues, List children) { + super(id, name); - this.id = id; - this.name = name; this.type = specType; + AttributeValues values = resetValues(specType); if(attributeValues != null) { for(AttributeValue attributeValue: attributeValues) { - this.attributeValues.put(attributeValue.getName(), attributeValue); + values.put(attributeValue); } } if(children != null) { - for(SpecHierarchy child: children) { - this.children.put(child.getSpecHierarchyID(), child); - this.sectionCounter++; - } + this.children.putAll(children); } - - List allChildren = new ArrayList(); - for(SpecHierarchy specHierarchy: this.children.values()) { - allChildren.add(specHierarchy); - allChildren.addAll(specHierarchy.getAllChildren()); - } - this.allSpecHierarchies.addAll(allChildren); - this.numberOfSpecObjects = allChildren.size(); } public Specification(Node specification, SpecType specType, Map specObjects) { - - this.id = specification.getAttributes().getNamedItem(ReqIFConst.IDENTIFIER).getTextContent(); - this.name = specification.getAttributes().getNamedItem(ReqIFConst.LONG_NAME).getTextContent(); - this.alternativeID = XmlUtils.alternativeID(specification); - this.type = specType; - - Element valuesElement = XmlUtils.firstChildElementByLocalName(specification, ReqIFConst.VALUES); - if(valuesElement != null) { - - for(Element attribute: XmlUtils.childElements(valuesElement)) { - - String attValNodeName = XmlUtils.localName(attribute); - { - - String attributeDefinitionRef = XmlUtils.firstChildElement( - XmlUtils.firstChildElementByLocalName(attribute, ReqIFConst.DEFINITION)).getTextContent().trim(); - String attributeDefinitionName = specType.getAttributeDefinitions().get(attributeDefinitionRef).getName(); - String attributeValue; - AttributeDefinition attributeDefinition = specType.getAttributeDefinition(attributeDefinitionRef); - - switch(attValNodeName.substring(attValNodeName.lastIndexOf("-")+1)) { - - case ReqIFConst.BOOLEAN: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { - attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); - }else{ - attributeValue = ""; - } - this.attributeValues.put(attributeDefinitionName, new AttributeValueBoolean(attributeValue, attributeDefinition)); - break; - - case ReqIFConst.INTEGER: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { - attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); - }else{ - attributeValue = ""; - } - this.attributeValues.put(attributeDefinitionName, new AttributeValueInteger(attributeValue, attributeDefinition)); - break; - - case ReqIFConst.STRING: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { - attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); - }else{ - attributeValue = ""; - } - this.attributeValues.put(attributeDefinitionName, new AttributeValueString(attributeValue, attributeDefinition)); - break; - - case ReqIFConst.ENUMERATION: // Multiselect: read ALL ENUM-VALUE-REF children, not just the first - List enumRefs = new ArrayList(); - List enumNames = new ArrayList(); - for(Element ref : XmlUtils.descendantsByLocalName(attribute, ReqIFConst.ENUM_VALUE_REF)) { - String refID = ref.getTextContent().trim(); - enumRefs.add(refID); - enumNames.add(specType.getEnumValueName(refID)); - } - this.attributeValues.put(attributeDefinitionName, new AttributeValueEnumeration(enumNames, enumRefs, attributeDefinition)); - break; - - case ReqIFConst.XHTML: this.attributeValues.put(attributeDefinitionName, new AttributeValueXHTML(attribute, attributeDefinition)); - break; - - // 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; + this(specification, specType, specObjects, ParseIssues.collecting()); + } - // Values of datatype kinds the parser does not model are - // kept generically instead of being dropped. - default: if(attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE) !=null) { - attributeValue = attribute.getAttributes().getNamedItem(ReqIFConst.THE_VALUE).getTextContent(); - }else{ - attributeValue = ""; - } - this.attributeValues.put(attributeDefinitionName, - new AttributeValue(attributeValue, attributeDefinition) - .setSourceElementName(XmlUtils.localName(attribute))); - break; - } - } - } - } + /** + * @param specType the resolved specification type; if null, the + * specification is kept with its unresolved type reference, + * but its values cannot be interpreted and are reported and + * skipped + * @param issues receives the problems found + */ + public Specification(Node specification, SpecType specType, Map specObjects, ParseIssues issues) { + super(specification); - if(this.attributeValues.size() < specType.getAttributeDefinitions().size()) { - - for(AttributeDefinition attributeDefinition: specType.getAttributeDefinitions().values()) { - - if(!this.attributeValues.containsKey(attributeDefinition.getName())) { - - if(attributeDefinition.getDataType() == null) { - throw new ExceptionSpecObject("Attribute definition not existing in ReqIF parser\n", attributeDefinition); - } - - switch(attributeDefinition.getDataType().getType()) { - - case ReqIFConst.BOOLEAN: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueBoolean(attributeDefinition.getDefaultValue(), attributeDefinition)); - break; - - case ReqIFConst.INTEGER: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueInteger(attributeDefinition.getDefaultValue(), attributeDefinition)); - break; - - case ReqIFConst.STRING: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueString(attributeDefinition.getDefaultValue(), attributeDefinition)); - break; - - case ReqIFConst.ENUMERATION: if(attributeDefinition instanceof AttributeDefinitionEnumeration) { - AttributeDefinitionEnumeration enumDefinition = (AttributeDefinitionEnumeration) attributeDefinition; - this.attributeValues.put(attributeDefinition.getName(), new AttributeValueEnumeration(enumDefinition.getDefaultValues(), enumDefinition.getDefaultValueRefs(), attributeDefinition)); - }else{ - this.attributeValues.put(attributeDefinition.getName(), new AttributeValueEnumeration(attributeDefinition.getDefaultValue(), attributeDefinition)); - } - break; - - case ReqIFConst.XHTML: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueXHTML(attributeDefinition.getDefaultValue(), attributeDefinition)); - break; - - case ReqIFConst.DATE: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueDate(attributeDefinition.getDefaultValue(), attributeDefinition)); - break; + this.type = specType; + this.specTypeRef = typeRef(specification, ReqIFConst.SPEC_TYPE_REF); - case ReqIFConst.DOUBLE: this.attributeValues.put(attributeDefinition.getName(), new AttributeValueDouble(attributeDefinition.getDefaultValue(), attributeDefinition)); - break; + readValues(specification, specType, this.specTypeRef, issues); - default: break; - } - } - } - } - Element childrenElement = XmlUtils.firstChildElementByLocalName(specification, ReqIFConst.CHILDREN); - if(childrenElement != null) { - - for(Element specHierarchy: XmlUtils.childElements(childrenElement)) { - - String specHierarchyID = XmlUtils.attribute(specHierarchy, ReqIFConst.IDENTIFIER); - - this.children.put(specHierarchyID, new SpecHierarchy(1, ++this.sectionCounter, specHierarchy, specObjects)); - } - } - - List allChildren = new ArrayList(); - for(SpecHierarchy specHierarchy: this.children.values()) { - allChildren.add(specHierarchy); - for(SpecHierarchy child: specHierarchy.getAllChildren()) { - allChildren.add(child); + int sectionCounter = 0; + for(Element specHierarchy: XmlUtils.childElements(childrenElement)) { + int section = sectionCounter + 1; + SpecHierarchy child = issues.readOrSkip("spec hierarchy", + () -> new SpecHierarchy(1, section, specHierarchy, specObjects, issues)); + if(child != null) { + sectionCounter = section; } + this.children.put(child, issues); } - this.allSpecHierarchies.addAll(allChildren); - this.numberOfSpecObjects = allChildren.size(); } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecificationType.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecificationType.java index d81202c..7ad8e9c 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecificationType.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/specification/SpecificationType.java @@ -5,13 +5,18 @@ import org.w3c.dom.Node; import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssues; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; public class SpecificationType extends SpecType { public SpecificationType(Node specType, Map dataTypes) { - super(specType, dataTypes); + this(specType, dataTypes, ParseIssues.collecting()); + } + + public SpecificationType(Node specType, Map dataTypes, ParseIssues issues) { + super(specType, dataTypes, issues); this.type = ReqIFConst.SPECIFICATION_TYPE; } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/util/SecureXml.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/util/SecureXml.java index 1e395fd..52e69a3 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/util/SecureXml.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/util/SecureXml.java @@ -4,11 +4,15 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerFactory; import javax.xml.validation.SchemaFactory; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; + /** * Factories for XML components with the same hardening applied everywhere. * @@ -26,6 +30,22 @@ private SecureXml() { * not resolve external entities */ public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException { + synchronized (SecureXml.class) { + if (factory == null) { + factory = newDocumentBuilderFactory(); + } + // a factory is not required to be thread-safe + return factory.newDocumentBuilder(); + } + } + + /** + * Looking up and configuring a factory costs more than parsing a small + * XHTML value, so one is kept; the builders it creates are not shared. + */ + private static DocumentBuilderFactory factory; + + private static DocumentBuilderFactory newDocumentBuilderFactory() throws ParserConfigurationException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); @@ -41,8 +61,32 @@ public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationExc } catch (IllegalArgumentException ignored) { // parser implementation does not support these attributes } + return factory; + } - return factory.newDocumentBuilder(); + /** + * @return a namespace-aware SAX reader with the same hardening as + * {@link #newDocumentBuilder()}. Unlike a DOM it keeps line numbers, + * which makes schema violations of a file traceable. + */ + public static XMLReader newXMLReader() throws ParserConfigurationException, SAXException { + + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setXIncludeAware(false); + + 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); + + XMLReader reader = factory.newSAXParser().getXMLReader(); + try { + reader.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + reader.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + } catch (SAXException ignored) { + // parser implementation does not support these properties + } + return reader; } /** diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/util/XmlUtils.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/util/XmlUtils.java index ad2bc26..336e30c 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/util/XmlUtils.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/util/XmlUtils.java @@ -5,7 +5,6 @@ import org.w3c.dom.Element; import org.w3c.dom.Node; -import org.w3c.dom.NodeList; /** * Helper methods for namespace- and whitespace-independent DOM navigation. @@ -39,17 +38,20 @@ public static String localName(Node node) { } /** - * @return all direct child nodes of type ELEMENT_NODE. + * @return all direct child nodes of type ELEMENT_NODE. Like all methods + * here, it walks the children with getFirstChild/getNextSibling: + * a NodeList's item(i) can cost as much as the list is long when + * lists are walked nested, which made reading large documents + * quadratic. */ 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)); + for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + elements.add((Element) child); } } return elements; @@ -62,10 +64,9 @@ 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); + for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + return (Element) child; } } return null; @@ -78,9 +79,7 @@ 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); + for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { if (child.getNodeType() == Node.ELEMENT_NODE && localName(child).equals(localName)) { return (Element) child; } @@ -93,7 +92,7 @@ public static Element firstChildElementByLocalName(Node parent, String localName */ public static List descendantsByLocalName(Node root, String localName) { List result = new ArrayList(); - collectDescendants(root, localName, result); + collectDescendants(root, localName, result, false); return result; } @@ -102,36 +101,57 @@ public static List descendantsByLocalName(Node root, String localName) * name, or null. */ public static Element firstDescendantByLocalName(Node root, String localName) { - List descendants = descendantsByLocalName(root, localName); - return descendants.isEmpty() ? null : descendants.get(0); + List first = new ArrayList(1); + collectDescendants(root, localName, first, true); + return first.isEmpty() ? null : first.get(0); } - private static void collectDescendants(Node node, String localName, List result) { + /** + * @param firstOnly stop at the first match + * @return whether to stop + */ + private static boolean collectDescendants(Node node, String localName, List result, boolean firstOnly) { if (node == null) { - return; + return false; } - NodeList children = node.getChildNodes(); - for (int i = 0; i < children.getLength(); i++) { - Node child = children.item(i); + for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { if (child.getNodeType() == Node.ELEMENT_NODE) { if (localName(child).equals(localName)) { result.add((Element) child); + if (firstOnly) { + return true; + } + } + if (collectDescendants(child, localName, result, firstOnly)) { + return true; } - collectDescendants(child, localName, result); } } + return false; } /** * Reads the optional ALTERNATIVE-ID of an identifiable element. * - * @return the IDENTIFIER of the ALTERNATIVE-ID child, or null if the element + * The schema wraps the alternative id twice: + * {@code }. + * The flat form {@code } is not + * schema-valid but is accepted as well, because earlier versions of this + * library wrote it. + * + * @return the IDENTIFIER of the alternative id, or null if the element * declares none */ public static String alternativeID(Node identifiable) { - Element alternativeID = firstChildElementByLocalName(identifiable, "ALTERNATIVE-ID"); - return alternativeID == null ? null : attribute(alternativeID, "IDENTIFIER"); + // literals instead of ReqIFConst: this package must not depend on the + // model packages, which depend on it + Element wrapper = firstChildElementByLocalName(identifiable, "ALTERNATIVE-ID"); + if (wrapper == null) { + return null; + } + Element nested = firstChildElementByLocalName(wrapper, "ALTERNATIVE-ID"); + return attribute(nested != null ? nested : wrapper, "IDENTIFIER"); } /** @@ -145,4 +165,36 @@ public static String attribute(Node node, String attributeName) { Node attribute = node.getAttributes().getNamedItem(attributeName); return attribute == null ? null : attribute.getTextContent(); } + + /** + * @return the text content of the named attribute, or "" if the node has + * no such attribute. + */ + public static String attributeOrEmpty(Node node, String attributeName) { + String value = attribute(node, attributeName); + return value == null ? "" : value; + } + + /** + * @return the xsd:boolean attribute ("true"/"1" is true, "false"/"0" + * false), or null if the node has no such attribute or its value + * is not an xsd:boolean - an unknown value must not turn into + * false and be written back as such + */ + public static Boolean booleanAttribute(Node node, String attributeName) { + String value = attribute(node, attributeName); + if (value == null) { + return null; + } + switch (value.trim()) { + case "true": + case "1": + return Boolean.TRUE; + case "false": + case "0": + return Boolean.FALSE; + default: + return null; + } + } } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/util/XsdTypes.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/util/XsdTypes.java new file mode 100644 index 0000000..b6c5888 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/util/XsdTypes.java @@ -0,0 +1,220 @@ +package de.uni_stuttgart.ils.reqif4j.util; + +import java.io.IOException; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.math.BigInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.XMLConstants; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; + +import org.xml.sax.SAXException; + +/** + * The lexical rules of the XML Schema types ReqIF uses, for checks and + * conversions the JDK does not offer directly. + */ +public final class XsdTypes { + + /** + * xsd:NCName restricted to ASCII, where all editions of XML agree on the + * name characters. + */ + private static final Pattern ASCII_NC_NAME = Pattern.compile("[A-Za-z_][A-Za-z0-9._-]*"); + + private static final Pattern ASCII = Pattern.compile("\\p{ASCII}*"); + + /** compiled on first use; only needed for identifiers with non-ASCII characters */ + private static Schema ncNameSchema; + + /** + * xsd:dateTime: year with at least four digits, optional fraction of + * seconds, optional time zone. Year 0000 is excluded, as in XML Schema 1.0, + * which the JDK's schema validator implements. + */ + private static final Pattern DATE_TIME = Pattern.compile( + "(-?)(?!0000)([1-9][0-9]{3,}|0[0-9]{3})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])" + + "T(([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\.[0-9]+)?|24:00:00(\\.0+)?)" + + "(Z|[+-]((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?"); + + private static final Pattern INTEGER = Pattern.compile("[+-]?[0-9]+"); + + /** xsd:double in XML Schema 1.0 notation (no "+INF") */ + private static final Pattern DOUBLE = Pattern.compile( + "[+-]?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([Ee][+-]?[0-9]+)?|-?INF|NaN"); + + private static final Pattern BOOLEAN = Pattern.compile("true|false|1|0"); + + private static final BigInteger FOUR = BigInteger.valueOf(4); + private static final BigInteger HUNDRED = BigInteger.valueOf(100); + private static final BigInteger FOUR_HUNDRED = BigInteger.valueOf(400); + + private XsdTypes() { + } + + + /** + * @return true if the text is a valid xsd:ID / xsd:NCName + * + * The editions of XML differ in which non-ASCII characters a name may + * contain, and the JDK's schema validator follows an older one than the + * current specification. Identifiers with non-ASCII characters are + * therefore checked by that validator itself, so this method and + * schema validation always agree. + */ + public static boolean isNCName(String text) { + + if (text == null || text.isEmpty()) { + return false; + } + if (ASCII.matcher(text).matches()) { + return ASCII_NC_NAME.matcher(text).matches(); + } + // the schema collapses whitespace before checking, an identifier + // written to a file must not contain any + if (!isXmlText(text) || text.codePoints().anyMatch(Character::isWhitespace)) { + return false; + } + try { + String document = "" + text.replace("&", "&").replace("<", "<") + ""; + ncNameSchema().newValidator().validate(new StreamSource(new StringReader(document))); + return true; + } catch (SAXException notAName) { + return false; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static synchronized Schema ncNameSchema() { + + if (ncNameSchema == null) { + String xsd = "" + + ""; + try { + ncNameSchema = SecureXml.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI) + .newSchema(new StreamSource(new StringReader(xsd))); + } catch (SAXException e) { + throw new IllegalStateException("Built-in NCName schema does not compile", e); + } + } + return ncNameSchema; + } + + /** + * @return true if every character of the text may appear in an XML 1.0 + * document; control characters (except tab, line feed, carriage + * return), unpaired surrogates and U+FFFE/U+FFFF may not + */ + public static boolean isXmlText(String text) { + + if (text == null) { + return true; + } + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (Character.isHighSurrogate(c) && i + 1 < text.length() && Character.isLowSurrogate(text.charAt(i + 1))) { + i++; + continue; + } + boolean allowed = c == 0x9 || c == 0xA || c == 0xD + || (c >= 0x20 && c <= 0xD7FF) + || (c >= 0xE000 && c <= 0xFFFD); + if (!allowed) { + return false; + } + } + return true; + } + + /** + * @return true if the text is a valid xsd:dateTime, including an existing + * day of month (no 30 February) + */ + public static boolean isDateTime(String text) { + + if (text == null) { + return false; + } + Matcher matcher = DATE_TIME.matcher(text.trim()); + if (!matcher.matches()) { + return false; + } + // the pattern allows day 31 in every month; the year may have any + // number of digits, so the leap year rule is applied on a BigInteger + BigInteger year = new BigInteger(matcher.group(1) + matcher.group(2)); + int month = Integer.parseInt(matcher.group(3)); + int day = Integer.parseInt(matcher.group(4)); + return day <= daysIn(month, year); + } + + private static int daysIn(int month, BigInteger year) { + switch (month) { + case 2: + boolean leap = year.mod(FOUR).signum() == 0 + && (year.mod(HUNDRED).signum() != 0 || year.mod(FOUR_HUNDRED).signum() == 0); + return leap ? 29 : 28; + case 4: + case 6: + case 9: + case 11: + return 30; + default: + return 31; + } + } + + /** + * @return true if the text is a valid xsd:integer (any number of digits) + */ + public static boolean isInteger(String text) { + return text != null && INTEGER.matcher(text.trim()).matches(); + } + + + /** + * @return true if the text is a valid xsd:double (XML Schema 1.0 notation) + */ + public static boolean isDouble(String text) { + return text != null && DOUBLE.matcher(text.trim()).matches(); + } + + /** + * @return true if the text is a valid xsd:boolean: true, false, 1 or 0 + */ + public static boolean isBoolean(String text) { + return text != null && BOOLEAN.matcher(text.trim()).matches(); + } + + /** + * Parses xsd:double notation, which spells infinity {@code INF} / + * {@code -INF}; Java's {@code Infinity} is accepted as well. + * + * @throws NumberFormatException if the text is not a double + */ + public static double parseDouble(String text) { + String trimmed = text.trim(); + switch (trimmed) { + case "INF": + case "+INF": return Double.POSITIVE_INFINITY; + case "-INF": return Double.NEGATIVE_INFINITY; + default: return Double.parseDouble(trimmed); + } + } + + /** + * @return the value in xsd:double notation + */ + public static String formatDouble(double value) { + if (value == Double.POSITIVE_INFINITY) { + return "INF"; + } + if (value == Double.NEGATIVE_INFINITY) { + return "-INF"; + } + return Double.toString(value); + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/validate/ImplementationGuideCheck.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/validate/ImplementationGuideCheck.java new file mode 100644 index 0000000..672d009 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/validate/ImplementationGuideCheck.java @@ -0,0 +1,299 @@ +package de.uni_stuttgart.ils.reqif4j.validate; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML; +import de.uni_stuttgart.ils.reqif4j.attributes.XHTML; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFz; +import de.uni_stuttgart.ils.reqif4j.specification.SpecElementWithAttributes; +import de.uni_stuttgart.ils.reqif4j.specification.SpecType; +import de.uni_stuttgart.ils.reqif4j.util.XhtmlParser; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; +import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLElementDiv; +import de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLElementObject; + +/** + * Checks the conventions of the ReqIF Implementation Guide of the + * ReqIF Implementor Forum (prostep ivip), which tools rely on when they + * exchange documents but the schema does not express: + * + *
    + *
  • the standard attributes ({@code ReqIF.Text}, {@code ReqIF.ChapterName}, + * {@code ReqIF.ForeignID}, ...) have the datatype tools expect: dates are + * DATE, {@code ReqIF.ForeignDeleted} is BOOLEAN, {@code ReqIF.Text} is XHTML, + * the names and descriptions are STRING or XHTML;
  • + *
  • embedded objects state their MIME type, reference a file of the + * archive by a relative path, and an object that is not a PNG image (e.g. an + * OLE object) contains a PNG image as preview for tools that cannot show + * it;
  • + *
  • a simplified XHTML value (IS-SIMPLIFIED) carries THE-ORIGINAL-VALUE and + * vice versa;
  • + *
  • when an archive is checked, every file an object references is in + * it.
  • + *
+ * + * Findings are warnings - the document is valid ReqIF, but importing tools + * may show it differently than intended - except for files missing from an + * archive, which are errors. Use it next to {@link ReqIFValidator}: + * + *
+ * ValidationResult conventions = new ImplementationGuideCheck().check(document);
+ * 
+ */ +public class ImplementationGuideCheck { + + /** PNG, the image format of previews */ + public static final String PNG = "image/png"; + + /** the datatype kinds each standard attribute is expected to have, sorted */ + private static final Map> STANDARD_ATTRIBUTES = Map.ofEntries( + Map.entry("ReqIF.Text", List.of(ReqIFConst.XHTML)), + Map.entry("ReqIF.ChapterName", List.of(ReqIFConst.STRING, ReqIFConst.XHTML)), + Map.entry("ReqIF.Name", List.of(ReqIFConst.STRING, ReqIFConst.XHTML)), + Map.entry("ReqIF.Description", List.of(ReqIFConst.STRING, ReqIFConst.XHTML)), + Map.entry("ReqIF.Prefix", List.of(ReqIFConst.STRING, ReqIFConst.XHTML)), + Map.entry("ReqIF.ForeignID", List.of(ReqIFConst.INTEGER, ReqIFConst.STRING, ReqIFConst.XHTML)), + Map.entry("ReqIF.ForeignCreatedBy", List.of(ReqIFConst.STRING, ReqIFConst.XHTML)), + Map.entry("ReqIF.ForeignModifiedBy", List.of(ReqIFConst.STRING, ReqIFConst.XHTML)), + Map.entry("ReqIF.ForeignCreatedOn", List.of(ReqIFConst.DATE)), + Map.entry("ReqIF.ForeignModifiedOn", List.of(ReqIFConst.DATE)), + Map.entry("ReqIF.ForeignDeleted", List.of(ReqIFConst.BOOLEAN))); + + /** a URI scheme, e.g. http: or file: */ + private static final Pattern SCHEME = Pattern.compile("^[A-Za-z][A-Za-z0-9+.-]+:.*"); + /** a Windows drive, e.g. C:\ */ + private static final Pattern DRIVE = Pattern.compile("^[A-Za-z]:[\\\\/].*"); + + /** + * Checks one document; references from objects to files cannot be + * checked without the archive, see {@link #check(ReqIFz)}. + */ + public ValidationResult check(ReqIFDocument document) { + ValidationResult result = new ValidationResult(); + check(document, new Scope(null, "", ""), result); + return result; + } + + /** + * Checks every document of an archive, and that the files their objects + * reference are in it. + */ + public ValidationResult check(ReqIFz archive) { + ValidationResult result = new ValidationResult(); + // entry names spelled like references: ./ and .. collapsed, decoded + Set files = new java.util.HashSet(); + for (String entry : archive.getAttachmentFiles().keySet()) { + String normalized = resolve("", entry); + files.add(normalized != null ? normalized : entry); + } + for (Map.Entry document : archive.getReqIFDocuments().entrySet()) { + String entry = document.getKey().replace('\\', '/'); + // objects reference files relative to the document + String directory = entry.contains("/") ? entry.substring(0, entry.lastIndexOf('/') + 1) : ""; + check(document.getValue(), new Scope(files, directory, document.getKey() + ": "), result); + } + return result; + } + + /** + * @param files the archive's files, or null if not known + * @param directory the directory of the document in the archive, "" or + * ending with / + * @param where what each message starts with + */ + private static final class Scope { + final Set files; + final String directory; + final String where; + + Scope(Set files, String directory, String where) { + this.files = files; + this.directory = directory; + this.where = where; + } + } + + private void check(ReqIFDocument document, Scope scope, ValidationResult result) { + String where = scope.where; + + ReqIFCoreContent content = document.getCoreContent(); + if (content == null) { + return; + } + for (SpecType specType : content.getSpecTypes().values()) { + for (AttributeDefinition definition : specType.getAttributeDefinitions().values()) { + checkStandardAttribute(definition, result, where); + AttributeValueXHTML defaultValue = xhtmlDefault(definition); + if (defaultValue != null) { + // objects show wherever the default applies + checkXhtml(definition.getID(), defaultValue, scope, result); + } + } + } + for (Collection elements : List.of(content.getSpecObjects().values(), + content.getSpecRelation().values(), content.getSpecifications().values())) { + for (SpecElementWithAttributes element : elements) { + for (AttributeValue value : element.getValues().values()) { + if (value instanceof AttributeValueXHTML) { + checkXhtml(element.getID(), (AttributeValueXHTML) value, scope, result); + } + } + } + } + } + + private static void checkStandardAttribute(AttributeDefinition definition, ValidationResult result, String where) { + + List expected = definition.getName() == null ? null : STANDARD_ATTRIBUTES.get(definition.getName()); + if (expected == null || definition.getDataType() == null) { + return; + } + String kind = ReqIFConst.datatypeSuffix(definition.getDataType().getType()); + if (kind != null && !expected.contains(kind)) { + result.warning(definition.getID(), where + "The standard attribute " + definition.getName() + " is of kind " + + kind + "; tools expect " + String.join(" or ", expected)); + } + } + + /** + * @return the default of an XHTML attribute, or null + */ + private static AttributeValueXHTML xhtmlDefault(AttributeDefinition definition) { + if (definition.getDefaultXhtmlValue() != null) { + return definition.getDefaultXhtmlValue(); + } + boolean xhtml = definition.getDataType() != null && ReqIFConst.XHTML.equals(definition.getDataType().getType()); + String markup = definition.getDefaultValue(); + try { + return xhtml && markup != null && !markup.isBlank() ? new AttributeValueXHTML(markup, definition) : null; + } catch (IllegalArgumentException e) { + // not well-formed: the validator reports that + return null; + } + } + + private static void checkXhtml(String ownerID, AttributeValueXHTML value, Scope scope, ValidationResult result) { + + String where = scope.where; + boolean hasOriginal = value.getOriginalValue() != null; + if (Boolean.TRUE.equals(value.getSimplified()) && !hasOriginal) { + result.warning(ownerID, where + "An XHTML value of attribute " + definitionID(value) + + " is IS-SIMPLIFIED but has no THE-ORIGINAL-VALUE"); + } else if (hasOriginal && !Boolean.TRUE.equals(value.getSimplified())) { + result.warning(ownerID, where + "An XHTML value of attribute " + definitionID(value) + + " has THE-ORIGINAL-VALUE but is not marked IS-SIMPLIFIED"); + } + // a preview often is in both THE-VALUE and THE-ORIGINAL-VALUE; it is + // reported once + Set checked = new java.util.HashSet(); + for (XHTMLElementDiv content : new XHTMLElementDiv[] { value.getDivValue(), value.getOriginalValue() }) { + if (content != null && content.getNode() != null) { + for (Element object : XmlUtils.descendantsByLocalName(content.getNode(), XHTML.OBJECT)) { + if (isXhtmlObject(object) && checked.add(object.getAttribute("data") + "|" + object.getAttribute("type"))) { + checkObject(ownerID, object, scope, result); + } + } + } + } + } + + private static boolean isXhtmlObject(Node node) { + return node instanceof Element && XHTML.OBJECT.equals(XmlUtils.localName(node)) + && XhtmlParser.XHTML_NAMESPACE.equals(node.getNamespaceURI()); + } + + private static void checkObject(String ownerID, Element element, Scope scope, ValidationResult result) { + + XHTMLElementObject object = new XHTMLElementObject(element); + String data = object.getData(); + String type = object.getMimeType() == null ? null : object.getMimeType().trim(); + boolean inline = data != null && data.regionMatches(true, 0, "data:", 0, 5); + String what = scope.where + "Object " + (data == null ? "without data" : abbreviate(data)); + + if (type == null || type.isEmpty()) { + result.warning(ownerID, what + " has no MIME type (type attribute)"); + } + if (data != null && !inline && isAbsolute(data)) { + result.warning(ownerID, what + " is referenced by an absolute location; " + + "objects reference files of the archive by a relative path"); + } else if (data != null && !inline && scope.files != null) { + String entry = resolve(scope.directory, data); + if (entry == null) { + result.error(ownerID, what + " points outside the archive"); + } else if (!scope.files.contains(entry)) { + result.error(ownerID, what + " is not in the archive"); + } + } + // an object a tool may not be able to show, e.g. an OLE object, comes + // with a PNG image as preview - unless it is the preview itself + boolean preview = isXhtmlObject(element.getParentNode()); + if (type != null && !type.isEmpty() && !PNG.equalsIgnoreCase(type) && !preview && !hasPngPreview(element)) { + result.warning(ownerID, what + " of type " + type + " has no PNG image as preview (a nested object)"); + } + } + + private static boolean hasPngPreview(Element object) { + for (Node child = object.getFirstChild(); child != null; child = child.getNextSibling()) { + if (isXhtmlObject(child) && PNG.equalsIgnoreCase( + String.valueOf(new XHTMLElementObject(child).getMimeType()).trim())) { + return true; + } + } + return false; + } + + /** keeps messages short, e.g. for inline data */ + private static String abbreviate(String data) { + return data.length() <= 100 ? data : data.substring(0, 100) + "..."; + } + + /** + * @return the archive entry a reference names: decoded (%20), resolved + * against the document's directory, with forward slashes; null if + * it climbs above the root of the archive + */ + private static String resolve(String directory, String data) { + String path = data.replace('\\', '/'); + try { + String decoded = new java.net.URI(path).getPath(); + if (decoded != null) { + path = decoded; + } + } catch (java.net.URISyntaxException e) { + // not a valid URI, e.g. with a blank: taken as written + } + java.util.Deque segments = new java.util.ArrayDeque(); + for (String segment : (directory + path).split("/")) { + if (segment.equals("..")) { + if (segments.pollLast() == null) { + return null; + } + } else if (!segment.isEmpty() && !segment.equals(".")) { + segments.addLast(segment); + } + } + return String.join("/", segments); + } + + /** a URI with a scheme, a path from the root or a Windows drive */ + private static boolean isAbsolute(String data) { + return DRIVE.matcher(data).matches() || SCHEME.matcher(data).matches() || data.startsWith("/") + || data.startsWith("\\"); + } + + private static String definitionID(AttributeValue value) { + return value.getAttributeDefinitionType() == null ? null : value.getAttributeDefinitionType().getID(); + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/validate/LocalSchemaResolver.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/validate/LocalSchemaResolver.java new file mode 100644 index 0000000..1917613 --- /dev/null +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/validate/LocalSchemaResolver.java @@ -0,0 +1,108 @@ +package de.uni_stuttgart.ils.reqif4j.validate; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.ls.DOMImplementationLS; +import org.w3c.dom.ls.LSInput; +import org.w3c.dom.ls.LSResourceResolver; + +import de.uni_stuttgart.ils.reqif4j.util.SecureXml; + +/** + * Resolves the remote imports of a schema to files in a local directory. + * + * The official {@code reqif.xsd} imports {@code xml.xsd} and the XHTML + * modules ({@code driver.xsd}, {@code xhtml-*-1.xsd}) by absolute http URL. + * Schema validation must not go online, so every http(s) location is looked up + * by its file name in the schema's own directory instead. Other locations + * (relative paths, {@code file:}, Windows drive letters) are left to the + * parser, which may only load local files. + * + * A remote import that is not available locally is answered with an empty + * input - never handed back to the parser, which could otherwise fetch it if + * it ignores the external access restriction - and remembered, so the caller + * can name it in the error message. + * + * Mapping by file name assumes the imported file names are unique, which holds + * for the ReqIF schema set. + */ +final class LocalSchemaResolver implements LSResourceResolver { + + /** a single path segment, so a location can never point outside the directory */ + private static final Pattern PLAIN_FILE_NAME = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._-]*"); + + private final Path schemaDirectory; + private final DOMImplementationLS inputs; + private final Map missing = new LinkedHashMap(); + + LocalSchemaResolver(Path schemaDirectory) { + this.schemaDirectory = schemaDirectory; + try { + this.inputs = (DOMImplementationLS) SecureXml.newDocumentBuilder().getDOMImplementation(); + } catch (ParserConfigurationException e) { + throw new IllegalStateException("No DOM implementation available", e); + } + } + + /** + * @return the remote imports that could not be found locally, as the + * location the schema referenced to the file name looked up + */ + Map getMissing() { + return this.missing; + } + + @Override + public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI) { + + URI location = resolve(systemId, baseURI); + if (location == null || !isRemote(location)) { + return null; + } + + String rawPath = location.getRawPath(); + String fileName = rawPath == null ? "" : rawPath.substring(rawPath.lastIndexOf('/') + 1); + Path local = PLAIN_FILE_NAME.matcher(fileName).matches() ? this.schemaDirectory.resolve(fileName) : null; + + LSInput input = this.inputs.createLSInput(); + input.setPublicId(publicId); + input.setBaseURI(baseURI); + if (local == null || !Files.isRegularFile(local)) { + this.missing.put(location.toString(), fileName); + input.setSystemId(location.toString()); + input.setStringData(""); + } else { + input.setSystemId(local.toUri().toString()); + } + return input; + } + + private static boolean isRemote(URI location) { + String scheme = location.getScheme(); + return "http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme); + } + + private static URI resolve(String systemId, String baseURI) { + + if (systemId == null) { + return null; + } + try { + URI location = new URI(systemId); + if (location.isAbsolute() || baseURI == null) { + return location; + } + return new URI(baseURI).resolve(location); + } catch (URISyntaxException | IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/validate/ReqIFValidator.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/validate/ReqIFValidator.java index 27a8052..fa9db30 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/validate/ReqIFValidator.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/validate/ReqIFValidator.java @@ -1,29 +1,46 @@ package de.uni_stuttgart.ils.reqif4j.validate; import java.io.IOException; +import java.math.BigInteger; import java.nio.file.Path; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import javax.xml.XMLConstants; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Source; import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition; +import de.uni_stuttgart.ils.reqif4j.attributes.UninterpretedAttributeValue; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinitionEnumeration; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueEnumeration; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML; import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; +import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeDouble; import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeEnumeration; +import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeEnumerationValue; +import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeInteger; +import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeString; +import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable; +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssue; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFHeader; import de.uni_stuttgart.ils.reqif4j.specification.RelationGroup; import de.uni_stuttgart.ils.reqif4j.specification.SpecHierarchy; import de.uni_stuttgart.ils.reqif4j.specification.SpecObject; @@ -31,6 +48,8 @@ import de.uni_stuttgart.ils.reqif4j.specification.SpecType; import de.uni_stuttgart.ils.reqif4j.specification.Specification; import de.uni_stuttgart.ils.reqif4j.util.SecureXml; +import de.uni_stuttgart.ils.reqif4j.util.XhtmlParser; +import de.uni_stuttgart.ils.reqif4j.util.XsdTypes; import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; /** @@ -42,15 +61,26 @@ * new ReqIFWriter().write(document, Path.of("out.reqif")); * * - * Validated are: identifiers present and globally unique, resolvable datatype - * and spec type references, attribute values matching their spec type, enum - * value references, relation endpoints, spec hierarchy targets and relation - * group references. + * Validated are: identifiers present, valid xsd:IDs and globally unique, + * resolvable datatype and spec type references, attribute values matching + * their spec type, enum value references, relation endpoints, spec hierarchy + * targets and relation group references. Values the schema constrains + * lexically - LAST-CHANGE, CREATION-TIME and DATE values (xsd:dateTime), + * enum KEYs (xsd:integer), REQ-IF-VERSION (fixed to 1.0) - are errors when + * they do not fit; values outside the limits of their datatype (MAX-LENGTH, + * MIN/MAX) are warnings, because the schema cannot express those limits. + * + * Required values the writer fills in when the model lacks them (see + * {@link ReqIFWriter#buildDocument(ReqIFDocument)}) are not reported. A + * document without errors is written schema-valid, except for the XHTML + * content model: which XHTML elements a value may use is only checked by + * {@link #validateAgainstSchema(ReqIFDocument, Path)}. * * This is a model-level check, not a schema check. The OMG ReqIF XSD is not * bundled with this library; if you have it, pass it to - * {@link #validateAgainstSchema(ReqIFDocument, Path)} for a full XML Schema - * validation on top. + * {@link #validateAgainstSchema(ReqIFDocument, Path)} (or, for a file on disk, + * {@link #validateAgainstSchema(Path, Path)}) for a full XML Schema validation + * on top. */ public class ReqIFValidator { @@ -59,6 +89,51 @@ public class ReqIFValidator { * {@link ValidationResult#throwIfInvalid()} */ public ValidationResult validate(ReqIFDocument document) { + return validate(document, Set.of(), Set.of()); + } + + /** + * Validates the documents of a .reqifz archive together. Relations may + * connect spec objects, and relation groups specifications, of another + * document of the archive (the schema types these references GLOBAL-REF), + * so they are resolved across all documents; everything else within each + * document. Each message names the document it is about. + */ + public ValidationResult validate(Map documents) { + + Set specObjects = new HashSet(); + Set specifications = new HashSet(); + for (ReqIFDocument document : documents.values()) { + if (document != null && document.getCoreContent() != null) { + specObjects.addAll(document.getCoreContent().getSpecObjects().keySet()); + specifications.addAll(document.getCoreContent().getSpecifications().keySet()); + } + } + ValidationResult result = new ValidationResult(); + for (Map.Entry document : documents.entrySet()) { + for (ValidationIssue issue : validate(document.getValue(), specObjects, specifications).getIssues()) { + result.add(issue.getSeverity(), issue.getElementID(), document.getKey() + ": " + issue.getMessage()); + } + } + return result; + } + + /** + * Validates the documents of an archive together, see + * {@link #validate(Map)}. + */ + public ValidationResult validate(de.uni_stuttgart.ils.reqif4j.reqif.ReqIFz archive) { + return validate(archive.getReqIFDocuments()); + } + + /** + * @param globalSpecObjects spec objects of other documents a relation + * may reference + * @param globalSpecifications specifications of other documents a + * relation group may reference + */ + private ValidationResult validate(ReqIFDocument document, Set globalSpecObjects, + Set globalSpecifications) { ValidationResult result = new ValidationResult(); @@ -66,10 +141,20 @@ public ValidationResult validate(ReqIFDocument document) { result.error(null, "Document is null"); return result; } + // What the parser had to skip or could not resolve. Warnings: they tell + // what happened to the file, while what is still wrong with the model - + // an unresolved reference, a value that does not fit - is reported as + // an error by the checks below, and stops being reported once fixed. + for (ParseIssue issue : document.getParseIssues()) { + result.warning(issue.getElementID(), "While reading: " + issue.getMessage()); + } + + Map identifiers = new HashMap(); if (document.getHeader() == null) { - result.warning(null, "Document has no THE-HEADER"); - } else if (isBlank(document.getHeader().getID())) { - result.error(null, "REQ-IF-HEADER has no IDENTIFIER"); + // the schema requires THE-HEADER, and the writer cannot invent its IDENTIFIER + result.error(null, "Document has no THE-HEADER"); + } else { + checkHeader(document.getHeader(), result, identifiers); } ReqIFCoreContent content = document.getCoreContent(); @@ -78,32 +163,91 @@ public ValidationResult validate(ReqIFDocument document) { return result; } - Map identifiers = new HashMap(); checkDatatypes(content, result, identifiers); checkSpecTypes(content, result, identifiers); checkSpecObjects(content, result, identifiers); - checkSpecRelations(content, result, identifiers); + checkSpecRelations(content, result, identifiers, globalSpecObjects); checkSpecifications(content, result, identifiers); - checkRelationGroups(content, result, identifiers); + checkRelationGroups(content, result, identifiers, globalSpecifications); return result; } + /** + * Empty header fields are fine, the writer fills them in; present ones must + * match the schema. + */ + private void checkHeader(ReqIFHeader header, ValidationResult result, Map identifiers) { + + if (isBlank(header.getID())) { + result.error(null, "REQ-IF-HEADER has no IDENTIFIER"); + } else { + checkIdentifier(header.getID(), "REQ-IF-HEADER", result, identifiers); + } + checkText(header.getID(), "TITLE", header.getTitle(), result); + checkText(header.getID(), "COMMENT", header.getComment(), result); + checkText(header.getID(), "REQ-IF-TOOL-ID", header.getToolID(), result); + checkText(header.getID(), "SOURCE-TOOL-ID", header.getSourceToolID(), result); + if (!isBlank(header.getCreationTime()) && !XsdTypes.isDateTime(header.getCreationTime())) { + result.error(header.getID(), "CREATION-TIME '" + header.getCreationTime() + "' is not an xsd:dateTime"); + } + if (!isBlank(header.getReqIFVersion()) && !ReqIFWriter.REQIF_VERSION.equals(header.getReqIFVersion())) { + result.error(header.getID(), "REQ-IF-VERSION is '" + header.getReqIFVersion() + + "', but the schema only allows " + ReqIFWriter.REQIF_VERSION); + } + } + private void checkDatatypes(ReqIFCoreContent content, ValidationResult result, Map identifiers) { for (Datatype datatype : content.getDatatypes().values()) { - checkIdentifier(datatype.getID(), "DATATYPE-DEFINITION", result, identifiers); + checkIdentifiable(datatype, "DATATYPE-DEFINITION", result, identifiers); + + if (datatype instanceof DatatypeInteger) { + DatatypeInteger integer = (DatatypeInteger) datatype; + checkBounds(datatype, integer.getMin() <= integer.getMax(), integer.getMin(), integer.getMax(), result); + + } else if (datatype instanceof DatatypeDouble) { + DatatypeDouble real = (DatatypeDouble) datatype; + checkBounds(datatype, real.getMin() <= real.getMax(), real.getMin(), real.getMax(), result); + + } else if (datatype instanceof DatatypeEnumeration) { + for (DatatypeEnumerationValue value : ((DatatypeEnumeration) datatype).getEnumValues().values()) { + checkIdentifiable(value, "ENUM-VALUE", result, identifiers); + checkText(value.getID(), "OTHER-CONTENT of enum value", value.getOtherContent(), result); + if (isBlank(value.getKey())) { + result.error(value.getID(), "Enum value '" + value.getName() + "' has no KEY"); + } else if (!XsdTypes.isInteger(value.getKey())) { + result.error(value.getID(), "KEY '" + value.getKey() + "' of enum value '" + value.getName() + + "' is not an integer"); + } + } + } + } + } + + private void checkBounds(Datatype datatype, boolean ordered, Object min, Object max, ValidationResult result) { + if (!ordered) { + result.warning(datatype.getID(), "Datatype '" + datatype.getName() + "' has MIN " + min + + " greater than MAX " + max + "; no value can satisfy it"); } } private void checkSpecTypes(ReqIFCoreContent content, ValidationResult result, Map identifiers) { for (SpecType specType : content.getSpecTypes().values()) { - checkIdentifier(specType.getID(), "SPEC-TYPE", result, identifiers); + checkIdentifiable(specType, "SPEC-TYPE", result, identifiers); for (AttributeDefinition definition : specType.getAttributeDefinitions().values()) { - checkIdentifier(definition.getID(), "ATTRIBUTE-DEFINITION", result, identifiers); + checkIdentifiable(definition, "ATTRIBUTE-DEFINITION", result, identifiers); + // written verbatim, so it must already be in schema notation; an XHTML + // default read from a document is well-formed by construction + boolean xhtmlDefault = definition.getDefaultXhtmlValue() != null && definition.getDataType() != null + && ReqIFConst.XHTML.equals(definition.getDataType().getType()); + if (definition.getDefaultValue() != null && !(definition instanceof AttributeDefinitionEnumeration) + && !xhtmlDefault) { + checkScalar(definition.getID(), "Default value", definition, definition.getDefaultValue(), result); + } Datatype datatype = definition.getDataType(); if (datatype == null) { @@ -153,30 +297,32 @@ private void checkEnumRefs(AttributeDefinition definition, List refs, Va private void checkSpecObjects(ReqIFCoreContent content, ValidationResult result, Map identifiers) { for (SpecObject specObject : content.getSpecObjects().values()) { - checkIdentifier(specObject.getID(), "SPEC-OBJECT", result, identifiers); + checkIdentifiable(specObject, "SPEC-OBJECT", result, identifiers); checkSpecTypeRef(content, specObject.getID(), specObject.getSpecTypeID(), "SPEC-OBJECT", result); checkAttributeValues(specObject.getID(), specObject.getSpecTypeID(), content, - specObject.getAttributes(), result); + specObject.getValues(), result); } } private void checkSpecRelations(ReqIFCoreContent content, ValidationResult result, - Map identifiers) { + Map identifiers, Set globalSpecObjects) { for (SpecRelation relation : content.getSpecRelation().values()) { - checkIdentifier(relation.getID(), "SPEC-RELATION", result, identifiers); + checkIdentifiable(relation, "SPEC-RELATION", result, identifiers); checkSpecTypeRef(content, relation.getID(), relation.getRelationTypeRef(), "SPEC-RELATION", result); - if (content.getSpecObject(relation.getSourceObjID()) == null) { + if (content.getSpecObject(relation.getSourceObjID()) == null + && !globalSpecObjects.contains(relation.getSourceObjID())) { result.error(relation.getID(), "Relation source " + relation.getSourceObjID() + " is not a known spec object"); } - if (content.getSpecObject(relation.getTargetObjID()) == null) { + if (content.getSpecObject(relation.getTargetObjID()) == null + && !globalSpecObjects.contains(relation.getTargetObjID())) { result.error(relation.getID(), "Relation target " + relation.getTargetObjID() + " is not a known spec object"); } checkAttributeValues(relation.getID(), relation.getRelationTypeRef(), content, - relation.getAttributes(), result); + relation.getValues(), result); } } @@ -184,39 +330,72 @@ private void checkSpecifications(ReqIFCoreContent content, ValidationResult resu Map identifiers) { for (Specification specification : content.getSpecifications().values()) { - checkIdentifier(specification.getID(), "SPECIFICATION", result, identifiers); + checkIdentifiable(specification, "SPECIFICATION", result, identifiers); checkSpecTypeRef(content, specification.getID(), specification.getSpecTypeID(), "SPECIFICATION", result); checkAttributeValues(specification.getID(), specification.getSpecTypeID(), content, - specification.getAttributes(), result); + specification.getValues(), result); for (SpecHierarchy hierarchy : specification.getAllSpecHierarchies()) { - checkIdentifier(hierarchy.getSpecHierarchyID(), "SPEC-HIERARCHY", result, identifiers); + checkIdentifiable(hierarchy, "SPEC-HIERARCHY", result, identifiers); - if (hierarchy.getSpecObject() == null) { - result.error(hierarchy.getSpecHierarchyID(), - "Spec hierarchy references a spec object that does not exist"); + if (hierarchy.getSpecObjectID() == null) { + result.error(hierarchy.getSpecHierarchyID(), "Spec hierarchy has no OBJECT reference"); - } else if (content.getSpecObject(hierarchy.getSpecObjectID()) == null) { + } else if (hierarchy.getSpecObject() == null + || content.getSpecObject(hierarchy.getSpecObjectID()) == null) { result.error(hierarchy.getSpecHierarchyID(), "Spec hierarchy references spec object " + hierarchy.getSpecObjectID() + ", which is not declared in SPEC-OBJECTS"); } + checkEditableAttributes(content, hierarchy, result); + } + } + } + + /** + * EDITABLE-ATTS names attributes of the node's object. (The writer derives + * each reference element from the definition, so its kind need not be + * checked.) + */ + private static void checkEditableAttributes(ReqIFCoreContent content, SpecHierarchy hierarchy, + ValidationResult result) { + + SpecType objectType = hierarchy.getSpecObject() == null ? null : hierarchy.getSpecObject().getResolvedSpecType(); + for (Map.Entry ref : hierarchy.getEditableAttributeRefs()) { + String id = ref.getKey(); + AttributeDefinition definition = objectType == null ? null : objectType.getAttributeDefinition(id); + if (definition == null && !declaresAttributeDefinition(content, id)) { + result.error(hierarchy.getID(), "EDITABLE-ATTS references attribute definition " + id + + ", which no spec type declares"); + } else if (definition == null && objectType != null) { + result.error(hierarchy.getID(), "EDITABLE-ATTS references attribute definition " + id + + ", which is not an attribute of spec type " + objectType.getID() + " of the node's object"); } } } + private static boolean declaresAttributeDefinition(ReqIFCoreContent content, String attributeDefinitionID) { + for (SpecType specType : content.getSpecTypes().values()) { + if (specType.getAttributeDefinition(attributeDefinitionID) != null) { + return true; + } + } + return false; + } + private void checkRelationGroups(ReqIFCoreContent content, ValidationResult result, - Map identifiers) { + Map identifiers, Set globalSpecifications) { for (RelationGroup group : content.getRelationGroups().values()) { - checkIdentifier(group.getID(), "RELATION-GROUP", result, identifiers); + checkIdentifiable(group, "RELATION-GROUP", result, identifiers); - if (group.getRelationGroupTypeRef() != null - && content.getSpecType(group.getRelationGroupTypeRef()) == null) { + if (isBlank(group.getRelationGroupTypeRef())) { + result.error(group.getID(), "Relation group has no type reference"); + } else if (content.getSpecType(group.getRelationGroupTypeRef()) == null) { result.error(group.getID(), "Relation group references type " + group.getRelationGroupTypeRef() + ", which is not declared in SPEC-TYPES"); } - checkSpecificationRef(content, group, group.getSourceSpecificationRef(), "source", result); - checkSpecificationRef(content, group, group.getTargetSpecificationRef(), "target", result); + checkSpecificationRef(content, group, group.getSourceSpecificationRef(), "source", result, globalSpecifications); + checkSpecificationRef(content, group, group.getTargetSpecificationRef(), "target", result, globalSpecifications); for (String relationRef : group.getSpecRelationRefs()) { if (content.getSpecRelation(relationRef) == null) { @@ -228,9 +407,11 @@ private void checkRelationGroups(ReqIFCoreContent content, ValidationResult resu } private void checkSpecificationRef(ReqIFCoreContent content, RelationGroup group, String specificationRef, - String role, ValidationResult result) { + String role, ValidationResult result, Set globalSpecifications) { - if (specificationRef != null && content.getSpecification(specificationRef) == null) { + if (isBlank(specificationRef)) { + result.error(group.getID(), "Relation group has no " + role + " specification"); + } else if (content.getSpecification(specificationRef) == null && !globalSpecifications.contains(specificationRef)) { result.error(group.getID(), "Relation group " + role + " specification " + specificationRef + " is not declared in SPECIFICATIONS"); } @@ -248,6 +429,7 @@ private void checkAttributeValues(String ownerID, String specTypeID, ReqIFCoreCo return; } + // the explicit values, the ones that are written, in document order for (AttributeValue attributeValue : attributeValues.values()) { AttributeDefinition definition = attributeValue.getAttributeDefinitionType(); @@ -256,9 +438,23 @@ private void checkAttributeValues(String ownerID, String specTypeID, ReqIFCoreCo continue; } if (specType.getAttributeDefinition(definition.getID()) == null) { - result.error(ownerID, "Attribute value '" + attributeValue.getName() + "' uses definition " + // a value of an undeclared definition knows only its IDENTIFIER + String label = attributeValue.getName() != null ? "Attribute value '" + attributeValue.getName() + "'" + : "A value"; + result.error(ownerID, label + " uses definition " + definition.getID() + ", which does not belong to spec type " + specTypeID); } + if (attributeValue instanceof UninterpretedAttributeValue) { + checkUninterpreted(ownerID, (UninterpretedAttributeValue) attributeValue, definition, specType, result); + continue; + } + if (attributeValue.getValue() != null && !(attributeValue instanceof AttributeValueEnumeration) + && !(attributeValue instanceof AttributeValueXHTML)) { + Object value = attributeValue.getValue(); + // typed values are written in xsd notation, so check that form + String text = value instanceof Double ? XsdTypes.formatDouble((Double) value) : String.valueOf(value); + checkScalar(ownerID, "Attribute '" + attributeValue.getName() + "'", definition, text, result); + } if (attributeValue instanceof AttributeValueEnumeration) { AttributeValueEnumeration enumValue = (AttributeValueEnumeration) attributeValue; checkEnumRefs(definition, enumValue.getValueRefs(), result, "enum value"); @@ -273,6 +469,39 @@ private void checkAttributeValues(String ownerID, String specTypeID, ReqIFCoreCo } } + /** + * A value kept as read is written back unchanged: its THE-VALUE must fit + * the kind of its own element, and a kind that does not match its + * definition is worth a warning, as importing tools may reject it. + */ + private static void checkUninterpreted(String ownerID, UninterpretedAttributeValue value, + AttributeDefinition definition, SpecType specType, ValidationResult result) { + + String elementName = value.getSourceElementName(); + String kind = ReqIFConst.kindOf(elementName); + String theValue = de.uni_stuttgart.ils.reqif4j.util.XmlUtils.attribute(value.getElement(), ReqIFConst.THE_VALUE); + boolean simpleKind = List.of(ReqIFConst.BOOLEAN, ReqIFConst.INTEGER, ReqIFConst.REAL, ReqIFConst.DATE, + ReqIFConst.STRING).contains(kind); + if (simpleKind && theValue == null) { + // written as read, so the required attribute stays missing + result.error(ownerID, elementName + " for " + definition.getID() + " has no THE-VALUE"); + } else if (simpleKind && !(ReqIFConst.BOOLEAN.equals(kind) && XsdTypes.isBoolean(theValue) + || ReqIFConst.INTEGER.equals(kind) && XsdTypes.isInteger(theValue) + || ReqIFConst.REAL.equals(kind) && XsdTypes.isDouble(theValue) + || ReqIFConst.DATE.equals(kind) && XsdTypes.isDateTime(theValue) + || ReqIFConst.STRING.equals(kind) && XsdTypes.isXmlText(theValue))) { + result.error(ownerID, elementName + " for " + definition.getID() + ": '" + theValue + + "' is not valid for its kind"); + } + AttributeDefinition declared = specType.getAttributeDefinition(definition.getID()); + String declaredKind = declared == null || declared.getDataType() == null ? null + : ReqIFConst.datatypeSuffix(declared.getDataType().getType()); + if (declaredKind != null && !declaredKind.equals(kind)) { + result.warning(ownerID, elementName + " for attribute '" + declared.getName() + "' (" + declared.getID() + + "), which is an " + declared.getElementName() + "; it is written as read"); + } + } + private void checkSpecTypeRef(ReqIFCoreContent content, String ownerID, String specTypeID, String kind, ValidationResult result) { @@ -286,7 +515,115 @@ private void checkSpecTypeRef(ReqIFCoreContent content, String ownerID, String s } /** - * ReqIF identifiers must be present and unique across the whole document. + * A value must be lexically valid for its datatype (an error: the written + * file would violate the schema) and should respect the datatype's limits + * (a warning: the schema cannot express them, but importing tools may + * reject the value). + * + * @param text the value as written, e.g. "42" or "2026-07-23T10:00:00Z" + */ + private void checkScalar(String ownerID, String what, AttributeDefinition definition, String text, + ValidationResult result) { + + Datatype datatype = definition.getDataType(); + if (datatype == null) { + return; + } + String category = datatype.getType(); + if (!XsdTypes.isXmlText(text)) { + result.error(ownerID, what + " contains characters XML does not allow (control characters)"); + return; + } + + if (ReqIFConst.ENUMERATION.equals(category)) { + // only reachable for a default of a definition that is not an + // AttributeDefinitionEnumeration; enum values are references + result.error(ownerID, what + " of an enumeration must be given as enum value references " + + "(an enumeration attribute definition), not as text"); + + } else if (ReqIFConst.XHTML.equals(category)) { + try { + XhtmlParser.parseDiv(text); + } catch (IllegalArgumentException e) { + result.error(ownerID, what + " is not well-formed XHTML"); + } + + } else if (ReqIFConst.DATE.equals(category)) { + if (!XsdTypes.isDateTime(text)) { + result.error(ownerID, what + ": '" + text + "' is not an xsd:dateTime"); + } + + } else if (ReqIFConst.BOOLEAN.equals(category)) { + if (!XsdTypes.isBoolean(text)) { + result.error(ownerID, what + ": '" + text + "' is not an xsd:boolean (true, false, 1 or 0)"); + } + + } else if (datatype instanceof DatatypeString) { + // xsd counts characters, not UTF-16 units + int length = text.codePointCount(0, text.length()); + int maxLength = ((DatatypeString) datatype).getMaxLength(); + if (length > maxLength) { + result.warning(ownerID, what + " is " + length + " characters long, but datatype '" + + datatype.getName() + "' allows " + maxLength); + } + + } else if (datatype instanceof DatatypeInteger) { + DatatypeInteger integer = (DatatypeInteger) datatype; + if (!XsdTypes.isInteger(text)) { + result.error(ownerID, what + ": '" + text + "' is not an integer"); + } else if (outside(new BigInteger(text.trim()), integer.getMin(), integer.getMax())) { + result.warning(ownerID, what + " " + text.trim() + " is outside the range " + integer.getMin() + + ".." + integer.getMax() + " of datatype '" + datatype.getName() + "'"); + } + + } else if (datatype instanceof DatatypeDouble) { + DatatypeDouble real = (DatatypeDouble) datatype; + if (!XsdTypes.isDouble(text)) { + result.error(ownerID, what + ": '" + text + "' is not an xsd:double"); + return; + } + double value = XsdTypes.parseDouble(text); + if (value < real.getMin() || value > real.getMax()) { + result.warning(ownerID, what + " " + text.trim() + " is outside the range " + real.getMin() + + ".." + real.getMax() + " of datatype '" + datatype.getName() + "'"); + } + } + } + + /** + * Text with characters XML does not allow cannot be written at all. + */ + private void checkText(String ownerID, String what, String text, ValidationResult result) { + if (!XsdTypes.isXmlText(text)) { + result.error(ownerID, what + " contains characters XML does not allow (control characters)"); + } + } + + private static boolean outside(BigInteger value, long min, long max) { + return value.compareTo(BigInteger.valueOf(min)) < 0 || value.compareTo(BigInteger.valueOf(max)) > 0; + } + + /** + * Checks the identifier and, if present, the LAST-CHANGE of an element. A + * missing LAST-CHANGE is fine: the writer fills it in. + */ + private void checkIdentifiable(Identifiable identifiable, String kind, ValidationResult result, + Map identifiers) { + + checkIdentifier(identifiable.getID(), kind, result, identifiers); + checkText(identifiable.getID(), "LONG-NAME of " + kind, identifiable.getName(), result); + checkText(identifiable.getID(), "DESC of " + kind, identifiable.getDesc(), result); + + String lastChange = identifiable.getLastChange(); + if (!isBlank(lastChange) && !XsdTypes.isDateTime(lastChange)) { + result.error(identifiable.getID(), "LAST-CHANGE '" + lastChange + "' of " + kind + + " is not an xsd:dateTime"); + } + } + + /** + * ReqIF identifiers must be present, valid xsd:IDs and unique across the + * whole document. * * Note that identifiers duplicated within one category cannot be * detected here: the parser keys its maps by identifier, so a duplicate has @@ -298,6 +635,10 @@ private void checkIdentifier(String id, String kind, ValidationResult result, Ma result.error(null, kind + " has no IDENTIFIER"); return; } + if (!XsdTypes.isNCName(id)) { + // e.g. starts with a digit or contains a blank or colon + result.error(id, "Identifier '" + id + "' of " + kind + " is not a valid xsd:ID"); + } String previousKind = identifiers.put(id, kind); if (previousKind != null) { result.error(id, "Identifier is used twice: as " + previousKind + " and as " + kind); @@ -306,48 +647,163 @@ private void checkIdentifier(String id, String kind, ValidationResult result, Ma /** - * Validates the written XML against an XML Schema. The OMG ReqIF XSD is not - * bundled with this library for licensing reasons; download it from the OMG - * and pass its path here. + * Validates the XML the writer produces for this document against an XML + * Schema. The OMG ReqIF XSD is not bundled with this library for licensing + * reasons; download it from the OMG and pass its path here. + * + * What is checked is the output, not the model: required values the writer + * fills in (LAST-CHANGE, header fields, see + * {@link ReqIFWriter#buildDocument(ReqIFDocument)}) are not reported as + * missing. + * + * The official {@code reqif.xsd} imports {@code xml.xsd}, {@code driver.xsd} + * and the XHTML modules ({@code xhtml-*-1.xsd}) by http URL. Validation never + * goes online: these imports are looked up by file name in the directory of + * {@code schema}, so place them next to it. * * @param schema path to reqif.xsd * @return the schema violations, empty if the document is schema-valid */ public ValidationResult validateAgainstSchema(ReqIFDocument document, Path schema) throws IOException { + return validate(new DOMSource(new ReqIFWriter().buildDocument(document)), schema); + } + + /** + * Validates a ReqIF file as it is on disk, e.g. a file received from another + * tool or one written by {@link ReqIFWriter}. Unlike + * {@link #validateAgainstSchema(ReqIFDocument, Path)} the findings carry the + * line numbers of the file. + * + * @param reqifFile the .reqif file to check + * @param schema path to reqif.xsd, with its imports next to it + * @return the schema violations, empty if the file is schema-valid + */ + public ValidationResult validateAgainstSchema(Path reqifFile, Path schema) throws IOException { + + try { + InputSource input = new InputSource(reqifFile.toUri().toString()); + return validate(new SAXSource(SecureXml.newXMLReader(), input), schema); + } catch (ParserConfigurationException | SAXException e) { + throw new IOException("Failed to create an XML reader", e); + } + } + + private ValidationResult validate(Source source, Path schema) throws IOException { + ValidationResult result = new ValidationResult(); + Path schemaFile = schema.toAbsolutePath(); + LocalSchemaResolver resolver = new LocalSchemaResolver(schemaFile.getParent()); + + // Problems while loading the schema itself - an unreadable import is only a + // warning to the parser - would make every finding below meaningless. + ValidationResult schemaProblems = new ValidationResult(); + IssueCollector schemaCollector = new IssueCollector(schemaProblems); + Schema xsd = null; try { SchemaFactory factory = SecureXml.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI); - Schema xsd = factory.newSchema(schema.toFile()); + factory.setResourceResolver(resolver); + factory.setErrorHandler(schemaCollector); + xsd = factory.newSchema(schemaFile.toFile()); + } catch (SAXException e) { + schemaCollector.reportUnlessReported(e); + } + if (xsd == null || !schemaProblems.getIssues().isEmpty() || !resolver.getMissing().isEmpty()) { + StringBuilder message = new StringBuilder("Schema could not be loaded: ").append(schemaFile); + for (ValidationIssue problem : schemaProblems.getIssues()) { + message.append("\n ").append(problem.getMessage()); + } + appendMissingImports(message, resolver, schemaFile); + result.error(null, message.toString()); + return result; + } + IssueCollector collector = new IssueCollector(result); + try { Validator validator = xsd.newValidator(); - validator.setErrorHandler(new ErrorHandler() { + validator.setErrorHandler(collector); + validator.validate(source); + } catch (SAXException e) { + // normally reported through the error handler already; anything else + // stopped the validation and must not be lost + collector.reportUnlessReported(e); + } + return result; + } - @Override - public void warning(SAXParseException exception) { - result.warning(null, message(exception)); - } + private static void appendMissingImports(StringBuilder message, LocalSchemaResolver resolver, Path schemaFile) { - @Override - public void error(SAXParseException exception) { - result.error(null, message(exception)); - } + if (resolver.getMissing().isEmpty()) { + return; + } + message.append("\nThe schema imports files that are not available locally. Download them and place them next to ") + .append(schemaFile.getFileName()).append(" in ").append(schemaFile.getParent()).append(':'); + for (Map.Entry missing : resolver.getMissing().entrySet()) { + message.append("\n ").append(missing.getValue()).append(" (").append(missing.getKey()).append(')'); + } + } - @Override - public void fatalError(SAXParseException exception) { - result.error(null, message(exception)); - } + /** + * Records parser findings as issues. Warnings stay warnings; errors and + * fatal errors are errors. + * + * After a fatal error the parser throws a new exception for the same + * problem; that one must not be reported again. Errors and warnings are + * never merged: findings on a DOM source carry no position, so equal + * messages may well be different problems. + */ + private static final class IssueCollector implements ErrorHandler { - private String message(SAXParseException exception) { - return "line " + exception.getLineNumber() + ": " + exception.getMessage(); - } - }); - validator.validate(new DOMSource(new ReqIFWriter().buildDocument(document))); + private final ValidationResult result; + private final Set fatalErrors = new HashSet(); - } catch (SAXException e) { - result.error(null, "Schema validation failed: " + e.getMessage()); + IssueCollector(ValidationResult result) { + this.result = result; + } + + @Override + public void warning(SAXParseException exception) { + this.result.warning(null, message(exception)); + } + + @Override + public void error(SAXParseException exception) { + this.result.error(null, message(exception)); + } + + @Override + public void fatalError(SAXParseException exception) { + this.fatalErrors.add(fingerprint(exception)); + this.result.error(null, message(exception)); + } + + /** + * For an exception that ended parsing: reports it unless it repeats a + * fatal error the handler has seen already. + */ + void reportUnlessReported(SAXException exception) { + if (!this.fatalErrors.contains(fingerprint(exception))) { + this.result.error(null, exception instanceof SAXParseException + ? message((SAXParseException) exception) + : exception.getMessage()); + } + } + + private static String fingerprint(SAXException exception) { + if (exception instanceof SAXParseException) { + SAXParseException parseException = (SAXParseException) exception; + return parseException.getSystemId() + ':' + parseException.getLineNumber() + ':' + + parseException.getColumnNumber() + ':' + parseException.getMessage(); + } + return String.valueOf(exception.getMessage()); + } + + private static String message(SAXParseException exception) { + // a DOM source has no line numbers + return exception.getLineNumber() < 0 + ? exception.getMessage() + : "line " + exception.getLineNumber() + ": " + exception.getMessage(); } - return result; } private static boolean isBlank(String value) { diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFElements.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFElements.java index c3a73fe..16e022a 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFElements.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFElements.java @@ -15,30 +15,9 @@ final class ReqIFElements { private ReqIFElements() { } - /** - * @return the ReqIF name of a datatype category (e.g. "REAL" for DOUBLE) - */ - static String datatypeSuffix(String datatypeCategory) { - - if (datatypeCategory == null) { - return null; - } - switch (datatypeCategory) { - case ReqIFConst.DOUBLE: - case ReqIFConst.REAL: return ReqIFConst.REAL; - case ReqIFConst.BOOLEAN: - case ReqIFConst.INTEGER: - case ReqIFConst.STRING: - case ReqIFConst.DATE: - case ReqIFConst.XHTML: - case ReqIFConst.ENUMERATION: return datatypeCategory; - default: return null; - } - } - /** @return e.g. DATATYPE-DEFINITION-STRING */ static String datatypeDefinition(String datatypeCategory) { - String suffix = datatypeSuffix(datatypeCategory); + String suffix = ReqIFConst.datatypeSuffix(datatypeCategory); return suffix == null ? null : "DATATYPE-DEFINITION-" + suffix; } @@ -47,21 +26,9 @@ static String datatypeDefinitionRef(String sourceElementName) { return sourceElementName + "-REF"; } - /** @return e.g. ATTRIBUTE-DEFINITION-STRING */ - static String attributeDefinition(String datatypeCategory) { - String suffix = datatypeSuffix(datatypeCategory); - return suffix == null ? null : "ATTRIBUTE-DEFINITION-" + suffix; - } - - /** @return e.g. ATTRIBUTE-DEFINITION-STRING-REF */ - static String attributeDefinitionRef(String datatypeCategory) { - String definition = attributeDefinition(datatypeCategory); - return definition == null ? null : definition + "-REF"; - } - /** @return e.g. ATTRIBUTE-VALUE-STRING */ static String attributeValue(String datatypeCategory) { - String suffix = datatypeSuffix(datatypeCategory); + String suffix = ReqIFConst.datatypeSuffix(datatypeCategory); return suffix == null ? null : "ATTRIBUTE-VALUE-" + suffix; } diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFWriter.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFWriter.java index 8c704e1..d0e13dd 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFWriter.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFWriter.java @@ -6,10 +6,12 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Clock; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; +import javax.xml.XMLConstants; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; @@ -20,17 +22,21 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; +import org.w3c.dom.NamedNodeMap; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition; +import de.uni_stuttgart.ils.reqif4j.attributes.UninterpretedAttributeValue; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinitionEnumeration; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueEnumeration; import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML; import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; +import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeDouble; import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeEnumeration; import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeEnumerationValue; import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeInteger; import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeString; +import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; @@ -43,6 +49,8 @@ import de.uni_stuttgart.ils.reqif4j.specification.Specification; import de.uni_stuttgart.ils.reqif4j.util.SecureXml; import de.uni_stuttgart.ils.reqif4j.util.XhtmlParser; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; +import de.uni_stuttgart.ils.reqif4j.util.XsdTypes; /** * Serializes a parsed {@link ReqIFDocument} back to ReqIF XML. @@ -63,11 +71,24 @@ public class ReqIFWriter { public static final String REQIF_NAMESPACE = "http://www.omg.org/spec/ReqIF/20110401/reqif.xsd"; + /** what all ReqIF namespaces, also those of drafts, begin with */ + private static final String REQIF_NAMESPACE_PREFIX = "http://www.omg.org/spec/ReqIF/"; public static final String XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; + /** REQ-IF-TOOL-ID and SOURCE-TOOL-ID written when the header has none. */ + public static final String DEFAULT_TOOL_ID = "reqif4j"; + /** The only REQ-IF-VERSION the schema allows. */ + public static final String REQIF_VERSION = "1.0"; + private static final String XHTML_PREFIX = "xhtml"; + /** user data keys of the output DOM holding the LAST-CHANGE defaults */ + private static final String DEFAULT_LAST_CHANGE = "reqif4j.defaultLastChange"; + private static final String NOW = "reqif4j.now"; + /** the modified elements, which take the time of writing as LAST-CHANGE */ + private static final String STAMPED = "reqif4j.stamped"; private boolean indent = false; + private Clock clock = Clock.systemUTC(); /** * Enables pretty-printing. @@ -83,6 +104,17 @@ public ReqIFWriter setIndent(boolean indent) { } + /** + * Sets the clock that supplies the timestamp for required dates the model + * does not carry; see {@link #buildDocument(ReqIFDocument)}. Mainly useful + * to make generated output reproducible. + */ + public ReqIFWriter setClock(Clock clock) { + this.clock = clock == null ? Clock.systemUTC() : clock; + return this; + } + + /** * Writes the document to the given file. */ @@ -96,20 +128,55 @@ public void write(ReqIFDocument document, Path file) throws IOException { * Writes the document to the given stream. The stream is not closed. */ public void write(ReqIFDocument document, OutputStream out) throws IOException { + serialize(document, out).apply(); + } + + /** + * Writes the document without yet taking the new LAST-CHANGE of modified + * elements into the model, for writers that store the output further and + * apply the stamps only once that succeeded. + */ + Stamps serialize(ReqIFDocument document, OutputStream out) throws IOException { + Stamps stamps = new Stamps(); try { - transformer().transform(new DOMSource(buildDocument(document)), new StreamResult(out)); + transformer().transform(new DOMSource(build(document, stamps)), new StreamResult(out)); } catch (TransformerException e) { throw new IOException("Failed to write ReqIF document", e); } + return stamps; + } + + /** + * The modified elements of a written document and the LAST-CHANGE they + * were written with. + */ + static final class Stamps { + + private final List elements = new ArrayList(); + private String lastChange; + + /** + * Takes the written LAST-CHANGE into the model; the elements are no + * longer modified. + */ + void apply() { + for (Identifiable element : this.elements) { + element.setLastChange(this.lastChange); + } + } } /** - * @return the document serialized as an XML string + * @return the document serialized as an XML string; like the other + * output methods, this takes the LAST-CHANGE of modified elements + * into the model, see {@link #buildDocument(ReqIFDocument)} */ public String toXml(ReqIFDocument document) { try { StringWriter writer = new StringWriter(); - transformer().transform(new DOMSource(buildDocument(document)), new StreamResult(writer)); + Stamps stamps = new Stamps(); + transformer().transform(new DOMSource(build(document, stamps)), new StreamResult(writer)); + stamps.apply(); return writer.toString(); } catch (TransformerException e) { throw new ReqIFWriteException("Failed to serialize ReqIF document", e); @@ -119,20 +186,64 @@ public String toXml(ReqIFDocument document) { /** * Builds the output DOM from the object model. + * + * The schema requires some values a model may lack, typically one that was + * generated rather than parsed. They are filled in on output only; this + * method does not change the model: + * + *
    + *
  • LAST-CHANGE of an element: the time of writing if the element was + * {@link Identifiable#isModified() modified}; otherwise, if it has none, + * the header's CREATION-TIME, or the time of writing if the header has + * none either;
  • + *
  • header CREATION-TIME: the time of writing;
  • + *
  • header REQ-IF-TOOL-ID and SOURCE-TOOL-ID: {@value #DEFAULT_TOOL_ID}; + * REQ-IF-VERSION: {@value #REQIF_VERSION}; TITLE: empty.
  • + *
+ * + * Not filled in, because no default would be meaningful: a missing header + * and missing identifiers. {@link de.uni_stuttgart.ils.reqif4j.validate.ReqIFValidator} + * reports both. + * + * The output methods ({@link #write(ReqIFDocument, OutputStream)}, + * {@link #toXml(ReqIFDocument)}, ...) in addition give modified elements + * the LAST-CHANGE they were written with once the output succeeded; they + * are then no longer modified, so writing again does not report a new + * change. */ public Document buildDocument(ReqIFDocument document) { + return build(document, new Stamps()); + } + + private Document build(ReqIFDocument document, Stamps stamps) { if (document == null) { throw new ReqIFWriteException("Cannot write a null ReqIF document"); } Document xml = newDocument(); + // one timestamp for the whole document, so all defaults agree + String now = this.clock.instant().truncatedTo(ChronoUnit.SECONDS).toString(); + String creationTime = document.getHeader() == null ? "" : document.getHeader().getCreationTime(); + xml.setUserData(DEFAULT_LAST_CHANGE, isBlank(creationTime) ? now : creationTime, null); + xml.setUserData(NOW, now, null); + stamps.lastChange = now; + xml.setUserData(STAMPED, stamps.elements, null); + Element root = xml.createElementNS(REQIF_NAMESPACE, "REQ-IF"); root.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:" + XHTML_PREFIX, XHTML_NAMESPACE); + if (document.getLang() != null) { + root.setAttributeNS(XMLConstants.XML_NS_URI, "xml:lang", document.getLang()); + } + if (document.getSchemaLocation() != null) { + root.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI); + root.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "xsi:schemaLocation", + schemaLocation(document.getSchemaLocation())); + } xml.appendChild(root); if (document.getHeader() != null) { - root.appendChild(header(xml, document.getHeader())); + root.appendChild(header(xml, document.getHeader(), now)); } root.appendChild(coreContent(xml, document.getCoreContent())); @@ -141,11 +252,19 @@ public Document buildDocument(ReqIFDocument document) { root.appendChild(xml.importNode(extension, true)); } + // only needed while building; not part of the returned document + xml.setUserData(DEFAULT_LAST_CHANGE, null, null); + xml.setUserData(NOW, null, null); + xml.setUserData(STAMPED, null, null); return xml; } - private Element header(Document xml, ReqIFHeader header) { + /** + * REQ-IF-HEADER is an xsd:sequence; only COMMENT and REPOSITORY-ID are + * optional, the other elements get a default if the header lacks them. + */ + private Element header(Document xml, ReqIFHeader header, String now) { Element theHeader = element(xml, ReqIFConst.THE_HEADER); Element reqifHeader = element(xml, ReqIFConst.REQ_IF_HEADER); @@ -153,11 +272,12 @@ private Element header(Document xml, ReqIFHeader header) { theHeader.appendChild(reqifHeader); appendTextIfPresent(xml, reqifHeader, ReqIFConst.COMMENT, header.getComment()); - appendTextIfPresent(xml, reqifHeader, ReqIFConst.CREATION_TIME, header.getCreationTime()); - appendTextIfPresent(xml, reqifHeader, ReqIFConst.REQ_IF_TOOL_ID, header.getToolID()); - appendTextIfPresent(xml, reqifHeader, ReqIFConst.REQ_IF_VERSION, header.getReqIFVersion()); - appendTextIfPresent(xml, reqifHeader, ReqIFConst.SOURCE_TOOL_ID, header.getSourceToolID()); - appendTextIfPresent(xml, reqifHeader, ReqIFConst.TITLE, header.getTitle()); + appendText(xml, reqifHeader, ReqIFConst.CREATION_TIME, orDefault(header.getCreationTime(), now)); + appendTextIfPresent(xml, reqifHeader, ReqIFConst.REPOSITORY_ID, header.getRepositoryID()); + appendText(xml, reqifHeader, ReqIFConst.REQ_IF_TOOL_ID, orDefault(header.getToolID(), DEFAULT_TOOL_ID)); + appendText(xml, reqifHeader, ReqIFConst.REQ_IF_VERSION, orDefault(header.getReqIFVersion(), REQIF_VERSION)); + appendText(xml, reqifHeader, ReqIFConst.SOURCE_TOOL_ID, orDefault(header.getSourceToolID(), DEFAULT_TOOL_ID)); + appendText(xml, reqifHeader, ReqIFConst.TITLE, nullToEmpty(header.getTitle())); return theHeader; } @@ -202,6 +322,13 @@ private Element coreContent(Document xml, ReqIFCoreContent content) { } reqifContent.appendChild(specRelations); + Element specifications = element(xml, ReqIFConst.SPECIFICATIONS); + for (Specification specification : content.getSpecifications().values()) { + specifications.appendChild(specification(xml, specification)); + } + reqifContent.appendChild(specifications); + + // REQ-IF-CONTENT is an xsd:sequence: the relation groups come last if (!content.getRelationGroups().isEmpty()) { Element relationGroups = element(xml, ReqIFConst.SPEC_RELATION_GROUPS); for (RelationGroup relationGroup : content.getRelationGroups().values()) { @@ -210,12 +337,6 @@ private Element coreContent(Document xml, ReqIFCoreContent content) { reqifContent.appendChild(relationGroups); } - Element specifications = element(xml, ReqIFConst.SPECIFICATIONS); - for (Specification specification : content.getSpecifications().values()) { - specifications.appendChild(specification(xml, specification)); - } - reqifContent.appendChild(specifications); - return coreContent; } @@ -233,15 +354,19 @@ private Element datatype(Document xml, Datatype datatype) { } Element definition = element(xml, elementName); - definition.setAttribute(ReqIFConst.IDENTIFIER, nullToEmpty(datatype.getID())); - definition.setAttribute(ReqIFConst.LONG_NAME, nullToEmpty(datatype.getName())); - appendAlternativeID(xml, definition, datatype.getAlternativeID()); + identifiable(xml, definition, datatype); if (datatype instanceof DatatypeInteger) { DatatypeInteger integer = (DatatypeInteger) datatype; definition.setAttribute(ReqIFConst.MIN, Long.toString(integer.getMin())); definition.setAttribute(ReqIFConst.MAX, Long.toString(integer.getMax())); + } else if (datatype instanceof DatatypeDouble) { + DatatypeDouble real = (DatatypeDouble) datatype; + definition.setAttribute(ReqIFConst.ACCURACY, Integer.toString(real.getAccuracy())); + definition.setAttribute(ReqIFConst.MIN, XsdTypes.formatDouble(real.getMin())); + definition.setAttribute(ReqIFConst.MAX, XsdTypes.formatDouble(real.getMax())); + } else if (datatype instanceof DatatypeString) { definition.setAttribute(ReqIFConst.MAX_LENGTH, Integer.toString(((DatatypeString) datatype).getMaxLength())); @@ -260,14 +385,12 @@ private Element datatype(Document xml, Datatype datatype) { private Element enumValue(Document xml, DatatypeEnumerationValue value) { Element enumValue = element(xml, ReqIFConst.ENUM_VALUE); - enumValue.setAttribute(ReqIFConst.IDENTIFIER, nullToEmpty(value.getID())); - enumValue.setAttribute(ReqIFConst.LONG_NAME, nullToEmpty(value.getName())); + identifiable(xml, enumValue, value); Element embedded = element(xml, ReqIFConst.EMBEDDED_VALUE); + // both attributes are required, OTHER-CONTENT even when empty embedded.setAttribute(ReqIFConst.KEY, nullToEmpty(value.getKey())); - if (value.getOtherContent() != null && !value.getOtherContent().isEmpty()) { - embedded.setAttribute(ReqIFConst.OTHER_CONTENT, value.getOtherContent()); - } + embedded.setAttribute(ReqIFConst.OTHER_CONTENT, nullToEmpty(value.getOtherContent())); Element properties = element(xml, ReqIFConst.PROPERTIES); properties.appendChild(embedded); @@ -291,9 +414,7 @@ private Element specType(Document xml, SpecType specType) { } Element type = element(xml, elementName); - type.setAttribute(ReqIFConst.IDENTIFIER, nullToEmpty(specType.getID())); - type.setAttribute(ReqIFConst.LONG_NAME, nullToEmpty(specType.getName())); - appendAlternativeID(xml, type, specType.getAlternativeID()); + identifiable(xml, type, specType); Element specAttributes = element(xml, ReqIFConst.SPEC_ATTRIBUTES); for (AttributeDefinition definition : specType.getAttributeDefinitions().values()) { @@ -310,26 +431,41 @@ private Element specType(Document xml, SpecType specType) { private Element attributeDefinition(Document xml, AttributeDefinition definition) { Datatype datatype = definition.getDataType(); - if (datatype == null) { - return null; - } - String elementName = ReqIFElements.attributeDefinition(datatype.getType()); - if (elementName == null) { - // datatype kinds the parser does not model: keep the original name - elementName = definition.getSourceElementName(); + if (datatype == null && definition.getUnresolvedElement() != null) { + // as read, with the properties the model may have changed + Element copy = asReqIF(xml, definition.getUnresolvedElement()); + for (String name : List.of(ReqIFConst.LONG_NAME, ReqIFConst.DESC, ReqIFConst.IS_EDITABLE)) { + copy.removeAttribute(name); + } + Element alternativeID = XmlUtils.firstChildElementByLocalName(copy, ReqIFConst.ALTERNATIVE_ID); + if (alternativeID != null) { + copy.removeChild(alternativeID); + } + Node first = copy.getFirstChild(); + identifiable(xml, copy, definition); + Element written = XmlUtils.firstChildElementByLocalName(copy, ReqIFConst.ALTERNATIVE_ID); + if (written != null && first != null) { + // the schema wants it first + copy.insertBefore(written, first); + } + setAttributeIfPresent(copy, ReqIFConst.IS_EDITABLE, toString(definition.getEditable())); + return copy; } - if (elementName == null) { + // datatype kinds the parser does not model keep their original name + String elementName = definition.getElementName(); + if (datatype == null || elementName == null) { return null; } Element attributeDefinition = element(xml, elementName); - attributeDefinition.setAttribute(ReqIFConst.IDENTIFIER, nullToEmpty(definition.getID())); - attributeDefinition.setAttribute(ReqIFConst.LONG_NAME, nullToEmpty(definition.getName())); - appendAlternativeID(xml, attributeDefinition, definition.getAlternativeID()); + identifiable(xml, attributeDefinition, definition); + setAttributeIfPresent(attributeDefinition, ReqIFConst.IS_EDITABLE, toString(definition.getEditable())); - if (definition instanceof AttributeDefinitionEnumeration - && ((AttributeDefinitionEnumeration) definition).isMultiValued()) { - attributeDefinition.setAttribute(ReqIFConst.MULTI_VALUED, "true"); + // required on every enumeration attribute definition, "false" included + if (ReqIFConst.ENUMERATION.equals(datatype.getType())) { + boolean multiValued = definition instanceof AttributeDefinitionEnumeration + && ((AttributeDefinitionEnumeration) definition).isMultiValued(); + attributeDefinition.setAttribute(ReqIFConst.MULTI_VALUED, Boolean.toString(multiValued)); } Element type = element(xml, ReqIFConst.TYPE); @@ -362,9 +498,24 @@ private Element defaultValue(Document xml, AttributeDefinition definition, Datat return defaultValue; } - if (definition.getDefaultValue() == null) { + // an enumeration default is a list of enum value references, which only + // an enumeration definition carries; text would be schema-invalid + if (definition.getDefaultValue() == null || ReqIFConst.ENUMERATION.equals(datatype.getType())) { return null; } + if (ReqIFConst.XHTML.equals(datatype.getType())) { + // an XHTML value carries its content in a THE-VALUE element, not in + // an attribute + AttributeValueXHTML parsed = definition.getDefaultXhtmlValue(); + Element value = xhtmlValue(xml, definition, + parsed != null ? parsed : new AttributeValueXHTML(definition.getDefaultValue(), definition)); + if (value == null) { + return null; + } + Element defaultValue = element(xml, ReqIFConst.DEFAULT_VALUE); + defaultValue.appendChild(value); + return defaultValue; + } String elementName = ReqIFElements.attributeValue(datatype.getType()); if (elementName == null) { return null; @@ -383,9 +534,8 @@ private Element defaultValue(Document xml, AttributeDefinition definition, Datat private Element specObject(Document xml, SpecObject specObject) { Element element = element(xml, ReqIFConst.SPEC_OBJECT); - element.setAttribute(ReqIFConst.IDENTIFIER, nullToEmpty(specObject.getID())); - appendAlternativeID(xml, element, specObject.getAlternativeID()); - element.appendChild(values(xml, specObject.getAttributes())); + identifiable(xml, element, specObject); + element.appendChild(values(xml, specObject.getValues())); element.appendChild(typeRef(xml, ReqIFConst.SPEC_OBJECT_TYPE, specObject.getSpecTypeID())); return element; } @@ -393,9 +543,8 @@ private Element specObject(Document xml, SpecObject specObject) { private Element specRelation(Document xml, SpecRelation specRelation) { Element element = element(xml, ReqIFConst.SPEC_RELATION); - element.setAttribute(ReqIFConst.IDENTIFIER, nullToEmpty(specRelation.getID())); - appendAlternativeID(xml, element, specRelation.getAlternativeID()); - element.appendChild(values(xml, specRelation.getAttributes())); + identifiable(xml, element, specRelation); + element.appendChild(values(xml, specRelation.getValues())); element.appendChild(typeRef(xml, ReqIFConst.SPEC_RELATION_TYPE, specRelation.getRelationTypeRef())); element.appendChild(objectRef(xml, ReqIFConst.SOURCE, specRelation.getSourceObjID())); element.appendChild(objectRef(xml, ReqIFConst.TARGET, specRelation.getTargetObjID())); @@ -405,10 +554,8 @@ private Element specRelation(Document xml, SpecRelation specRelation) { private Element specification(Document xml, Specification specification) { Element element = element(xml, ReqIFConst.SPECIFICATION); - element.setAttribute(ReqIFConst.IDENTIFIER, nullToEmpty(specification.getID())); - element.setAttribute(ReqIFConst.LONG_NAME, nullToEmpty(specification.getName())); - appendAlternativeID(xml, element, specification.getAlternativeID()); - element.appendChild(values(xml, specification.getAttributes())); + identifiable(xml, element, specification); + element.appendChild(values(xml, specification.getValues())); element.appendChild(typeRef(xml, ReqIFConst.SPECIFICATION_TYPE, specification.getSpecTypeID())); List children = specification.getChildren(); @@ -430,10 +577,27 @@ private Element children(Document xml, List hierarchies) { private Element specHierarchy(Document xml, SpecHierarchy hierarchy) { Element element = element(xml, ReqIFConst.SPEC_HIERARCHY); - element.setAttribute(ReqIFConst.IDENTIFIER, nullToEmpty(hierarchy.getSpecHierarchyID())); - appendAlternativeID(xml, element, hierarchy.getAlternativeID()); - - if (hierarchy.getSpecObject() != null) { + identifiable(xml, element, hierarchy); + + setAttributeIfPresent(element, ReqIFConst.IS_TABLE_INTERNAL, toString(hierarchy.getTableInternal())); + setAttributeIfPresent(element, ReqIFConst.IS_EDITABLE, toString(hierarchy.getEditable())); + if (!hierarchy.getEditableAttributeRefs().isEmpty()) { + Element editableAtts = element(xml, ReqIFConst.EDITABLE_ATTS); + SpecType objectType = hierarchy.getSpecObject() == null ? null + : hierarchy.getSpecObject().getResolvedSpecType(); + for (java.util.Map.Entry ref : hierarchy.getEditableAttributeRefs()) { + // the reference must name the kind the definition is written as + AttributeDefinition definition = objectType == null ? null + : objectType.getAttributeDefinition(ref.getKey()); + Element refElement = element(xml, definition != null && definition.getElementName() != null + ? definition.getElementName() + "-REF" : ref.getValue()); + refElement.setTextContent(ref.getKey()); + editableAtts.appendChild(refElement); + } + element.appendChild(editableAtts); + } + // written also when unresolved, so the reference is not lost + if (hierarchy.getSpecObjectID() != null) { element.appendChild(objectRef(xml, ReqIFConst.OBJECT, hierarchy.getSpecObjectID())); } List nested = hierarchy.getChildren(); @@ -448,14 +612,9 @@ private Element values(Document xml, java.util.Map attri Element values = element(xml, ReqIFConst.VALUES); - List sorted = new ArrayList(attributeValues.values()); - // deterministic output regardless of the map implementation - sorted.sort(Comparator.comparing(value -> { - AttributeDefinition definition = value.getAttributeDefinitionType(); - return definition == null || definition.getID() == null ? "" : definition.getID(); - })); - - for (AttributeValue attributeValue : sorted) { + // only the element's own values, in document order; defaults stay with + // their attribute definitions + for (AttributeValue attributeValue : attributeValues.values()) { Element written = attributeValue(xml, attributeValue); if (written != null) { values.appendChild(written); @@ -466,8 +625,12 @@ private Element values(Document xml, java.util.Map attri private Element attributeValue(Document xml, AttributeValue attributeValue) { + if (attributeValue instanceof UninterpretedAttributeValue) { + return asReqIF(xml, ((UninterpretedAttributeValue) attributeValue).getElement()); + } AttributeDefinition definition = attributeValue.getAttributeDefinitionType(); - if (definition == null || definition.getDataType() == null) { + // a definition that cannot be written cannot be referenced either + if (definition == null || definition.getDataType() == null || definition.getElementName() == null) { return null; } String datatypeCategory = definition.getDataType().getType(); @@ -476,11 +639,10 @@ private Element attributeValue(Document xml, AttributeValue attributeValue) { if (!(attributeValue instanceof AttributeValueEnumeration)) { return null; } - List refs = ((AttributeValueEnumeration) attributeValue).getValueRefs(); - if (refs.isEmpty()) { - return null; - } - return enumerationValue(xml, definition, refs); + // an empty selection is a value too ("nothing selected"); left out, the + // definition's default would apply when the document is read again + return enumerationValue(xml, definition, + ((AttributeValueEnumeration) attributeValue).getValueRefs()); } if (ReqIFConst.XHTML.equals(datatypeCategory)) { @@ -501,7 +663,8 @@ private Element attributeValue(Document xml, AttributeValue attributeValue) { } Element element = element(xml, elementName); - element.setAttribute(ReqIFConst.THE_VALUE, String.valueOf(value)); + element.setAttribute(ReqIFConst.THE_VALUE, + value instanceof Double ? XsdTypes.formatDouble((Double) value) : String.valueOf(value)); element.appendChild(definitionRef(xml, definition)); return element; } @@ -523,15 +686,29 @@ private Element enumerationValue(Document xml, AttributeDefinition definition, L private Element xhtmlValue(Document xml, AttributeDefinition definition, AttributeValue attributeValue) { + // THE-VALUE must hold an XHTML div or p; an attribute without content + // is left out instead of being written as an invalid empty value + Node content = xhtmlContent(xml, attributeValue); + if (content == null) { + return null; + } + Element element = element(xml, ReqIFElements.attributeValue(ReqIFConst.XHTML)); element.appendChild(definitionRef(xml, definition)); Element theValue = element(xml, ReqIFConst.THE_VALUE); - Node content = xhtmlContent(xml, attributeValue); - if (content != null) { - theValue.appendChild(content); - } + theValue.appendChild(content); element.appendChild(theValue); + + if (attributeValue instanceof AttributeValueXHTML) { + AttributeValueXHTML xhtml = (AttributeValueXHTML) attributeValue; + setAttributeIfPresent(element, ReqIFConst.IS_SIMPLIFIED, toString(xhtml.getSimplified())); + if (xhtml.getOriginalValue() != null && xhtml.getOriginalValue().getNode() != null) { + Element original = element(xml, ReqIFConst.THE_ORIGINAL_VALUE); + original.appendChild(xml.importNode(xhtml.getOriginalValue().getNode(), true)); + element.appendChild(original); + } + } return element; } @@ -555,14 +732,70 @@ private Node xhtmlContent(Document xml, AttributeValue attributeValue) { return xml.importNode(XhtmlParser.parseDiv(value.toString()), true); } + /** + * @return the schema location with the namespace of a ReqIF draft + * replaced by the one written; each even token is a namespace, + * each odd one the schema file for it + */ + private static String schemaLocation(String schemaLocation) { + String[] tokens = schemaLocation.trim().split("\\s+"); + for (int i = 0; i < tokens.length; i += 2) { + if (tokens[i].startsWith(REQIF_NAMESPACE_PREFIX)) { + tokens[i] = REQIF_NAMESPACE; + } + } + return String.join(" ", tokens); + } + + /** + * @return whether the element is a ReqIF one: in a ReqIF namespace, or - + * in a document without namespace - named like one (ReqIF element + * names are upper case, XHTML ones lower case) + */ + private static boolean isReqIFElement(Node node) { + String namespace = node.getNamespaceURI(); + if (namespace == null || namespace.isEmpty()) { + return XmlUtils.localName(node).matches("[A-Z][A-Z0-9-]*"); + } + return namespace.startsWith(REQIF_NAMESPACE_PREFIX); + } + + /** + * Copies an element read from a document. Its ReqIF elements are put into + * the namespace written - the source may use a prefix or an earlier ReqIF + * namespace - everything else, such as XHTML, stays as it is. + */ + private static Element asReqIF(Document xml, Element source) { + Element copy = (Element) xml.importNode(source, true); + return (Element) toReqIFNamespace(xml, copy); + } + + private static Node toReqIFNamespace(Document xml, Node node) { + Node result = node; + if (node.getNodeType() == Node.ELEMENT_NODE && isReqIFElement(node)) { + result = xml.renameNode(node, REQIF_NAMESPACE, XmlUtils.localName(node)); + } + if (result.getNodeType() == Node.ELEMENT_NODE) { + // declarations of a ReqIF namespace would contradict the renamed elements + NamedNodeMap attributes = result.getAttributes(); + for (int i = attributes.getLength() - 1; i >= 0; i--) { + Node attribute = attributes.item(i); + if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(attribute.getNamespaceURI()) + && attribute.getNodeValue().startsWith(REQIF_NAMESPACE_PREFIX)) { + ((Element) result).removeAttributeNode((org.w3c.dom.Attr) attribute); + } + } + } + for (Node child = result.getFirstChild(); child != null; child = child.getNextSibling()) { + child = toReqIFNamespace(xml, child); + } + return result; + } + private Element definitionRef(Document xml, AttributeDefinition definition) { Element definitionElement = element(xml, ReqIFConst.DEFINITION); - String refName = ReqIFElements.attributeDefinitionRef(definition.getDataType().getType()); - if (refName == null && definition.getSourceElementName() != null) { - refName = definition.getSourceElementName() + "-REF"; - } - Element ref = element(xml, refName); + Element ref = element(xml, definition.getElementName() + "-REF"); ref.setTextContent(nullToEmpty(definition.getID())); definitionElement.appendChild(ref); return definitionElement; @@ -589,9 +822,7 @@ private Element objectRef(Document xml, String wrapper, String specObjectID) { private Element relationGroup(Document xml, RelationGroup relationGroup) { Element element = element(xml, ReqIFConst.RELATION_GROUP); - element.setAttribute(ReqIFConst.IDENTIFIER, nullToEmpty(relationGroup.getID())); - element.setAttribute(ReqIFConst.LONG_NAME, nullToEmpty(relationGroup.getName())); - appendAlternativeID(xml, element, relationGroup.getAlternativeID()); + identifiable(xml, element, relationGroup); element.appendChild(wrappedRef(xml, ReqIFConst.TYPE, ReqIFConst.RELATION_GROUP_TYPE_REF, relationGroup.getRelationGroupTypeRef())); @@ -622,16 +853,46 @@ private Element wrappedRef(Document xml, String wrapper, String refName, String } /** - * ALTERNATIVE-ID is the first child of an identifiable element. + * Writes IDENTIFIER, LONG-NAME, DESC, LAST-CHANGE and ALTERNATIVE-ID. Optional + * properties the element does not have are left out rather than written + * empty, so a document round-trips unchanged; the required LAST-CHANGE gets + * the document's default. + * + * The ALTERNATIVE-ID is wrapped twice, as the schema prescribes: + * {@code }. + * It must be the first child, so call this right after creating the element. */ - private void appendAlternativeID(Document xml, Element parent, String alternativeID) { + private void identifiable(Document xml, Element element, Identifiable identifiable) { - if (alternativeID == null || alternativeID.isEmpty()) { - return; + element.setAttribute(ReqIFConst.IDENTIFIER, nullToEmpty(identifiable.getID())); + setAttributeIfPresent(element, ReqIFConst.LONG_NAME, identifiable.getName()); + setAttributeIfPresent(element, ReqIFConst.DESC, identifiable.getDesc()); + // required by the schema; a modified element changed now, see + // buildDocument for the default of one that never had a value + if (identifiable.isModified()) { + @SuppressWarnings("unchecked") + List stamped = (List) xml.getUserData(STAMPED); + stamped.add(identifiable); + } + String lastChange = identifiable.isModified() ? (String) xml.getUserData(NOW) + : isBlank(identifiable.getLastChange()) ? (String) xml.getUserData(DEFAULT_LAST_CHANGE) + : identifiable.getLastChange(); + element.setAttribute(ReqIFConst.LAST_CHANGE, lastChange); + + String alternativeID = identifiable.getAlternativeID(); + if (alternativeID != null && !alternativeID.isEmpty()) { + Element wrapper = element(xml, ReqIFConst.ALTERNATIVE_ID); + Element alternative = element(xml, ReqIFConst.ALTERNATIVE_ID); + alternative.setAttribute(ReqIFConst.IDENTIFIER, alternativeID); + wrapper.appendChild(alternative); + element.appendChild(wrapper); + } + } + + private static void setAttributeIfPresent(Element element, String name, String value) { + if (value != null) { + element.setAttribute(name, value); } - Element element = element(xml, ReqIFConst.ALTERNATIVE_ID); - element.setAttribute(ReqIFConst.IDENTIFIER, alternativeID); - parent.appendChild(element); } private Element element(Document xml, String name) { @@ -643,6 +904,11 @@ private void appendTextIfPresent(Document xml, Element parent, String name, Stri if (text == null || text.isEmpty()) { return; } + appendText(xml, parent, name, text); + } + + private void appendText(Document xml, Element parent, String name, String text) { + Element element = element(xml, name); element.setTextContent(text); parent.appendChild(element); @@ -652,6 +918,18 @@ private static String nullToEmpty(String value) { return value == null ? "" : value; } + private static String toString(Boolean value) { + return value == null ? null : value.toString(); + } + + private static String orDefault(String value, String fallback) { + return isBlank(value) ? fallback : value; + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } + private Document newDocument() { try { return SecureXml.newDocumentBuilder().newDocument(); diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFzWriter.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFzWriter.java index edbc8ac..4c4ad76 100644 --- a/src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFzWriter.java +++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/write/ReqIFzWriter.java @@ -6,7 +6,9 @@ import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -16,19 +18,19 @@ /** * Writes a .reqifz archive: one or more ReqIF documents together with the - * images they reference. + * files they reference - images, embedded OLE objects, PDFs or anything else. * - * Image entries are stored under the path the XHTML {@code object} elements - * reference (forward slashes), so a written archive resolves its images the - * same way a tool-generated one does: + * Attachments are stored under the path the XHTML {@code object} elements + * reference (forward slashes), so a written archive resolves them the same + * way a tool-generated one does: * *
- * Map<String, byte[]> pictures = Map.of("files/diagram.png", pngBytes);
- * new ReqIFzWriter().write(document, "spec.reqif", pictures, Path.of("spec.reqifz"));
+ * Map<String, byte[]> attachments = Map.of("files/diagram.png", pngBytes);
+ * new ReqIFzWriter().write(document, "spec.reqif", attachments, Path.of("spec.reqifz"));
  * 
* - * An archive that was read can be re-packed without consuming its picture - * streams, because {@link ReqIFz} keeps the extracted files: + * An archive that was read can be re-packed; every attachment is copied from + * the files {@link ReqIFz} extracted, without loading them into memory: * *
  * try (ReqIFz source = new ReqIFz("in.reqifz")) {
@@ -38,6 +40,11 @@
  */
 public class ReqIFzWriter {
 
+	/** The content of one archive entry. */
+	private interface Content {
+		void writeTo(OutputStream out) throws IOException;
+	}
+
 	private final ReqIFWriter documentWriter;
 
 	public ReqIFzWriter() {
@@ -53,96 +60,109 @@ public ReqIFzWriter(ReqIFWriter documentWriter) {
 
 
 	/**
-	 * Writes a single document without images.
+	 * Writes a single document without attachments.
 	 */
 	public void write(ReqIFDocument document, String entryName, Path archive) throws IOException {
 		write(document, entryName, null, archive);
 	}
 
 	/**
-	 * Writes a single document together with its images.
+	 * Writes a single document together with its attachments.
 	 *
-	 * @param entryName name of the ReqIF entry inside the archive,
-	 *                  e.g. {@code spec.reqif}
-	 * @param pictures  image data keyed by the archive path referenced from the
-	 *                  XHTML object elements, e.g. {@code files/diagram.png};
-	 *                  may be null
+	 * @param entryName   name of the ReqIF entry inside the archive,
+	 *                    e.g. {@code spec.reqif}
+	 * @param attachments file contents keyed by the archive path referenced
+	 *                    from the XHTML object elements, e.g.
+	 *                    {@code files/diagram.png}; may be null
 	 */
-	public void write(ReqIFDocument document, String entryName, Map pictures, Path archive)
+	public void write(ReqIFDocument document, String entryName, Map attachments, Path archive)
 			throws IOException {
 
 		Map documents = new LinkedHashMap();
 		documents.put(normalizeEntryName(entryName), document);
-		write(documents, pictures, archive);
+		write(documents, attachments, archive);
 	}
 
 	/**
-	 * Writes several documents and their images into one archive.
+	 * Writes several documents and their attachments into one archive.
 	 */
-	public void write(Map documents, Map pictures, Path archive)
+	public void write(Map documents, Map attachments, Path archive)
 			throws IOException {
 
-		if (documents == null || documents.isEmpty()) {
-			throw new ReqIFWriteException("A .reqifz archive must contain at least one ReqIF document");
-		}
-
-		// Collect and check the names first: a zip cannot hold the same entry
-		// twice, and the raw ZipException would not say which name clashed.
-		Map entries = new LinkedHashMap();
-		for (Map.Entry document : documents.entrySet()) {
-			addEntry(entries, normalizeEntryName(document.getKey()), documentBytes(document.getValue()));
-		}
-		if (pictures != null) {
-			for (Map.Entry picture : pictures.entrySet()) {
-				addEntry(entries, normalizeEntryName(picture.getKey()), picture.getValue());
-			}
-		}
-
-		try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(archive))) {
-			for (Map.Entry entry : entries.entrySet()) {
-				writeEntry(zip, entry.getKey(), entry.getValue());
+		Map contents = new LinkedHashMap();
+		if (attachments != null) {
+			for (Map.Entry attachment : attachments.entrySet()) {
+				byte[] bytes = attachment.getValue();
+				contents.put(attachment.getKey(), out -> {
+					if (bytes != null) {
+						out.write(bytes);
+					}
+				});
 			}
 		}
+		writeArchive(documents, contents, archive);
 	}
 
 	/**
 	 * Re-packs an archive that was read: the documents are serialized from the
-	 * object model (so modifications are included) while the images are copied
-	 * from the extracted files.
+	 * object model (so modifications are included) while every other entry -
+	 * images, OLE objects, PDFs, ... - is copied unchanged from the extracted
+	 * files.
 	 */
 	public void write(ReqIFz source, Path archive) throws IOException {
 
-		Map pictures = new LinkedHashMap();
-		for (Map.Entry picture : source.getPictureFiles().entrySet()) {
-			pictures.put(picture.getKey(), Files.readAllBytes(picture.getValue().toPath()));
+		Map contents = new LinkedHashMap();
+		for (Map.Entry attachment : source.getAttachmentFiles().entrySet()) {
+			Path file = attachment.getValue().toPath();
+			contents.put(attachment.getKey(), out -> Files.copy(file, out));
 		}
-		write(source.getReqIFDocuments(), pictures, archive);
+		writeArchive(source.getReqIFDocuments(), contents, archive);
 	}
 
 
-	private static void addEntry(Map entries, String name, byte[] content) {
+	private void writeArchive(Map documents, Map attachments, Path archive)
+			throws IOException {
 
-		if (entries.containsKey(name)) {
-			throw new ReqIFWriteException("Archive entry '" + name + "' is added twice; "
-					+ "document and picture names must be unique within the archive");
+		if (documents == null || documents.isEmpty()) {
+			throw new ReqIFWriteException("A .reqifz archive must contain at least one ReqIF document");
 		}
-		entries.put(name, content);
-	}
 
-	private byte[] documentBytes(ReqIFDocument document) throws IOException {
+		// Collect and check the names first: a zip cannot hold the same entry
+		// twice, and the raw ZipException would not say which name clashed.
+		// Documents are serialized up front, so a failure leaves no half
+		// written archive behind.
+		Map entries = new LinkedHashMap();
+		List stamps = new ArrayList();
+		for (Map.Entry document : documents.entrySet()) {
+			ByteArrayOutputStream serialized = new ByteArrayOutputStream();
+			stamps.add(this.documentWriter.serialize(document.getValue(), serialized));
+			byte[] bytes = serialized.toByteArray();
+			addEntry(entries, normalizeEntryName(document.getKey()), out -> out.write(bytes));
+		}
+		for (Map.Entry attachment : attachments.entrySet()) {
+			addEntry(entries, normalizeEntryName(attachment.getKey()), attachment.getValue());
+		}
 
-		ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-		this.documentWriter.write(document, (OutputStream) bytes);
-		return bytes.toByteArray();
+		try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(archive))) {
+			for (Map.Entry entry : entries.entrySet()) {
+				zip.putNextEntry(new ZipEntry(entry.getKey()));
+				entry.getValue().writeTo(zip);
+				zip.closeEntry();
+			}
+		}
+		// the documents are saved: modified elements keep the written LAST-CHANGE
+		for (ReqIFWriter.Stamps written : stamps) {
+			written.apply();
+		}
 	}
 
-	private static void writeEntry(ZipOutputStream zip, String name, byte[] content) throws IOException {
+	private static void addEntry(Map entries, String name, Content content) {
 
-		zip.putNextEntry(new ZipEntry(name));
-		if (content != null) {
-			zip.write(content);
+		if (entries.containsKey(name)) {
+			throw new ReqIFWriteException("Archive entry '" + name + "' is added twice; "
+					+ "document and attachment names must be unique within the archive");
 		}
-		zip.closeEntry();
+		entries.put(name, content);
 	}
 
 	/**
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java
index 3b82a7c..db61769 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLElement.java
@@ -4,7 +4,6 @@
 import java.util.List;
 
 import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
 
 import de.uni_stuttgart.ils.reqif4j.attributes.XHTML;
 import de.uni_stuttgart.ils.reqif4j.util.XmlUtils;
@@ -43,11 +42,8 @@ public XHTMLElement(Node xhtmlElement, XHTMLNode parent) {
 	
 	private void addChildren(Node xhtmlElement) {
 		
-		NodeList children = xhtmlElement.getChildNodes();
-		
-		for(int child = 0; child < children.getLength(); child++) {
-				
-			Node childNode = children.item(child);
+		for(Node childNode = xhtmlElement.getFirstChild(); childNode != null; childNode = childNode.getNextSibling()) {
+
 			// 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);
diff --git a/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java
index d822535..6fbed73 100644
--- a/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java
+++ b/src/main/java/de/uni_stuttgart/ils/reqif4j/xhtml/XHTMLNode.java
@@ -1,5 +1,7 @@
 package de.uni_stuttgart.ils.reqif4j.xhtml;
 
+import javax.xml.XMLConstants;
+
 import org.w3c.dom.NamedNodeMap;
 import org.w3c.dom.Node;
 
@@ -97,14 +99,47 @@ protected String attributesToString() {
 
 		StringBuilder sb = new StringBuilder();
 		NamedNodeMap attributes = this.node.getAttributes();
+		Set plainNames = new HashSet();
+		for(int attribute = 0; attribute < attributes.getLength(); attribute++) {
+			String namespace = attributes.item(attribute).getNamespaceURI();
+			if(namespace == null || namespace.isEmpty()) {
+				plainNames.add(XmlUtils.localName(attributes.item(attribute)));
+			}
+		}
+		Set declaredPrefixes = new HashSet();
 		for(int attribute = 0; attribute < attributes.getLength(); attribute++) {
 
 			Node attributeNode = attributes.item(attribute);
 			String name = attributeNode.getNodeName();
-			if(name.equals("xmlns") || name.startsWith("xmlns:")) {
+			if(name.equals(XMLConstants.XMLNS_ATTRIBUTE) || name.startsWith(XMLConstants.XMLNS_ATTRIBUTE + ":")) {
 				continue;
 			}
-			sb.append(' ').append(XmlUtils.localName(attributeNode))
+			String namespace = attributeNode.getNamespaceURI();
+			String localName = XmlUtils.localName(attributeNode);
+			String attributeName;
+			if(namespace == null || namespace.isEmpty()) {
+				attributeName = localName;
+			} else if(XMLConstants.XML_NS_URI.equals(namespace)) {
+				// xml:lang and xml:space keep their prefix: "lang" next to "xml:lang"
+				// would be a duplicate attribute, and the XHTML schema has no "space"
+				attributeName = XMLConstants.XML_NS_PREFIX + ":" + localName;
+			} else if(de.uni_stuttgart.ils.reqif4j.util.XhtmlParser.XHTML_NAMESPACE.equals(namespace)) {
+				// xhtml:class means class; next to a plain class it is a duplicate
+				if(plainNames.contains(localName)) {
+					continue;
+				}
+				attributeName = localName;
+			} else {
+				// an attribute of another vocabulary keeps its namespace, so the
+				// markup stays well-formed and nothing is lost
+				String prefix = attributeNode.getPrefix() == null ? "ns" : attributeNode.getPrefix();
+				if(declaredPrefixes.add(prefix)) {
+					sb.append(' ').append(XMLConstants.XMLNS_ATTRIBUTE).append(':').append(prefix)
+					  .append("=\"").append(escapeAttribute(namespace)).append('"');
+				}
+				attributeName = prefix + ":" + localName;
+			}
+			sb.append(' ').append(attributeName)
 			  .append("=\"").append(escapeAttribute(attributeNode.getNodeValue())).append('"');
 		}
 		return sb.toString();
diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/BuilderCompletenessTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/BuilderCompletenessTest.java
new file mode 100644
index 0000000..88e8b8c
--- /dev/null
+++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/BuilderCompletenessTest.java
@@ -0,0 +1,255 @@
+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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Path;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.List;
+
+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.build.ReqIFBuildException;
+import de.uni_stuttgart.ils.reqif4j.build.ReqIFBuilder;
+import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF;
+import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent;
+import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument;
+import de.uni_stuttgart.ils.reqif4j.specification.RelationGroup;
+import de.uni_stuttgart.ils.reqif4j.specification.SpecHierarchy;
+import de.uni_stuttgart.ils.reqif4j.specification.SpecObject;
+import de.uni_stuttgart.ils.reqif4j.validate.ReqIFValidator;
+import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter;
+
+/**
+ * Everything the parser reads can also be built: relation groups, the common
+ * element properties, the document language, tool extensions, editability
+ * and simplified XHTML values.
+ */
+class BuilderCompletenessTest {
+
+	private static final Clock FIXED = Clock.fixed(Instant.parse("2026-10-11T12:34:56Z"), ZoneOffset.UTC);
+
+	private static ReqIFBuilder base() {
+		return ReqIFBuilder.create()
+				.header(h -> h.id("hdr").creationTime("2026-07-23T10:00:00Z"))
+				.stringDatatype("dt-string", "String", 255)
+				.xhtmlDatatype("dt-xhtml", "XHTML")
+				.specObjectType("st-req", "Requirement", t -> t
+						.stringAttribute("ad-id", "ReqIF.ForeignID", "dt-string")
+						.xhtmlAttribute("ad-text", "ReqIF.Text", "dt-xhtml")
+						.editable("ad-id", false)
+						.editable("ad-text", true))
+				.specificationType("st-spec", "Specification", t -> t
+						.xhtmlAttribute("ad-spec-text", "Text", "dt-xhtml"))
+				.specRelationType("st-rel", "refines", t -> { })
+				.relationGroupType("st-group", "Trace", t -> { })
+				.specObject("so-1", "st-req", o -> o
+						.set("ad-id", "R-1")
+						.setXhtml("ad-text", "

Preview

", + "

The complete content

")) + .specObject("so-2", "st-req", o -> o.set("ad-id", "R-2")) + .specRelation("sr-1", "st-rel", "so-2", "so-1") + .specification("spec-1", "System", "st-spec", s -> s + .setXhtml("ad-spec-text", "

About

") + .child("sh-1", "so-1", h -> h + .tableInternal(false) + .editable(true) + .editableAttributes("ad-text") + .child("sh-2", "so-2"))) + .specification("spec-2", "Software", "st-spec", s -> s.child("sh-3", "so-2")); + } + + private static ReqIFDocument full() { + return base() + .relationGroup("rg-1", "System to software", "st-group", "spec-1", "spec-2", "sr-1") + .with("so-1", e -> e.setDesc("Boot").setAlternativeID("REQ-1").setLastChange("2026-07-01T08:00:00Z")) + .with("sh-2", e -> e.setName("Child")) + .with("ad-id", e -> e.setDesc("Foreign key")) + .with("rg-1", e -> e.setDesc("Trace view")) + .lang("de") + .toolExtension("") + .toolExtension("ID") + .build(); + } + + @Test + void everythingBuiltIsWrittenSchemaValidAndReadBack(@TempDir Path dir) throws Exception { + + ReqIFDocument document = full(); + assertTrue(new ReqIFValidator().validate(document).isValid(), new ReqIFValidator().validate(document).toString()); + OfficialReqIFSchema.assertSchemaValid(document); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + ReqIFDocument read = new ReqIF(TestFixtures.write(dir, "built.reqif", xml).toString()).getReqIFDocument(); + ReqIFCoreContent content = read.getCoreContent(); + + RelationGroup group = content.getRelationGroup("rg-1"); + assertEquals("st-group", group.getRelationGroupTypeRef()); + assertEquals("spec-1", group.getSourceSpecificationRef()); + assertEquals("spec-2", group.getTargetSpecificationRef()); + assertEquals(List.of("sr-1"), group.getSpecRelationRefs()); + assertEquals("Trace view", group.getDesc()); + + SpecObject so1 = content.getSpecObject("so-1"); + assertEquals("Boot", so1.getDesc()); + assertEquals("REQ-1", so1.getAlternativeID()); + assertEquals("2026-07-01T08:00:00Z", so1.getLastChange(), "set explicitly"); + assertEquals("2026-07-23T10:00:00Z", content.getSpecObject("so-2").getLastChange(), "the default"); + assertEquals("Child", content.getSpecHierarchy("sh-2").getName()); + assertEquals("Foreign key", content.getSpecType("st-req").getAttributeDefinition("ad-id").getDesc()); + assertEquals(Boolean.FALSE, content.getSpecType("st-req").getAttributeDefinition("ad-id").getEditable()); + assertEquals(Boolean.TRUE, content.getSpecType("st-req").getAttributeDefinition("ad-text").getEditable()); + + SpecHierarchy sh1 = content.getSpecHierarchy("sh-1"); + assertEquals(Boolean.FALSE, sh1.getTableInternal()); + assertEquals(Boolean.TRUE, sh1.getEditable()); + assertEquals(List.of("ad-text"), sh1.getEditableAttributeIDs()); + assertEquals("ATTRIBUTE-DEFINITION-XHTML-REF", sh1.getEditableAttributeRefs().get(0).getValue()); + + AttributeValueXHTML text = (AttributeValueXHTML) so1.getValues().get("ad-text"); + assertEquals(Boolean.TRUE, text.getSimplified()); + assertNotNull(text.getOriginalValue()); + assertTrue(text.getOriginalValue().toString().contains("complete"), text.getOriginalValue().toString()); + assertTrue(text.getDivValue().toString().contains("Preview")); + + assertEquals("de", read.getLang()); + assertEquals(1, read.getToolExtensions().size(), "one TOOL-EXTENSIONS holds both"); + assertTrue(xml.contains("ID"), xml); + } + + @Test + void buildingTwiceWritesTheSame() { + ReqIFWriter writer = new ReqIFWriter().setClock(FIXED); + assertEquals(writer.toXml(full()), writer.toXml(full())); + } + + @Test + void relationGroupsReferenceWhatExists() { + + assertThrows(ReqIFBuildException.class, + () -> base().relationGroup("rg", null, "st-rel", "spec-1", "spec-2"), "not a group type"); + assertThrows(ReqIFBuildException.class, + () -> base().relationGroup("rg", null, "st-group", "spec-9", "spec-2")); + assertThrows(ReqIFBuildException.class, + () -> base().relationGroup("rg", null, "st-group", "spec-1", "spec-9")); + assertThrows(ReqIFBuildException.class, + () -> base().relationGroup("rg", null, "st-group", "spec-1", "spec-2", "sr-9")); + assertThrows(ReqIFBuildException.class, + () -> base().relationGroup("so-1", null, "st-group", "spec-1", "spec-2"), "identifier taken"); + + // a group without relations is fine + ReqIFDocument document = base().relationGroup("rg", null, "st-group", "spec-1", "spec-2").build(); + assertEquals(List.of(), document.getCoreContent().getRelationGroup("rg").getSpecRelationRefs()); + } + + @Test + void aFailedRelationGroupLeavesItsIdentifierFree() { + ReqIFBuilder builder = base(); + assertThrows(ReqIFBuildException.class, () -> builder.relationGroup("rg", null, "st-rel", "spec-1", "spec-2")); + builder.relationGroup("rg", null, "st-group", "spec-1", "spec-2"); + assertNotNull(builder.build().getCoreContent().getRelationGroup("rg")); + } + + @Test + void emptyToolExtensionsAndSpecificationValuesAreBuilt() throws Exception { + ReqIFDocument document = base() + .specification("spec-3", null, "st-spec", s -> s.setXhtml("ad-spec-text", "

Preview

", "

All

")) + .toolExtension("") + .toolExtension("") + .build(); + OfficialReqIFSchema.assertSchemaValid(document); + AttributeValueXHTML value = (AttributeValueXHTML) document.getCoreContent().getSpecification("spec-3") + .getValues().get("ad-spec-text"); + assertEquals(Boolean.TRUE, value.getSimplified()); + } + + @Test + void editableAttributesBelongToTheObjectsType(@TempDir Path dir) throws Exception { + + ReqIFDocument document = full(); + SpecHierarchy sh1 = document.getCoreContent().getSpecHierarchy("sh-1"); + assertThrows(IllegalArgumentException.class, () -> sh1.setEditableAttributes( + List.of(document.getCoreContent().getSpecType("st-spec").getAttributeDefinition("ad-spec-text")))); + sh1.setEditableAttributes(null); + assertEquals(List.of(), sh1.getEditableAttributeIDs()); + + // as read from a file + String xml = new ReqIFWriter().setClock(FIXED).toXml(full()); + String otherType = xml.replace( + "ad-text", + "ad-spec-text"); + ReqIFDocument read = new ReqIF(TestFixtures.write(dir, "type.reqif", otherType).toString()).getReqIFDocument(); + String report = new ReqIFValidator().validate(read).toString(); + assertTrue(report.contains("not an attribute of spec type st-req"), report); + } + + @Test + void editableAttributeReferencesAreWrittenAsTheirDefinitionsKind(@TempDir Path dir) throws Exception { + + String xml = new ReqIFWriter().setClock(FIXED).toXml(full()).replace( + "ad-text", + "ad-text"); + ReqIFDocument wrongKind = new ReqIF(TestFixtures.write(dir, "kind.reqif", xml).toString()).getReqIFDocument(); + + String written = new ReqIFWriter().setClock(FIXED).toXml(wrongKind); + + assertTrue(written.contains("ad-text"), + written); + OfficialReqIFSchema.assertSchemaValid(wrongKind); + } + + @Test + void aFailedCallLeavesItsIdentifiersFree() { + ReqIFBuilder builder = base(); + assertThrows(ReqIFBuildException.class, () -> builder.specification("spec-x", null, "st-spec", + s -> s.child("sh-x", "so-1", h -> h.editableAttributes("ad-none")))); + builder.specification("spec-x", null, "st-spec", s -> s.child("sh-x", "so-1")); + assertNotNull(builder.build().getCoreContent().getSpecHierarchy("sh-x")); + } + + @Test + void elementsHaveTypesOfTheirKind() { + assertThrows(ReqIFBuildException.class, () -> base().specObject("so-x", "st-group")); + assertThrows(ReqIFBuildException.class, () -> base().specObject("so-x", "st-spec")); + assertThrows(ReqIFBuildException.class, () -> base().specRelation("sr-x", "st-req", "so-1", "so-2")); + assertThrows(ReqIFBuildException.class, () -> base().specification("spec-x", null, "st-rel", s -> { })); + } + + @Test + void anInvalidLastChangeIsRefused() { + ReqIFBuilder builder = base().with("so-1", e -> e.setLastChange("2026-07-01T08:00:00Z")); + assertThrows(ReqIFBuildException.class, () -> builder.with("so-1", e -> e.setLastChange("yesterday"))); + assertEquals("2026-07-01T08:00:00Z", builder.build().getCoreContent().getSpecObject("so-1").getLastChange()); + } + + @Test + void unknownElementsAndBadExtensionsAreRefused() { + + assertThrows(ReqIFBuildException.class, () -> base().with("nothing", e -> e.setDesc("x"))); + assertThrows(ReqIFBuildException.class, () -> base().toolExtension("")); + assertThrows(ReqIFBuildException.class, () -> base().toolExtension(""), "no namespace"); + assertThrows(ReqIFBuildException.class, () -> base().toolExtension("vendor blob"), "text"); + assertThrows(ReqIFBuildException.class, () -> base().toolExtension( + ""), "ReqIF's namespace"); + assertThrows(ReqIFBuildException.class, () -> base().specObject("so-x", "st-req", + o -> o.setXhtml("ad-text", "", "

b

")), "an empty simplified value"); + assertThrows(ReqIFBuildException.class, () -> base().toolExtension( + "
"), "must not break out of its element"); + assertThrows(ReqIFBuildException.class, () -> base().specObjectType("st-x", null, + t -> t.editable("ad-none", true))); + assertThrows(ReqIFBuildException.class, () -> base().specification("spec-x", null, "st-spec", + s -> s.child("sh-x", "so-1", h -> h.editableAttributes("ad-spec-text"))), + "not an attribute of the object's type"); + assertThrows(ReqIFBuildException.class, () -> base().specObject("so-x", "st-req", + o -> o.setXhtml("ad-text", "

a

", "")), "an empty original"); + assertThrows(ReqIFBuildException.class, () -> base().specObject("so-x", "st-req", + o -> o.setXhtml("ad-id", "

a

", "

b

")), "not an XHTML attribute"); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/CanonicalXml.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/CanonicalXml.java new file mode 100644 index 0000000..465d517 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/CanonicalXml.java @@ -0,0 +1,143 @@ +package de.uni_stuttgart.ils.reqif4j; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeMap; + +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import de.uni_stuttgart.ils.reqif4j.util.SecureXml; + +/** + * Renders a ReqIF document as an indented tree that is independent of + * everything the ReqIF schema leaves open, so two documents with the same + * content compare equal: + * + *
    + *
  • namespace prefixes and declarations (names are compared by namespace + * URI and local name), attribute order, and indentation;
  • + *
  • the order of child elements that all have different names - the + * ReqIF elements are xsd:all groups - while repeated elements (lists such as + * VALUES or CHILDREN) keep their order;
  • + *
  • empty elements without attributes, e.g. an empty VALUES or + * SPEC-ATTRIBUTES, which carry no content.
  • + *
+ * + * XHTML content is compared exactly: element order, text and whitespace. + */ +final class CanonicalXml { + + private static final String XHTML = "http://www.w3.org/1999/xhtml"; + private static final String XMLNS = "http://www.w3.org/2000/xmlns/"; + + private CanonicalXml() { + } + + static String of(String xml) throws Exception { + Document document = SecureXml.newDocumentBuilder() + .parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))); + StringBuilder out = new StringBuilder(); + render(document.getDocumentElement(), 0, out); + return out.toString(); + } + + private static void render(Element element, int depth, StringBuilder out) { + + boolean xhtml = XHTML.equals(element.getNamespaceURI()); + indent(depth, out); + out.append('<').append(name(element)); + + TreeMap attributes = new TreeMap(); + NamedNodeMap map = element.getAttributes(); + for (int i = 0; i < map.getLength(); i++) { + Attr attribute = (Attr) map.item(i); + if (!XMLNS.equals(attribute.getNamespaceURI())) { + attributes.put(name(attribute), attribute.getValue()); + } + } + attributes.forEach((key, value) -> out.append(' ').append(key).append("=\"").append(value).append('"')); + out.append(">\n"); + + List children = new ArrayList(); + for (Node child = element.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + if (xhtml || !isEmpty((Element) child)) { + children.add(child); + } + } else if (child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE) { + String text = child.getNodeValue(); + if (xhtml || !text.isBlank()) { + children.add(child); + } + } + } + if (!xhtml && distinctElementNames(children)) { + children.sort((a, b) -> name(a).compareTo(name(b))); + } + + for (Node child : children) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + render((Element) child, depth + 1, out); + } else { + indent(depth + 1, out); + String text = child.getNodeValue(); + out.append("\"").append(xhtml ? text : text.trim()).append("\"\n"); + } + } + } + + private static boolean isEmpty(Element element) { + return !hasAttributes(element) && !element.hasChildNodes() + || !hasAttributes(element) && element.getTextContent().isBlank() && !hasElementChild(element); + } + + /** namespace declarations are not attributes of the content */ + private static boolean hasAttributes(Element element) { + NamedNodeMap attributes = element.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + if (!javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(attributes.item(i).getNamespaceURI())) { + return true; + } + } + return false; + } + + private static boolean hasElementChild(Element element) { + for (Node child = element.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + return true; + } + } + return false; + } + + private static boolean distinctElementNames(List children) { + Set names = new HashSet(); + for (Node child : children) { + if (child.getNodeType() != Node.ELEMENT_NODE || !names.add(name(child))) { + return false; + } + } + return true; + } + + private static String name(Node node) { + String namespace = node.getNamespaceURI(); + String local = node.getLocalName() != null ? node.getLocalName() : node.getNodeName(); + return namespace == null ? local : "{" + namespace + "}" + local; + } + + private static void indent(int depth, StringBuilder out) { + for (int i = 0; i < depth; i++) { + out.append(" "); + } + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/CorpusTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/CorpusTest.java new file mode 100644 index 0000000..f4886e7 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/CorpusTest.java @@ -0,0 +1,216 @@ +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.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.io.TempDir; + +import de.uni_stuttgart.ils.reqif4j.attributes.UninterpretedAttributeValue; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFz; +import de.uni_stuttgart.ils.reqif4j.specification.SpecObject; +import de.uni_stuttgart.ils.reqif4j.validate.ReqIFValidator; +import de.uni_stuttgart.ils.reqif4j.validate.ValidationResult; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFzWriter; + +/** + * Exports of real tools (DOORS, Polarion, ReqIF Studio, Enterprise Architect, + * Capella, StrictDoc) and the test cases of the ReqIF Implementor Forum, see + * src/test/resources/corpus/README.md. Many of them are not schema-valid - + * excerpts that reference types they do not contain, duplicate identifiers - + * which is what makes them worth testing against. + */ +class CorpusTest { + + private static final Path CORPUS = Path.of("src", "test", "resources", "corpus"); + private static final Clock FIXED = Clock.fixed(Instant.parse("2026-10-11T12:00:00Z"), ZoneOffset.UTC); + + /** documents whose rewrite differs from the original, and why */ + private static final Map NOT_LOSSLESS = Map.of( + "StrictDoc/01_example/sample.reqif", + "lacks the required LAST-CHANGE and MAX-LENGTH, which the writer fills in", + "org.eclipse.rmf/01_specRelationTest/sample.reqif", + "uses the namespace of a ReqIF draft (20101201); it is written in the ReqIF 1.2 namespace", + "org.eclipse.rmf/02_sample/sample.reqif", + "uses the namespace of a ReqIF draft (20101201); it is written in the ReqIF 1.2 namespace"); + + private static List files(String extension) throws IOException { + try (Stream files = Files.walk(CORPUS)) { + return files.filter(file -> file.toString().endsWith(extension)).sorted().collect(Collectors.toList()); + } + } + + private static String name(Path file) { + return CORPUS.relativize(file).toString().replace('\\', '/'); + } + + private static String write(ReqIFDocument document) { + return new ReqIFWriter().setClock(FIXED).toXml(document); + } + + private static ReqIFDocument read(Path dir, String name, String xml) throws IOException { + return new ReqIF(TestFixtures.write(dir, name, xml).toString()).getReqIFDocument(); + } + + @TestFactory + Stream everyDocumentIsWrittenBackAsRead(@TempDir Path dir) throws IOException { + return files(".reqif").stream().map(file -> DynamicTest.dynamicTest(name(file), () -> { + + String original = Files.readString(file, StandardCharsets.UTF_8); + String written = write(new ReqIF(file.toString()).getReqIFDocument()); + + String reason = NOT_LOSSLESS.get(name(file)); + if (reason == null) { + assertEquals(CanonicalXml.of(original), CanonicalXml.of(written), "not lossless"); + } else { + // listed: once it becomes lossless, the entry must go + assertNotEquals(CanonicalXml.of(original), CanonicalXml.of(written), reason); + } + // writing what was written changes nothing + assertEquals(written, write(read(dir, "again.reqif", written)), "not stable"); + })); + } + + /** + * The validator's contract: a document without errors is written + * schema-valid; and it raises no errors for a schema-valid file. + */ + @TestFactory + Stream theValidatorAgreesWithTheSchema(@TempDir Path dir) throws IOException { + return files(".reqif").stream().map(file -> DynamicTest.dynamicTest(name(file), () -> { + + Path xsd = OfficialReqIFSchema.reqifXsd(); + ReqIFValidator validator = new ReqIFValidator(); + ReqIFDocument document = new ReqIF(file.toString()).getReqIFDocument(); + ValidationResult model = validator.validate(document); + boolean inputValid = validator.validateAgainstSchema(file, xsd).isValid(); + ValidationResult output = validator.validateAgainstSchema(TestFixtures.write(dir, "out.reqif", write(document)), xsd); + + if (model.isValid()) { + assertTrue(output.isValid(), "no errors reported, but the output is not schema-valid:\n" + output); + } + if (inputValid) { + assertTrue(model.isValid(), "errors reported for a schema-valid file:\n" + model); + } + })); + } + + @TestFactory + Stream everyArchiveIsRepackedWithAllItsEntries(@TempDir Path dir) throws IOException { + return files(".reqifz").stream().map(file -> DynamicTest.dynamicTest(name(file), () -> { + + Path repacked = dir.resolve(file.getParent().getFileName() + ".reqifz"); + try (ReqIFz archive = new ReqIFz(file.toString())) { + new ReqIFzWriter(new ReqIFWriter().setClock(FIXED)).write(archive, repacked); + } + Map before = entries(file); + Map after = entries(repacked); + assertEquals(before.keySet(), after.keySet()); + for (Map.Entry entry : before.entrySet()) { + String other = after.get(entry.getKey()); + if (entry.getKey().endsWith(".reqif")) { + assertEquals(CanonicalXml.of(entry.getValue()), CanonicalXml.of(other), entry.getKey()); + } else { + assertEquals(entry.getValue(), other, entry.getKey()); + } + } + })); + } + + /** archive entries, documents as text and attachments as their bytes in hex */ + private static Map entries(Path archive) throws IOException { + Map entries = new TreeMap(); + try (ZipFile zip = new ZipFile(archive.toFile())) { + for (ZipEntry entry : java.util.Collections.list(zip.entries())) { + if (entry.isDirectory()) { + continue; + } + try (InputStream in = zip.getInputStream(entry)) { + byte[] bytes = in.readAllBytes(); + entries.put(entry.getName(), entry.getName().endsWith(".reqif") + ? new String(bytes, StandardCharsets.UTF_8) + : java.util.HexFormat.of().formatHex(bytes)); + } + } + } + return entries; + } + + @Test + void relationsMayConnectTheDocumentsOfAnArchive() throws IOException { + + // ReqIF Implementor Forum TC1700: the relation of one document connects + // the objects of the other (GLOBAL-REF) + try (ReqIFz archive = new ReqIFz(CORPUS.resolve("ci.eclipse.org/TC1700/sample.reqifz").toString())) { + ReqIFDocument relations = archive.getReqIFDocuments().values().stream() + .filter(document -> !document.getCoreContent().getSpecRelation().isEmpty()).findFirst().get(); + + assertFalse(new ReqIFValidator().validate(relations).isValid(), "on its own, the objects are missing"); + assertTrue(new ReqIFValidator().validate(archive).isValid()); + } + } + + @Test + void valuesThatDoNotFitTheirDefinitionAreKeptAsWritten() throws IOException { + + // Polarion writes some ReqIF.Text values as ATTRIBUTE-VALUE-STRING, + // although the attribute is XHTML + ReqIFDocument document = new ReqIF(CORPUS.resolve("Polarion/02_example/sample.reqif").toString()).getReqIFDocument(); + SpecObject object = document.getCoreContent().getSpecObjects().values().stream() + .filter(o -> o.getValue("rmf-a6468304-cb04-41a2-b714-f2531c8acc4b") instanceof UninterpretedAttributeValue) + .findFirst().get(); + + assertEquals("ATTRIBUTE-VALUE-STRING", + object.getValue("rmf-a6468304-cb04-41a2-b714-f2531c8acc4b").getSourceElementName()); + assertTrue(new ReqIFValidator().validate(document).toString() + .contains("ATTRIBUTE-VALUE-STRING for attribute 'ReqIF.Text'"), new ReqIFValidator().validate(document).toString()); + } + + @Test + void anExcerptKeepsTheValuesOfTypesItDoesNotContain() throws IOException { + + // a DOORS export cut out of a module: the object's type is missing + ReqIFDocument document = new ReqIF(CORPUS.resolve("Doors/03_example_from_a_user/sample.reqif").toString()) + .getReqIFDocument(); + SpecObject object = document.getCoreContent().getSpecObjects().values().iterator().next(); + + assertEquals(12, object.getValues().size()); + assertTrue(object.getValues().values().stream().allMatch(value -> value instanceof UninterpretedAttributeValue)); + assertTrue(write(document).contains(">demo")); + assertEquals("demo", object.getValues().values().iterator().next().getValue(), "the text of THE-VALUE"); + } + + @Test + void aDraftNamespaceIsWrittenAsReqIF12() throws IOException { + + String written = write(new ReqIF(CORPUS.resolve("org.eclipse.rmf/02_sample/sample.reqif").toString()) + .getReqIFDocument()); + + assertTrue(written.contains("xsi:schemaLocation=\"http://www.omg.org/spec/ReqIF/20110401/reqif.xsd reqif.xsd " + + "http://www.w3.org/1999/xhtml driver.xsd\""), written); + assertFalse(written.contains("20101201"), written); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/ExplicitValuesTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/ExplicitValuesTest.java new file mode 100644 index 0000000..22f8cf0 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/ExplicitValuesTest.java @@ -0,0 +1,193 @@ +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValue; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueEnumeration; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueString; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.specification.ReqIFImplementationGuideClassifier; +import de.uni_stuttgart.ils.reqif4j.specification.SpecObject; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; + +/** + * Values are kept as the document has them - by attribute definition, in + * document order - and defaults are not turned into values on the way out. + */ +class ExplicitValuesTest { + + @Test + void explicitValuesAreKeptByDefinitionInDocumentOrder(@TempDir Path tempDir) throws Exception { + + SpecObject boot = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF).getReqIFCoreContent().getSpecObject("so-1"); + + assertEquals(List.of("ad-id", "ad-text", "ad-prio", "ad-safety", "ad-due", "ad-status", "ad-review", "ad-effort"), + new ArrayList<>(boot.getValues().keySet())); + assertEquals("REQ-1", boot.getValue("ad-id").getValue()); + } + + @Test + void defaultsAreEffectiveButNotExplicit(@TempDir Path tempDir) throws Exception { + + SpecObject withoutPriority = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF).getReqIFCoreContent().getSpecObject("so-2"); + + assertNull(withoutPriority.getValue("ad-prio"), "so-2 has no own Priority value"); + assertEquals(3, withoutPriority.getAttribute("Priority"), "the definition's default applies"); + assertEquals(List.of("ad-id", "ad-safety"), new ArrayList<>(withoutPriority.getValues().keySet())); + } + + @Test + void defaultsAreNotWrittenAsValues(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF); + + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + + // so-2 carries exactly its two own values, not the Priority default 3 + // or invented ones such as false for unset booleans + String so2 = xml.substring(xml.indexOf("IDENTIFIER=\"so-2\""), xml.indexOf("", xml.indexOf("IDENTIFIER=\"so-2\""))); + assertEquals(List.of("ad-id", "ad-safety"), definitionRefs(so2)); + + // re-read, the default still applies through the definition + ReqIF roundTripped = parse(tempDir, xml); + assertEquals(3, roundTripped.getReqIFCoreContent().getSpecObject("so-2").getAttribute("Priority")); + } + + @Test + void valuesAreWrittenInDocumentOrder(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF); + + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + + String so1 = xml.substring(xml.indexOf("IDENTIFIER=\"so-1\""), xml.indexOf("")); + assertEquals(List.of("ad-id", "ad-text", "ad-prio", "ad-safety", "ad-due", "ad-status", "ad-review", "ad-effort"), + definitionRefs(so1)); + } + + @Test + void valuesOfDefinitionsSharingALongNameAreAllKept(@TempDir Path tempDir) throws Exception { + + // DOORS exports regularly reuse attribute names; keyed by name, one of + // the two values used to be lost + String sameName = TestFixtures.SCHEMA_VALID_REQIF.replace("LONG-NAME=\"Safety relevant\"", "LONG-NAME=\"ReqIF.ForeignID\""); + ReqIF reqif = parse(tempDir, sameName); + SpecObject boot = reqif.getReqIFCoreContent().getSpecObject("so-1"); + + assertEquals("REQ-1", boot.getValue("ad-id").getValue()); + assertEquals(true, boot.getValue("ad-safety").getValue()); + + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + assertTrue(xml.contains(""), xml); + assertTrue(xml.contains(""), xml); + } + + @Test + void theNameViewIsReadOnly(@TempDir Path tempDir) throws Exception { + + SpecObject boot = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF).getReqIFCoreContent().getSpecObject("so-1"); + AttributeValue any = boot.getValue("ad-id"); + + // a change through this view would not be written, so it is refused + assertThrows(UnsupportedOperationException.class, () -> boot.getAttributes().put("x", any)); + assertThrows(UnsupportedOperationException.class, () -> boot.getValues().remove("ad-id")); + assertFalse(boot.getAttributes().isEmpty()); + } + + @Test + void anExplicitValueIsNotHiddenByADefaultOfTheSameName(@TempDir Path tempDir) throws Exception { + + // ad-prio (default 3, no value on so-1 once its value is removed) and + // ad-id share a name; so-1's explicit ad-id value must win + String sameName = TestFixtures.SCHEMA_VALID_REQIF.replace("LONG-NAME=\"Priority\"", "LONG-NAME=\"ReqIF.ForeignID\""); + SpecObject boot = parse(tempDir, sameName).getReqIFCoreContent().getSpecObject("so-1"); + boot.removeValue("ad-prio"); + + assertEquals("REQ-1", boot.getAttribute("ReqIF.ForeignID")); + } + + @Test + void anEmptyEnumerationSelectionSurvivesARoundTrip(@TempDir Path tempDir) throws Exception { + + // "nothing selected" on so-1, while the definition defaults to Draft + String withDefault = TestFixtures.SCHEMA_VALID_REQIF + .replace("ev-draft", "") + .replace("LONG-NAME=\"Review state\" MULTI-VALUED=\"false\">", + "LONG-NAME=\"Review state\" MULTI-VALUED=\"false\">" + + "" + + "ad-review" + + "ev-draft" + + ""); + ReqIF reqif = parse(tempDir, withDefault); + assertEquals(List.of(), reviewState(reqif, "so-1")); + assertEquals(List.of("ev-draft"), reviewState(reqif, "so-2"), "so-2 gets the default"); + + ReqIF roundTripped = parse(tempDir, new ReqIFWriter().toXml(reqif.getReqIFDocument())); + + assertEquals(List.of(), reviewState(roundTripped, "so-1"), "the empty selection must not turn into the default"); + OfficialReqIFSchema.assertSchemaValid(reqif.getReqIFDocument()); + } + + @Test + void valuesCanBeSetAndRemoved(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF); + SpecObject draft = reqif.getReqIFCoreContent().getSpecObject("so-2"); + AttributeValue foreignID = new AttributeValueString("REQ-2b", + reqif.getReqIFCoreContent().getSpecType("st-req").getAttributeDefinition("ad-id")); + + draft.setValue(foreignID); + draft.removeValue("ad-safety"); + + assertEquals(List.of("ad-id"), new ArrayList<>(draft.getValues().keySet())); + assertEquals("REQ-2b", draft.getAttribute("ReqIF.ForeignID")); + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + assertTrue(xml.contains(""), xml); + } + + @Test + void theContentCategoryFollowsTheValues(@TempDir Path tempDir) throws Exception { + + // attribute-based classification: ReqIF.ChapterName makes a headline + String chapter = TestFixtures.SCHEMA_VALID_REQIF.replace("LONG-NAME=\"ReqIF.ForeignID\"", "LONG-NAME=\"ReqIF.ChapterName\""); + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "chapter.reqif", chapter).toString(), + new ReqIFImplementationGuideClassifier()); + SpecObject boot = reqif.getReqIFCoreContent().getSpecObject("so-1"); + assertTrue(boot.isHeadline()); + + boot.removeValue("ad-id"); + + assertFalse(boot.isHeadline(), "without a chapter name it is no headline any more"); + } + + private static List reviewState(ReqIF reqif, String specObjectID) { + return ((AttributeValueEnumeration) reqif.getReqIFCoreContent().getSpecObject(specObjectID) + .getAttributes().get("Review state")).getValueRefs(); + } + + private static ReqIF parse(Path dir, String content) throws Exception { + return new ReqIF(TestFixtures.write(dir, "doc-" + System.nanoTime() + ".reqif", content).toString()); + } + + private static List definitionRefs(String xml) { + List refs = new ArrayList<>(); + Matcher matcher = Pattern.compile("([^<]+)<").matcher(xml); + while (matcher.find()) { + refs.add(matcher.group(1)); + } + return refs; + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/HierarchyXhtmlContentTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/HierarchyXhtmlContentTest.java new file mode 100644 index 0000000..8844d1c --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/HierarchyXhtmlContentTest.java @@ -0,0 +1,77 @@ +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.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML; +import de.uni_stuttgart.ils.reqif4j.build.ReqIFBuilder; +import de.uni_stuttgart.ils.reqif4j.build.ValuesBuilder; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; +import de.uni_stuttgart.ils.reqif4j.specification.SpecHierarchy; +import de.uni_stuttgart.ils.reqif4j.util.XhtmlParser; + +/** + * Which XHTML value a hierarchy node shows, and how XHTML attributes are + * rendered. + */ +class HierarchyXhtmlContentTest { + + /** ReqIF.Text is declared before ReqIF.ChapterName, which has a default */ + private static SpecHierarchy node(Consumer values) { + ReqIFDocument document = ReqIFBuilder.create() + .xhtmlDatatype("dt-xhtml", "XHTML") + .specObjectType("st", "Requirement", t -> t + .xhtmlAttribute("ad-text", "ReqIF.Text", "dt-xhtml") + .attribute("ad-chapter", "ReqIF.ChapterName", "dt-xhtml", "

TBD

")) + .specificationType("st-spec", "Spec", t -> { }) + .specObject("so", "st", values) + .specification("spec", null, "st-spec", s -> s.child("sh", "so")) + .build(); + return document.getCoreContent().getSpecHierarchy("sh"); + } + + private static String firstBlock(SpecHierarchy node) { + return node.getXHTMLDivContent().get(0).toString(); + } + + @Test + void theSpecTypeOrderDecidesNotTheValueOrder() { + SpecHierarchy node = node(o -> o.setXhtml("ad-chapter", "

Chapter

").setXhtml("ad-text", "

Text

")); + assertTrue(firstBlock(node).contains("Text"), firstBlock(node)); + } + + @Test + void anEmptyOwnValueIsNotReplacedByAnotherAttributesDefault() { + SpecHierarchy node = node(o -> o.setXhtml("ad-text", "")); + assertNull(node.getXHTMLDivContent()); + assertEquals(0, node.getXHTMLContent().size()); + } + + @Test + void withoutOwnValuesTheDefaultIsShown() { + SpecHierarchy node = node(o -> { }); + assertTrue(firstBlock(node).contains("TBD"), firstBlock(node)); + } + + @Test + void attributesOfOtherVocabulariesKeepTheirNamespace() { + ReqIFDocument document = ReqIFBuilder.create() + .xhtmlDatatype("dt-xhtml", "XHTML") + .specObjectType("st", null, t -> t.xhtmlAttribute("ad-text", null, "dt-xhtml")) + .specObject("so", "st", o -> o.setXhtml("ad-text", + "
" + + "

t

")) + .build(); + String rendered = (String) ((AttributeValueXHTML) document.getCoreContent().getSpecObject("so") + .getValues().get("ad-text")).getValue(); + + assertTrue(rendered.contains("x:mark=\"1\"") && rendered.contains("xmlns:x=\"urn:x\""), rendered); + assertTrue(rendered.contains("class=\"c\"") && !rendered.contains("dup"), rendered); + XhtmlParser.parseDiv(rendered); // well-formed + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/IdentifiableTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/IdentifiableTest.java new file mode 100644 index 0000000..788fb9d --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/IdentifiableTest.java @@ -0,0 +1,246 @@ +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition; +import de.uni_stuttgart.ils.reqif4j.build.ReqIFBuilder; +import de.uni_stuttgart.ils.reqif4j.datatypes.Datatype; +import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeEnumeration; +import de.uni_stuttgart.ils.reqif4j.reqif.Identifiable; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFParseException; +import de.uni_stuttgart.ils.reqif4j.specification.SpecHierarchy; +import de.uni_stuttgart.ils.reqif4j.specification.SpecType; +import de.uni_stuttgart.ils.reqif4j.specification.Specification; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; + +/** + * IDENTIFIER, LONG-NAME, DESC, LAST-CHANGE and ALTERNATIVE-ID are read and + * written for every identifiable ReqIF element, so they survive a round trip. + */ +class IdentifiableTest { + + @Test + void identifiablePropertiesAreReadForEveryKindOfElement(@TempDir Path tempDir) throws Exception { + + ReqIFCoreContent content = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF); + + Datatype string = content.getDatatype("dt-string"); + assertEquals("String", string.getName()); + assertEquals("Plain text", string.getDesc()); + assertEquals("2026-07-01T08:00:00Z", string.getLastChange()); + assertEquals("alt-dt-string", string.getAlternativeID(), "nested ALTERNATIVE-ID form"); + + Identifiable draft = ((DatatypeEnumeration) content.getDatatype("dt-enum")).getEnumValues().get("ev-draft"); + assertEquals("Not reviewed yet", draft.getDesc()); + assertEquals("2026-07-01T08:00:00Z", draft.getLastChange()); + + SpecType requirement = content.getSpecType("st-req"); + assertEquals("A single requirement", requirement.getDesc()); + assertEquals("2026-07-02T08:00:00Z", requirement.getAttributeDefinition("ad-id").getLastChange()); + + Identifiable boot = content.getSpecObject("so-1"); + assertEquals("Boot", boot.getName(), "spec objects have a LONG-NAME too"); + assertEquals("First requirement", boot.getDesc()); + assertEquals("2026-07-03T09:15:00Z", boot.getLastChange()); + assertEquals("REQ-1", boot.getAlternativeID()); + + Identifiable withoutName = content.getSpecObject("so-2"); + assertNull(withoutName.getName()); + assertNull(withoutName.getDesc()); + + assertEquals("so-2 refines so-1", content.getSpecRelation("sr-1").getName()); + assertEquals("Trace link", content.getSpecRelation("sr-1").getDesc()); + + Specification specification = content.getSpecification("spec-1"); + assertEquals("Top level", specification.getDesc()); + SpecHierarchy chapter = specification.getChildren().get(0); + assertEquals("1 Boot", chapter.getName()); + assertEquals("Chapter", chapter.getDesc()); + assertEquals("2026-07-05T11:00:00Z", chapter.getLastChange()); + + assertEquals("All refinements", content.getRelationGroup("rg-1").getDesc()); + } + + @Test + void roundTripPreservesTheIdentifiablePropertiesOfAllElements(@TempDir Path tempDir) throws Exception { + + ReqIF original = new ReqIF(TestFixtures.write(tempDir, "in.reqif", TestFixtures.SCHEMA_VALID_REQIF).toString()); + String written = new ReqIFWriter().toXml(original.getReqIFDocument()); + ReqIFCoreContent roundTripped = parse(tempDir, written); + + Map> before = identifiables(original.getReqIFCoreContent()); + assertEquals(28, before.size(), "every identifiable of the fixture is collected"); + assertEquals(before, identifiables(roundTripped)); + } + + @Test + void alternativeIDIsWrittenInTheNestedSchemaForm(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "in.reqif", TestFixtures.SCHEMA_VALID_REQIF).toString()); + + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + + assertTrue(xml.contains(""), xml); + } + + @Test + void missingOptionalPropertiesAreNotInvented(@TempDir Path tempDir) throws Exception { + + // LONG-NAME is optional in ReqIF; reading it used to throw a + // NullPointerException for datatypes, spec types, attribute definitions + // and specifications + String withoutLongNames = TestFixtures.SCHEMA_VALID_REQIF + .replace(" LONG-NAME=\"Bool\"", "") + .replace(" LONG-NAME=\"Requirement\"", "") + .replace(" LONG-NAME=\"Safety relevant\"", "") + .replace(" LONG-NAME=\"System Spec\"", ""); + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "in.reqif", withoutLongNames).toString()); + ReqIFCoreContent content = reqif.getReqIFCoreContent(); + + assertNull(content.getDatatype("dt-bool").getName()); + assertNull(content.getSpecType("st-req").getName()); + assertNull(content.getSpecType("st-req").getAttributeDefinition("ad-safety").getName()); + assertNull(content.getSpecification("spec-1").getName()); + + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + assertTrue(xml.contains(""), xml); + assertFalse(xml.contains("LONG-NAME=\"\""), "an absent LONG-NAME must not be written empty"); + assertFalse(xml.contains("DESC=\"\""), "an absent DESC must not be written empty"); + } + + @Test + void valuesOfUnnamedAttributeDefinitionsDoNotOverwriteEachOther(@TempDir Path tempDir) throws Exception { + + // values are kept by attribute LONG-NAME; without one, the definition's + // IDENTIFIER is the key, so the two values stay apart + String unnamed = TestFixtures.SCHEMA_VALID_REQIF + .replace(" LONG-NAME=\"ReqIF.ForeignID\"", "") + .replace(" LONG-NAME=\"Safety relevant\"", ""); + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "in.reqif", unnamed).toString()); + + assertEquals("REQ-1", reqif.getReqIFCoreContent().getSpecObject("so-1").getAttribute("ad-id")); + assertEquals(true, reqif.getReqIFCoreContent().getSpecObject("so-1").getAttribute("ad-safety")); + + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + assertTrue(xml.contains(""), xml); + assertTrue(xml.contains(""), xml); + } + + @Test + void elementWithoutIdentifierIsSkippedAndReported(@TempDir Path tempDir) throws Exception { + + String withoutIdentifier = TestFixtures.SCHEMA_VALID_REQIF + .replace(""))) + .getDocumentElement(); + + ReqIFParseException failure = assertThrows(ReqIFParseException.class, + () -> new de.uni_stuttgart.ils.reqif4j.specification.SpecObjectType(withoutIdentifier, java.util.Map.of())); + + assertEquals("SPEC-OBJECT-TYPE without IDENTIFIER", failure.getMessage()); + } + + @Test + void generatedElementsCarryDescAndLastChange() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr").title("T")) + .stringDatatype("dt-string", "String", 100) + .specObjectType("st-req", "Requirement", t -> t.stringAttribute("ad-name", "Name", "dt-string")) + .specObject("so-1", "st-req", o -> o.set("ad-name", "x")) + .build(); + // LAST-CHANGE set last: a change after it would count as newer + document.getCoreContent().getSpecObject("so-1") + .setDesc("generated") + .setAlternativeID("ALT-1") + .setLastChange("2026-10-11T12:00:00Z"); + + String xml = new ReqIFWriter().toXml(document); + + assertTrue(xml.contains(""), xml); + assertTrue(xml.contains(""), xml); + } + + @Test + void writerFollowsTheSchemaOrderAndSkipsEmptyXhtml(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "in.reqif", TestFixtures.SCHEMA_VALID_REQIF).toString()); + + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + + // REQ-IF-CONTENT is an xsd:sequence ending with the relation groups + assertTrue(xml.indexOf("") < xml.indexOf(""), xml); + // so-2 has no ReqIF.Text; an empty THE-VALUE would be schema-invalid + assertFalse(xml.contains(""), xml); + } + + private static ReqIFCoreContent parse(Path dir, String content) throws Exception { + return new ReqIF(TestFixtures.write(dir, "doc-" + System.nanoTime() + ".reqif", content).toString()) + .getReqIFCoreContent(); + } + + /** + * @return every identifiable element of the content, by kind and id, with + * its LONG-NAME, DESC, LAST-CHANGE and ALTERNATIVE-ID + */ + private static Map> identifiables(ReqIFCoreContent content) { + + Map> all = new TreeMap>(); + for (Datatype datatype : content.getDatatypes().values()) { + put(all, "datatype", datatype); + if (datatype instanceof DatatypeEnumeration) { + ((DatatypeEnumeration) datatype).getEnumValues().values().forEach(value -> put(all, "enum-value", value)); + } + } + for (SpecType specType : content.getSpecTypes().values()) { + put(all, "spec-type", specType); + for (AttributeDefinition definition : specType.getAttributeDefinitions().values()) { + put(all, "attribute-definition", definition); + } + } + content.getSpecObjects().values().forEach(specObject -> put(all, "spec-object", specObject)); + content.getSpecRelation().values().forEach(relation -> put(all, "spec-relation", relation)); + for (Specification specification : content.getSpecifications().values()) { + put(all, "specification", specification); + specification.getAllSpecHierarchies().forEach(hierarchy -> put(all, "spec-hierarchy", hierarchy)); + } + content.getRelationGroups().values().forEach(group -> put(all, "relation-group", group)); + return all; + } + + private static void put(Map> all, String kind, Identifiable identifiable) { + all.put(kind + ":" + identifiable.getID(), Arrays.asList(identifiable.getName(), identifiable.getDesc(), + identifiable.getLastChange(), identifiable.getAlternativeID())); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/ImplementationGuideCheckTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/ImplementationGuideCheckTest.java new file mode 100644 index 0000000..88fd7c7 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/ImplementationGuideCheckTest.java @@ -0,0 +1,184 @@ +package de.uni_stuttgart.ils.reqif4j; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import de.uni_stuttgart.ils.reqif4j.build.ReqIFBuilder; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFz; +import de.uni_stuttgart.ils.reqif4j.validate.ImplementationGuideCheck; +import de.uni_stuttgart.ils.reqif4j.validate.ValidationIssue; +import de.uni_stuttgart.ils.reqif4j.validate.ValidationResult; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFzWriter; + +class ImplementationGuideCheckTest { + + private static final String OLE_WITH_PREVIEW = "
" + + "Drawing
"; + + /** an object with a ReqIF.Text value of this markup */ + private static ReqIFDocument document(String markup) { + return ReqIFBuilder.create() + .stringDatatype("dt-string", "String", 255) + .xhtmlDatatype("dt-xhtml", "XHTML") + .dateDatatype("dt-date", "Date") + .specObjectType("st", "Requirement", t -> t + .xhtmlAttribute("ad-text", "ReqIF.Text", "dt-xhtml") + .stringAttribute("ad-name", "ReqIF.Name", "dt-string") + .dateAttribute("ad-created", "ReqIF.ForeignCreatedOn", "dt-date") + .stringAttribute("ad-own", "Own attribute", "dt-string")) + .specObject("so-1", "st", o -> o.setXhtml("ad-text", markup)) + .build(); + } + + private static List messages(ValidationResult result) { + return result.getIssues().stream().map(ValidationIssue::getMessage).collect(Collectors.toList()); + } + + @Test + void theReferenceFilesOfTheImplementorForumFollowTheGuide() throws IOException { + try (Stream files = Files.walk(Path.of("src", "test", "resources", "corpus", "ci.eclipse.org"))) { + for (Path file : files.filter(f -> f.toString().endsWith(".reqif")).collect(Collectors.toList())) { + ValidationResult result = new ImplementationGuideCheck().check(new ReqIF(file.toString()).getReqIFDocument()); + assertTrue(result.getIssues().isEmpty(), file + ":\n" + result); + } + } + } + + @Test + void standardAttributesHaveTheExpectedDatatype() { + + ReqIFDocument document = ReqIFBuilder.create() + .stringDatatype("dt-string", "String", 255) + .specObjectType("st", "Requirement", t -> t + .stringAttribute("ad-text", "ReqIF.Text", "dt-string") + .stringAttribute("ad-created", "ReqIF.ForeignCreatedOn", "dt-string") + .stringAttribute("ad-name", "ReqIF.Name", "dt-string") + .stringAttribute("ad-own", "Own attribute", "dt-string")) + .build(); + + assertEquals(List.of( + "The standard attribute ReqIF.Text is of kind STRING; tools expect XHTML", + "The standard attribute ReqIF.ForeignCreatedOn is of kind STRING; tools expect DATE"), + messages(new ImplementationGuideCheck().check(document))); + assertTrue(new ImplementationGuideCheck().check(document(OLE_WITH_PREVIEW)).getIssues().isEmpty()); + } + + @Test + void objectsStateTheirTypeAndComeWithAPreview() { + + ValidationResult result = new ImplementationGuideCheck().check(document("
" + + "Drawing" + + "Photo" + + "Diagram" + + "
")); + + assertEquals(List.of( + "Object files/drawing.ole of type application/oleobject has no PNG image as preview (a nested object)", + "Object files/photo.png has no MIME type (type attribute)", + "Object http://example.org/diagram.png is referenced by an absolute location; " + + "objects reference files of the archive by a relative path"), + messages(result)); + assertTrue(result.isValid(), "conventions are warnings"); + } + + @Test + void theFilesObjectsReferenceAreInTheArchive(@TempDir Path dir) throws IOException { + + Path archive = dir.resolve("spec.reqifz"); + new ReqIFzWriter().write(document(OLE_WITH_PREVIEW), "spec.reqif", + Map.of("files/drawing.png", new byte[] { 1, 2, 3 }), archive); + + try (ReqIFz read = new ReqIFz(archive.toString())) { + ValidationResult result = new ImplementationGuideCheck().check(read); + + assertEquals(List.of("spec.reqif: Object files/drawing.ole is not in the archive"), messages(result)); + assertTrue(!result.isValid()); + } + } + + @Test + void referencesAreResolvedAgainstTheDocumentsFolder(@TempDir Path dir) throws IOException { + + Path archive = dir.resolve("spec.reqifz"); + new ReqIFzWriter().write(document("
" + + "encoded" + + "blank and ./" + + "up" + + "inline" + + "
"), "sub/spec.reqif", + Map.of("sub/files/My Drawing.png", new byte[] { 1 }, "sub/files/logo.png", new byte[] { 2 }, + "shared/icon.png", new byte[] { 3 }), archive); + + try (ReqIFz read = new ReqIFz(archive.toString())) { + ValidationResult result = new ImplementationGuideCheck().check(read); + assertTrue(result.getIssues().isEmpty(), result.toString()); + } + } + + @Test + void referencesDoNotLeaveTheArchive(@TempDir Path dir) throws IOException { + + Path archive = dir.resolve("spec.reqifz"); + new ReqIFzWriter().write(document("
up
"), + "spec.reqif", Map.of("shared/logo.png", new byte[] { 1 }), archive); + + try (ReqIFz read = new ReqIFz(archive.toString())) { + assertEquals(List.of("spec.reqif: Object ../shared/logo.png points outside the archive"), + messages(new ImplementationGuideCheck().check(read))); + } + } + + @Test + void eachObjectIsReportedOnceAndDefaultsAreChecked() { + + // the preview in THE-VALUE is the one nested in THE-ORIGINAL-VALUE + ReqIFDocument document = ReqIFBuilder.create() + .xhtmlDatatype("dt-xhtml", "XHTML") + .specObjectType("st", null, t -> t + .xhtmlAttribute("ad-text", "ReqIF.Text", "dt-xhtml") + .attribute("ad-logo", "Logo", "dt-xhtml", + "
logo
")) + .specObject("so-1", "st", o -> o.setXhtml("ad-text", + "
preview
", + "
" + + "preview
")) + .build(); + + assertEquals(List.of( + "Object files/logo.emf of type image/x-emf has no PNG image as preview (a nested object)", + "Object files/drawing.png has no MIME type (type attribute)", + "Object files/drawing.ole of type application/oleobject has no PNG image as preview (a nested object)"), + messages(new ImplementationGuideCheck().check(document))); + } + + @Test + void simplifiedValuesCarryTheirOriginal(@TempDir Path dir) throws IOException { + + ReqIFDocument simplified = ReqIFBuilder.create() + .xhtmlDatatype("dt-xhtml", "XHTML") + .specObjectType("st", null, t -> t.xhtmlAttribute("ad-text", "ReqIF.Text", "dt-xhtml")) + .specObject("so-1", "st", o -> o.setXhtml("ad-text", "

preview

", "

all

")) + .build(); + assertTrue(new ImplementationGuideCheck().check(simplified).getIssues().isEmpty()); + + String notMarked = new ReqIFWriter().toXml(simplified).replace("IS-SIMPLIFIED=\"true\"", "IS-SIMPLIFIED=\"false\""); + ReqIFDocument read = new ReqIF(TestFixtures.write(dir, "in.reqif", notMarked).toString()).getReqIFDocument(); + + assertEquals(List.of("An XHTML value of attribute ad-text has THE-ORIGINAL-VALUE but is not marked IS-SIMPLIFIED"), + messages(new ImplementationGuideCheck().check(read))); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/LargeDocumentTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/LargeDocumentTest.java new file mode 100644 index 0000000..4cc1d66 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/LargeDocumentTest.java @@ -0,0 +1,105 @@ +package de.uni_stuttgart.ils.reqif4j; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +import java.nio.file.Path; +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import de.uni_stuttgart.ils.reqif4j.build.ReqIFBuilder; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; +import de.uni_stuttgart.ils.reqif4j.specification.SpecObject; +import de.uni_stuttgart.ils.reqif4j.validate.ReqIFValidator; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; + +/** + * Guards against work that grows with the square of the document size: a + * specification with tens of thousands of nodes side by side is built, + * written, read, validated and changed within a generous time limit, which + * a quadratic step would exceed by far. + */ +class LargeDocumentTest { + + private static final int OBJECTS = 20_000; + + @Test + void tensOfThousandsOfObjectsTakeSeconds(@TempDir Path dir) { + assertTimeoutPreemptively(Duration.ofSeconds(60), () -> { + + ReqIFBuilder builder = ReqIFBuilder.create() + .stringDatatype("dt-string", "String", 255) + .xhtmlDatatype("dt-xhtml", "XHTML") + .specObjectType("st", "Requirement", t -> t + .stringAttribute("ad-id", "ReqIF.ForeignID", "dt-string") + .xhtmlAttribute("ad-text", "ReqIF.Text", "dt-xhtml")) + .specificationType("st-spec", "Specification", t -> { }); + for (int i = 0; i < OBJECTS; i++) { + String id = "so-" + i; + builder.specObject(id, "st", o -> o.set("ad-id", id).setXhtml("ad-text", "

Requirement " + id + "

")); + } + builder.specification("spec", "Spec", "st-spec", s -> { + for (int i = 0; i < OBJECTS; i++) { + s.child("sh-" + i, "so-" + i); + } + }); + Path file = dir.resolve("large.reqif"); + new ReqIFWriter().write(builder.build(), file); + + ReqIFDocument read = new ReqIF(file.toString()).getReqIFDocument(); + ReqIFCoreContent content = read.getCoreContent(); + assertEquals(OBJECTS, content.getSpecification("spec").getNumberOfSpecObjects()); + assertEquals(0, new ReqIFValidator().validate(read).getErrors().size()); + + // changes look nodes up by identifier + for (int i = 0; i < 200; i++) { + content.addSpecObject(new SpecObject("so-new-" + i, content.getSpecType("st"), java.util.List.of())); + content.removeSpecObject("so-" + i); + } + content.moveSpecHierarchy("sh-" + (OBJECTS - 1), "spec", 0); + assertEquals(OBJECTS - 200, content.getSpecification("spec").getNumberOfSpecObjects()); + new ReqIFWriter().write(read, dir.resolve("changed.reqif")); + }); + } + + @Test + void largeHierarchiesAreChangedNodeByNode() { + assertTimeoutPreemptively(Duration.ofSeconds(60), () -> { + + ReqIFBuilder builder = ReqIFBuilder.create() + .specObjectType("st", "Requirement", t -> { }) + .specificationType("st-spec", "Specification", t -> { }) + .specObject("so-heading", "st"); + for (int i = 0; i < OBJECTS; i++) { + builder.specObject("so-" + i, "st"); + } + // one heading with all others below it, and one more node + builder.specification("spec", "Spec", "st-spec", s -> s.child("sh-heading", "so-heading", h -> { + for (int i = 0; i < OBJECTS; i++) { + h.child("sh-" + i, "so-" + i); + } + })); + ReqIFCoreContent content = builder.build().getCoreContent(); + + // the heading goes, all its children move up at once + content.removeSpecObject("so-heading"); + assertEquals(OBJECTS, content.getSpecification("spec").getNumberOfSections()); + + // node by node: inserted, looked up and moved + for (int i = 0; i < OBJECTS; i++) { + content.addSpecHierarchy("sh-" + i, 0, + new de.uni_stuttgart.ils.reqif4j.specification.SpecHierarchy("sh-new-" + i, 0, 0, + content.getSpecObject("so-" + i), null)); + } + for (int i = 0; i < OBJECTS; i++) { + content.moveSpecHierarchy("sh-new-" + i, "spec", content.getSpecification("spec").getNumberOfSections()); + } + assertEquals(2 * OBJECTS, content.getSpecification("spec").getNumberOfSections()); + assertEquals(2 * OBJECTS, content.getSpecification("spec").getNumberOfSpecObjects()); + }); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/LosslessRoundTripTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/LosslessRoundTripTest.java new file mode 100644 index 0000000..34c2363 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/LosslessRoundTripTest.java @@ -0,0 +1,57 @@ +package de.uni_stuttgart.ils.reqif4j; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +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.write.ReqIFWriter; + +/** + * The acceptance test for lossless import and export: a schema-valid document + * that uses every element and attribute of the ReqIF schema is written back + * with the same content. + */ +class LosslessRoundTripTest { + + static String fullFeatureDocument() throws Exception { + try (InputStream in = LosslessRoundTripTest.class.getResourceAsStream("/full-feature.reqif")) { + return new String(in.readAllBytes(), StandardCharsets.UTF_8); + } + } + + @Test + void everyElementAndAttributeSurvivesARoundTrip(@TempDir Path tempDir) throws Exception { + + String original = fullFeatureDocument(); + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "in.reqif", original).toString()); + + String written = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + + assertEquals(CanonicalXml.of(original), CanonicalXml.of(written)); + } + + @Test + void theFixtureIsSchemaValidAndStaysSo(@TempDir Path tempDir) throws Exception { + + Path in = TestFixtures.write(tempDir, "in.reqif", fullFeatureDocument()); + OfficialReqIFSchema.assertSchemaValid(in); + + OfficialReqIFSchema.assertSchemaValid(new ReqIF(in.toString()).getReqIFDocument()); + } + + @Test + void writingIsStable(@TempDir Path tempDir) throws Exception { + + ReqIF first = new ReqIF(TestFixtures.write(tempDir, "in.reqif", fullFeatureDocument()).toString()); + String once = new ReqIFWriter().toXml(first.getReqIFDocument()); + ReqIF second = new ReqIF(TestFixtures.write(tempDir, "once.reqif", once).toString()); + + assertEquals(once, new ReqIFWriter().toXml(second.getReqIFDocument())); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/ModelCompletenessTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/ModelCompletenessTest.java new file mode 100644 index 0000000..6267f7e --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/ModelCompletenessTest.java @@ -0,0 +1,173 @@ +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.math.BigInteger; +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.AttributeValueInteger; +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueXHTML; +import de.uni_stuttgart.ils.reqif4j.build.ReqIFBuilder; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent; +import de.uni_stuttgart.ils.reqif4j.specification.SpecHierarchy; +import de.uni_stuttgart.ils.reqif4j.validate.ReqIFValidator; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; + +/** + * The model exposes every part of the ReqIF schema that used to be dropped + * when reading: big integers, REPOSITORY-ID, xml:lang, IS-EDITABLE, + * IS-TABLE-INTERNAL, EDITABLE-ATTS, THE-ORIGINAL-VALUE and IS-SIMPLIFIED. + */ +class ModelCompletenessTest { + + private ReqIF reqif; + private ReqIFCoreContent content; + + @BeforeEach + void parse(@TempDir Path tempDir) throws Exception { + this.reqif = new ReqIF(TestFixtures.write(tempDir, "in.reqif", LosslessRoundTripTest.fullFeatureDocument()).toString()); + this.content = this.reqif.getReqIFCoreContent(); + } + + @Test + void integersAreNotLimitedToInt() { + + AttributeValueInteger big = (AttributeValueInteger) this.content.getSpecObject("so-1").getValue("ad-count"); + assertEquals(99999999999L, big.getValue(), "a Long once the value exceeds int"); + assertEquals(new BigInteger("99999999999"), big.getBigInteger()); + + assertEquals(7, this.content.getSpecObject("so-2").getAttribute("Count"), "an Integer while it fits"); + + AttributeValueInteger huge = new AttributeValueInteger("123456789012345678901234567890", + this.content.getSpecType("st-req").getAttributeDefinition("ad-count")); + assertEquals(new BigInteger("123456789012345678901234567890"), huge.getValue()); + } + + @Test + void documentAndHeaderMetadata() { + + assertEquals("de", this.reqif.getReqIFDocument().getLang()); + assertEquals("repo-42", this.reqif.getReqIFHeader().getRepositoryID()); + + String xml = new ReqIFWriter().toXml(ReqIFBuilder.create() + .header(h -> h.id("hdr").repositoryID("repo-7").creationTime("2026-07-23T10:00:00Z")) + .build()); + assertTrue(xml.contains("2026-07-23T10:00:00Zrepo-7"), xml); + } + + @Test + void editabilityAndTableFlags() { + + assertEquals(false, this.content.getSpecType("st-req").getAttributeDefinition("ad-id").getEditable()); + assertEquals(true, this.content.getSpecType("st-req").getAttributeDefinition("ad-text").getEditable()); + assertNull(this.content.getSpecType("st-req").getAttributeDefinition("ad-due").getEditable()); + + SpecHierarchy chapter = this.content.getSpecification("spec-1").getChildren().get(0); + assertEquals(true, chapter.getEditable()); + assertEquals(false, chapter.getTableInternal()); + assertEquals(List.of("ad-text", "ad-status", "ad-text"), chapter.getEditableAttributeIDs(), + "in document order, a repeated reference included"); + + SpecHierarchy row = chapter.getChildren().get(0); + assertEquals(true, row.getTableInternal()); + assertNull(row.getEditable()); + assertTrue(row.getEditableAttributeIDs().isEmpty()); + } + + @Test + void originalAndSimplifiedXhtml() { + + AttributeValueXHTML text = (AttributeValueXHTML) this.content.getSpecObject("so-1").getValue("ad-text"); + + assertEquals(true, text.getSimplified()); + assertTrue(text.getOriginalValue().toString().contains("files/diagram.ole"), text.getOriginalValue().toString()); + assertTrue(!text.getValue().toString().contains("diagram.ole"), "THE-VALUE is the simplified content"); + } + + @Test + void xhtmlValueMadeOfASingleParagraph() { + + AttributeValueXHTML paragraph = (AttributeValueXHTML) this.content.getSpecObject("so-2").getValue("ad-text"); + + assertEquals("

A single paragraph.

", paragraph.getValue()); + assertEquals(1, paragraph.getElementList().size()); + assertEquals("P", paragraph.getElementList().getElementType(0)); + } + + @Test + void xhtmlDefaultKeepsItsFormAndOriginal() { + + AttributeValueXHTML defaultText = this.content.getSpecType("st-req").getAttributeDefinition("ad-text") + .getDefaultXhtmlValue(); + + assertEquals("

to be written

", defaultText.getValue()); + assertEquals(true, defaultText.getSimplified()); + assertTrue(defaultText.getOriginalValue().toString().contains("written")); + } + + @Test + void xhtmlDefaultWithTheOriginalValueFirst(@TempDir Path tempDir) throws Exception { + + // xsd:all: THE-ORIGINAL-VALUE may come before THE-VALUE; xml:lang must + // survive rendering, so lang next to xml:lang is no duplicate + String reordered = LosslessRoundTripTest.fullFeatureDocument().replace( + "to be writtento be written", + "to be writtento be written"); + ReqIF withOriginalFirst = new ReqIF(TestFixtures.write(tempDir, "reordered.reqif", reordered).toString()); + de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition text = + withOriginalFirst.getReqIFCoreContent().getSpecType("st-req").getAttributeDefinition("ad-text"); + + // the attribute order is the DOM's; what counts is that both survive once + String rendered = text.getDefaultValue(); + assertTrue(rendered.matches("to be written

") + && rendered.contains(" lang=") && rendered.contains("xml:lang="), rendered); + assertTrue(new ReqIFValidator().validate(withOriginalFirst.getReqIFDocument()).isValid()); + } + + @Test + void xhtmlContentOfAHierarchyNodePrefersTheObjectsOwnText() { + + // so-2 has its own ReqIF.Text; no other XHTML default may stand in + SpecHierarchy row = this.content.getSpecification("spec-1").getChildren().get(0).getChildren().get(0); + + assertEquals("A single paragraph.", ((de.uni_stuttgart.ils.reqif4j.xhtml.XHTMLElement) row.getXHTMLDivContent().get(0)) + .getChildren().get(0).toString()); + } + + @Test + void invalidBooleanFlagIsNotFlipped(@TempDir Path tempDir) throws Exception { + + // xsd:boolean is case sensitive; TRUE is not false, it is unknown + String upperCase = LosslessRoundTripTest.fullFeatureDocument().replace("IS-EDITABLE=\"true\" IS-TABLE-INTERNAL", "IS-EDITABLE=\"TRUE\" IS-TABLE-INTERNAL"); + ReqIF withInvalid = new ReqIF(TestFixtures.write(tempDir, "invalid.reqif", upperCase).toString()); + + SpecHierarchy chapter = withInvalid.getReqIFCoreContent().getSpecification("spec-1").getChildren().get(0); + assertNull(chapter.getEditable()); + assertTrue(!new ReqIFWriter().toXml(withInvalid.getReqIFDocument()).contains("IS-EDITABLE=\"false\" IS-TABLE-INTERNAL")); + } + + @Test + void unresolvedHierarchyReferenceIsKept(@TempDir Path tempDir) throws Exception { + + String dangling = LosslessRoundTripTest.fullFeatureDocument() + .replace("so-2", "so-gone"); + ReqIF withDangling = new ReqIF(TestFixtures.write(tempDir, "dangling.reqif", dangling).toString()); + SpecHierarchy row = withDangling.getReqIFCoreContent().getSpecification("spec-1").getChildren().get(0).getChildren().get(0); + + assertNull(row.getSpecObject()); + assertEquals("so-gone", row.getSpecObjectID()); + assertNull(row.getXHTMLContent()); + assertNull(row.getXHTMLDivContent()); + assertTrue(new ReqIFWriter().toXml(withDangling.getReqIFDocument()).contains("so-gone")); + assertTrue(new ReqIFValidator().validate(withDangling.getReqIFDocument()).getErrors().stream() + .anyMatch(issue -> issue.getMessage().contains("references spec object so-gone"))); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/ModelCoverageTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/ModelCoverageTest.java index 48ff620..8e22e15 100644 --- a/src/test/java/de/uni_stuttgart/ils/reqif4j/ModelCoverageTest.java +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/ModelCoverageTest.java @@ -113,4 +113,19 @@ void writingStaysIdempotentWithTheNewElements() { assertEquals(new ReqIFWriter().toXml(original.getReqIFDocument()), new ReqIFWriter().toXml(roundTripped.getReqIFDocument())); } + + @Test + void onlyTheToolExtensionsOfTheDocumentItselfAreKept(@TempDir Path tempDir) throws Exception { + + // a vendor extension may contain an element named TOOL-EXTENSIONS; it + // belongs to the extension, and the schema allows one TOOL-EXTENSIONS only + String nested = TestFixtures.REQIF_FIXTURE.replace( + "", + ""); + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "nested.reqif", nested).toString()); + + assertEquals(1, reqif.getReqIFDocument().getToolExtensions().size()); + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + assertEquals(xml.indexOf(""), xml.lastIndexOf(""), xml); + } } diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/MutationApiTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/MutationApiTest.java new file mode 100644 index 0000000..34871ce --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/MutationApiTest.java @@ -0,0 +1,489 @@ +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigInteger; +import java.nio.file.Path; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.List; + +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.ReqIFCoreContent; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; +import de.uni_stuttgart.ils.reqif4j.specification.RelationGroup; +import de.uni_stuttgart.ils.reqif4j.specification.SpecHierarchy; +import de.uni_stuttgart.ils.reqif4j.specification.SpecObject; +import de.uni_stuttgart.ils.reqif4j.specification.SpecRelation; +import de.uni_stuttgart.ils.reqif4j.specification.Specification; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; + +/** + * Changing a document after it was read or built: values, hierarchy, + * relations and the removal of elements, with LAST-CHANGE maintained by the + * writer. + */ +class MutationApiTest { + + private static final Clock FIXED = Clock.fixed(Instant.parse("2026-10-11T12:34:56Z"), ZoneOffset.UTC); + private static final String NOW = "LAST-CHANGE=\"2026-10-11T12:34:56Z\""; + + private static ReqIFDocument read(Path dir) throws Exception { + return new ReqIF(TestFixtures.write(dir, "in.reqif", TestFixtures.SCHEMA_VALID_REQIF).toString()) + .getReqIFDocument(); + } + + private static ReqIFDocument reread(Path dir, ReqIFDocument document) throws Exception { + return new ReqIF(TestFixtures.write(dir, "out.reqif", + new ReqIFWriter().setClock(FIXED).toXml(document)).toString()).getReqIFDocument(); + } + + @Test + void changedValuesGetANewLastChangeAndTheRestKeepsTheirs(@TempDir Path dir) throws Exception { + + ReqIFDocument document = read(dir); + SpecObject so1 = document.getCoreContent().getSpecObject("so-1"); + so1.set("ad-prio", 7).setEnum("ad-status", "ev-draft", "ev-approved").setXhtml("ad-text", + "

New text

"); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + + assertTrue(xml.contains("IDENTIFIER=\"so-1\" " + NOW), xml); + assertTrue(xml.contains(" so1.set("ad-unknown", "x")); + assertThrows(IllegalArgumentException.class, () -> so1.set("ad-prio", "not a number")); + assertThrows(IllegalArgumentException.class, () -> so1.setEnum("ad-status", "ev-unknown")); + assertFalse(so1.isModified(), "a refused change leaves the object unchanged"); + } + + @Test + void removingAValueMarksTheObject(@TempDir Path dir) throws Exception { + + SpecObject so1 = read(dir).getCoreContent().getSpecObject("so-1"); + + assertNull(so1.removeValue("ad-not-set")); + assertFalse(so1.isModified()); + so1.removeValue("ad-prio"); + assertTrue(so1.isModified()); + assertFalse(so1.getValues().containsKey("ad-prio")); + } + + @Test + void removingAnObjectLiftsItsChildrenAndDropsItsRelations(@TempDir Path dir) throws Exception { + + ReqIFDocument document = read(dir); + ReqIFCoreContent content = document.getCoreContent(); + + assertEquals("so-1", content.removeSpecObject("so-1").getID()); + + Specification spec = content.getSpecification("spec-1"); + assertEquals(List.of("sh-2"), ids(spec.getChildren())); + SpecHierarchy sh2 = spec.getChildren().get(0); + assertEquals(1, sh2.getHierarchyLvl()); + assertEquals(1, sh2.getSection()); + assertNull(content.getSpecRelation("sr-1")); + assertEquals(List.of(), content.getRelationGroup("rg-1").getSpecRelationRefs()); + assertTrue(spec.isModified()); + assertTrue(content.getRelationGroup("rg-1").isModified()); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + assertFalse(xml.contains("so-1"), xml); + assertTrue(xml.contains("IDENTIFIER=\"spec-1\" " + NOW), xml); + assertTrue(xml.contains(" content.moveSpecHierarchy("sh-1", "sh-2", 0)); + assertThrows(IllegalArgumentException.class, () -> content.moveSpecHierarchy("sh-1", "sh-1", 0)); + assertThrows(IllegalArgumentException.class, () -> content.moveSpecHierarchy("sh-2", "spec-1", 3)); + assertThrows(IllegalArgumentException.class, () -> content.moveSpecHierarchy("sh-2", "nowhere", 0)); + assertEquals(List.of("sh-new", "sh-1"), ids(spec.getChildren())); + + content.moveSpecHierarchy("sh-2", "spec-1", 2); + assertEquals(List.of("sh-new", "sh-1", "sh-2"), ids(spec.getChildren())); + assertEquals(1, content.getSpecHierarchy("sh-2").getHierarchyLvl()); + assertEquals(3, content.getSpecHierarchy("sh-2").getSection()); + assertTrue(content.getSpecHierarchy("sh-1").isModified(), "sh-1 lost a child"); + + content.moveSpecHierarchy("sh-new", "sh-2", 0); + assertEquals(2, content.getSpecHierarchy("sh-new").getHierarchyLvl()); + assertEquals(2, content.getSpecHierarchy("sh-new").getSection()); + assertEquals(1, content.getSpecHierarchy("sh-1").getSection()); + + // within the same parent, the position counts after the move + spec.addChild(new SpecHierarchy("sh-last", 0, 0, so2, null)); + content.moveSpecHierarchy("sh-1", "spec-1", 2); + assertEquals(List.of("sh-2", "sh-last", "sh-1"), ids(spec.getChildren())); + + assertEquals("sh-2", content.removeSpecHierarchy("sh-2").getID()); + assertNull(content.getSpecHierarchy("sh-new"), "the node goes with everything below it"); + assertNull(content.removeSpecHierarchy("sh-2")); + assertEquals(List.of("sh-last", "sh-1"), ids(spec.getChildren())); + assertTrue(content.getSpecObjects().containsKey("so-2"), "the objects stay"); + + assertThrows(IllegalArgumentException.class, + () -> spec.addChild(new SpecHierarchy("sh-1", 0, 0, so2, null))); + OfficialReqIFSchema.assertSchemaValid(document); + } + + @Test + void typesAndDatatypesInUseAreNotRemoved(@TempDir Path dir) throws Exception { + + ReqIFCoreContent content = read(dir).getCoreContent(); + + IllegalStateException inUse = assertThrows(IllegalStateException.class, () -> content.removeSpecType("st-req")); + assertTrue(inUse.getMessage().contains("so-1"), inUse.getMessage()); + assertThrows(IllegalStateException.class, () -> content.removeSpecType("st-rel")); + assertThrows(IllegalStateException.class, () -> content.removeSpecType("st-spec")); + assertThrows(IllegalStateException.class, () -> content.removeSpecType("st-group")); + assertThrows(IllegalStateException.class, () -> content.removeDatatype("dt-int")); + assertNull(content.removeSpecType("st-unknown")); + assertNull(content.removeDatatype("dt-unknown")); + + assertEquals("rg-1", content.removeRelationGroup("rg-1").getID()); + assertEquals("st-group", content.removeSpecType("st-group").getID()); + assertTrue(content.getSpecRelation().containsKey("sr-1"), "the group's relations stay"); + } + + @Test + void removingASpecificationRemovesItsRelationGroups(@TempDir Path dir) throws Exception { + + ReqIFDocument document = read(dir); + ReqIFCoreContent content = document.getCoreContent(); + + content.removeSpecification("spec-1"); + + assertNull(content.getRelationGroup("rg-1")); + assertNull(content.getSpecHierarchy("sh-1")); + assertEquals(2, content.getSpecObjects().size()); + assertEquals(1, content.getSpecRelation().size()); + OfficialReqIFSchema.assertSchemaValid(document); + } + + @Test + void identifiersCannotBeTakenTwice(@TempDir Path dir) throws Exception { + + ReqIFCoreContent content = read(dir).getCoreContent(); + SpecObject so1 = content.getSpecObject("so-1"); + + assertSame(content, content.addSpecObject(so1), "adding the same element again changes nothing"); + assertThrows(IllegalArgumentException.class, + () -> content.addSpecObject(new SpecObject("so-1", content.getSpecType("st-req"), null))); + assertThrows(IllegalArgumentException.class, + () -> content.addRelationGroup(new RelationGroup("dt-string", null, "st-group", "spec-1", "spec-1", null))); + assertThrows(IllegalArgumentException.class, + () -> content.addSpecObject(new SpecObject((String) null, content.getSpecType("st-req"), List.of()))); + } + + @Test + void relationsAndGroupsAreRewired(@TempDir Path dir) throws Exception { + + ReqIFDocument document = read(dir); + ReqIFCoreContent content = document.getCoreContent(); + SpecRelation sr1 = content.getSpecRelation("sr-1"); + RelationGroup rg1 = content.getRelationGroup("rg-1"); + + sr1.setSourceObjID("so-1").setTargetObjID("so-2"); + content.addSpecRelation(new SpecRelation("sr-2", content.getSpecType("st-rel"), "so-1", "so-2", null)); + rg1.addSpecRelationRef("sr-2").addSpecRelationRef("sr-2"); + assertEquals(List.of("sr-1", "sr-2"), rg1.getSpecRelationRefs()); + assertTrue(rg1.removeSpecRelationRef("sr-1")); + assertFalse(rg1.removeSpecRelationRef("sr-1")); + + ReqIFCoreContent written = reread(dir, document).getCoreContent(); + assertEquals("so-1", written.getSpecRelation("sr-1").getSourceObjID()); + assertEquals("so-2", written.getSpecRelation("sr-1").getTargetObjID()); + assertEquals("2026-10-11T12:34:56Z", written.getSpecRelation("sr-1").getLastChange()); + assertEquals(List.of("sr-2"), written.getRelationGroup("rg-1").getSpecRelationRefs()); + assertEquals("2026-10-11T12:34:56Z", written.getRelationGroup("rg-1").getLastChange()); + OfficialReqIFSchema.assertSchemaValid(document); + } + + @Test + void clearModifiedKeepsTheLastChange(@TempDir Path dir) throws Exception { + + ReqIFDocument document = read(dir); + ReqIFCoreContent content = document.getCoreContent(); + content.getSpecObject("so-1").set("ad-prio", 9); + content.getSpecHierarchy("sh-2").setEditable(true); + + content.clearModified(); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + assertFalse(xml.contains("2026-10-11"), xml); + } + + @Test + void builtDocumentsStartUnchanged() { + + ReqIFDocument document = new de.uni_stuttgart.ils.reqif4j.build.ReqIFBuilder() + .header(h -> h.id("hdr").creationTime("2026-07-23T10:00:00Z")) + .stringDatatype("dt-string", "String", 255) + .specObjectType("st-req", "Requirement", t -> t.stringAttribute("ad-id", "ID", "dt-string")) + .specObject("so-1", "st-req", o -> o.set("ad-id", "R-1")) + .specObject("so-2", "st-req", o -> o.set("ad-id", "R-2")) + .build(); + + document.getCoreContent().getSpecObject("so-2").set("ad-id", "R-2b"); + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + + assertTrue(xml.contains(" spec.addChild(sh2), "already placed"); + assertThrows(IllegalArgumentException.class, () -> sh1.removeChild("sh-2").addChild(sh1), "below itself"); + assertThrows(IllegalArgumentException.class, () -> sh2.addChild(sh2), "below itself"); + assertNull(sh2.getParent()); + + spec.addChild(sh2); + assertSame(spec, sh2.getParent()); + assertEquals(List.of("sh-1", "sh-2"), ids(spec.getAllSpecHierarchies())); + + // the flattened lists follow changes further down + sh2.addChild(new SpecHierarchy("sh-3", 0, 0, content.getSpecObject("so-1"), null)); + assertEquals(List.of("sh-1", "sh-2", "sh-3"), ids(spec.getAllSpecHierarchies())); + assertEquals(3, spec.getNumberOfSpecObjects()); + assertEquals(List.of("sh-1", "sh-2", "sh-3"), ids(content.getOrderedSpecHierarchyList())); + assertThrows(UnsupportedOperationException.class, () -> spec.getAllSpecHierarchies().clear()); + } + + @Test + void aNodeOfAnotherParentIsNotTakenByAConstructor(@TempDir Path dir) throws Exception { + + ReqIFCoreContent content = read(dir).getCoreContent(); + SpecObject so1 = content.getSpecObject("so-1"); + SpecHierarchy fresh = new SpecHierarchy("sh-fresh", 0, 0, so1, null); + SpecHierarchy placed = content.getSpecHierarchy("sh-2"); + + assertThrows(IllegalArgumentException.class, + () -> new SpecHierarchy("sh-x", 0, 0, so1, List.of(fresh, placed))); + assertNull(fresh.getParent(), "nothing was taken"); + assertSame(content.getSpecHierarchy("sh-1"), placed.getParent()); + content.addSpecHierarchy("spec-1", 0, fresh); + } + + @Test + void aRefusedRemovalChangesNothing() { + + ReqIFCoreContent content = new ReqIFCoreContent(); + de.uni_stuttgart.ils.reqif4j.specification.SpecType type = new de.uni_stuttgart.ils.reqif4j.specification.SpecType( + "st", null, de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst.SPEC_OBJECT_TYPE); + SpecObject a = new SpecObject("so-a", type, List.of()); + SpecObject b = new SpecObject("so-b", type, List.of()); + content.addSpecType(type).addSpecObject(a).addSpecObject(b); + // two nodes sh-x, one below the node showing so-a: as read from a + // document that repeats an identifier + SpecHierarchy showsA = new SpecHierarchy("sh-a", 1, 1, a, + List.of(new SpecHierarchy("sh-x", 2, 1, b, null))); + content.addSpecification(new Specification("spec", null, null, null, + List.of(showsA, new SpecHierarchy("sh-x", 1, 2, b, null)))); + + assertThrows(IllegalStateException.class, () -> content.removeSpecObject("so-a")); + + assertSame(a, content.getSpecObject("so-a")); + assertEquals(List.of("sh-a", "sh-x"), ids(content.getSpecification("spec").getChildren())); + assertEquals(List.of("sh-x"), ids(showsA.getChildren())); + } + + @Test + void theContentIsChangedThroughItsMethodsOnly(@TempDir Path dir) throws Exception { + + ReqIFCoreContent content = read(dir).getCoreContent(); + + assertThrows(UnsupportedOperationException.class, () -> content.getSpecObjects().remove("so-1")); + assertThrows(UnsupportedOperationException.class, () -> content.getSpecRelation().clear()); + assertThrows(UnsupportedOperationException.class, () -> content.getRelationGroups().remove("rg-1")); + } + + @Test + void writingTwiceGivesTheSameLastChange(@TempDir Path dir) throws Exception { + + ReqIFDocument document = read(dir); + SpecObject so1 = document.getCoreContent().getSpecObject("so-1"); + so1.set("ad-prio", 8); + + String first = new ReqIFWriter().setClock(FIXED).toXml(document); + String second = new ReqIFWriter().setClock(Clock.fixed(Instant.parse("2027-01-01T00:00:00Z"), ZoneOffset.UTC)) + .toXml(document); + + assertEquals(first, second); + assertEquals("2026-10-11T12:34:56Z", so1.getLastChange()); + assertFalse(so1.isModified()); + } + + @Test + void aChangeAfterAnExplicitLastChangeCounts(@TempDir Path dir) throws Exception { + + ReqIFDocument document = read(dir); + SpecObject so1 = document.getCoreContent().getSpecObject("so-1"); + SpecObject so2 = document.getCoreContent().getSpecObject("so-2"); + so1.setDesc("changed").setLastChange("2026-08-01T00:00:00Z"); + so2.setLastChange("2026-08-01T00:00:00Z").setDesc("changed"); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + + assertTrue(xml.contains("IDENTIFIER=\"so-1\" LAST-CHANGE=\"2026-08-01T00:00:00Z\""), xml); + assertTrue(xml.contains("IDENTIFIER=\"so-2\" " + NOW), xml); + } + + @Test + void liftingNeverPutsTwoEqualIdentifiersSideBySide() { + + ReqIFCoreContent content = new ReqIFCoreContent(); + de.uni_stuttgart.ils.reqif4j.specification.SpecType type = new de.uni_stuttgart.ils.reqif4j.specification.SpecType( + "st", null, de.uni_stuttgart.ils.reqif4j.reqif.ReqIFConst.SPEC_OBJECT_TYPE); + SpecObject a = new SpecObject("so-a", type, List.of()); + SpecObject b = new SpecObject("so-b", type, List.of()); + content.addSpecType(type).addSpecObject(a).addSpecObject(b); + // [X (so-a) > G (so-b), G (so-a)]: once both so-a nodes are gone, one G is left + content.addSpecification(new Specification("spec", null, null, null, List.of( + new SpecHierarchy("sh-x", 1, 1, a, List.of(new SpecHierarchy("sh-g", 2, 1, b, null)))))); + Specification spec = content.getSpecification("spec"); + spec.addChild(new SpecHierarchy("sh-g", 0, 0, a, null)); + + content.removeSpecObject("so-a"); + + assertEquals(List.of("sh-g"), ids(spec.getChildren())); + assertSame(b, spec.getChildren().get(0).getSpecObject()); + assertEquals(1, spec.getChildren().get(0).getHierarchyLvl()); + } + + @Test + void identifiersAreUniqueAcrossTheDocument(@TempDir Path dir) throws Exception { + + ReqIFCoreContent content = read(dir).getCoreContent(); + SpecObject so1 = content.getSpecObject("so-1"); + + assertThrows(IllegalArgumentException.class, + () -> content.addSpecHierarchy("spec-1", 0, new SpecHierarchy("sh-2", 0, 0, so1, null))); + assertThrows(IllegalArgumentException.class, + () -> content.addSpecHierarchy("sh-2", 0, new SpecHierarchy("so-1", 0, 0, so1, null))); + assertThrows(IllegalArgumentException.class, () -> content.addSpecHierarchy("sh-new", 0, + new SpecHierarchy("sh-3", 0, 0, so1, null)), "no such parent"); + assertThrows(IllegalArgumentException.class, () -> content.addSpecHierarchy("spec-1", 0, + new SpecHierarchy("sh-3", 0, 0, so1, List.of(new SpecHierarchy("sh-1", 0, 0, so1, null)))), + "a node below clashes"); + assertThrows(IllegalArgumentException.class, + () -> content.addSpecObject(new SpecObject("sh-1", content.getSpecType("st-req"), List.of()))); + assertThrows(IllegalArgumentException.class, + () -> content.addSpecObject(new SpecObject(" ", content.getSpecType("st-req"), List.of()))); + assertNull(content.getSpecHierarchy("sh-3")); + + content.addSpecHierarchy("sh-2", 0, new SpecHierarchy("sh-3", 0, 0, so1, null)); + assertEquals(3, content.getSpecHierarchy("sh-3").getHierarchyLvl()); + } + + @Test + void newElementsInAReadDocumentGetTheTimeOfWriting(@TempDir Path dir) throws Exception { + + ReqIFDocument document = read(dir); + ReqIFCoreContent content = document.getCoreContent(); + SpecObject added = new SpecObject("so-3", content.getSpecType("st-req"), List.of()); + content.addSpecObject(added); + content.addSpecHierarchy("spec-1", 1, new SpecHierarchy("sh-3", 0, 0, added, null)); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + + assertTrue(xml.contains(" zipWriter.write(document, "a.reqif", dir)); + assertTrue(so1.isModified(), "a failed write is no save"); + + zipWriter.write(document, "a.reqif", dir.resolve("out.reqifz")); + assertFalse(so1.isModified()); + assertEquals("2026-10-11T12:34:56Z", so1.getLastChange()); + } + + @Test + void enumSelectionsMustNotContainNull(@TempDir Path dir) throws Exception { + + SpecObject so1 = read(dir).getCoreContent().getSpecObject("so-1"); + + assertThrows(IllegalArgumentException.class, () -> so1.setEnum("ad-status", (String[]) null)); + assertThrows(IllegalArgumentException.class, () -> so1.setEnum("ad-status", "ev-draft", null)); + IllegalArgumentException wrongSetter = assertThrows(IllegalArgumentException.class, + () -> so1.set("ad-status", "ev-draft")); + assertTrue(wrongSetter.getMessage().contains("setEnum"), wrongSetter.getMessage()); + } + + private static List ids(List nodes) { + return nodes.stream().map(SpecHierarchy::getID).toList(); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/OfficialReqIFSchema.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/OfficialReqIFSchema.java new file mode 100644 index 0000000..e2a2507 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/OfficialReqIFSchema.java @@ -0,0 +1,205 @@ +package de.uni_stuttgart.ils.reqif4j; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Assumptions; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; +import de.uni_stuttgart.ils.reqif4j.util.SecureXml; +import de.uni_stuttgart.ils.reqif4j.util.XmlUtils; +import de.uni_stuttgart.ils.reqif4j.validate.ReqIFValidator; +import de.uni_stuttgart.ils.reqif4j.validate.ValidationResult; + +/** + * Provides the official OMG ReqIF 1.2 schema for conformance tests. + * + * The XSD is not bundled with the library (licensing), so it is downloaded on + * first use together with everything it imports ({@code xml.xsd}, + * {@code driver.xsd} and the XHTML modules) and cached flat, by file name, in + * one directory - the layout {@link ReqIFValidator#validateAgainstSchema} + * expects. + * + * The cache directory is {@code $REQIF_SCHEMA_DIR}, or {@code target/reqif-schema} + * if the variable is not set. Without network access the conformance tests are + * skipped locally; on CI ({@code CI=true}) a failed download fails the build, + * so the tests can never silently stop running there. + */ +public final class OfficialReqIFSchema { + + public static final String REQIF_XSD_URL = "https://www.omg.org/spec/ReqIF/20110401/reqif.xsd"; + + private static Path cachedSchema; + private static String unavailable; + + private OfficialReqIFSchema() { + } + + /** + * @return the local reqif.xsd with all its imports next to it + */ + public static synchronized Path reqifXsd() { + + if (cachedSchema != null) { + return cachedSchema; + } + if (unavailable == null) { + Path directory = directory(); + try { + Files.createDirectories(directory); + download(directory); + cachedSchema = directory.resolve(fileName(URI.create(REQIF_XSD_URL))); + return cachedSchema; + } catch (IOException | RuntimeException e) { + unavailable = "Official ReqIF schema is not available in " + directory + ": " + e; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + unavailable = "Interrupted while downloading the ReqIF schema"; + } + } + // remembered, so offline every further test gives up at once instead of + // waiting for another connect timeout + if ("true".equalsIgnoreCase(System.getenv("CI"))) { + fail(unavailable); + } + Assumptions.abort(unavailable); + return null; + } + + /** + * Fails with all schema violations if the XML the writer produces for the + * document is not valid against the official ReqIF schema. + */ + public static void assertSchemaValid(ReqIFDocument document) throws IOException { + assertValid(new ReqIFValidator().validateAgainstSchema(document, reqifXsd())); + } + + /** + * Fails with all schema violations if the file is not valid against the + * official ReqIF schema. + */ + public static void assertSchemaValid(Path reqifFile) throws IOException { + assertValid(new ReqIFValidator().validateAgainstSchema(reqifFile, reqifXsd())); + } + + private static void assertValid(ValidationResult result) { + assertTrue(result.getIssues().isEmpty(), "not valid against the ReqIF schema:\n" + result); + } + + private static Path directory() { + String configured = System.getenv("REQIF_SCHEMA_DIR"); + return configured == null || configured.isBlank() + ? Path.of("target", "reqif-schema") + : Path.of(configured); + } + + /** + * Fetches reqif.xsd and, transitively, every schema it imports or includes. + * Files already in the cache are not downloaded again. + */ + private static void download(Path directory) throws IOException, InterruptedException { + + HttpClient client = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .proxy(ProxySelector.getDefault()) + .connectTimeout(Duration.ofSeconds(20)) + .build(); + + Deque pending = new ArrayDeque(); + Set seen = new HashSet(); + pending.add(URI.create(REQIF_XSD_URL)); + + while (!pending.isEmpty()) { + URI location = pending.poll(); + String fileName = fileName(location); + if (!seen.add(fileName)) { + continue; + } + + Path local = directory.resolve(fileName); + if (!Files.isRegularFile(local)) { + fetch(client, location, local); + } + for (URI imported : referencedSchemas(local)) { + pending.add(location.resolve(imported)); + } + } + } + + private static void fetch(HttpClient client, URI location, Path target) throws IOException, InterruptedException { + + // w3.org and omg.org serve the same files over https; the http URLs in + // the schemas would need a plain http proxy that CI and sandboxes lack + URI secure = "http".equalsIgnoreCase(location.getScheme()) + ? URI.create("https" + location.toString().substring("http".length())) + : location; + + HttpRequest request = HttpRequest.newBuilder(secure) + .timeout(Duration.ofSeconds(60)) + .header("User-Agent", "reqif4j-tests") + .GET() + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + try (InputStream body = response.body()) { + if (response.statusCode() != 200) { + throw new IOException("HTTP " + response.statusCode() + " for " + secure); + } + // write to a temporary file first, so an interrupted download never + // leaves a truncated schema in the cache + Path partial = Files.createTempFile(target.getParent(), target.getFileName().toString(), ".part"); + try { + Files.copy(body, partial, StandardCopyOption.REPLACE_EXISTING); + Files.move(partial, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } finally { + Files.deleteIfExists(partial); + } + } + } + + /** + * @return the schemaLocation of every xs:import, xs:include and xs:redefine + * of the schema; locations mentioned in annotations are ignored + */ + private static Set referencedSchemas(Path schema) throws IOException { + + Set locations = new HashSet(); + Document xsd; + try { + xsd = SecureXml.newDocumentBuilder().parse(schema.toFile()); + } catch (Exception e) { + throw new IOException("Cached schema " + schema + " is not readable; delete it to download it again", e); + } + for (String kind : new String[] { "import", "include", "redefine" }) { + for (Element reference : XmlUtils.descendantsByLocalName(xsd, kind)) { + String location = reference.getAttribute("schemaLocation"); + if (!location.isEmpty() && "http://www.w3.org/2001/XMLSchema".equals(reference.getNamespaceURI())) { + locations.add(URI.create(location)); + } + } + } + return locations; + } + + private static String fileName(URI location) { + String path = location.getPath(); + return path.substring(path.lastIndexOf('/') + 1); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/ReqIFValidatorTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/ReqIFValidatorTest.java index 5bf04e7..270ed4b 100644 --- a/src/test/java/de/uni_stuttgart/ils/reqif4j/ReqIFValidatorTest.java +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/ReqIFValidatorTest.java @@ -16,9 +16,9 @@ import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; import de.uni_stuttgart.ils.reqif4j.specification.RelationGroup; +import de.uni_stuttgart.ils.reqif4j.specification.SpecHierarchy; import de.uni_stuttgart.ils.reqif4j.specification.SpecObject; import de.uni_stuttgart.ils.reqif4j.validate.ReqIFValidator; -import de.uni_stuttgart.ils.reqif4j.validate.ValidationIssue; import de.uni_stuttgart.ils.reqif4j.validate.ValidationResult; /** @@ -62,7 +62,20 @@ void parsedFixtureIsValid(@TempDir Path tempDir) throws Exception { ValidationResult result = validate(reqif.getReqIFDocument()); - assertTrue(result.isValid(), "the fixture must validate cleanly, got:\n" + result); + // The main fixture deliberately carries a date-only DATE value, which the + // reader accepts leniently but the schema does not (xsd:dateTime needs a + // time). That must be the only finding. + assertEquals(1, result.getIssues().size(), result.toString()); + assertTrue(hasError(result, "'2026-01-01' is not an xsd:dateTime"), result.toString()); + } + + @Test + void schemaValidFixtureIsValid(@TempDir Path tempDir) throws Exception { + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "valid.reqif", TestFixtures.SCHEMA_VALID_REQIF).toString()); + + ValidationResult result = validate(reqif.getReqIFDocument()); + + assertTrue(result.getIssues().isEmpty(), "the fixture must validate cleanly, got:\n" + result); } @Test @@ -73,19 +86,21 @@ void generatedDocumentIsValid() { @Test void missingSpecObjectOfARelationIsReported() { ReqIFDocument document = validBuilder().build(); - // remove the relation target after building - document.getCoreContent().getSpecObjects().remove("so-2"); + // point the relation at an object that does not exist + document.getCoreContent().getSpecRelation("sr-1").setTargetObjID("so-gone"); ValidationResult result = validate(document); assertFalse(result.isValid()); - assertTrue(hasError(result, "Relation target so-2 is not a known spec object"), result.toString()); + assertTrue(hasError(result, "Relation target so-gone is not a known spec object"), result.toString()); } @Test void unknownSpecHierarchyTargetIsReported() { ReqIFDocument document = validBuilder().build(); - document.getCoreContent().getSpecObjects().remove("so-1"); + // a node that shows an object the content does not have + SpecObject elsewhere = new SpecObject("so-gone", document.getCoreContent().getSpecType("st-req"), List.of()); + document.getCoreContent().getSpecification("spec-1").addChild(new SpecHierarchy("sh-gone", 1, 2, elsewhere, null)); ValidationResult result = validate(document); @@ -123,15 +138,31 @@ void singleValuedEnumWithSeveralValuesIsReported() { @Test void identifierUsedTwiceAcrossCategoriesIsReported() { ReqIFDocument document = validBuilder().build(); - // reuse the datatype id for a relation group - document.getCoreContent().addRelationGroup( - new RelationGroup("dt-string", "Clash", "st-rel", "spec-1", "spec-1", List.of())); + // reuse the datatype id for a hierarchy node; a document read from a + // file may have that + SpecObject so1 = document.getCoreContent().getSpecObject("so-1"); + document.getCoreContent().getSpecification("spec-1").addChild(new SpecHierarchy("dt-string", 1, 2, so1, null)); ValidationResult result = validate(document); assertTrue(hasError(result, "Identifier is used twice"), result.toString()); } + @Test + void editableAttributesMustBeDeclared() throws Exception { + // as read from a file; the model refuses such a list + String xml = new de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter().toXml(validBuilder().build()).replace( + "so-1", + "ad-ghost" + + "so-1"); + ReqIFDocument document = new ReqIFDocument(new java.io.ByteArrayInputStream( + xml.getBytes(java.nio.charset.StandardCharsets.UTF_8)), "ghost.reqif"); + + ValidationResult result = validate(document); + + assertTrue(hasError(result, "EDITABLE-ATTS references attribute definition ad-ghost"), result.toString()); + } + @Test void relationGroupWithUnknownReferencesIsReported() { ReqIFDocument document = validBuilder().build(); @@ -146,27 +177,28 @@ void relationGroupWithUnknownReferencesIsReported() { } @Test - void missingHeaderIsOnlyAWarning() { + void missingHeaderIsAnError() { ReqIFDocument document = new ReqIFDocument(null, validBuilder().build().getCoreContent()); ValidationResult result = validate(document); - assertTrue(result.isValid(), "a missing header must not make the document invalid"); + // the schema requires THE-HEADER, and the writer cannot invent its IDENTIFIER + assertFalse(result.isValid()); assertEquals(1, result.getIssues().size()); - assertEquals(ValidationIssue.Severity.WARNING, result.getIssues().get(0).getSeverity()); + assertEquals("Document has no THE-HEADER", result.getIssues().get(0).getMessage()); } @Test void throwIfInvalidReportsEveryError() { ReqIFDocument document = validBuilder().build(); - document.getCoreContent().getSpecObjects().remove("so-2"); + document.getCoreContent().getSpecRelation("sr-1").setTargetObjID("so-gone"); IllegalStateException failure = assertThrows(IllegalStateException.class, () -> validate(document).throwIfInvalid()); assertTrue(failure.getMessage().contains("ReqIF document is invalid")); - assertTrue(failure.getMessage().contains("so-2"), failure.getMessage()); + assertTrue(failure.getMessage().contains("so-gone"), failure.getMessage()); } @Test diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/ReqIFzAttachmentsTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/ReqIFzAttachmentsTest.java new file mode 100644 index 0000000..97af2fd --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/ReqIFzAttachmentsTest.java @@ -0,0 +1,152 @@ +package de.uni_stuttgart.ils.reqif4j; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFz; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFzWriter; + +/** + * A .reqifz archive keeps every file next to its documents - not only + * pictures - and extracting it leaves nothing behind. + */ +class ReqIFzAttachmentsTest { + + private static final Map ATTACHMENTS = new LinkedHashMap<>(); + static { + ATTACHMENTS.put("files/diagram.ole", new byte[] { (byte) 0xD0, (byte) 0xCF, 0x11, (byte) 0xE0 }); + ATTACHMENTS.put("files/diagram.png", new byte[] { (byte) 0x89, 'P', 'N', 'G' }); + ATTACHMENTS.put("files/logo.GIF", "GIF89a".getBytes(StandardCharsets.US_ASCII)); + ATTACHMENTS.put("files/sub/manual.pdf", "%PDF-1.7".getBytes(StandardCharsets.US_ASCII)); + ATTACHMENTS.put("files/photo.JPG", new byte[] { (byte) 0xFF, (byte) 0xD8 }); + } + + @Test + void everyEntryIsKeptAsAnAttachment(@TempDir Path tempDir) throws Exception { + + try (ReqIFz reqifz = new ReqIFz(archive(tempDir).toString())) { + + assertEquals(List.of("spec.reqif"), List.copyOf(reqifz.getReqIFDocuments().keySet())); + assertEquals(List.copyOf(ATTACHMENTS.keySet()), List.copyOf(reqifz.getAttachmentFiles().keySet())); + assertEquals(List.of("files/diagram.png", "files/logo.GIF", "files/photo.JPG"), + List.copyOf(reqifz.getPictureFiles().keySet()), "pictures are recognized case-insensitively"); + for (Map.Entry attachment : reqifz.getAttachmentFiles().entrySet()) { + assertArrayEquals(ATTACHMENTS.get(attachment.getKey()), Files.readAllBytes(attachment.getValue().toPath())); + } + } + } + + @Test + void repackingKeepsEveryAttachment(@TempDir Path tempDir) throws Exception { + + Path repacked = tempDir.resolve("out.reqifz"); + try (ReqIFz reqifz = new ReqIFz(archive(tempDir).toString())) { + new ReqIFzWriter().write(reqifz, repacked); + } + + try (ZipFile zip = new ZipFile(repacked.toFile())) { + for (Map.Entry attachment : ATTACHMENTS.entrySet()) { + ZipEntry entry = zip.getEntry(attachment.getKey()); + assertTrue(entry != null, attachment.getKey() + " is lost"); + try (InputStream in = zip.getInputStream(entry)) { + assertArrayEquals(attachment.getValue(), in.readAllBytes()); + } + } + assertTrue(zip.getEntry("spec.reqif") != null); + } + } + + @Test + void extractingLeavesNothingBehind(@TempDir Path tempDir) throws Exception { + + Path archive = archive(tempDir); + File extracted; + try (ReqIFz reqifz = new ReqIFz(archive.toString())) { + extracted = reqifz.getAttachmentFiles().get("files/diagram.ole"); + assertTrue(extracted.isFile()); + } + + assertFalse(extracted.exists(), "close() deletes the extracted files"); + try (var siblings = Files.list(tempDir)) { + assertEquals(List.of(archive), siblings.toList(), "no directory is created next to the archive"); + } + } + + @Test + void archiveCanBeReadFromAStream(@TempDir Path tempDir) throws Exception { + + try (ReqIFz reqifz = new ReqIFz(Files.newInputStream(archive(tempDir)), "upload.reqifz")) { + assertEquals("upload.reqifz", reqifz.getName()); + assertEquals(1, reqifz.getReqIFDocumentsCount()); + assertEquals(ATTACHMENTS.size(), reqifz.getAttachmentFiles().size()); + } + } + + @Test + void documentEntriesAreRecognizedCaseInsensitively(@TempDir Path tempDir) throws Exception { + + Path archive = tempDir.resolve("upper.reqifz"); + try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(archive))) { + put(zip, "SPEC.REQIF", TestFixtures.SCHEMA_VALID_REQIF.getBytes(StandardCharsets.UTF_8)); + put(zip, "notes.notreqif", new byte[] { 1 }); + } + + try (ReqIFz reqifz = new ReqIFz(archive.toString())) { + assertEquals(List.of("SPEC.REQIF"), List.copyOf(reqifz.getReqIFDocuments().keySet())); + assertEquals(List.of("notes.notreqif"), List.copyOf(reqifz.getAttachmentFiles().keySet())); + } + } + + @Test + void duplicateAndBackslashEntriesDoNotBreakReading(@TempDir Path tempDir) throws Exception { + + Path archive = tempDir.resolve("odd.reqifz"); + try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(archive))) { + put(zip, "spec.reqif", TestFixtures.SCHEMA_VALID_REQIF.getBytes(StandardCharsets.UTF_8)); + put(zip, "files\\old.png", new byte[] { 1 }); + // some tools append an updated entry under the same name + put(zip, "files/old.png", new byte[] { 2 }); + } + + try (ReqIFz reqifz = new ReqIFz(archive.toString())) { + assertEquals(List.of("files/old.png"), List.copyOf(reqifz.getAttachmentFiles().keySet())); + assertArrayEquals(new byte[] { 2 }, Files.readAllBytes(reqifz.getAttachmentFiles().get("files/old.png").toPath()), + "the later entry wins, as when unzipping"); + } + } + + private static Path archive(Path dir) throws IOException { + Path archive = dir.resolve("in.reqifz"); + try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(archive))) { + put(zip, "spec.reqif", TestFixtures.SCHEMA_VALID_REQIF.getBytes(StandardCharsets.UTF_8)); + for (Map.Entry attachment : ATTACHMENTS.entrySet()) { + put(zip, attachment.getKey(), attachment.getValue()); + } + } + return archive; + } + + private static void put(ZipOutputStream zip, String name, byte[] content) throws IOException { + zip.putNextEntry(new ZipEntry(name)); + zip.write(content); + zip.closeEntry(); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/RequiredValuesTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/RequiredValuesTest.java new file mode 100644 index 0000000..c9a3e35 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/RequiredValuesTest.java @@ -0,0 +1,262 @@ +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.w3c.dom.Document; + +import de.uni_stuttgart.ils.reqif4j.attributes.AttributeValueBoolean; +import de.uni_stuttgart.ils.reqif4j.build.ReqIFBuildException; +import de.uni_stuttgart.ils.reqif4j.build.ReqIFBuilder; +import de.uni_stuttgart.ils.reqif4j.datatypes.DatatypeDouble; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; +import de.uni_stuttgart.ils.reqif4j.specification.SpecType; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; + +/** + * The writer emits every value the ReqIF schema requires, also when the model + * does not carry it - typically for documents generated with the builder. + */ +class RequiredValuesTest { + + private static final Clock FIXED = Clock.fixed(Instant.parse("2026-10-11T12:34:56.789Z"), ZoneOffset.UTC); + + @Test + void elementsWithoutLastChangeGetTheHeaderCreationTime() { + + ReqIFDocument document = minimalDocument(h -> h.id("hdr").creationTime("2026-07-23T10:00:00Z")); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + + assertTrue(xml.contains(""), xml); + assertFalse(xml.contains("2026-10-11T12:34:56"), "the clock is not needed when the header has a creation time"); + } + + @Test + void withoutCreationTimeTheTimeOfWritingIsUsedEverywhere() { + + ReqIFDocument document = minimalDocument(h -> h.id("hdr")); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + + // truncated to seconds, one timestamp for header and elements alike + assertTrue(xml.contains("2026-10-11T12:34:56Z"), xml); + assertTrue(xml.contains(""), xml); + } + + @Test + void writingDoesNotChangeTheModel() { + + ReqIFDocument document = minimalDocument(h -> h.id("hdr")); + + new ReqIFWriter().setClock(FIXED).toXml(document); + + assertNull(document.getCoreContent().getSpecObject("so-1").getLastChange()); + assertEquals("", document.getHeader().getCreationTime()); + } + + @Test + void existingLastChangeIsKept(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "in.reqif", TestFixtures.SCHEMA_VALID_REQIF).toString()); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(reqif.getReqIFDocument()); + + assertTrue(xml.contains("IDENTIFIER=\"so-1\" LAST-CHANGE=\"2026-07-03T09:15:00Z\""), xml); + assertFalse(xml.contains("2026-10-11"), xml); + } + + @Test + void missingHeaderFieldsGetDefaults() { + + ReqIFDocument document = minimalDocument(h -> h.id("hdr").reqifVersion("")); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + + assertTrue(xml.contains("" + + "2026-10-11T12:34:56Z" + + "reqif4j" + + "1.0" + + "reqif4j" + + "" + + "</REQ-IF-HEADER>"), xml); + } + + @Test + void requiredEnumerationAttributesAreAlwaysWritten() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .enumerationDatatype("dt-enum", "Color", e -> e.value("ev-red", "Red", "1")) + .specObjectType("st", "Type", t -> t.enumerationAttribute("ad-color", "Color", "dt-enum", false)) + .build(); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + + assertTrue(xml.contains("<EMBEDDED-VALUE KEY=\"1\" OTHER-CONTENT=\"\"/>"), xml); + assertTrue(xml.contains("IDENTIFIER=\"ad-color\" LAST-CHANGE=\"2026-10-11T12:34:56Z\" LONG-NAME=\"Color\" MULTI-VALUED=\"false\""), xml); + } + + @Test + void realDatatypeRoundTripsAccuracyAndBounds(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "in.reqif", TestFixtures.SCHEMA_VALID_REQIF).toString()); + DatatypeDouble real = (DatatypeDouble) reqif.getReqIFCoreContent().getDatatype("dt-real"); + assertEquals(3, real.getAccuracy()); + assertEquals(-1.5, real.getMin()); + assertEquals(1000.0, real.getMax()); + + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + + assertTrue(xml.contains("ACCURACY=\"3\" IDENTIFIER=\"dt-real\" LAST-CHANGE=\"2026-07-01T08:00:00Z\" LONG-NAME=\"Real\" MAX=\"1000.0\" MIN=\"-1.5\""), xml); + assertTrue(xml.contains("<ATTRIBUTE-VALUE-REAL THE-VALUE=\"2.5\">"), xml); + } + + @Test + void realDatatypeWithoutBoundsGetsSchemaValidDefaults() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .realDatatype("dt-real", "Real") + .realDatatype("dt-ratio", "Ratio", 2, 0.0, Double.POSITIVE_INFINITY) + .build(); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + + assertTrue(xml.contains("ACCURACY=\"15\" IDENTIFIER=\"dt-real\""), xml); + assertTrue(xml.contains("MAX=\"1.7976931348623157E308\" MIN=\"-1.7976931348623157E308\""), xml); + // xsd:double spells infinity INF, not Java's "Infinity" + assertTrue(xml.contains("ACCURACY=\"2\" IDENTIFIER=\"dt-ratio\""), xml); + assertTrue(xml.contains("MAX=\"INF\" MIN=\"0.0\""), xml); + } + + @Test + void realBoundsSpelledInXsdNotationAreRead(@TempDir Path tempDir) throws Exception { + + String infinite = TestFixtures.SCHEMA_VALID_REQIF.replace("MIN=\"-1.5\" MAX=\"1000.0\"", "MIN=\"-INF\" MAX=\"INF\""); + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "in.reqif", infinite).toString()); + + DatatypeDouble real = (DatatypeDouble) reqif.getReqIFCoreContent().getDatatype("dt-real"); + + assertEquals(Double.NEGATIVE_INFINITY, real.getMin()); + assertEquals(Double.POSITIVE_INFINITY, real.getMax()); + } + + @Test + void infiniteRealValuesAreWrittenInXsdNotationAndReadBack(@TempDir Path tempDir) throws Exception { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .realDatatype("dt-real", "Real", 2, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY) + .specObjectType("st", "Type", t -> t.realAttribute("ad-effort", "Effort", "dt-real")) + .specObject("so-1", "st", o -> o.set("ad-effort", Double.POSITIVE_INFINITY)) + .build(); + Path file = tempDir.resolve("out.reqif"); + new ReqIFWriter().setClock(FIXED).write(document, file); + + String xml = Files.readString(file); + assertTrue(xml.contains("<ATTRIBUTE-VALUE-REAL THE-VALUE=\"INF\">"), xml); + + // the library reads back what it writes + ReqIF reqif = new ReqIF(file.toString()); + assertEquals(Double.POSITIVE_INFINITY, reqif.getReqIFCoreContent().getSpecObject("so-1").getAttribute("Effort")); + } + + @Test + void headerWithoutIdentifierGetsAFreeDefault() { + + ReqIFDocument withoutHeader = minimalDocument(h -> { }); + ReqIFDocument clashing = ReqIFBuilder.create() + .stringDatatype(ReqIFBuilder.DEFAULT_HEADER_ID, "String", 10) + .build(); + + assertEquals("reqif4j-header", withoutHeader.getHeader().getID()); + assertEquals("reqif4j-header-2", clashing.getHeader().getID()); + } + + @Test + void inconsistentRealDatatypeIsRefused() { + + assertThrows(ReqIFBuildException.class, + () -> ReqIFBuilder.create().realDatatype("dt", "Real", -1, 0.0, 1.0)); + assertThrows(ReqIFBuildException.class, + () -> ReqIFBuilder.create().realDatatype("dt", "Real", 2, 10.0, 0.0)); + assertThrows(ReqIFBuildException.class, + () -> ReqIFBuilder.create().realDatatype("dt", "Real", 2, Double.NaN, 0.0)); + } + + @Test + void xhtmlDefaultValueIsWrittenAsMarkup() throws Exception { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .xhtmlDatatype("dt-xhtml", "XHTML") + .specObjectType("st", "Type", t -> t.attribute("ad-text", "Text", "dt-xhtml", "<p>to be written</p>")) + .build(); + + String xml = new ReqIFWriter().setClock(FIXED).toXml(document); + + assertTrue(xml.contains("<DEFAULT-VALUE><ATTRIBUTE-VALUE-XHTML><DEFINITION>" + + "<ATTRIBUTE-DEFINITION-XHTML-REF>ad-text</ATTRIBUTE-DEFINITION-XHTML-REF></DEFINITION>" + + "<THE-VALUE><div xmlns=\"http://www.w3.org/1999/xhtml\"><p>to be written</p></div></THE-VALUE>"), xml); + OfficialReqIFSchema.assertSchemaValid(document); + } + + @Test + void xhtmlAndBooleanDefaultsSurviveARoundTrip(@TempDir Path tempDir) throws Exception { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .xhtmlDatatype("dt-xhtml", "XHTML") + .booleanDatatype("dt-bool", "Bool") + .specObjectType("st", "Type", t -> t + .attribute("ad-text", "Text", "dt-xhtml", "<p>tbd</p>") + .attribute("ad-flag", "Flag", "dt-bool", "1")) + .build(); + Path file = tempDir.resolve("out.reqif"); + new ReqIFWriter().setClock(FIXED).write(document, file); + + ReqIF reqif = new ReqIF(file.toString()); + SpecType type = reqif.getReqIFCoreContent().getSpecType("st"); + + assertEquals("<div><p>tbd</p></div>", type.getAttributeDefinition("ad-text").getDefaultValue()); + assertEquals("1", type.getAttributeDefinition("ad-flag").getDefaultValue()); + // xsd:boolean "1" is true + assertEquals(true, new AttributeValueBoolean("1", type.getAttributeDefinition("ad-flag")).getValue()); + // written again, the default is still there + assertTrue(new ReqIFWriter().toXml(reqif.getReqIFDocument()).contains("<p>tbd</p>")); + } + + @Test + void outputCarriesNoInternalState() { + + Document xml = new ReqIFWriter().buildDocument(minimalDocument(h -> h.id("hdr"))); + + assertNull(xml.getUserData("reqif4j.defaultLastChange")); + } + + private static ReqIFDocument minimalDocument(Consumer<ReqIFBuilder.HeaderBuilder> header) { + return ReqIFBuilder.create() + .header(header) + .stringDatatype("dt-string", "String", 100) + .specObjectType("st-req", "Requirement", t -> t.stringAttribute("ad-name", "Name", "dt-string")) + .specificationType("st-spec", "Spec", t -> { }) + .specObject("so-1", "st-req", o -> o.set("ad-name", "x")) + .specification("spec-1", "Spec", "st-spec", s -> s.child("sh-1", "so-1")) + .build(); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/RobustnessTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/RobustnessTest.java index 450a674..417f536 100644 --- a/src/test/java/de/uni_stuttgart/ils/reqif4j/RobustnessTest.java +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/RobustnessTest.java @@ -82,6 +82,8 @@ void emptyNumericValuesDefaultToZero(@TempDir Path tempDir) throws Exception { assertEquals(0, so1.getAttribute("Priority"), "a missing THE-VALUE on an integer attribute must parse as 0, not crash"); + assertTrue(reqif.getReqIFDocument().getParseIssues().get(0).getMessage().contains("has no THE-VALUE"), + "but it is reported"); } @Test diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/SchemaConformanceTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/SchemaConformanceTest.java new file mode 100644 index 0000000..9d6861b --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/SchemaConformanceTest.java @@ -0,0 +1,161 @@ +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.assertTrue; + +import java.nio.file.Files; +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.build.ReqIFBuilder; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; +import de.uni_stuttgart.ils.reqif4j.validate.ReqIFValidator; +import de.uni_stuttgart.ils.reqif4j.validate.ValidationIssue; +import de.uni_stuttgart.ils.reqif4j.validate.ValidationResult; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; + +/** + * Validation against the official OMG ReqIF schema, whose imports (xml.xsd, + * driver.xsd, XHTML modules) are resolved from the schema's directory instead + * of being fetched over http. + */ +class SchemaConformanceTest { + + @Test + void validFileIsAcceptedByTheOfficialSchema(@TempDir Path tempDir) throws Exception { + + Path file = TestFixtures.write(tempDir, "valid.reqif", TestFixtures.SCHEMA_VALID_REQIF); + + OfficialReqIFSchema.assertSchemaValid(file); + } + + @Test + void violationsOfAFileAreReportedWithLineNumbers(@TempDir Path tempDir) throws Exception { + + String withoutLastChange = TestFixtures.SCHEMA_VALID_REQIF.replace( + "<SPEC-OBJECT IDENTIFIER=\"so-2\" LAST-CHANGE=\"2026-07-03T09:20:00Z\">", + "<SPEC-OBJECT IDENTIFIER=\"so-2\">"); + Path file = TestFixtures.write(tempDir, "invalid.reqif", withoutLastChange); + int line = lineOf(withoutLastChange, "<SPEC-OBJECT IDENTIFIER=\"so-2\">"); + + ValidationResult result = new ReqIFValidator() + .validateAgainstSchema(file, OfficialReqIFSchema.reqifXsd()); + + List<ValidationIssue> errors = result.getErrors(); + assertEquals(1, errors.size(), result.toString()); + assertTrue(errors.get(0).getMessage().contains("LAST-CHANGE"), result.toString()); + assertTrue(errors.get(0).getMessage().startsWith("line " + line + ":"), result.toString()); + } + + @Test + void malformedFileIsReportedOnce(@TempDir Path tempDir) throws Exception { + + Path file = TestFixtures.write(tempDir, "truncated.reqif", + "<REQ-IF xmlns=\"http://www.omg.org/spec/ReqIF/20110401/reqif.xsd\"><THE-HEADER>"); + + ValidationResult result = new ReqIFValidator() + .validateAgainstSchema(file, OfficialReqIFSchema.reqifXsd()); + + // the parser reports a fatal error and then throws it again + assertEquals(1, result.getErrors().size(), result.toString()); + } + + @Test + void documentAndFileValidationReportTheSameFindings(@TempDir Path tempDir) throws Exception { + + // two objects with the same disallowed XHTML element: on a DOM source + // the findings have no position, but they are still two findings + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .xhtmlDatatype("dt-xhtml", "XHTML") + .specObjectType("st", "Type", t -> t.xhtmlAttribute("ad-text", "Text", "dt-xhtml")) + .specObject("so-1", "st", o -> o.setXhtml("ad-text", "<font>x</font>")) + .specObject("so-2", "st", o -> o.setXhtml("ad-text", "<font>x</font>")) + .build(); + Path file = tempDir.resolve("out.reqif"); + new ReqIFWriter().write(document, file); + + ValidationResult ofDocument = new ReqIFValidator().validateAgainstSchema(document, OfficialReqIFSchema.reqifXsd()); + ValidationResult ofFile = new ReqIFValidator().validateAgainstSchema(file, OfficialReqIFSchema.reqifXsd()); + + assertFalse(ofFile.isValid()); + assertEquals(ofFile.getErrors().size(), ofDocument.getErrors().size(), ofDocument.toString()); + } + + @Test + void roundTripOfASchemaValidDocumentStaysSchemaValid(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = new ReqIF(TestFixtures.write(tempDir, "in.reqif", TestFixtures.SCHEMA_VALID_REQIF).toString()); + + // the XML the writer produces for the model ... + OfficialReqIFSchema.assertSchemaValid(reqif.getReqIFDocument()); + + // ... and the file written to disk + Path written = tempDir.resolve("out.reqif"); + new ReqIFWriter().write(reqif.getReqIFDocument(), written); + OfficialReqIFSchema.assertSchemaValid(written); + } + + @Test + void documentGeneratedWithTheBuilderIsSchemaValid() throws Exception { + + // the README example, plus the kinds whose required attributes are easy + // to drop: REAL bounds, a single-valued enumeration, empty OTHER-CONTENT + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr-1").title("My Spec").toolID("reqif4j") + .creationTime("2026-07-23T10:00:00Z")) + .stringDatatype("dt-string", "String", 4096) + .xhtmlDatatype("dt-xhtml", "XHTML") + .realDatatype("dt-real", "Real") + .enumerationDatatype("dt-enum", "Color", e -> e + .value("ev-red", "Red", "1", "#ff0000") + .value("ev-blue", "Blue", "2")) + .specObjectType("st-req", "Requirement Type", t -> t + .stringAttribute("ad-title", "ReqIF.Name", "dt-string") + .xhtmlAttribute("ad-text", "ReqIF.Text", "dt-xhtml") + .realAttribute("ad-effort", "Effort", "dt-real") + .enumerationAttribute("ad-color", "Colors", "dt-enum", true, List.of("ev-blue")) + .enumerationAttribute("ad-main", "Main color", "dt-enum", false)) + .specificationType("st-spec", "Specification Type", t -> { }) + .specRelationType("st-rel", "satisfies", t -> { }) + .specObject("so-1", "st-req", o -> o + .set("ad-title", "First requirement") + .set("ad-effort", 2.5) + .setEnum("ad-color", "ev-red", "ev-blue") + .setEnum("ad-main", "ev-red") + .setXhtml("ad-text", "<p>The system shall boot.</p>")) + .specObject("so-2", "st-req") + .specRelation("sr-1", "st-rel", "so-1", "so-2") + .specification("spec-1", "Main Spec", "st-spec", s -> s + .child("sh-1", "so-1", c -> c.child("sh-2", "so-2"))) + .build(); + + OfficialReqIFSchema.assertSchemaValid(document); + } + + @Test + void missingImportsAreNamedInsteadOfFetched(@TempDir Path tempDir) throws Exception { + + // only reqif.xsd, without xml.xsd / driver.xsd next to it + Path schema = Files.copy(OfficialReqIFSchema.reqifXsd(), tempDir.resolve("reqif.xsd")); + Path file = TestFixtures.write(tempDir, "valid.reqif", TestFixtures.SCHEMA_VALID_REQIF); + + ValidationResult result = new ReqIFValidator().validateAgainstSchema(file, schema); + + assertFalse(result.isValid()); + String message = result.getErrors().get(0).getMessage(); + assertTrue(message.startsWith("Schema could not be loaded"), message); + assertTrue(message.contains("xml.xsd (http://www.w3.org/2001/xml.xsd)"), message); + assertTrue(message.contains("place them next to reqif.xsd"), message); + } + + private static int lineOf(String text, String fragment) { + String before = text.substring(0, text.indexOf(fragment)); + return (int) before.chars().filter(c -> c == '\n').count() + 1; + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java index 869bf2e..2a36c89 100644 --- a/src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/TestFixtures.java @@ -190,6 +190,173 @@ private TestFixtures() { </REQ-IF> """; + /** + * A document that is valid against the official OMG ReqIF schema and uses + * every Identifiable property (LAST-CHANGE, LONG-NAME, DESC, ALTERNATIVE-ID) + * on the elements that carry one, including spec objects, relations and + * hierarchy nodes. Values are set for some attributes only, and the + * ALTERNATIVE-ID uses the nested form the schema prescribes. Also covers the + * required attributes that are easy to drop: REAL ACCURACY/MIN/MAX, + * MULTI-VALUED="false" and an empty OTHER-CONTENT. + */ + public static final String SCHEMA_VALID_REQIF = """ + <?xml version="1.0" encoding="UTF-8"?> + <REQ-IF xmlns="http://www.omg.org/spec/ReqIF/20110401/reqif.xsd" xmlns:xhtml="http://www.w3.org/1999/xhtml"> + <THE-HEADER> + <REQ-IF-HEADER IDENTIFIER="header-1"> + <COMMENT>Created by: Tester</COMMENT> + <CREATION-TIME>2026-07-23T10:00:00Z</CREATION-TIME> + <REQ-IF-TOOL-ID>reqif4j</REQ-IF-TOOL-ID> + <REQ-IF-VERSION>1.0</REQ-IF-VERSION> + <SOURCE-TOOL-ID>test</SOURCE-TOOL-ID> + <TITLE>Schema-valid fixture + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dt-string + + + dt-xhtml + + + dt-int + + + ad-prio + + + + + dt-bool + + + dt-date + + + dt-enum + + + dt-enum + + + dt-real + + + + + + + + + + + + + ad-id + + + ad-text + The system shall boot within 5 s. + + + ad-prio + + + ad-safety + + + ad-due + + + ad-status + ev-approved + + + ad-review + ev-draft + + + ad-effort + + + st-req + + + + + ad-id + + + ad-safety + + + st-req + + + + + st-rel + so-2 + so-1 + + + + + st-spec + + + so-1 + + + so-2 + + + + + + + + + st-group + spec-1 + spec-1 + sr-1 + + + + + + """; + /** * Writes the given content to {@code dir/name} and returns the path. */ diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/TolerantImportTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/TolerantImportTest.java new file mode 100644 index 0000000..e9b6f87 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/TolerantImportTest.java @@ -0,0 +1,249 @@ +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.assertTrue; + +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import de.uni_stuttgart.ils.reqif4j.reqif.ParseIssue; +import de.uni_stuttgart.ils.reqif4j.attributes.UninterpretedAttributeValue; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIF; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFCoreContent; +import de.uni_stuttgart.ils.reqif4j.specification.SpecObject; +import de.uni_stuttgart.ils.reqif4j.validate.ReqIFValidator; +import de.uni_stuttgart.ils.reqif4j.validate.ValidationResult; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; + +/** + * A document with broken parts is still read: what cannot be used is skipped + * and reported, what does not fit its datatype is kept as written, and the + * validator turns the reports into errors. + */ +class TolerantImportTest { + + @Test + void unknownSpecTypeKeepsTheObjectAndItsReference(@TempDir Path tempDir) throws Exception { + + // so-1 is the first object referencing st-req + ReqIF reqif = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF.replaceFirst( + "st-req", "st-gone")); + SpecObject boot = reqif.getReqIFCoreContent().getSpecObject("so-1"); + + assertNotNull(boot, "the object is kept"); + assertEquals("st-gone", boot.getSpecTypeID()); + assertNull(boot.getSpecTypeName()); + assertEquals(8, boot.getValues().size(), "the values are kept, uninterpreted"); + assertTrue(boot.getValues().values().stream().allMatch(v -> v instanceof UninterpretedAttributeValue)); + assertEquals("REQ-1", boot.getValue("ad-id").getValue(), "THE-VALUE as written"); + assertEquals(List.of("[so-1] SPEC-OBJECT references type st-gone, which is not declared; its 8 values are kept as written"), + issues(reqif), "one report per defect"); + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + assertTrue(xml.contains("st-gone")); + assertTrue(xml.contains("ad-effort"), xml); + } + + @Test + void valuesOfAnUnknownTypeAreCheckedAsWritten(@TempDir Path tempDir) throws Exception { + + // two values for one definition, one without the required THE-VALUE + ReqIF reqif = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF + .replaceFirst("st-req", "st-gone") + // so-1's boolean value: no THE-VALUE, and the definition of another value + .replaceFirst("(\\s*)ad-safety", + "$1ad-id")); + SpecObject boot = reqif.getReqIFCoreContent().getSpecObject("so-1"); + + assertTrue(issues(reqif).contains("[so-1] SPEC-OBJECT references type st-gone, which is not declared; " + + "its 7 values are kept as written"), issues(reqif).toString()); + assertTrue(issues(reqif).contains("[so-1] Attribute definition ad-id has two values; the later one replaces the earlier")); + assertEquals("ev-approved", boot.getValue("ad-status").getValue(), "the enum value IDENTIFIERs"); + assertTrue(boot.getValue("ad-text").getValue().toString().startsWith("The system"), boot.getValue("ad-text").getValue().toString()); + String report = new de.uni_stuttgart.ils.reqif4j.validate.ReqIFValidator().validate(reqif.getReqIFDocument()).toString(); + assertFalse(report.contains("has no THE-VALUE"), "the type is unknown, so the values are not checked:\n" + report); + } + + @Test + void valuesKeptAsWrittenJoinTheReqIFNamespace(@TempDir Path tempDir) throws Exception { + + // a document without namespace, as some tools write it + ReqIF reqif = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF + .replace(" xmlns=\"http://www.omg.org/spec/ReqIF/20110401/reqif.xsd\"", "") + .replaceFirst("st-req", "st-gone")); + + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + + assertFalse(xml.contains("xmlns=\"\""), xml); + assertTrue(xml.contains(""), xml); + } + + @Test + void valueOfAnUnknownDefinitionIsKeptAsWritten(@TempDir Path tempDir) throws Exception { + + // the only value referencing ad-effort is so-1's + ReqIF reqif = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF.replace( + "ad-effort", + "ad-gone")); + + assertEquals(List.of("[so-1] Value references attribute definition ad-gone, which spec type st-req does not " + + "declare; the value is kept as written"), issues(reqif)); + SpecObject boot = reqif.getReqIFCoreContent().getSpecObject("so-1"); + assertEquals(8, boot.getValues().size()); + assertTrue(boot.getValue("ad-gone") instanceof UninterpretedAttributeValue); + assertTrue(new ReqIFWriter().toXml(reqif.getReqIFDocument()) + .contains("ad-gone")); + assertTrue(new de.uni_stuttgart.ils.reqif4j.validate.ReqIFValidator().validate(reqif.getReqIFDocument()) + .toString().contains("uses definition ad-gone, which does not belong to spec type st-req")); + } + + @Test + void valuesThatDoNotFitTheirDatatypeAreKeptAsWritten(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF + .replace("", "") + .replace("", "") + .replace("", "")); + SpecObject boot = reqif.getReqIFCoreContent().getSpecObject("so-1"); + + // no NumberFormatException; the text survives for the user to fix + assertEquals("one", boot.getValue("ad-prio").getValue()); + assertEquals("1.5d", boot.getValue("ad-effort").getValue(), "Java accepts 1.5d, xsd:double does not"); + assertEquals("yes", boot.getValue("ad-safety").getValue()); + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + assertTrue(xml.contains(""), xml); + + ValidationResult result = new ReqIFValidator().validate(reqif.getReqIFDocument()); + assertTrue(messages(result).containsAll(List.of( + "Attribute 'Priority': 'one' is not an integer", + "Attribute 'Effort': '1.5d' is not an xsd:double", + "Attribute 'Safety relevant': 'yes' is not an xsd:boolean (true, false, 1 or 0)")), result.toString()); + } + + @Test + void identifiersUsedTwiceAreReported(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF + .replace(" issue.contains("twice")).collect(Collectors.toList())); + } + + @Test + void elementsWithoutIdentifierAreSkipped(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF + .replace("dt-date", + "dt-gone")); + + assertEquals(List.of("[ad-due] Attribute definition references a datatype that is not declared " + + "in DATATYPES; it and its values are kept as written"), issues(reqif)); + // both are written back as read + String xml = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + assertTrue(xml.contains("dt-gone"), xml); + assertTrue(xml.contains("ad-due"), xml); + + // what the model changes is written, the rest as read + de.uni_stuttgart.ils.reqif4j.attributes.AttributeDefinition due = reqif.getReqIFCoreContent() + .getSpecType("st-req").getAttributeDefinition("ad-due"); + due.setName("Deadline"); + due.setEditable(true); + String changed = new ReqIFWriter().toXml(reqif.getReqIFDocument()); + assertTrue(changed.contains("IDENTIFIER=\"ad-due\" IS-EDITABLE=\"true\""), changed); + assertTrue(changed.contains("LONG-NAME=\"Deadline\""), changed); + assertFalse(due.isModified(), "stamped like every other element"); + } + + @Test + void theValidatorReportsWhatTheParserSkippedAsWarnings(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF.replace("", "")); + + assertEquals("three", reqif.getReqIFCoreContent().getSpecObject("so-2").getAttribute("Priority")); + assertTrue(messages(new ReqIFValidator().validate(reqif.getReqIFDocument())) + .contains("Default value: 'three' is not an integer")); + } + + @Test + void unresolvedRelationTypeIsKept(@TempDir Path tempDir) throws Exception { + + ReqIF reqif = parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF.replace( + "st-rel", "st-gone")); + + assertEquals("st-gone", reqif.getReqIFCoreContent().getSpecRelation("sr-1").getSpecTypeID()); + } + + @Test + void modelConstructorsReadTolerantlyAsBefore() throws Exception { + + org.w3c.dom.Element relation = de.uni_stuttgart.ils.reqif4j.util.SecureXml.newDocumentBuilder() + .parse(new org.xml.sax.InputSource(new java.io.StringReader( + ""))) + .getDocumentElement(); + + // no type to interpret the value by: skipped, not an exception + assertTrue(new de.uni_stuttgart.ils.reqif4j.specification.SpecRelation(relation, null).getValues().isEmpty()); + } + + @Test + void aCleanDocumentHasNoIssues(@TempDir Path tempDir) throws Exception { + assertEquals(List.of(), issues(parse(tempDir, TestFixtures.SCHEMA_VALID_REQIF))); + assertEquals(List.of(), issues(parse(tempDir, LosslessRoundTripTest.fullFeatureDocument()))); + } + + private static ReqIF parse(Path dir, String content) throws Exception { + return new ReqIF(TestFixtures.write(dir, "doc-" + System.nanoTime() + ".reqif", content).toString()); + } + + private static List issues(ReqIF reqif) { + return reqif.getReqIFDocument().getParseIssues().stream().map(ParseIssue::toString).collect(Collectors.toList()); + } + + private static List messages(ValidationResult result) { + return result.getIssues().stream().map(issue -> issue.getMessage()).collect(Collectors.toList()); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/ValidatorSchemaRulesTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/ValidatorSchemaRulesTest.java new file mode 100644 index 0000000..29c4883 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/ValidatorSchemaRulesTest.java @@ -0,0 +1,248 @@ +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.assertTrue; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import de.uni_stuttgart.ils.reqif4j.build.ReqIFBuilder; +import de.uni_stuttgart.ils.reqif4j.reqif.ReqIFDocument; +import de.uni_stuttgart.ils.reqif4j.specification.RelationGroup; +import de.uni_stuttgart.ils.reqif4j.validate.ReqIFValidator; +import de.uni_stuttgart.ils.reqif4j.validate.ValidationIssue; +import de.uni_stuttgart.ils.reqif4j.validate.ValidationResult; +import de.uni_stuttgart.ils.reqif4j.write.ReqIFWriter; + +/** + * The model validator reports what would make the written file + * schema-invalid as an error, and values outside their datatype's limits - + * which the schema cannot express - as a warning. + */ +class ValidatorSchemaRulesTest { + + @Test + void identifierMustBeAnXsdID() { + + ReqIFDocument document = document(b -> b.specObject("1st requirement", "st-req")); + + assertEquals(List.of("ERROR [1st requirement]: Identifier '1st requirement' of SPEC-OBJECT is not a valid xsd:ID"), + issues(document)); + } + + @Test + void lastChangeMustBeAnXsdDateTime() { + + ReqIFDocument document = document(b -> { }); + document.getCoreContent().getSpecObject("so-1").setLastChange("yesterday"); + + assertEquals(List.of("ERROR [so-1]: LAST-CHANGE 'yesterday' of SPEC-OBJECT is not an xsd:dateTime"), + issues(document)); + } + + @Test + void headerFieldsMustMatchTheSchema() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr").creationTime("2026-07-23").reqifVersion("1.2")) + .build(); + + assertEquals(List.of( + "ERROR [hdr]: CREATION-TIME '2026-07-23' is not an xsd:dateTime", + "ERROR [hdr]: REQ-IF-VERSION is '1.2', but the schema only allows 1.0"), issues(document)); + } + + @Test + void enumValuesNeedAnIntegerKeyAndAUniqueIdentifier() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .enumerationDatatype("dt-enum", "Color", e -> e + .value("ev-red", "Red", "red") + .value("hdr", "Clash", "2")) + .build(); + + assertEquals(List.of( + "ERROR [ev-red]: KEY 'red' of enum value 'Red' is not an integer", + "ERROR [hdr]: Identifier is used twice: as REQ-IF-HEADER and as ENUM-VALUE"), issues(document)); + } + + @Test + void dateValuesAndDefaultsMustBeXsdDateTimes() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .dateDatatype("dt-date", "Date") + .specObjectType("st", "Type", t -> t.attribute("ad-due", "Due", "dt-date", "2026-02-30T00:00:00Z")) + .specObject("so-1", "st", o -> o.set("ad-due", "31.12.2026")) + .build(); + + assertEquals(List.of( + "ERROR [ad-due]: Default value: '2026-02-30T00:00:00Z' is not an xsd:dateTime", + "ERROR [so-1]: Attribute 'Due': '31.12.2026' is not an xsd:dateTime"), issues(document)); + } + + @Test + void defaultValuesAreWrittenVerbatimSoMustBeInSchemaNotation() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .booleanDatatype("dt-bool", "Bool") + .realDatatype("dt-real", "Real") + .integerDatatype("dt-int", "Int", 0, 10) + .specObjectType("st", "Type", t -> t + .attribute("ad-flag", "Flag", "dt-bool", "yes") + .attribute("ad-effort", "Effort", "dt-real", "1.5d") + .attribute("ad-count", "Count", "dt-int", "three")) + .build(); + + assertEquals(List.of( + "ERROR [ad-flag]: Default value: 'yes' is not an xsd:boolean (true, false, 1 or 0)", + "ERROR [ad-effort]: Default value: '1.5d' is not an xsd:double", + "ERROR [ad-count]: Default value: 'three' is not an integer"), issues(document)); + } + + @Test + void enumValueWithoutKeyIsNamedAsSuch() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .enumerationDatatype("dt-enum", "Color", e -> e.value("ev-red", "Red", null)) + .build(); + + assertEquals(List.of("ERROR [ev-red]: Enum value 'Red' has no KEY"), issues(document)); + } + + @Test + void stringLengthCountsCharactersNotUtf16Units() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .stringDatatype("dt-short", "Short", 2) + .specObjectType("st", "Type", t -> t.stringAttribute("ad-icon", "Icon", "dt-short")) + .specObject("so-1", "st", o -> o.set("ad-icon", "\uD83D\uDE00\uD83D\uDE00")) + .build(); + + assertEquals(List.of(), issues(document), "two emoji are two characters"); + } + + @Test + void enumerationDefaultGivenAsTextIsAnErrorAndNotWritten() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .enumerationDatatype("dt-enum", "Color", e -> e.value("ev-red", "Red", "1")) + .specObjectType("st", "Type", t -> t.attribute("ad-color", "Color", "dt-enum", "ev-red")) + .build(); + + assertEquals(List.of("ERROR [ad-color]: Default value of an enumeration must be given as enum value " + + "references (an enumeration attribute definition), not as text"), issues(document)); + assertFalse(new ReqIFWriter().toXml(document).contains("")); + } + + @Test + void malformedXhtmlDefaultIsAnError() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .xhtmlDatatype("dt-xhtml", "XHTML") + .specObjectType("st", "Type", t -> t.attribute("ad-text", "Text", "dt-xhtml", "a < b")) + .build(); + + assertEquals(List.of("ERROR [ad-text]: Default value is not well-formed XHTML"), issues(document)); + } + + @Test + void relationGroupNeedsTypeSourceAndTarget() { + + ReqIFDocument document = document(b -> { }); + document.getCoreContent().addRelationGroup(new RelationGroup("rg-1", "Group", null, null, null, null)); + + assertEquals(List.of( + "ERROR [rg-1]: Relation group has no type reference", + "ERROR [rg-1]: Relation group has no source specification", + "ERROR [rg-1]: Relation group has no target specification"), issues(document)); + } + + @Test + void textXmlCannotHoldIsAnError() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr").title("bell\u0007")) + .stringDatatype("dt-string", "String", 100) + .specObjectType("st", "Type", t -> t.stringAttribute("ad-name", "Name", "dt-string")) + .specObject("so-1", "st", o -> o.set("ad-name", "a\u0001b")) + .build(); + document.getCoreContent().getSpecObject("so-1").setDesc("lone \uD800"); + + assertEquals(List.of( + "ERROR [hdr]: TITLE contains characters XML does not allow (control characters)", + "ERROR [so-1]: DESC of SPEC-OBJECT contains characters XML does not allow (control characters)", + "ERROR [so-1]: Attribute 'Name' contains characters XML does not allow (control characters)"), + issues(document)); + } + + @Test + void valuesOutsideTheirDatatypeAreWarnings() throws Exception { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .stringDatatype("dt-short", "Short", 3) + .integerDatatype("dt-prio", "Priority", 1, 5) + .realDatatype("dt-ratio", "Ratio", 2, 0.0, 1.0) + .specObjectType("st", "Type", t -> t + .stringAttribute("ad-code", "Code", "dt-short") + .integerAttribute("ad-prio", "Priority", "dt-prio") + .realAttribute("ad-ratio", "Ratio", "dt-ratio")) + .specObject("so-1", "st", o -> o.set("ad-code", "ABCD").set("ad-prio", 9).set("ad-ratio", 1.5)) + .build(); + + ValidationResult result = new ReqIFValidator().validate(document); + + assertTrue(result.isValid(), "range violations do not make the file schema-invalid"); + assertEquals(List.of( + "WARNING [so-1]: Attribute 'Code' is 4 characters long, but datatype 'Short' allows 3", + "WARNING [so-1]: Attribute 'Priority' 9 is outside the range 1..5 of datatype 'Priority'", + "WARNING [so-1]: Attribute 'Ratio' 1.5 is outside the range 0.0..1.0 of datatype 'Ratio'"), + issues(document)); + + // the contract: a document without errors is written schema-valid + OfficialReqIFSchema.assertSchemaValid(document); + } + + @Test + void datatypeWithMinAboveMaxIsAWarning() { + + ReqIFDocument document = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .integerDatatype("dt-int", "Broken", 10, 1) + .build(); + + assertEquals(List.of("WARNING [dt-int]: Datatype 'Broken' has MIN 10 greater than MAX 1; no value can satisfy it"), + issues(document)); + } + + /** + * A valid document with one spec type and one spec object, plus whatever + * the test adds. + */ + private static ReqIFDocument document(Consumer additions) { + ReqIFBuilder builder = ReqIFBuilder.create() + .header(h -> h.id("hdr")) + .stringDatatype("dt-string", "String", 100) + .specObjectType("st-req", "Requirement", t -> t.stringAttribute("ad-name", "Name", "dt-string")) + .specObject("so-1", "st-req", o -> o.set("ad-name", "x")); + additions.accept(builder); + return builder.build(); + } + + private static List issues(ReqIFDocument document) { + return new ReqIFValidator().validate(document).getIssues().stream() + .map(ValidationIssue::toString) + .collect(Collectors.toList()); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/util/XsdTypesTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/util/XsdTypesTest.java new file mode 100644 index 0000000..bd10c4b --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/util/XsdTypesTest.java @@ -0,0 +1,92 @@ +package de.uni_stuttgart.ils.reqif4j.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class XsdTypesTest { + + @Test + void ncNames() { + for (String valid : new String[] { "so-1", "_x", "REQ.1", "Anforderung-\u00E4", "a\u00B7b", "_1", "\u03A9mega" }) { + assertTrue(XsdTypes.isNCName(valid), valid); + } + for (String invalid : new String[] { "", "1-req", "-a", ".a", "a b", "a:b", "a/b", "a#b", "\u00E4 b", "\u00E4\u0001" }) { + assertFalse(XsdTypes.isNCName(invalid), invalid); + } + assertFalse(XsdTypes.isNCName(null)); + } + + @Test + void nonAsciiNamesFollowTheJdkSchemaValidator() { + // Glagolitic was added to Unicode after the name tables the JDK's schema + // validator uses; XML 1.0 fifth edition would allow it + assertFalse(XsdTypes.isNCName("\u2C00x")); + } + + @Test + void xmlText() { + assertTrue(XsdTypes.isXmlText("tab\tnewline\n\u00E4\uD83D\uDE00")); + assertTrue(XsdTypes.isXmlText(null)); + assertFalse(XsdTypes.isXmlText("a\u0001b")); + assertFalse(XsdTypes.isXmlText("lone \uD800 surrogate")); + assertFalse(XsdTypes.isXmlText("\uFFFE")); + } + + @Test + void dateTimes() { + for (String valid : new String[] { "2026-07-23T10:00:00Z", "2026-07-23T10:00:00", "2026-07-23T10:00:00.123+02:00", + "2024-02-29T00:00:00Z", "2000-02-29T00:00:00Z", "2026-07-23T24:00:00Z", "12026-01-01T00:00:00Z", + "-0044-03-15T12:00:00Z", "1000000000-02-29T00:00:00Z", "30000000000-12-31T00:00:00Z" }) { + assertTrue(XsdTypes.isDateTime(valid), valid); + } + for (String invalid : new String[] { "", "2026-01-01", "2026-07-23 10:00:00", "2026-13-01T00:00:00Z", + "2026-02-30T00:00:00Z", "2025-02-29T00:00:00Z", "2026-07-23T10:00Z", "2026-07-23T10:00:00+15:00", + "26-07-23T10:00:00Z", "yesterday", "0000-01-01T00:00:00Z", "1900-02-29T00:00:00Z", + "2026-04-31T00:00:00Z", "1000000001-02-29T00:00:00Z" }) { + assertFalse(XsdTypes.isDateTime(invalid), invalid); + } + } + + @Test + void integers() { + assertTrue(XsdTypes.isInteger("42")); + assertTrue(XsdTypes.isInteger("-7")); + assertTrue(XsdTypes.isInteger("+0012")); + assertTrue(XsdTypes.isInteger("123456789012345678901234567890")); + assertFalse(XsdTypes.isInteger("")); + assertFalse(XsdTypes.isInteger("1.0")); + assertFalse(XsdTypes.isInteger("abc")); + assertFalse(XsdTypes.isInteger(null)); + } + + @Test + void doublesAndBooleansLexically() { + for (String valid : new String[] { "1", "-1.5", "+.5", "1.", "1e3", "1.5E-3", "INF", "-INF", "NaN" }) { + assertTrue(XsdTypes.isDouble(valid), valid); + } + for (String invalid : new String[] { "", "Infinity", "+INF", "1.5d", "2f", "0x1p3", "1,5", "e3" }) { + assertFalse(XsdTypes.isDouble(invalid), invalid); + } + for (String valid : new String[] { "true", "false", "1", "0" }) { + assertTrue(XsdTypes.isBoolean(valid), valid); + } + for (String invalid : new String[] { "", "yes", "TRUE", "2" }) { + assertFalse(XsdTypes.isBoolean(invalid), invalid); + } + } + + @Test + void doublesInXsdNotation() { + assertEquals(Double.POSITIVE_INFINITY, XsdTypes.parseDouble("INF")); + assertEquals(Double.NEGATIVE_INFINITY, XsdTypes.parseDouble(" -INF ")); + assertEquals(Double.POSITIVE_INFINITY, XsdTypes.parseDouble("Infinity")); + assertEquals(1.5e3, XsdTypes.parseDouble("1.5E3")); + assertEquals("INF", XsdTypes.formatDouble(Double.POSITIVE_INFINITY)); + assertEquals("-INF", XsdTypes.formatDouble(Double.NEGATIVE_INFINITY)); + assertEquals("NaN", XsdTypes.formatDouble(Double.NaN)); + assertEquals("2.5", XsdTypes.formatDouble(2.5)); + } +} diff --git a/src/test/java/de/uni_stuttgart/ils/reqif4j/validate/LocalSchemaResolverTest.java b/src/test/java/de/uni_stuttgart/ils/reqif4j/validate/LocalSchemaResolverTest.java new file mode 100644 index 0000000..0a5d7e1 --- /dev/null +++ b/src/test/java/de/uni_stuttgart/ils/reqif4j/validate/LocalSchemaResolverTest.java @@ -0,0 +1,82 @@ +package de.uni_stuttgart.ils.reqif4j.validate; + +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.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.w3c.dom.ls.LSInput; + +/** + * The resolver must map remote schema imports to local files and must never + * hand a remote location back to the parser, which could fetch it. + */ +class LocalSchemaResolverTest { + + private static final String XSD = "http://www.w3.org/2001/XMLSchema"; + + @Test + void remoteImportIsMappedToTheFileWithTheSameName(@TempDir Path dir) throws Exception { + + Path local = Files.writeString(dir.resolve("xml.xsd"), ""); + LocalSchemaResolver resolver = new LocalSchemaResolver(dir); + + LSInput input = resolver.resolveResource(XSD, null, null, "http://www.w3.org/2001/xml.xsd", null); + + assertEquals(local.toUri().toString(), input.getSystemId()); + assertTrue(resolver.getMissing().isEmpty()); + } + + @Test + void relativeImportOfARemoteSchemaIsMappedAsWell(@TempDir Path dir) throws Exception { + + Path local = Files.writeString(dir.resolve("xhtml-notations-1.xsd"), ""); + LocalSchemaResolver resolver = new LocalSchemaResolver(dir); + + LSInput input = resolver.resolveResource(XSD, null, null, "xhtml-notations-1.xsd", + "https://www.w3.org/MarkUp/SCHEMA/xhtml-framework-1.xsd"); + + assertEquals(local.toUri().toString(), input.getSystemId()); + } + + @Test + void missingRemoteImportIsAnsweredLocallyAndRemembered(@TempDir Path dir) { + + LocalSchemaResolver resolver = new LocalSchemaResolver(dir); + + LSInput input = resolver.resolveResource(XSD, null, null, "http://www.w3.org/2001/xml.xsd", null); + + // an empty input instead of null: null would let the parser fetch the URL + assertEquals("", input.getStringData()); + assertEquals("xml.xsd", resolver.getMissing().get("http://www.w3.org/2001/xml.xsd")); + } + + @Test + void remoteLocationCannotEscapeTheSchemaDirectory(@TempDir Path dir) throws Exception { + + Path schemas = Files.createDirectory(dir.resolve("schemas")); + Files.writeString(dir.resolve("other.xsd"), ""); + LocalSchemaResolver resolver = new LocalSchemaResolver(schemas); + + LSInput input = resolver.resolveResource(XSD, null, null, "http://host/x/..%5C..%5Cother.xsd", null); + + assertEquals("", input.getStringData()); + assertEquals(1, resolver.getMissing().size()); + } + + @Test + void localLocationsAreLeftToTheParser(@TempDir Path dir) { + + LocalSchemaResolver resolver = new LocalSchemaResolver(dir); + String base = dir.resolve("reqif.xsd").toUri().toString(); + + assertNull(resolver.resolveResource(XSD, null, null, "driver.xsd", base)); + assertNull(resolver.resolveResource(XSD, null, null, "C:/schemas/common.xsd", null)); + assertNull(resolver.resolveResource(XSD, null, null, base, null)); + assertTrue(resolver.getMissing().isEmpty()); + } +} diff --git a/src/test/resources/corpus/Doors/01_anonimized_example_date_data_type/sample.reqif b/src/test/resources/corpus/Doors/01_anonimized_example_date_data_type/sample.reqif new file mode 100644 index 0000000..50cc859 --- /dev/null +++ b/src/test/resources/corpus/Doors/01_anonimized_example_date_data_type/sample.reqif @@ -0,0 +1,68 @@ + + + + + 2018-10-12T16:43:58 + 5b6cc2367b684c19 + dummy from doors + 1.0 + dummy from doors + dummy from doors, 12 October 2018 + + + + + + + + + + + + + + _37894a8b-4c87-4b7c-9805-4f8b45c75ab8 + + + + + + + + + _d1cbf7fe-5d6f-4d0a-b14a-c83e3b64477e + + + + + _d1cbf7fe-5d6f-4d0a-b14a-c83e3b64477e_CREATEDON + + + + + + + + + + + _9e869d18-b121-4f58-a3b0-3a3aba2ccfe6 + + + + + _d1cbf7fe-5d6f-4d0a-b14a-c83e3b64477e_CREATEDON-DOORS-MODULE + + + + + + + + + + + + + + diff --git a/src/test/resources/corpus/Doors/02_example_from_a_user/sample.reqif b/src/test/resources/corpus/Doors/02_example_from_a_user/sample.reqif new file mode 100644 index 0000000..d25a787 --- /dev/null +++ b/src/test/resources/corpus/Doors/02_example_from_a_user/sample.reqif @@ -0,0 +1,3995 @@ + + + + + 2018-10-12T16:43:58 + 5b6cc2367b684c19 + IBM Rational DOORS + 1.0 + IBM Rational DOORS + IBM Rational DOORS, 12 October 2018 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _17a56810-ddec-4c0b-9a7f-3c5994ee1ed9 + + + + + _86154e3e-5f07-415f-8d56-2f2d0257ab1d + + + + + _69eac2a3-f05d-46db-9e8f-036420b5f259 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _37894a8b-4c87-4b7c-9805-4f8b45c75ab8 + + + + + _cbdbd88c-f26c-42ab-96b2-39c61a3e0b60 + + + + + _d1cbf7fe-5d6f-4d0a-b14a-c83e3b64477e_CREATEDTHRU + + + _65892872-d57a-4f53-8cdd-77e38e3597d2 + + + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _37894a8b-4c87-4b7c-9805-4f8b45c75ab8 + + + + + _495ec1b7-e312-4731-8e1e-9db10a270334 + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _86154e3e-5f07-415f-8d56-2f2d0257ab1d + + + + + _d1cbf7fe-5d6f-4d0a-b14a-c83e3b64477e_PICTURE + + + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _17a56810-ddec-4c0b-9a7f-3c5994ee1ed9 + + + + + _69eac2a3-f05d-46db-9e8f-036420b5f259 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _86154e3e-5f07-415f-8d56-2f2d0257ab1d + + + + + _684c6f8c-f7f3-42d3-b2f2-9934254a6196 + + + + + + + _da621508-c6f0-473b-ae7a-be3f931bcd06 + + + + + _d6331a38-d646-48ef-b822-37f00332ffa4 + + + _f6889c51-ca2d-4728-83f4-f0f4479c5c27 + + + + + + + _9c3fd553-2fa4-4b9b-86d7-6309ba4657a1 + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _69eac2a3-f05d-46db-9e8f-036420b5f259 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _39f2dca9-78aa-40d7-a6ff-fc4089f3b466 + + + + + _86154e3e-5f07-415f-8d56-2f2d0257ab1d + + + + + _ec11479c-b7e2-4727-9496-26f954ce58ab + + + + + _2d0d84a6-0780-4260-873f-860d22cc824d + + + + + _9881aa69-4adb-4bf4-94e3-234c7802496c + + + _9c98c320-17cd-480f-9a8e-da3ab36f7810 + + + + + + + _80162b6f-70e1-44ef-9491-b10a34e20c72 + + + + + _86154e3e-5f07-415f-8d56-2f2d0257ab1d + + + + + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _37894a8b-4c87-4b7c-9805-4f8b45c75ab8 + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _37894a8b-4c87-4b7c-9805-4f8b45c75ab8 + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + _ee75e6f3-12f5-4cbf-9159-1515d345ad2a + + + + + + + + + _c353bebc-3341-473c-a6cd-9d1771becd34 + + + + + _9d5545c6-de3e-4099-9a3f-4ca45900a8f8 + + + + + _01ddd33b-d2f5-447e-92c2-634d4b766357 + + + + + _132ba67c-2939-4b23-9cca-5f3503af70f2 + + + + + _822a3206-c701-4cfa-92db-acd9f5b82ce7 + + + + + _2b3294ed-876a-4ae6-bc7e-3dd51f27798b + + + + + _e75de79f-e00c-48d9-a972-b4199e1f6042 + + + + + _a3970b22-a021-4b91-9b88-e343737458e9_CREATEDTHRU + + + _f7bfb4bb-6554-431c-b5ff-b4547a0a6924 + + + + + + + _822a3206-c701-4cfa-92db-acd9f5b82ce7 + + + + + _2b3294ed-876a-4ae6-bc7e-3dd51f27798b + + + + + _7235e3bf-c4b3-4ca7-b0cc-6aaf5c372974 + + + + + _822a3206-c701-4cfa-92db-acd9f5b82ce7 + + + + + _822a3206-c701-4cfa-92db-acd9f5b82ce7 + + + + + _132ba67c-2939-4b23-9cca-5f3503af70f2 + + + + + _9d5545c6-de3e-4099-9a3f-4ca45900a8f8 + + + + + _a3970b22-a021-4b91-9b88-e343737458e9_PICTURE + + + + + + + _822a3206-c701-4cfa-92db-acd9f5b82ce7 + + + + + _c353bebc-3341-473c-a6cd-9d1771becd34 + + + + + _01ddd33b-d2f5-447e-92c2-634d4b766357 + + + + + _132ba67c-2939-4b23-9cca-5f3503af70f2 + + + + + _b2ede47a-5f34-4f76-a005-dc0ac4af39bc + + + + + _822a3206-c701-4cfa-92db-acd9f5b82ce7 + + + + + _01ddd33b-d2f5-447e-92c2-634d4b766357 + + + + + _18cfbb86-dd63-417f-b419-cd9dab3f77c7 + + + + + _bde48e36-75a6-4ba6-b0c5-41650ff2e989 + + + _93f57381-bd97-42bd-9e1b-64f00089f54b + + + + + + + _e12e9510-336c-4859-a1a7-e7b4e23d5e0b + + + + + _939bc5fd-5a3f-4f9f-92a7-625369bff16f + + + + + _c353bebc-3341-473c-a6cd-9d1771becd34 + + + + + _9d5545c6-de3e-4099-9a3f-4ca45900a8f8 + + + + + _e12e9510-336c-4859-a1a7-e7b4e23d5e0b + + + + + _9d5545c6-de3e-4099-9a3f-4ca45900a8f8 + + + + + _e12e9510-336c-4859-a1a7-e7b4e23d5e0b + + + + + _9d5545c6-de3e-4099-9a3f-4ca45900a8f8 + + + + + _9d5545c6-de3e-4099-9a3f-4ca45900a8f8 + + + + + _9d5545c6-de3e-4099-9a3f-4ca45900a8f8 + + + + + _e12e9510-336c-4859-a1a7-e7b4e23d5e0b + + + + + _663bd2dc-4d74-42f1-b896-7d60a21baf1a + + + + + _a3970b22-a021-4b91-9b88-e343737458e9_TABLETYPE + + + _3f80b6ab-7190-46b9-94e2-4215fa8069cf + + + + + + + _132ba67c-2939-4b23-9cca-5f3503af70f2 + + + + + _ad920993-841c-4599-b028-588eb7c47b27 + + + + + _9d5545c6-de3e-4099-9a3f-4ca45900a8f8 + + + + + _4c305993-5d02-456c-a087-2e6efdfc864c + + + + + _5f3a1108-a1b2-4c42-9847-de4057a838ce + + + _0783c027-1db3-42ed-a5d2-d7dbf4247a9e + + + + + + + _9d5545c6-de3e-4099-9a3f-4ca45900a8f8 + + + + + + + + + _822a3206-c701-4cfa-92db-acd9f5b82ce7 + + + + + _2b3294ed-876a-4ae6-bc7e-3dd51f27798b + + + + + _822a3206-c701-4cfa-92db-acd9f5b82ce7 + + + + + _822a3206-c701-4cfa-92db-acd9f5b82ce7 + + + + + _2b3294ed-876a-4ae6-bc7e-3dd51f27798b + + + + + _822a3206-c701-4cfa-92db-acd9f5b82ce7 + + + + + _822a3206-c701-4cfa-92db-acd9f5b82ce7 + + + + + + + + + _8689901f-2fde-4a7c-8d37-e6b8fb2474c1 + + + + + _750bdaea-ecfc-4a18-93ce-3055aca07393 + + + + + _750bdaea-ecfc-4a18-93ce-3055aca07393 + + + + + _b8b152d1-15f4-4297-8507-09b6bb47a91f + + + -- + + + + + + + _b8360cf7-6726-43c4-ac88-d0e7787c041d + + + + + _3e5ba69a-f08a-4053-876e-cb1cf3c77d4d + + + + + _f19df108-7669-4395-a2bf-54d448cfb47b + + + + + _6255801f-091d-4354-a68f-89cb40fd8f41_CREATEDTHRU + + + _ac1f713e-52fd-4a0b-a2f2-1a710d7a45d8 + + + + + + + _750bdaea-ecfc-4a18-93ce-3055aca07393 + + + + + _1895597b-a109-442b-b9b1-affb84029717 + + + -- + + + + + + + _fe806ebd-7598-4db3-9a62-c80e259fc2ab + + + + + _3f4f691c-a3f8-4490-bcd2-d12c6548f917 + + + _8587664f-2478-4038-8868-1ab0bb21f375 + + + + + + + _f4da0c16-1ec4-485d-8f79-0fa7a107e1ef + + + + + _e51167a3-70ea-43a8-9c36-9e2ee1fc5d44 + + + _720a01a6-bf86-41bb-9254-e267863d5c12 + + + + + + + _b3a9c2d4-d305-4e34-a195-8a4ab3b2548d + + + + + _0bffcb42-ed13-4c96-8c8a-12200ba9f9e8 + + + _902ee09e-9b82-4c5f-abb4-332dbafd1636 + + + + + + + _3eeea3e9-a999-4e9a-a08d-051dab4d921d + + + + + _c620ff22-9fdd-4d71-862f-2cdfcac33246 + + + _532e432e-ac07-4a9d-841c-9f890d9ffe26 + + + + + + + _e8221a4a-a5e3-4ebe-b928-644a097751f0 + + + + + _c9bcd7cb-2d33-4dbb-9fdc-1e8c14d95267 + + + _a8a6d072-25e3-4764-a579-a4855d805899 + + + + + + + _b8360cf7-6726-43c4-ac88-d0e7787c041d + + + + + _3e5ba69a-f08a-4053-876e-cb1cf3c77d4d + + + + + _b8360cf7-6726-43c4-ac88-d0e7787c041d + + + + + _b8360cf7-6726-43c4-ac88-d0e7787c041d + + + + + _750bdaea-ecfc-4a18-93ce-3055aca07393 + + + + + _06b93fc4-accf-469f-8560-c02f9e10af88 + + + + + _5ad2ed4e-e7be-4b77-924c-d0c2976a4ef6 + + + _73cd3c0d-3a61-47aa-803a-d1d8e787eea8 + + + + + + + _80f34405-341c-43e1-b865-199baf35afdd + + + + + _b8360cf7-6726-43c4-ac88-d0e7787c041d + + + + + _fe806ebd-7598-4db3-9a62-c80e259fc2ab + + + + + _530bd0b2-8eab-48c5-8c76-8335819dbc08 + + + _8587664f-2478-4038-8868-1ab0bb21f375 + + + + + + + _b3a9c2d4-d305-4e34-a195-8a4ab3b2548d + + + + + _cd4f05b7-e36d-41d8-a1fe-58c4a69f9109 + + + _902ee09e-9b82-4c5f-abb4-332dbafd1636 + + + + + + + _f4da0c16-1ec4-485d-8f79-0fa7a107e1ef + + + + + _6c8c275f-8b83-4276-9790-1c2b044738b9 + + + _720a01a6-bf86-41bb-9254-e267863d5c12 + + + + + + + _3eeea3e9-a999-4e9a-a08d-051dab4d921d + + + + + _a48ef6f1-3fd5-4ffb-be32-27737bb80a4d + + + _532e432e-ac07-4a9d-841c-9f890d9ffe26 + + + + + + + _e8221a4a-a5e3-4ebe-b928-644a097751f0 + + + + + _d465000b-a5ef-4cdb-a702-a836d4827aa5 + + + _a8a6d072-25e3-4764-a579-a4855d805899 + + + + + + + _5a9493e2-9c89-4a31-823b-42b5b628ce44 + + + + + _b0f2ec2d-c2c4-4396-a682-6085f6623564 + + + _5f412e4d-a8be-4ee1-824c-2761e100726b + + + + + + + _3bbe546c-ecfe-442b-8afa-56564a275ae5 + + + + + _e8d47f65-c244-48f0-95c2-4a4baf2e49a3 + + + + + _8689901f-2fde-4a7c-8d37-e6b8fb2474c1 + + + + + _07b4e5ce-18ef-49bf-8bc3-40e6631f8add + + + + + _3bbe546c-ecfe-442b-8afa-56564a275ae5 + + + + + _07b4e5ce-18ef-49bf-8bc3-40e6631f8add + + + + + _3bbe546c-ecfe-442b-8afa-56564a275ae5 + + + + + _07b4e5ce-18ef-49bf-8bc3-40e6631f8add + + + + + _07b4e5ce-18ef-49bf-8bc3-40e6631f8add + + + + + _07b4e5ce-18ef-49bf-8bc3-40e6631f8add + + + + + _3bbe546c-ecfe-442b-8afa-56564a275ae5 + + + + + _88be8c31-4ca1-4f74-9d1a-588fd80b0ea1 + + + + + _6255801f-091d-4354-a68f-89cb40fd8f41_TABLETYPE + + + _005c348b-140e-42b6-ab6d-21995871f154 + + + + + + + _07b4e5ce-18ef-49bf-8bc3-40e6631f8add + + + + + + + + + _b8360cf7-6726-43c4-ac88-d0e7787c041d + + + + + _3e5ba69a-f08a-4053-876e-cb1cf3c77d4d + + + + + _b8360cf7-6726-43c4-ac88-d0e7787c041d + + + + + _b8360cf7-6726-43c4-ac88-d0e7787c041d + + + + + _3e5ba69a-f08a-4053-876e-cb1cf3c77d4d + + + + + _b8360cf7-6726-43c4-ac88-d0e7787c041d + + + + + _b8360cf7-6726-43c4-ac88-d0e7787c041d + + + + + + + + + _68eb82f0-f0cf-4206-8972-3136c1eacbfd + + + + + _236c70fe-82af-4234-808c-b864aed5cefe + + + + + _5b2fc301-28d1-44c5-a6e9-177f69f86a21 + + + + + _0bd3016d-daa6-4af1-b0d7-06fde0e3a45b + + + + + _9772572b-b9dd-4176-965d-2035109e5be7_CREATEDTHRU + + + _43589df7-6074-4cd6-a93d-cdbe91ea4601 + + + + + + + _236c70fe-82af-4234-808c-b864aed5cefe + + + + + _5b2fc301-28d1-44c5-a6e9-177f69f86a21 + + + + + _236c70fe-82af-4234-808c-b864aed5cefe + + + + + _236c70fe-82af-4234-808c-b864aed5cefe + + + + + _d55f50b0-710a-4dce-a837-4781e2d9da79 + + + + + _a82f72c6-0878-44af-9ec2-4d7fce535902 + + + + + + + + + _236c70fe-82af-4234-808c-b864aed5cefe + + + + + _5b2fc301-28d1-44c5-a6e9-177f69f86a21 + + + + + _236c70fe-82af-4234-808c-b864aed5cefe + + + + + _236c70fe-82af-4234-808c-b864aed5cefe + + + + + _5b2fc301-28d1-44c5-a6e9-177f69f86a21 + + + + + _236c70fe-82af-4234-808c-b864aed5cefe + + + + + _236c70fe-82af-4234-808c-b864aed5cefe + + + + + + + + + _32a00585-16ce-4f36-ab6b-aae8a6b5680f + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _8bfc3373-c05a-4ac9-9f3e-78cefe60bd01 + + + + + _721de354-f14e-4b14-b6df-0b142caeb7ce + + + + + _659b5d67-bd3e-4341-9c13-e0aab523b4ec_CREATEDTHRU + + + _56380d9b-b034-4bf2-98a2-2d6b7d6495d5 + + + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _8bfc3373-c05a-4ac9-9f3e-78cefe60bd01 + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _df001e20-7249-4bc8-a771-ca979288e74f + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _3cb3d477-79ee-4ba8-b1e4-b6a5595303aa + + + + + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _8bfc3373-c05a-4ac9-9f3e-78cefe60bd01 + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _8bfc3373-c05a-4ac9-9f3e-78cefe60bd01 + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + _0c92237f-9811-4f55-b697-df3efa11cf51 + + + + + + + + + _af2c9e93-2dda-41c1-ab20-02445b26f694 + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _3bedac02-881a-4140-85b8-8b000f680c7d + + + + + _591e208d-2412-4eed-bf9b-0e4c16c0608a + + + + + _ce281e4e-7bb0-429d-a1de-f2e6233b86de_CREATEDTHRU + + + _85e4b47b-6aa0-4028-b5dd-400ff5880d14 + + + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _3bedac02-881a-4140-85b8-8b000f680c7d + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _72a4cdd8-e9ab-45f9-96e7-9229807d32cd + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _72a4cdd8-e9ab-45f9-96e7-9229807d32cd + + + + + _c77416b4-9cc0-42e1-92d0-01ecdc202016 + + + + + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _3bedac02-881a-4140-85b8-8b000f680c7d + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _3bedac02-881a-4140-85b8-8b000f680c7d + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + _1ee55e1e-51eb-45d0-9a36-b2a30c97bbca + + + + + + + + + _9c5844f5-9138-48d0-a28a-3af5d894a98e + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _34c9fd07-f824-4184-9794-f51cde622d14 + + + + + _5c559e5d-93ca-4175-bc43-b730a51ede53 + + + + + _68ba0236-ffb5-4b62-825a-f55dae08b53f_CREATEDTHRU + + + _d728fb7a-b89c-4f7b-81e9-41402b1a59a9 + + + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _34c9fd07-f824-4184-9794-f51cde622d14 + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _ff80ff06-2bb3-4a62-8fec-e2dbaeef1b7d + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _e65e0dd4-628a-4d5a-b5fc-0729df606e02 + + + + + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _34c9fd07-f824-4184-9794-f51cde622d14 + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _34c9fd07-f824-4184-9794-f51cde622d14 + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + _33aca405-9cb8-43b4-9e43-26479cd7e7bd + + + + + + + + + _c8b756ec-56d9-4f83-be4a-be20247b95a2 + + + + + _78bae8d6-f699-45af-bfcd-d664eced0af1 + + + + + _a4deeb85-0ff8-4ce1-b2a6-b4d4e6699e86 + + + + + _2ad86880-8af7-4140-85b0-4bf89db5fe2e + + + + + _6514bf1e-b7ac-416f-85ef-f8d06cffd1ab_CREATEDTHRU + + + _097a0755-1c07-4a3b-b8f4-011a7f43253f + + + + + + + _78bae8d6-f699-45af-bfcd-d664eced0af1 + + + + + _a4deeb85-0ff8-4ce1-b2a6-b4d4e6699e86 + + + + + _78bae8d6-f699-45af-bfcd-d664eced0af1 + + + + + _78bae8d6-f699-45af-bfcd-d664eced0af1 + + + + + _df0113d6-58d3-4031-80aa-d255d333ed38 + + + + + _b7c5a8f9-f343-4dc9-9e6a-d52d7471115f + + + + + + + + + _78bae8d6-f699-45af-bfcd-d664eced0af1 + + + + + _a4deeb85-0ff8-4ce1-b2a6-b4d4e6699e86 + + + + + _78bae8d6-f699-45af-bfcd-d664eced0af1 + + + + + _78bae8d6-f699-45af-bfcd-d664eced0af1 + + + + + _a4deeb85-0ff8-4ce1-b2a6-b4d4e6699e86 + + + + + _78bae8d6-f699-45af-bfcd-d664eced0af1 + + + + + _78bae8d6-f699-45af-bfcd-d664eced0af1 + + + + + + + + + _402cdd2b-9ca4-4cc1-a398-f9c091a5a0e2 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _c5490f15-daa0-4e29-a3d4-7b4a0b171e82 + + + + + _f4116fd3-b7ef-46d1-afc1-2f3a3ce23d95 + + + + + _bfc1a44e-d113-440f-b9c5-17063276417d_CREATEDTHRU + + + _f1fcad3c-dd35-4ebb-80ae-f1d5e4830bae + + + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _c5490f15-daa0-4e29-a3d4-7b4a0b171e82 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _6f0a087d-fcb9-4779-ad47-c1fc42dd46a7 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _5a4c4e35-38e4-477f-bcdb-b5c19a47cc82 + + + + + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _c5490f15-daa0-4e29-a3d4-7b4a0b171e82 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _c5490f15-daa0-4e29-a3d4-7b4a0b171e82 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + _16a261a3-1f4e-465c-9bc0-d66e0b4e7939 + + + + + + + + + _55c2126b-e2f4-47d3-b767-441cb966d163 + + + + + _36744c97-c0a6-4d37-816b-e7a4f17f2205 + + + + + _4642a0e3-6a03-46f5-a748-1fc10c9b3124 + + + + + _ec467a15-0877-4f50-9263-6824e5ad2f33 + + + + + _93fdcb78-c12f-4273-ab3c-650594472e96_CREATEDTHRU + + + _f918a8da-a8a1-4a9d-951c-72dac4a017fd + + + + + + + _36744c97-c0a6-4d37-816b-e7a4f17f2205 + + + + + _4642a0e3-6a03-46f5-a748-1fc10c9b3124 + + + + + _36744c97-c0a6-4d37-816b-e7a4f17f2205 + + + + + _36744c97-c0a6-4d37-816b-e7a4f17f2205 + + + + + _c345953e-3e17-4613-848a-1ef061d6db41 + + + + + _8b805239-c8de-4db6-b414-93337b563e08 + + + + + + + + + _36744c97-c0a6-4d37-816b-e7a4f17f2205 + + + + + _4642a0e3-6a03-46f5-a748-1fc10c9b3124 + + + + + _36744c97-c0a6-4d37-816b-e7a4f17f2205 + + + + + _36744c97-c0a6-4d37-816b-e7a4f17f2205 + + + + + _4642a0e3-6a03-46f5-a748-1fc10c9b3124 + + + + + _36744c97-c0a6-4d37-816b-e7a4f17f2205 + + + + + _36744c97-c0a6-4d37-816b-e7a4f17f2205 + + + + + + + + + _5f1deeab-8b0c-4f26-9846-f7b3dd72e173 + + + + + _98d41281-cdaa-4379-a3f1-2cd892ca062d + + + + + _02eafdfa-6a08-4811-a0ab-9d6884eafd53 + + + + + _0b6b54c1-bcc9-476e-9789-e46907ff741a + + + + + _387fb54a-f93f-455b-91e9-c34908303d73_CREATEDTHRU + + + _ce58b036-de59-41ac-bebe-469d84713a68 + + + + + + + _98d41281-cdaa-4379-a3f1-2cd892ca062d + + + + + _02eafdfa-6a08-4811-a0ab-9d6884eafd53 + + + + + _98d41281-cdaa-4379-a3f1-2cd892ca062d + + + + + _98d41281-cdaa-4379-a3f1-2cd892ca062d + + + + + _c8995aaf-652a-47ec-87e8-c4d98d33776a + + + + + _72afd989-a4ea-4be8-ba0a-f8fb0cd0aa86 + + + + + + + + + _98d41281-cdaa-4379-a3f1-2cd892ca062d + + + + + _02eafdfa-6a08-4811-a0ab-9d6884eafd53 + + + + + _98d41281-cdaa-4379-a3f1-2cd892ca062d + + + + + _98d41281-cdaa-4379-a3f1-2cd892ca062d + + + + + _02eafdfa-6a08-4811-a0ab-9d6884eafd53 + + + + + _98d41281-cdaa-4379-a3f1-2cd892ca062d + + + + + _98d41281-cdaa-4379-a3f1-2cd892ca062d + + + + + + + + + _b98067f1-55e4-44e2-8f4a-a37fc6f439d9 + + + + + _9725fb48-d8cb-4e7e-af2e-2c7eb740de2e + + + + + _19c25103-9f44-440d-9141-8f61e2349920 + + + + + _a85eb354-8dbe-47fa-85d3-bb4ad821fe39 + + + + + _417bdd8a-e990-4331-ba37-cc354b818bde_CREATEDTHRU + + + _891a2fcc-4f43-407f-80ef-e74ce06b1e8d + + + + + + + _9725fb48-d8cb-4e7e-af2e-2c7eb740de2e + + + + + _19c25103-9f44-440d-9141-8f61e2349920 + + + + + _9725fb48-d8cb-4e7e-af2e-2c7eb740de2e + + + + + _9725fb48-d8cb-4e7e-af2e-2c7eb740de2e + + + + + _b20ee3d8-42cb-4719-920f-ef831e55e8f8 + + + + + _fe902261-d5ab-49b3-b683-0b33914ee08f + + + + + + + + + _9725fb48-d8cb-4e7e-af2e-2c7eb740de2e + + + + + _19c25103-9f44-440d-9141-8f61e2349920 + + + + + _9725fb48-d8cb-4e7e-af2e-2c7eb740de2e + + + + + _9725fb48-d8cb-4e7e-af2e-2c7eb740de2e + + + + + _19c25103-9f44-440d-9141-8f61e2349920 + + + + + _9725fb48-d8cb-4e7e-af2e-2c7eb740de2e + + + + + _9725fb48-d8cb-4e7e-af2e-2c7eb740de2e + + + + + + + + + _a26d0227-f3c5-48d2-b688-edaa680c76b2 + + + + + _d73c6ac7-2049-4e66-a385-1b87f0f9d8d9 + + + + + _b1e2eeb4-d749-4b1f-a388-c0fd2f59a243 + + + + + _2c6d2533-9be2-4bb8-adef-65a72c76b0a4 + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_CREATEDTHRU + + + _3652b128-76e4-4e0e-aeec-1c284925f741 + + + + + + + _d73c6ac7-2049-4e66-a385-1b87f0f9d8d9 + + + + + _b1e2eeb4-d749-4b1f-a388-c0fd2f59a243 + + + + + _d73c6ac7-2049-4e66-a385-1b87f0f9d8d9 + + + + + _d73c6ac7-2049-4e66-a385-1b87f0f9d8d9 + + + + + _bde5b563-aea4-4b26-97d4-6bf29a833480 + + + + + _13260461-4efb-41d8-b34c-d96c4fa34c95 + + + + + + + + + _d73c6ac7-2049-4e66-a385-1b87f0f9d8d9 + + + + + _b1e2eeb4-d749-4b1f-a388-c0fd2f59a243 + + + + + _d73c6ac7-2049-4e66-a385-1b87f0f9d8d9 + + + + + _d73c6ac7-2049-4e66-a385-1b87f0f9d8d9 + + + + + _b1e2eeb4-d749-4b1f-a388-c0fd2f59a243 + + + + + _d73c6ac7-2049-4e66-a385-1b87f0f9d8d9 + + + + + _d73c6ac7-2049-4e66-a385-1b87f0f9d8d9 + + + + + + + + + _566a6afa-bfaf-4de8-8d74-58588b5bec6a + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _317d8419-2449-42b6-b8d2-0bec2e758a51 + + + + + _0f09b17a-ccbf-4e76-bac9-93d04b295f05 + + + + + _f7e5ace6-b5b7-432f-a633-3d470ea4311e_CREATEDTHRU + + + _b31fbb88-9c63-45cf-874e-4cb954b92382 + + + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _317d8419-2449-42b6-b8d2-0bec2e758a51 + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _b609ee39-c017-4fe2-bdc4-7b7294accc33 + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _1253c271-80ac-4e3b-b79e-f175f0576e87 + + + + + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _317d8419-2449-42b6-b8d2-0bec2e758a51 + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _317d8419-2449-42b6-b8d2-0bec2e758a51 + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + _1c9078ce-c7c5-4a66-b40c-2c8b8a525e9f + + + + + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8 + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_ABSOLUTENUMBER + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_CREATEDBY + + + susan + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_CREATEDON + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_LASTMODIFIEDBY + + + susan + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_LASTMODIFIEDON + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_OBJECTHEADING + + + Carbon Trust Standard + + + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8 + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_ABSOLUTENUMBER + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_CREATEDBY + + + susan + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_CREATEDON + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_LASTMODIFIEDBY + + + susan + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_LASTMODIFIEDON + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_OBJECTHEADING + + + CRC and CCA + + + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8 + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_ABSOLUTENUMBER + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_CREATEDBY + + + susan + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_CREATEDON + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_LASTMODIFIEDBY + + + susan + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_LASTMODIFIEDON + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_OBJECTHEADING + + + Picture or Whitepaper + + + + + + + + + + + _22020a9d-bd6d-45e9-9303-69dc17cf7c97 + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_CREATEDBY-DOORS-MODULE + + + susan + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_CREATEDON-DOORS-MODULE + + + + + _22020a9d-bd6d-45e9-9303-69dc17cf7c97_DESCRIPTION-DOORS-MODULE + + + References to external governance standards + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_LASTMODIFIEDBY-DOORS-MODULE + + + susan + + + + + _7f123ed4-98dd-4eed-b96a-edc8828963a8_LASTMODIFIEDON-DOORS-MODULE + + + + + _22020a9d-bd6d-45e9-9303-69dc17cf7c97_NAME-DOORS-MODULE + + + AMR Standards Documents + + + + + _22020a9d-bd6d-45e9-9303-69dc17cf7c97_PREFIX-DOORS-MODULE + + + AMR-REF- + + + + + + + _1_01ea51ea-ed54-471e-8c08-19e093f930f7 + + + + + _2_7e1c200d-5c01-4b93-941d-95fadf8feb0c + + + + + _3_a4285dbc-8259-4577-a093-ffd735084332 + + + + + + + + + + + + diff --git a/src/test/resources/corpus/Doors/03_example_from_a_user/sample.reqif b/src/test/resources/corpus/Doors/03_example_from_a_user/sample.reqif new file mode 100644 index 0000000..dbcf063 --- /dev/null +++ b/src/test/resources/corpus/Doors/03_example_from_a_user/sample.reqif @@ -0,0 +1,86 @@ + + + + + + + + _d1cbf7fe-5d6f-4d0a-b14a-c83e3b64477e + + + + + _d1cbf7fe-5d6f-4d0a-b14a-c83e3b64477e_CREATEDBY + + demo + + + + _3cad4538-378f-4df6-bf81-11423fa19ab5 + + + + + + _0683ad5c-be2a-46c4-aaf7-0c6ce8e436f2 + + Test Data: + + + + _6970c4af-44c0-4f47-88cb-fbb230f9b695 + + Manually enter data + + + + _1130028d-48cd-4e82-a8d5-a12991a94241 + + some data Subtitlesome data More data using even more data + + + + _8816a980-e244-42a5-b7cb-f1107671be9f + + Not Approved + + + + _424e8159-494a-474e-b3aa-e96757244cd7 + + Not Approved + + + + _d1cbf7fe-5d6f-4d0a-b14a-c83e3b64477e_LASTMODIFIEDBY + + userName + + + + _d1cbf7fe-5d6f-4d0a-b14a-c83e3b64477e_LASTMODIFIEDON + + + + + _d1cbf7fe-5d6f-4d0a-b14a-c83e3b64477e_OBJECTTEXT + + More info about the spec object + + + + _df868432-82f4-4ca1-8129-a4d1e49439f7 + + 67 + + + + _295e207f-e060-4b94-9845-26a5e6cf9d50 + + + + + + + + diff --git a/src/test/resources/corpus/Doors/04_example_from_a_user/sample.reqif b/src/test/resources/corpus/Doors/04_example_from_a_user/sample.reqif new file mode 100644 index 0000000..30ad908 --- /dev/null +++ b/src/test/resources/corpus/Doors/04_example_from_a_user/sample.reqif @@ -0,0 +1,95 @@ + + + + + 2022-01-01T00:01:01.260Z + + IBM + 1.0 + IBM + IBM Engineering Requirements + + + + + + + + _ee420d2b-599c-49ea-a298-6a732ece21eb + + + + + _3ca983d9-5ec5-4752-898f-37ab52beafd0 + + +
<FOOOOO> BAR
+
+
+ + + _1f20e97d-8b0f-4f64-a093-deedc195e91a + + +
"New Header" Template reqFile
+
+
+ + + _dc18f331-d466-4de2-8182-0ef4ced9e174 + + + + + _d2325dd8-82f3-43d6-b50b-6eb92820fd55 + + + + + _d09cedc9-c249-4ff3-affb-e863dcb732b9 + + + + + _05ee3f1d-6c0c-4e86-8bca-67cc5355db9c + + +
+ + + + + _91651ec0-8832-4553-a201-cad184aa6fdb + + + _03fdb162-e81a-4724-809e-2b660994e18f + + + + + _136696e2-ada5-407a-b65b-e271a96ab983 + + + + + _a450fa62-c5ac-43fc-9d08-c90432810d15 + + + + + _977a9dd7-8efb-40dd-8456-6e1b548a27c1 + + + + + _318a9d27-fdd6-44ea-8117-5ecc5b6b422a + + + + + + + + + + diff --git a/src/test/resources/corpus/Doors/05_example_from_a_user/sample.reqif b/src/test/resources/corpus/Doors/05_example_from_a_user/sample.reqif new file mode 100644 index 0000000..1bf07c4 --- /dev/null +++ b/src/test/resources/corpus/Doors/05_example_from_a_user/sample.reqif @@ -0,0 +1,50 @@ + + + + + 2022-01-01T00:01:01.260Z + + IBM + 1.0 + IBM + IBM Engineering Requirements + + + + + + + + _ee420d2b-599c-49ea-a298-6a732ece21eb + + + + + _3ca983d9-5ec5-4752-898f-37ab52beafd0 + + +
<New Header> Template reqFile
+
+
+ + + _1f20e97d-8b0f-4f64-a093-deedc195e91a + + +
<New Header< Template reqFile
+
+
+ + + _05ee3f1d-6c0c-4e86-8bca-67cc5355db9c + + +
+ + + + + + + + diff --git a/src/test/resources/corpus/Doors/06_example_from_a_user/sample.reqif b/src/test/resources/corpus/Doors/06_example_from_a_user/sample.reqif new file mode 100644 index 0000000..2f14690 --- /dev/null +++ b/src/test/resources/corpus/Doors/06_example_from_a_user/sample.reqif @@ -0,0 +1,88 @@ + + + + + 2022-01-01T00:01:01.260Z + + IBM + 1.0 + IBM + IBM Engineering Requirements + + + + + + + + + + + + + + + + + + + + + _92c4bf49-17fc-4fde-8994-a246546c6bc5 + + + + + + + + + _ee420d2b-599c-49ea-a298-6a732ece21eb + + + + + _3ca983d9-5ec5-4752-898f-37ab52beafd0 + + +
<A Header> Subtitle
+
+
+
+
+
+ + + + + + _04650f03-0093-48ec-bf6a-bc0108a415a3 + + + + + _2dfa99dd-619f-4f4c-9a2d-c92caa4a33df + + + + + + + _82602fb9-8243-4753-a395-2714b4082b4c + + + + + _2ce107e5-6bf2-4c45-b6b0-a097a4f279d2 + + + + + + + + + +
+
+
diff --git a/src/test/resources/corpus/Doors/10_example_capella_requirements_vp/sample.reqif b/src/test/resources/corpus/Doors/10_example_capella_requirements_vp/sample.reqif new file mode 100644 index 0000000..7032688 --- /dev/null +++ b/src/test/resources/corpus/Doors/10_example_capella_requirements_vp/sample.reqif @@ -0,0 +1,123 @@ + + + + + 2017-04-25T15:44:26.000+02:00 + 524d2a940b0233d5 + IBM Rational DOORS + 1.0 + IBM Rational DOORS + IBM Rational DOORS, 25 April 2017 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _i6FNMMkgEee8KsfWrp9EJQ + + + + + _i6FNMMkgEee8KsfWrp9EJQ + + + + + _ahyOEMk3Eee5A_N9aQFa1w + + + + + + + + + _i6FNMMkgEee8KsfWrp9EJQ + + + + + + + + + + + _25YvEMkyEee5A_N9aQFa1w + + + PUID-1 + + + + + _cf7-0MkhEee8KsfWrp9EJQ + + + Requirement-1 + + + + + _gXZ9oMk3Eee5A_N9aQFa1w + + + _0VDX0Mk3Eee5A_N9aQFa1w + + + + + _LaeSsMkhEee8KsfWrp9EJQ + + + + + + + + + + _NAXScMkhEee8KsfWrp9EJQ + + + + + _xen_QMkhEee8KsfWrp9EJQ + + + + + + + + + + + + diff --git a/src/test/resources/corpus/LICENSE-Apache-2.0.txt b/src/test/resources/corpus/LICENSE-Apache-2.0.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/src/test/resources/corpus/LICENSE-Apache-2.0.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/test/resources/corpus/Polarion/01_anonimized_example/sample.reqif b/src/test/resources/corpus/Polarion/01_anonimized_example/sample.reqif new file mode 100644 index 0000000..e5362ad --- /dev/null +++ b/src/test/resources/corpus/Polarion/01_anonimized_example/sample.reqif @@ -0,0 +1,3920 @@ + + + + + 2022-02-01T08:22:28.005-08:00 + Ppp ReqIF Roundtrip + 1.0 + Ppp + SRS + + + + + + + + + + + + + + + + + + ppp_type_string + + + + + ppp_type_string + + + + + ppp_type_date_time + + + + + ppp_type_string + + + + + ppp_type_rich_text + + + + + + + + + ppp_type_string + + + + + ppp_type_string + + + + + ppp_type_date_time + + + + + ppp_type_string + + + + + ppp_type_rich_text + + + + + + + + + ppp_type_string + + + + + ppp_type_string + + + + + ppp_type_date_time + + + + + ppp_type_string + + + + + ppp_type_rich_text + + + + + + + + + ppp_type_string + + + + + ppp_type_string + + + + + ppp_type_date_time + + + + + ppp_type_string + + + + + ppp_type_rich_text + + + + + + + + + ppp_type_string + + + + + ppp_type_string + + + + + ppp_type_date_time + + + + + ppp_type_string + + + + + ppp_type_rich_text + + + + + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-be2b2748-cbdd-4f1d-bfc0-7c094533c295 + + + + + rmf-86621d74-66ec-4072-b391-108751c2a85b + + + + + rmf-68ae3a55-d7f7-4d4e-8089-334c5e44d63f + + + + + rmf-1b4fc8f8-4211-43f0-8340-f51dcafc0d0e + + + + + _a899912b-02c9-4ecc-809b-3dcc6671a1c7 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + Anonymized + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + Anonymized + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + Anonymized + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + Anonymized + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + Anonymized + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + Anonymized + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + Anonymized + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-0ebf173d-49d4-49ec-b170-8deac3238f86 + + + + + rmf-f14d9142-c2a8-4e78-8a7f-e93f0ee0fbfc + + + + + rmf-8ad26cf3-dcae-4b0a-8434-d1945ba5178d + + + + + rmf-1976b3b4-56a4-4933-a9e7-b44533b15655 + + + + + _c72b4453-3b94-4da4-8b01-1a92766fe276 + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + Anonymized + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + Anonymized + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + rmf-1b53584c-a9ad-47f4-b83a-0e7985fa3f7e + + + + + rmf-7d0ed062-e964-424c-8305-45067118d959 + + + aaa + + + + + rmf-7caf14b5-e997-47eb-a85d-6bd80a5aef3c + + + + + rmf-1759a733-cd44-4740-a63f-88afc01d7b28 + + + + + rmf-5b05da73-825a-46cf-945b-81b619fc9480 + + + + + _b26dff7a-531b-427e-8daf-9fa21e4f0dbe + + + + + + + ppp_live_document + + + + + rmf-5ce70afc-c8b0-4db3-8428-032702653c74 + + + + + rmf-f5382428-8664-43f2-b3d0-6c84164986f9 + + + + + rmf-4946b1e8-5a43-4aef-b57d-b4704aca847b + + + + + rmf-a7f454b6-bec5-4cd1-ba33-7272c53ff1d5 + + + + + rmf-58ad1ca6-6365-4c74-ac1d-b821587c0289 + + + + + + + rmf-d9836185-503e-4da6-afd4-c6e6d488378b + + + + + + + rmf-dd71677f-ea71-4e46-9a4b-7295b949a590 + + + + + rmf-90ccbc2c-b056-4b99-8749-cc587cd17a79 + + + + + rmf-3f4f4937-eed9-40b2-a9e0-c268547a4420 + + + + + rmf-5a486ec4-d2f0-4661-9c97-836d33356d73 + + + + + rmf-a22ae77b-e3c5-404d-acf7-6bbcd2cdb6cc + + + + + rmf-c4158cc4-7394-4d02-8a06-4ee93cca4688 + + + + + rmf-ffd7bb95-c76f-44b4-adfd-9305b58e7aba + + + + + rmf-c33bed7b-f4ae-4749-9839-91aafd3a56a9 + + + + + + + + + rmf-0733fe3c-9d6f-4363-b663-8ee137f9c187 + + + + + rmf-ce811a8e-635f-4cbf-9f5d-1919698f6359 + + + + + rmf-72750b25-20b3-4440-8ecf-7da720170ca6 + + + + + rmf-3e0ae326-c60f-4206-b017-0dc7e418afef + + + + + rmf-2616fa87-f1f5-4c87-9725-4a084d718be6 + + + + + rmf-85c7695b-a2d1-45ec-bdf3-f17e4ded59ac + + + + + + + rmf-9269d32b-91b1-42c7-8458-39f0ce5f7346 + + + + + rmf-1b18ef37-ca1f-4e79-954d-747df083f861 + + + + + rmf-d6634dcc-6e3c-4aa2-b53c-fb322e0a9305 + + + + + rmf-ad6f6f0a-f86d-4cdf-ad89-3c60f1bf6bd2 + + + + + rmf-b1f3bf08-b82f-416e-9728-7a23dece64f8 + + + + + + + rmf-fb70ea4f-5d57-46ee-a07c-5cd05771c959 + + + + + rmf-dab0d57a-54e4-47cf-949f-52e2ff9cdf07 + + + + + rmf-a9303057-ca16-427f-bdc0-fd15bf056198 + + + + + + + rmf-34991130-27fe-4251-b157-f2c53b63ed82 + + + + + rmf-456e1235-9b60-401f-949f-52512a567387 + + + + + + + + + rmf-47be0608-c2e2-4798-9737-ef78e6a0818c + + + + + + + rmf-0fc2f245-7059-47d1-a78e-dd46ce547318 + + + + + rmf-f302a2f1-3a17-4caa-b252-74d45569fced + + + + + rmf-08a69e16-7888-4fc6-be97-e16e85189c91 + + + + + rmf-effd9e2c-f962-4c26-99a3-b9ddc478d838 + + + + + rmf-96364a8b-ead8-4c2d-b19d-9d3fb88de7b4 + + + + + rmf-e3701a17-6e80-469a-81c0-af4a12ab1c32 + + + + + rmf-6285f5da-c325-4419-b369-311f0f65bb55 + + + + + rmf-62b082cb-e3dd-47ce-a450-2819b222d0a3 + + + + + rmf-00d6dd17-aa61-4c14-b9f4-5fb9b840ea57 + + + + + rmf-a3a28d18-8c6b-4ebf-84a0-12f9839aae77 + + + + + rmf-3356e4da-5a2f-49f7-aac9-11cd8a0886b9 + + + + + rmf-bd45cff4-2401-4a2c-99b8-505cd6fb2297 + + + + + rmf-c6f8ea4b-d8b1-49b5-b9bc-91dd01c3a3f4 + + + + + rmf-4ceb485c-a6b5-4d92-812d-50fa93ef0d95 + + + + + rmf-038de526-d99a-4b28-856d-5962b16d2bb9 + + + + + rmf-c5ef2ad8-3464-4bb7-a880-f0925e627a78 + + + + + rmf-e51b1800-dea5-4c0b-a3ae-f143385ae0b4 + + + + + rmf-ad8db166-28ef-4060-928f-84139e8b314d + + + + + rmf-a5c0aa7f-273c-4e8e-8c4d-adb10418c455 + + + + + rmf-e6028401-f6df-4296-b0b1-1ef3ebcb36d5 + + + + + rmf-62382f49-999e-468a-8501-24033c7421e9 + + + + + rmf-7af27aff-5f02-476e-a727-a555e3dbe3e8 + + + + + rmf-ded0ebaa-9a81-46fb-8d70-f592a3c513f1 + + + + + + + rmf-9a3e717e-9ddf-4ae9-a620-95ecb23a49dc + + + + + rmf-002b343b-41b5-48ff-b5e0-3b306d62d235 + + + + + rmf-33158661-b5c0-4970-a959-8c5ba8e7312f + + + + + rmf-41a4902f-dbd0-4528-af7f-8fe55fa4e017 + + + + + rmf-25222058-479c-43b0-8bee-f39b52fd88f3 + + + + + rmf-280f98bb-cca2-4327-8bb3-38aa92f34efc + + + + + rmf-a9bdea60-9803-4d41-a90a-7e9102151e30 + + + + + + + rmf-66aa9897-4898-4ba6-90f4-5962e91803cd + + + + + rmf-ff323f5d-06cf-4891-b93c-75f2b11dcbd0 + + + + + rmf-70d8a03f-115a-459e-a25c-574d3cd84b97 + + + + + rmf-4341ac24-ff0d-4cbd-93b7-d945fe80f778 + + + + + rmf-f336503e-c87c-43e5-bf64-2ae5286178d5 + + + + + rmf-1330517c-212a-476c-adf8-57a7ae199b40 + + + + + rmf-a5d10d56-8645-46b0-b1a0-28c6d56f2ffc + + + + + rmf-272a4e04-2dcd-409e-846c-a1efacdd22ad + + + + + + + rmf-a4c74353-336a-4fb0-b946-2eae6e06d771 + + + + + rmf-a7e09bc9-c37b-4253-929d-5c9f6201e635 + + + + + rmf-3a38a6fe-b782-443e-8dc3-57b372c211ab + + + + + rmf-30d24dc7-92fb-4ba5-98e1-5419d40d5219 + + + + + rmf-49ec55a5-22f4-4df4-a14e-62010a84ffbe + + + + + rmf-3f43d9e6-1e66-4225-ab38-f6b2866db1bf + + + + + rmf-e2407e9a-895e-483d-858e-2b2863a6810f + + + + + rmf-b9edb6cf-8995-4971-a0cd-910df687674b + + + + + + + + + + + rmf-33337d7a-4e8a-4086-808c-1e0a4cf6b3ae + + + + + rmf-3d003299-be19-4af7-95c9-fbd59490830e + + + + + rmf-3b8f6b8e-799c-4964-80f3-710c97bb5bc7 + + + + + rmf-9b26cd89-70c8-4dec-a657-bfe2bc9ae197 + + + + + rmf-ec44dd78-69de-4f70-8fbd-b60a304e5ff7 + + + + + rmf-3cae46d2-9341-4882-ba4e-05682ae91dae + + + + + rmf-fa85705e-26b2-4b53-8a2d-04cf3b0d7339 + + + + + rmf-4c25ae26-6894-4fe8-ac94-1aa5f7e8f423 + + + + + rmf-94a34cf6-3a97-4672-834d-21f31cee9d3c + + + + + + + rmf-9a01bad4-b5e7-40cd-af3a-9caf3d5ad63c + + + + + rmf-7b34726a-2c38-41f7-98fb-3acf4da745ef + + + + + rmf-851b366d-0657-4ca3-bd52-3566243ecb79 + + + + + rmf-3c33bcb2-4124-4878-bbd6-6bc5158a91e5 + + + + + rmf-912713c7-2908-4d73-8df1-2e3f0d9a1dba + + + + + rmf-616c68cc-5562-48b4-b33a-44f2fa777884 + + + + + + + rmf-7d395818-1675-46db-9dab-ab14cf959eb4 + + + + + rmf-c592c9f8-ce3c-4f20-9e9e-f7aba4d75422 + + + + + rmf-84887606-0458-4cae-b8be-8292a467bb78 + + + + + rmf-338b7540-a86b-4746-a317-696ff1335b5d + + + + + + + + + rmf-a3f0fb8a-b7d0-41ef-a764-048fac319a05 + + + + + rmf-692b8ea0-fc54-45ca-9ed8-08fb6ff2320b + + + + + rmf-b801363a-491b-4fe5-94ab-0649d7397cfe + + + + + + + rmf-47efeec4-75f7-4ce4-afbc-329f665ccd3f + + + + + rmf-f68cf5fa-c35d-42d4-87af-6dae9e1140ab + + + + + + + + diff --git a/src/test/resources/corpus/Polarion/02_example/sample.reqif b/src/test/resources/corpus/Polarion/02_example/sample.reqif new file mode 100644 index 0000000..016fc79 --- /dev/null +++ b/src/test/resources/corpus/Polarion/02_example/sample.reqif @@ -0,0 +1,196 @@ + + + + + 2021-07-01T01:12:06.121Z + Polarion ReqIF Roundtrip + 1.0 + Polarion + Test export + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + polarion_type_string + + + + + polarion_type_string + + + + + polarion_type_date_time + + + + + polarion_type_string + + + + + polarion_type_rich_text + + + + + + + + + polarion_type_string + + + + + polarion_type_string + + + + + polarion_type_date_time + + + + + polarion_type_string + + + + + polarion_type_rich_text + + + + + rmf-74251127-bfc7-4823-bd6c-dec51864729e + + + + + + + + + + + rmf-d8e70e15-dd88-4f6a-a383-07b7d25466a2 + + + + + rmf-a6468304-cb04-41a2-b714-f2531c8acc4b + + + + + _820f7cab-e89d-4793-a093-15c66d554fa4 + + + + + + + rmf-27e728bc-3e42-4d4c-b7cb-da6a9e4b68ba + + + + + rmf-da80204e-9dd2-4954-aa8e-e93732255ecb + + + + + rmf-9515c994-87fa-4dff-b66b-ad407bcffbf5 + + + rmf-4ec70808-b34d-42da-a6d9-16dedc6a1854 + + + + + rmf-1056139a-17cf-4dee-ba90-157337945bf1 + + + + + rmf-9161875d-39ee-4106-a28d-3b60242ec66e + + + + + rmf-7d67ecd1-e8ff-4804-b385-548d6fe8ae00 + + + The Lorem Ipsum shall do something. + + + + + _21afe70a-ac89-48e0-b394-d1cba35c3537 + + + + + + + polarion_live_document + + + + + rmf-bd312f1z-fa7c-4de3-b8f3-ab105179fccc + + + + + rmf-1d312f18-fa7c-4de3-b8f3-ab105179fddf + + + + + + + + + + diff --git a/src/test/resources/corpus/Polarion/03_reqifz_example/sample.reqifz b/src/test/resources/corpus/Polarion/03_reqifz_example/sample.reqifz new file mode 100644 index 0000000..d52bc52 Binary files /dev/null and b/src/test/resources/corpus/Polarion/03_reqifz_example/sample.reqifz differ diff --git a/src/test/resources/corpus/README.md b/src/test/resources/corpus/README.md new file mode 100644 index 0000000..c9ccfae --- /dev/null +++ b/src/test/resources/corpus/README.md @@ -0,0 +1,37 @@ +# ReqIF test corpus + +Exports of real requirements tools and the reference files of the ReqIF +Implementor Forum, used by `CorpusTest`: every document must be read +without failing, written back as read (or differ for a stated reason), +written the same way a second time, and the validator must agree with the +official ReqIF schema. + +## Source and license + +Copied from the test suite of the Python library *reqif*, +, commit +`b36694051d6b0502682bcd8f83567d4e1f387d1b`, folders +`tests/integration/reqif_software` and `tests/integration/examples`. +That project is licensed under the Apache License 2.0 +([LICENSE-Apache-2.0.txt](LICENSE-Apache-2.0.txt)); the files are +redistributed unchanged under that license. Duplicates were left out, and +the folder names of the Implementor Forum files were shortened. + +## Files + +| File | Tool | Notes | +|---|---|---| +| `Doors/01_anonimized_example_date_data_type` | IBM DOORS | excerpt: references a specification type and attribute definitions it does not contain | +| `Doors/02_example_from_a_user` | IBM DOORS | schema-valid | +| `Doors/03_example_from_a_user` | IBM DOORS | excerpt without `THE-HEADER`; the object's type is missing, its 12 values are kept as written | +| `Doors/04_…`, `Doors/05_…` | IBM DOORS | excerpts referencing missing types; `xsi:schemaLocation` | +| `Doors/06_example_from_a_user` | IBM DOORS | an attribute definition references a missing datatype; it is kept as written | +| `Doors/10_example_capella_requirements_vp` | Capella Requirements VP | schema-valid | +| `Polarion/01_anonimized_example` | Siemens Polarion | anonymized: DATE values `aaa` | +| `Polarion/02_example`, `Polarion/03_reqifz_example` | Siemens Polarion | an identifier used twice; `ReqIF.Text` values written as `ATTRIBUTE-VALUE-STRING` although the attribute is XHTML | +| `ReqIF_Studio/01_anonimized_example` | ReqIF Studio | relations reference a type that is missing | +| `SparxSystems_Enterprise_Architect_8.0/01_example` | Enterprise Architect 8 | relations and hierarchy reference objects that are missing | +| `StrictDoc/01_example` | StrictDoc | lacks the required `LAST-CHANGE` and `MAX-LENGTH`; the writer fills them in | +| `StrictDoc/02_reqifz_example` | StrictDoc | archive with an SVG attachment | +| `ci.eclipse.org/TC1000` … `TC1801` | ReqIF Implementor Forum reference files (Eclipse RMF) | schema-valid; `TC1700` is an archive whose relations connect the objects of another document | +| `org.eclipse.rmf/01_specRelationTest`, `02_sample` | Eclipse RMF | namespace of a ReqIF draft (`20101201`), identifiers that are no xsd:ID; written in the ReqIF 1.2 namespace | diff --git a/src/test/resources/corpus/ReqIF_Studio/01_anonimized_example/sample.reqif b/src/test/resources/corpus/ReqIF_Studio/01_anonimized_example/sample.reqif new file mode 100644 index 0000000..d9731e4 --- /dev/null +++ b/src/test/resources/corpus/ReqIF_Studio/01_anonimized_example/sample.reqif @@ -0,0 +1,4079 @@ + + + + + ...Anonymized... + 2021-02-08T16:37:07.454+01:00 + ReqIF Studio (https://reqif.academy) + 1.0 + ReqIF Studio (https://reqif.academy) + ...Anonymized... + + + + + + + + + + + + + + + + + + + _gFhrVGojEeuExICsU7Acmg + + + + + _gFhrVWojEeuExICsU7Acmg + + + + + _gFhrVmojEeuExICsU7Acmg + + + + + _gFhrU2ojEeuExICsU7Acmg + + + + + + + + + + _gFhrV2ojEeuExICsU7Acmg + + + + + + + + + + + _gFhrXGojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + _gFhrW2ojEeuExICsU7Acmg + + + + + _gFhrXWojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _aqZG4GxpEeuaU7fHySy8Bw + + + + + _gFhrWmojEeuExICsU7Acmg + + + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _TZi8AGxUEeuaU7fHySy8Bw + + + _QPtMsGokEeuExICsU7Acmg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _20aokGroEeuTd-Zu7PczSg + + + _Yu60sGokEeuExICsU7Acmg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _TfkQUGrnEeuTd-Zu7PczSg + + + _TrbpQGq_EeuTd-Zu7PczSg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _LOKUcGrfEeuTd-Zu7PczSg + + + _UPNrwGq_EeuTd-Zu7PczSg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _KGVqYGrXEeuTd-Zu7PczSg + + + _qdgB8Gq2EeuTd-Zu7PczSg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _sbtuEGxWEeuaU7fHySy8Bw + + + _fgM5AGrZEeuTd-Zu7PczSg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _J6ZeIGrtEeuTd-Zu7PczSg + + + _Wq6u0GrjEeuTd-Zu7PczSg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _edL2kGu6EeuNUYnTveUm8Q + + + _1UOoMGroEeuTd-Zu7PczSg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _edL2kGu6EeuNUYnTveUm8Q + + + _11FSgGroEeuTd-Zu7PczSg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _KGVqYGrXEeuTd-Zu7PczSg + + + _X0N8MGrCEeuTd-Zu7PczSg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _Pv9HoGq-EeuTd-Zu7PczSg + + + _7rlUAGq1EeuTd-Zu7PczSg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _KGVqYGrXEeuTd-Zu7PczSg + + + _UPNrwGq_EeuTd-Zu7PczSg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _KGVqYGrXEeuTd-Zu7PczSg + + + _TrbpQGq_EeuTd-Zu7PczSg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + _gFhrYmojEeuExICsU7Acmg + + + + + _KGVqYGrXEeuTd-Zu7PczSg + + + _TIkMwGq_EeuTd-Zu7PczSg + + + _gFhrYGojEeuExICsU7Acmg + + + + + + + + + _gFhrX2ojEeuExICsU7Acmg + + + ...Anonymized... + + + + + _gFhrXmojEeuExICsU7Acmg + + + + + _gFhrY2ojEeuExICsU7Acmg + + + + + _gFhrZ2ojEeuExICsU7Acmg + + + + + _nTa1YGojEeuExICsU7Acmg + + + + + _6okS0GrsEeuTd-Zu7PczSg + + + + + + + _QPtMsGokEeuExICsU7Acmg + + + + + _J6ZeIGrtEeuTd-Zu7PczSg + + + + + + + _Yu60sGokEeuExICsU7Acmg + + + + + _eLxe8GokEeuExICsU7Acmg + + + + + _wexVMGokEeuExICsU7Acmg + + + + + _0CcQQGokEeuExICsU7Acmg + + + + + + + __rTw0GokEeuExICsU7Acmg + + + + + _8m6AIGouEeuExICsU7Acmg + + + + + + _EyI8MGovEeuExICsU7Acmg + + + + + + _G6B1wGovEeuExICsU7Acmg + + + + + + + _O2e6gGolEeuExICsU7Acmg + + + + + _fbkccGowEeuExICsU7Acmg + + + + + _f_JDkGowEeuExICsU7Acmg + + + + + + + _W70B4GowEeuExICsU7Acmg + + + + + _ggD_UGowEeuExICsU7Acmg + + + + + _hEC2IGowEeuExICsU7Acmg + + + + + + + + + _u9QAMGq1EeuTd-Zu7PczSg + + + + + _7rlUAGq1EeuTd-Zu7PczSg + + + + + _Ezr4QGrHEeuTd-Zu7PczSg + + + + + _oN_wcGxWEeuaU7fHySy8Bw + + + + + _rVHSYGxWEeuaU7fHySy8Bw + + + + + _rjxZcGxWEeuaU7fHySy8Bw + + + + + _rwSygGxWEeuaU7fHySy8Bw + + + + + _r71UIGxWEeuaU7fHySy8Bw + + + + + _sG5UoGxWEeuaU7fHySy8Bw + + + + + _sRR_sGxWEeuaU7fHySy8Bw + + + + + _sbtuEGxWEeuaU7fHySy8Bw + + + + + + + + + _qdgB8Gq2EeuTd-Zu7PczSg + + + + + _26so0Gq-EeuTd-Zu7PczSg + + + + + _CTFdYGq_EeuTd-Zu7PczSg + + + + + _8a4XEGrCEeuTd-Zu7PczSg + + + + + + + _REHBsGq_EeuTd-Zu7PczSg + + + + + _Hy9poGrDEeuTd-Zu7PczSg + + + + + + + _SAxeQGq_EeuTd-Zu7PczSg + + + + + _D0gvsGrGEeuTd-Zu7PczSg + + + + + + + _SmO7wGq_EeuTd-Zu7PczSg + + + + + _iPkZYGrFEeuTd-Zu7PczSg + + + + + + + _TIkMwGq_EeuTd-Zu7PczSg + + + + + _JEvTMGrGEeuTd-Zu7PczSg + + + + + + + _TrbpQGq_EeuTd-Zu7PczSg + + + + + _sc1JgGrDEeuTd-Zu7PczSg + + + + + + + _UPNrwGq_EeuTd-Zu7PczSg + + + + + _BDMwkGrEEeuTd-Zu7PczSg + + + + + + + _4lCjYGrBEeuTd-Zu7PczSg + + + + + _N9DF8GrGEeuTd-Zu7PczSg + + + + + + + + + + _fgM5AGrZEeuTd-Zu7PczSg + + + + + _rUwFIGrZEeuTd-Zu7PczSg + + + + + + _tPfTgGrZEeuTd-Zu7PczSg + + + + + + _tgmRoGrZEeuTd-Zu7PczSg + + + + + + + + + _kIaQ0Gu2EeuNUYnTveUm8Q + + + + + _QnzZ0Gq3EeuTd-Zu7PczSg + + + + + + _iuSeEGq3EeuTd-Zu7PczSg + + + + + _lC8oUGq3EeuTd-Zu7PczSg + + + + + __cnqwGq3EeuTd-Zu7PczSg + + + + + _GtDLcGq4EeuTd-Zu7PczSg + + + + + + + _mjRWIGq4EeuTd-Zu7PczSg + + + + + _bG488Gq5EeuTd-Zu7PczSg + + + + + _xXvI0Gq5EeuTd-Zu7PczSg + + + + + _GZ1poGq6EeuTd-Zu7PczSg + + + + + + + _hJzk0Gq9EeuTd-Zu7PczSg + + + + + _AI4sIGq-EeuTd-Zu7PczSg + + + + + _Pv9HoGq-EeuTd-Zu7PczSg + + + + + + + _X0N8MGrCEeuTd-Zu7PczSg + + + + + _eylnEGrDEeuTd-Zu7PczSg + + + + + + + + _TGElgGrXEeuTd-Zu7PczSg + + + + + _21Ql0GrXEeuTd-Zu7PczSg + + + + + + _4f7-8GrXEeuTd-Zu7PczSg + + + + + _ljqswGrYEeuTd-Zu7PczSg + + + + + + + + + + _fUBfUGu3EeuNUYnTveUm8Q + + + + + _KGVqYGrXEeuTd-Zu7PczSg + + + + + _yrwnkGrcEeuTd-Zu7PczSg + + + + + _90DKoGrcEeuTd-Zu7PczSg + + + + + _f7IksGrdEeuTd-Zu7PczSg + + + + + + + _yK9LIGrdEeuTd-Zu7PczSg + + + + + _cRERQGreEeuTd-Zu7PczSg + + + + + _sY_-IGreEeuTd-Zu7PczSg + + + + + + + _BuAz4GrfEeuTd-Zu7PczSg + + + + + _LOKUcGrfEeuTd-Zu7PczSg + + + + + _Sw4pkGrgEeuTd-Zu7PczSg + + + + + _TUBL0GrgEeuTd-Zu7PczSg + + + + + _tCj7sGriEeuTd-Zu7PczSg + + + + + + + + + _-c1sIGrgEeuTd-Zu7PczSg + + + + + _Wq6u0GrjEeuTd-Zu7PczSg + + + + + _NsSqIGrnEeuTd-Zu7PczSg + + + + + _TfkQUGrnEeuTd-Zu7PczSg + + + + + + + _0vPeEGrjEeuTd-Zu7PczSg + + + + + _mhArgGroEeuTd-Zu7PczSg + + + + + _d8LzYCBFEeylfveJ9xaUGQ + + + + + + + + + _04oRUGroEeuTd-Zu7PczSg + + + + + _1ITFAGroEeuTd-Zu7PczSg + + + + + + _1UOoMGroEeuTd-Zu7PczSg + + + + + + _1e2j0GroEeuTd-Zu7PczSg + + + + + + _11FSgGroEeuTd-Zu7PczSg + + + + + + _2BjBMGroEeuTd-Zu7PczSg + + + + + + _2WWzkGroEeuTd-Zu7PczSg + + + + + + _dI66sGusEeuNUYnTveUm8Q + + + + + _eQ1coGu6EeuNUYnTveUm8Q + + + + + _edL2kGu6EeuNUYnTveUm8Q + + + + + + + _zDdzkGr1EeuTd-Zu7PczSg + + + + + _2MMyAGroEeuTd-Zu7PczSg + + + + + + _2qtS8GroEeuTd-Zu7PczSg + + + + + + _2gYSQGroEeuTd-Zu7PczSg + + + + + + _20aokGroEeuTd-Zu7PczSg + + + + + + _B9RbAGunEeuNUYnTveUm8Q + + + + + + + + _NS0ZIGu2EeuNUYnTveUm8Q + + + + + _NdbGoGu2EeuNUYnTveUm8Q + + + + + _NmyeAGu2EeuNUYnTveUm8Q + + + + + _Nv9BEGu2EeuNUYnTveUm8Q + + + + + _N4q4MGu2EeuNUYnTveUm8Q + + + + + + + _OYOiAGu2EeuNUYnTveUm8Q + + + + + _d4_dwGxREeuaU7fHySy8Bw + + + + + _eOhB0GxREeuaU7fHySy8Bw + + + + + _eZYOAGxREeuaU7fHySy8Bw + + + + + _ekcOgGxREeuaU7fHySy8Bw + + + + + _et1bEGxREeuaU7fHySy8Bw + + + + + _e3l0AGxREeuaU7fHySy8Bw + + + + + _fA458GxREeuaU7fHySy8Bw + + + + + _fLUBQGxREeuaU7fHySy8Bw + + + + + _fUo8YGxREeuaU7fHySy8Bw + + + + + _feRZgGxREeuaU7fHySy8Bw + + + + + _bcEdcGxTEeuaU7fHySy8Bw + + + + + _bmj2MGxTEeuaU7fHySy8Bw + + + + + _bvg94GxTEeuaU7fHySy8Bw + + + + + _b3-9YGxTEeuaU7fHySy8Bw + + + + + _cAyUEGxTEeuaU7fHySy8Bw + + + + + _SfThUGxUEeuaU7fHySy8Bw + + + + + _SrPEgGxUEeuaU7fHySy8Bw + + + + + _S0l00GxUEeuaU7fHySy8Bw + + + + + _S9tUkGxUEeuaU7fHySy8Bw + + + + + _TPBuEGxUEeuaU7fHySy8Bw + + + + + _TZi8AGxUEeuaU7fHySy8Bw + + + + + _cYMugGxTEeuaU7fHySy8Bw + + + + + _JztYQGxWEeuaU7fHySy8Bw + + + + + _KoFbgGxWEeuaU7fHySy8Bw + + + + + + + + + + diff --git a/src/test/resources/corpus/SparxSystems_Enterprise_Architect_8.0/01_example/sample.reqif b/src/test/resources/corpus/SparxSystems_Enterprise_Architect_8.0/01_example/sample.reqif new file mode 100644 index 0000000..00105d3 --- /dev/null +++ b/src/test/resources/corpus/SparxSystems_Enterprise_Architect_8.0/01_example/sample.reqif @@ -0,0 +1,232 @@ + + + + + Optional comment associated with the Exchange Document as a whole + 2011-06-13T10:24:18+01:00 + A4123EB9-CC82-4B62-95E1-31CB3203E39C + SparxSystems Enterprise Architect 8.0 + 1.0 + microTool in-Step + Example ReqIF file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Text + + + + + Status + + + + + + + + + Text + + + + + Status + + + + + + + + + Text + + + + + Status + + + + + + + + + + + + + + + + + + + + + + + FUNC-REQ + + + + + FUNC-REQ-TXT + + + + + FUNC-REQ-STATUS + + + Proposed + + + + + + + FUNC-REQ + + + + + FUNC-REQ-TXT + + + + + FUNC-REQ-STATUS + + + Proposed + + + + + + + PERF-REQ + + + + + PERF-REQ-TXT + + + + + PERF-REQ-STATUS + + + Proposed + + + + + + + + + FUNC-REQ-1 + + + FUNC-REQ-2 + + + deriveReq + + + + + + + + + FUNC-REQ-1 + + + + + FUNC-REQ-2 + + + + + + + URS + + + + + + + + diff --git a/src/test/resources/corpus/StrictDoc/01_example/sample.reqif b/src/test/resources/corpus/StrictDoc/01_example/sample.reqif new file mode 100644 index 0000000..7b44835 --- /dev/null +++ b/src/test/resources/corpus/StrictDoc/01_example/sample.reqif @@ -0,0 +1,450 @@ + + + + + 2023-10-28T18:09:00.468457+02:00 + strictdoc + 1.0 + strictdoc + Documentation export by StrictDoc + + + + + + + + + + + + + SDOC_DATATYPE_SINGLE_LINE_STRING + + + + + SDOC_DATATYPE_SINGLE_LINE_STRING + + + + + SDOC_DATATYPE_SINGLE_LINE_STRING + + + + + SDOC_DATATYPE_SINGLE_LINE_STRING + + + + + SDOC_DATATYPE_SINGLE_LINE_STRING + + + + + SDOC_DATATYPE_SINGLE_LINE_STRING + + + + + SDOC_DATATYPE_SINGLE_LINE_STRING + + + + + SDOC_DATATYPE_SINGLE_LINE_STRING + + + + + SDOC_DATATYPE_SINGLE_LINE_STRING + + + + + + + + + + + + ReqIF.ChapterName + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.ChapterName + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.Name + + + + + ReqIF.Text + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.Name + + + + + ReqIF.Text + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.Name + + + + + ReqIF.Text + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.ChapterName + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.ChapterName + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.Name + + + + + ReqIF.Text + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.Name + + + + + ReqIF.Text + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.Name + + + + + ReqIF.Text + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.ChapterName + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.ChapterName + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.ChapterName + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.ChapterName + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.Name + + + + + ReqIF.Text + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.ChapterName + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.Name + + + + + ReqIF.Text + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + ReqIF.ChapterName + + + + + REQUIREMENT_OR_SECTION_55880716ee0e4c2d9ed88783c323dc07 + + + + + + + + + SDOC_SPECIFICATION_TYPE_SINGLETON + + + + + SECTION-0e3961d4-b93e-4209-9bfe-8b43831e09bc + + + + + SECTION-56518691-1a69-40b7-ac31-aab358ace026 + + + + + REQUIREMENT-bcfba793-7caa-46c0-8522-ba9e2d919df9 + + + + + REQUIREMENT-e83944ee-8223-4a8f-a59c-370191e140d1 + + + + + REQUIREMENT-e82abedd-a637-4f2c-889e-a8454b30d4a4 + + + + + + + + + SECTION-8b8ed5fb-f7b5-4603-8cb4-35d854f4fcae + + + + + SECTION-9a5186b8-5ae6-4096-99ef-13d808b103b0 + + + + + REQUIREMENT-67ef625f-b903-47e1-aa9f-d60e568074d4 + + + + + REQUIREMENT-21fb0ad6-05eb-428f-b67b-c279328db503 + + + + + REQUIREMENT-5fd3d97c-4acc-4f48-9f3a-bb8b7a399b23 + + + + + + + + + SECTION-c0b3ed74-ea4e-408f-9a3b-456c599943a7 + + + + + SECTION-3e241867-bbd1-4d8f-8457-055f41295b9d + + + + + SECTION-bf0bc7d9-24e2-4963-9009-d4e565652136 + + + + + SECTION-d63b35da-678f-4d8d-b95d-8a4afd2d7fb9 + + + + + REQUIREMENT-7f4e29b0-f41b-47d2-a38a-7361df93777d + + + + + SECTION-71112096-3522-4338-b6e9-484315decee6 + + + + + + + REQUIREMENT-3259d4fe-6714-4f0f-9089-bef67eb4112c + + + + + + + + + + + + + SECTION-bccb2673-a5ff-4665-95ad-c8c14ba4cf7a + + + + + + + + + + diff --git a/src/test/resources/corpus/StrictDoc/02_reqifz_example/sample.reqifz b/src/test/resources/corpus/StrictDoc/02_reqifz_example/sample.reqifz new file mode 100644 index 0000000..bd54d17 Binary files /dev/null and b/src/test/resources/corpus/StrictDoc/02_reqifz_example/sample.reqifz differ diff --git a/src/test/resources/corpus/ci.eclipse.org/TC1000/sample.reqif b/src/test/resources/corpus/ci.eclipse.org/TC1000/sample.reqif new file mode 100644 index 0000000..e04589e --- /dev/null +++ b/src/test/resources/corpus/ci.eclipse.org/TC1000/sample.reqif @@ -0,0 +1,160 @@ + + + + + 2021-01-22T12:56:38.364Z + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + 1.0 + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + TC 1000 'Simple Content' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ID_TC1000_DatatypeDefinitionBoolean + + + + + ID_TC1000_DatatypeDefinitionBoolean + + + + + ID_TC1000_DatatypeDefinitionInteger + + + + + ID_TC1000_DatatypeDefinitionString + + + + + ID_TC1000_DatatypeDefinitionReal + + + + + ID_TC1000_DatatypeDefinitionDate + + + + + ID_TC1000_DatatypeDefinitionEnumeration + + + + + ID_TC1000_DatatypeDefinitionEnumeration + + + + + + + + + + + + ID_TC1000_AttributeDefinitionBoolean_TC1000T + + + + + ID_TC1000_AttributeDefinitionBoolean_TC1000F + + + + + ID_TC1000_AttributeDefinitionInteger + + + + + ID_TC1000_AttributeDefinitionString + + + + + ID_TC1000_AttributeDefinitionReal + + + + + ID_TC1000_AttributeDefinitionDate + + + + + ID_TC1000_DatatypeDefinitionEnumeration_EnumValue_Yellow + + + ID_TC1000_AttributeDefinitionEnumeration + + + + + ID_TC1000_DatatypeDefinitionEnumeration_EnumValue_Yellow + ID_TC1000_DatatypeDefinitionEnumeration_EnumValue_Red + ID_TC1000_DatatypeDefinitionEnumeration_EnumValue_Green + + + ID_TC1000_AttributeDefinitionEnumerationMultiValue + + + + + ID_TC1000_SpecObjectType + + + + + + + ID_TC1000_SpecificationType + + + + + ID_TC1000_SpecObject + + + + + + + + diff --git a/src/test/resources/corpus/ci.eclipse.org/TC1100/sample.reqif b/src/test/resources/corpus/ci.eclipse.org/TC1100/sample.reqif new file mode 100644 index 0000000..2025a5d --- /dev/null +++ b/src/test/resources/corpus/ci.eclipse.org/TC1100/sample.reqif @@ -0,0 +1,316 @@ + + + + + 2021-01-22T12:56:38.638Z + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + 1.0 + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + TC 1100 'Unformatted common system attributes & SpecHierarchy' + + + + + + + + + + + + + + + ID_TC1100_DatatypeDefinitionString + + + + + ID_TC18xx_DatatypeDefinitionXhtml + + + + + ID_TC1100_DatatypeDefinitionString + + + + + ID_TC1100_DatatypeDefinitionDate + + + + + ID_TC1100_DatatypeDefinitionString + + + + + + + + + ID_TC18xx_DatatypeDefinitionXhtml + + + + + ID_TC1100_DatatypeDefinitionString + + + + + ID_TC1100_DatatypeDefinitionDate + + + + + ID_TC1100_DatatypeDefinitionString + + + + + + + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignID + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionXhtml_ReqIF.Name + + + Obj1 + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignCreatedBy + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionDate_ReqIF.ForeignCreatedOn + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignModifiedBy + + + + + ID_TC1100_SpecObjectType + + + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignID + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionXhtml_ReqIF.Name + + + Obj1.1 + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignCreatedBy + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionDate_ReqIF.ForeignCreatedOn + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignModifiedBy + + + + + ID_TC1100_SpecObjectType + + + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignID + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionXhtml_ReqIF.Name + + + Obj1.1.1 + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignCreatedBy + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionDate_ReqIF.ForeignCreatedOn + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignModifiedBy + + + + + ID_TC1100_SpecObjectType + + + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignID + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionXhtml_ReqIF.Name + + + Obj1.2 + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignCreatedBy + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionDate_ReqIF.ForeignCreatedOn + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignModifiedBy + + + + + ID_TC1100_SpecObjectType + + + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignID + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionXhtml_ReqIF.Name + + + Obj2 + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignCreatedBy + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionDate_ReqIF.ForeignCreatedOn + + + + + ID_TC1100_SpecObjectTypeAttributeDefinitionString_ReqIF.ForeignModifiedBy + + + + + ID_TC1100_SpecObjectType + + + + + + + + + ID_TC1100_SpecificationTypeAttributeDefinitionXHTML_ReqIF.Name + + + Specification1 + + + + + ID_TC1100_SpecificationTypeAttributeDefinitionString_ReqIF.ForeignCreatedBy + + + + + ID_TC1100_SpecificationTypeAttributeDefinitionDate_ReqIF.ForeignCreatedOn + + + + + ID_TC1100_SpecificationTypeAttributeDefinitionString_ReqIF.ForeignModifiedBy + + + + + ID_TC1100_SpecificationType + + + + + ID_TC1100_SpecObject1 + + + + + ID_TC1100_SpecObject11 + + + + + ID_TC1100_SpecObject111 + + + + + + + ID_TC1100_SpecObject12 + + + + + + + ID_TC1100_SpecObject2 + + + + + + + + diff --git a/src/test/resources/corpus/ci.eclipse.org/TC1200/sample.reqif b/src/test/resources/corpus/ci.eclipse.org/TC1200/sample.reqif new file mode 100644 index 0000000..1f7e7ea --- /dev/null +++ b/src/test/resources/corpus/ci.eclipse.org/TC1200/sample.reqif @@ -0,0 +1,2177 @@ + + + + + 2021-01-22T12:56:32.580Z + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + 1.0 + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + TC 1200 'Formated Content' + + + + + + + + + + + + + + ID_TC1200_DatatypeDefinitionXHTML + + + + + ID_TC1200_DatatypeDefinitionString + + + + + + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + + text before h1XhtmlH1Typetext after h1 + text before h2XhtmlH2Typetext after h2 + text before h3XhtmlH3Typetext after h3 + text before h4XhtmlH4Typetext after h4 + text before h5XhtmlH5Typetext after h5 + text before h6XhtmlH6Typetext after h6 + text before ul + XhtmlLiType + text after ul + text before ol + XhtmlLiType + text after ol + text before dl + XhtmlDtType + XhtmlDdType + text after dl + text before pXhtmlPTypetext after p + text before divXhtmlDivTypetext after div + text before preXhtmlPreTypetext after pre + text before blockquotetext after blockquote + text before addressXhtmlAddressTypetext after address + text before hrtext after hr + text before table + + + + text after table + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlPType + text before h1XhtmlH1Typetext after h1 + text before h2XhtmlH2Typetext after h2 + text before h3XhtmlH3Typetext after h3 + text before h4XhtmlH4Typetext after h4 + text before h5XhtmlH5Typetext after h5 + text before h6XhtmlH6Typetext after h6 + text before ul + XhtmlLiType + text after ul + text before ol + XhtmlLiType + text after ol + text before dl + XhtmlDtType + XhtmlDdType + text after dl + text before pXhtmlPTypetext after p + text before divXhtmlDivTypetext after div + text before preXhtmlPreTypetext after pre + text before blockquotetext after blockquote + text before addressXhtmlAddressTypetext after address + text before hrtext after hr + text before table + + + + text after table + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + XhtmlLiType + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + XhtmlLiType + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + XhtmlDtType + XhtmlDdType + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + XhtmlH1Type + XhtmlH2Type + XhtmlH3Type + XhtmlH4Type + XhtmlH5Type + XhtmlH6Type + + XhtmlLiType + + + XhtmlLiType + + + XhtmlDtType + XhtmlDdType + + XhtmlPType + XhtmlDivType + XhtmlPreType + + XhtmlAddressType + + + + + + + XhtmlEditType + XhtmlEditType + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + XhtmlCaptionType + + + + + + + + + + + + + + + + + + + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + XhtmlLiType + + text before h1XhtmlH1Typetext after h1 + text before h2XhtmlH2Typetext after h2 + text before h3XhtmlH3Typetext after h3 + text before h4XhtmlH4Typetext after h4 + text before h5XhtmlH5Typetext after h5 + text before h6XhtmlH6Typetext after h6 + text before ul + XhtmlLiType + text after ul + text before ol + XhtmlLiType + text after ol + text before dl + XhtmlDtType + XhtmlDdType + text after dl + text before pXhtmlPTypetext after p + text before divXhtmlDivTypetext after div + text before preXhtmlPreTypetext after pre + text before blockquotetext after blockquote + text before addressXhtmlAddressTypetext after address + text before hrtext after hr + text before table + + + + text after table + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + XhtmlDtType + XhtmlDdType + + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + XhtmlDdType + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + XhtmlDtType + XhtmlDdType + XhtmlDtType + + text before h1XhtmlH1Typetext after h1 + text before h2XhtmlH2Typetext after h2 + text before h3XhtmlH3Typetext after h3 + text before h4XhtmlH4Typetext after h4 + text before h5XhtmlH5Typetext after h5 + text before h6XhtmlH6Typetext after h6 + text before ul + XhtmlLiType + text after ul + text before ol + XhtmlLiType + text after ol + text before dl + XhtmlDtType + XhtmlDdType + text after dl + text before pXhtmlPTypetext after p + text before divXhtmlDivTypetext after div + text before preXhtmlPreTypetext after pre + text before blockquotetext after blockquote + text before addressXhtmlAddressTypetext after address + text before hrtext after hr + text before table + + + + text after table + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + + + + + + + + + + + + + + + + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + + + + + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + + + + + + + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + + + + + + + + + + + + + + + + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + + + + + + + + + + + + + + + + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + + + + + XhtmlThType + XhtmlTdType + + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + + + + + + + text before h1XhtmlH1Typetext after h1 + text before h2XhtmlH2Typetext after h2 + text before h3XhtmlH3Typetext after h3 + text before h4XhtmlH4Typetext after h4 + text before h5XhtmlH5Typetext after h5 + text before h6XhtmlH6Typetext after h6 + text before ul + XhtmlLiType + text after ul + text before ol + XhtmlLiType + text after ol + text before dl + XhtmlDtType + XhtmlDdType + text after dl + text before pXhtmlPTypetext after p + text before divXhtmlDivTypetext after div + text before preXhtmlPreTypetext after pre + text before blockquotetext after blockquote + text before addressXhtmlAddressTypetext after address + text before hrtext after hr + text before table + + + + text after table + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_AttributeDefinitionString + + + + + ID_TC1200_AttributeDefinitionXHTML + + + XhtmlDivType + + + + + + + text before h1XhtmlH1Typetext after h1 + text before h2XhtmlH2Typetext after h2 + text before h3XhtmlH3Typetext after h3 + text before h4XhtmlH4Typetext after h4 + text before h5XhtmlH5Typetext after h5 + text before h6XhtmlH6Typetext after h6 + text before ul + XhtmlLiType + text after ul + text before ol + XhtmlLiType + text after ol + text before dl + XhtmlDtType + XhtmlDdType + text after dl + text before pXhtmlPTypetext after p + text before divXhtmlDivTypetext after div + text before preXhtmlPreTypetext after pre + text before blockquotetext after blockquote + text before addressXhtmlAddressTypetext after address + text before hrtext after hr + text before table + + + + text after table + text before brtext after br + text before spanXhtmlSpanTypetext after span + text before emXhtmlEmTypetext after em + text before strongXhtmlStrongTypetext after strong + text before dfnXhtmlDfnTypetext after dfn + text before codeXhtmlCodeTypetext after code + text before sampXhtmlSampTypetext after samp + text before kbdXhtmlKbdTypetext after kbd + text before varXhtmlVarTypetext after var + text before citeXhtmlCiteTypetext after cite + text before abbrXhtmlAbbrTypetext after abbr + text before acronymXhtmlAcronymTypetext after acronym + text before qXhtmlQTypetext after q + text before ttXhtmlInlPresTypetext after tt + text before iXhtmlInlPresTypetext after i + text before bXhtmlInlPresTypetext after b + text before bigXhtmlInlPresTypetext after big + text before smallXhtmlInlPresTypetext after small + text before subXhtmlInlPresTypetext after sub + text before supXhtmlInlPresTypetext after sup + text before aXhtmlATypetext after a + text before insXhtmlEditTypetext after ins + text before delXhtmlEditTypetext after del + + + + + + + ID_TC1200_SpecObjectType + + + + + + + ID_TC1200_SpecificationType + + + + + ID_TC1200_SpecObject_xhtml.p.type + + + + + ID_TC1200_SpecObject_xhtml.div.type + + + + + ID_TC1200_SpecObject_xhtml.br.type + + + + + ID_TC1200_SpecObject_xhtml.span.type + + + + + ID_TC1200_SpecObject_xhtml.em.type + + + + + ID_TC1200_SpecObject_xhtml.strong.type + + + + + ID_TC1200_SpecObject_xhtml.dfn.type + + + + + ID_TC1200_SpecObject_xhtml.code.type + + + + + ID_TC1200_SpecObject_xhtml.samp.type + + + + + ID_TC1200_SpecObject_xhtml.kbd.type + + + + + ID_TC1200_SpecObject_xhtml.var.type + + + + + ID_TC1200_SpecObject_xhtml.cite.type + + + + + ID_TC1200_SpecObject_xhtml.abbr.type + + + + + ID_TC1200_SpecObject_xhtml.acronym.type + + + + + ID_TC1200_SpecObject_xhtml.q.type + + + + + ID_TC1200_SpecObject_xhtml.inl.pres.type + + + + + ID_TC1200_SpecObject_xhtml.a.type + + + + + ID_TC1200_SpecObject_xhtml.edit.type + + + + + ID_TC1200_SpecObject_xhtml.h1.type + + + + + ID_TC1200_SpecObject_xhtml.h2.type + + + + + ID_TC1200_SpecObject_xhtml.h3.type + + + + + ID_TC1200_SpecObject_xhtml.h4.type + + + + + ID_TC1200_SpecObject_xhtml.h5.type + + + + + ID_TC1200_SpecObject_xhtml.h6.type + + + + + ID_TC1200_SpecObject_xhtml.ul.type + + + + + ID_TC1200_SpecObject_xhtml.ol.type + + + + + ID_TC1200_SpecObject_xhtml.dl.type + + + + + ID_TC1200_SpecObject_xhtml.pre.type + + + + + ID_TC1200_SpecObject_xhtml.blockquote.type + + + + + ID_TC1200_SpecObject_xhtml.address.type + + + + + ID_TC1200_SpecObject_xhtml.hr.type + + + + + ID_TC1200_SpecObject_xhtml.table.type + + + + + ID_TC1200_SpecObject_xhtml.li.type + + + + + ID_TC1200_SpecObject_xhtml.dt.type + + + + + ID_TC1200_SpecObject_xhtml.dd.type + + + + + ID_TC1200_SpecObject_xhtml.caption.type + + + + + ID_TC1200_SpecObject_xhtml.thead.type + + + + + ID_TC1200_SpecObject_xhtml.col.type + + + + + ID_TC1200_SpecObject_xhtml.colgroup.type + + + + + ID_TC1200_SpecObject_xhtml.tfoot.type + + + + + ID_TC1200_SpecObject_xhtml.tbody.type + + + + + ID_TC1200_SpecObject_xhtml.tr.type + + + + + ID_TC1200_SpecObject_xhtml.td.type + + + + + ID_TC1200_SpecObject_xhtml.th.type + + + + + + + + diff --git a/src/test/resources/corpus/ci.eclipse.org/TC1300/sample.reqif b/src/test/resources/corpus/ci.eclipse.org/TC1300/sample.reqif new file mode 100644 index 0000000..c55f797 --- /dev/null +++ b/src/test/resources/corpus/ci.eclipse.org/TC1300/sample.reqif @@ -0,0 +1,126 @@ + + + + + 2021-01-22T12:56:38.320Z + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + 1.0 + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + TC 1300 'SpecRelation' + + + + + + + + + + + + + + ID_TC1300_DatatypeDefinitionString + + + + + + + + + + ID_TC1300_DatatypeDefinitionXHTML + + + + + + + + + + + + ID_TC1300_AttributeDefinitionString + + + + + ID_TC1300_SpecObjectType + + + + + + + ID_TC1300_AttributeDefinitionString + + + + + ID_TC1300_SpecObjectType + + + + + + + + + ID_TC1300_AttributeDefinitionString_SpecRelation + + + TC 1300 SpecRelation + + + + + ID_TC1300_SpecObject2 + + + ID_TC1300_SpecObject1 + + + ID_TC1300_SpecRelationType + + + + + + + ID_TC1300_SpecificationType + + + + + ID_TC1300_SpecObject1 + + + + + ID_TC1300_SpecObject2 + + + + + + + + + ID_TC1300_SpecRelation + + + ID_TC1300_SpecRelationGroupType + + + ID_TC1300_Specification + + + ID_TC1300_Specification + + + + + + diff --git a/src/test/resources/corpus/ci.eclipse.org/TC1400/sample.reqif b/src/test/resources/corpus/ci.eclipse.org/TC1400/sample.reqif new file mode 100644 index 0000000..e7d719a --- /dev/null +++ b/src/test/resources/corpus/ci.eclipse.org/TC1400/sample.reqif @@ -0,0 +1,181 @@ + + + + + 2021-01-22T12:56:38.424Z + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + 1.0 + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + TC 1400 'Internal Table' + + + + + + + + + + + + + + ID_TC1400_DatatypeDefinitionString + + + + + + + + + ID_TC1400_DatatypeDefinitionString + + + + + ID_TC1400_DatatypeDefinitionString + + + + + ID_TC1400_DatatypeDefinitionInteger + + + + + + + + + + + + ID_TC1400_SpecObjectTypeRegularAttributeDefinitionString_ReqIF.Name + + + + + ID_TC1400_SpecObjectTypeRegular + + + + + + + ID_TC1400_SpecObjectTypeRegularAttributeDefinitionString_ReqIF.Name + + + + + ID_TC1400_SpecObjectTypeRegular + + + + + + + ID_TC1400_SpecObjectTypeInternalTableAttributeDefinitionString_ReqIF.Name + + + + + ID_TC1400_SpecObjectTypeInternalTableAttributeDefinitionString_ReqIF.ChangeDescription + + + + + ID_TC1400_SpecObjectTypeInternalTableAttributeDefinitionInteger_ReqIF.Revision + + + + + ID_TC1400_SpecObjectTypeInternalTable + + + + + + + ID_TC1400_SpecObjectTypeInternalTableAttributeDefinitionString_ReqIF.Name + + + + + ID_TC1400_SpecObjectTypeInternalTableAttributeDefinitionString_ReqIF.ChangeDescription + + + + + ID_TC1400_SpecObjectTypeInternalTableAttributeDefinitionInteger_ReqIF.Revision + + + + + ID_TC1400_SpecObjectTypeInternalTable + + + + + + + ID_TC1400_SpecObjectTypeInternalTableAttributeDefinitionString_ReqIF.Name + + + + + ID_TC1400_SpecObjectTypeInternalTableAttributeDefinitionString_ReqIF.ChangeDescription + + + + + ID_TC1400_SpecObjectTypeInternalTableAttributeDefinitionInteger_ReqIF.Revision + + + + + ID_TC1400_SpecObjectTypeInternalTable + + + + + + + ID_TC1400_SpecificationType + + + + + ID_TC1400_SpecObject1 + + + + + ID_TC1400_SpecObject11 + + + + + ID_TC1400_SpecObject111 + + + + + ID_TC1400_SpecObject112 + + + + + ID_TC1400_SpecObject113 + + + + + + + + + + + + diff --git a/src/test/resources/corpus/ci.eclipse.org/TC1700/sample.reqifz b/src/test/resources/corpus/ci.eclipse.org/TC1700/sample.reqifz new file mode 100644 index 0000000..cdedae7 Binary files /dev/null and b/src/test/resources/corpus/ci.eclipse.org/TC1700/sample.reqifz differ diff --git a/src/test/resources/corpus/ci.eclipse.org/TC1800/sample.reqif b/src/test/resources/corpus/ci.eclipse.org/TC1800/sample.reqif new file mode 100644 index 0000000..bac2e5c --- /dev/null +++ b/src/test/resources/corpus/ci.eclipse.org/TC1800/sample.reqif @@ -0,0 +1,333 @@ + + + + + 2021-01-22T12:56:38.677Z + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + 1.0 + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + TC 1800 'HIS Exchange Process OEM -> SUP (initial)' + + + + + + + + + + + + + + + + + + + + + + + + + + + + _RB77ZFyxEeumRtWSJE-orw + + + + + _RB77Y1yxEeumRtWSJE-orw + + + + + _RB77Y1yxEeumRtWSJE-orw + + + + + _RB77ZVyxEeumRtWSJE-orw + + + + + + + + + _RB77ZFyxEeumRtWSJE-orw + + + + + + + + + + + _RB77a1yxEeumRtWSJE-orw + + + Obj-01 + + + + + _RB77bFyxEeumRtWSJE-orw + + + + + _RB77bVyxEeumRtWSJE-orw + + + + + _RB77ZlyxEeumRtWSJE-orw + + + _RB77blyxEeumRtWSJE-orw + + + + + _RB77alyxEeumRtWSJE-orw + + + + + + + _RB77a1yxEeumRtWSJE-orw + + + Obj-03 + + + + + _RB77bFyxEeumRtWSJE-orw + + + + + _RB77bVyxEeumRtWSJE-orw + + + + + _RB77ZlyxEeumRtWSJE-orw + + + _RB77blyxEeumRtWSJE-orw + + + + + _RB77alyxEeumRtWSJE-orw + + + + + + + _RB77a1yxEeumRtWSJE-orw + + + Obj-06 + + + + + _RB77bFyxEeumRtWSJE-orw + + + + + _RB77bVyxEeumRtWSJE-orw + + + + + _RB77ZlyxEeumRtWSJE-orw + + + _RB77blyxEeumRtWSJE-orw + + + + + _RB77alyxEeumRtWSJE-orw + + + + + + + _RB77a1yxEeumRtWSJE-orw + + + Obj-07 + + + + + _RB77bFyxEeumRtWSJE-orw + + + + + _RB77bVyxEeumRtWSJE-orw + + + + + _RB77ZlyxEeumRtWSJE-orw + + + _RB77blyxEeumRtWSJE-orw + + + + + _RB77alyxEeumRtWSJE-orw + + + + + + + _RB77a1yxEeumRtWSJE-orw + + + Obj-08 + + + + + _RB77bFyxEeumRtWSJE-orw + + + + + _RB77bVyxEeumRtWSJE-orw + + + + + _RB77ZlyxEeumRtWSJE-orw + + + _RB77blyxEeumRtWSJE-orw + + + + + _RB77alyxEeumRtWSJE-orw + + + + + + + _RB77a1yxEeumRtWSJE-orw + + + Obj-09 + + + + + _RB77bFyxEeumRtWSJE-orw + + + + + _RB77bVyxEeumRtWSJE-orw + + + + + _RB77ZlyxEeumRtWSJE-orw + + + _RB77blyxEeumRtWSJE-orw + + + + + _RB77alyxEeumRtWSJE-orw + + + + + + + + + _RB77cFyxEeumRtWSJE-orw + + + Spec1 + + + + + _RB77b1yxEeumRtWSJE-orw + + + + + _RB77cVyxEeumRtWSJE-orw + + + + + _RB77eFyxEeumRtWSJE-orw + + + + + _RB77f1yxEeumRtWSJE-orw + + + + + _RB77hlyxEeumRtWSJE-orw + + + + + _RB77jVyxEeumRtWSJE-orw + + + + + _RB77lFyxEeumRtWSJE-orw + + + + + + + + + _RB77cFyxEeumRtWSJE-orw + + + Spec2 + + + + + _RB77b1yxEeumRtWSJE-orw + + + + + + diff --git a/src/test/resources/corpus/ci.eclipse.org/TC1801/sample.reqif b/src/test/resources/corpus/ci.eclipse.org/TC1801/sample.reqif new file mode 100644 index 0000000..f96ce11 --- /dev/null +++ b/src/test/resources/corpus/ci.eclipse.org/TC1801/sample.reqif @@ -0,0 +1,335 @@ + + + + + 2021-01-22T12:56:38.677Z + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + 1.0 + RMF - Requirements Modeling Framework (http://www.eclipse.org/rmf) + TC 1800 'HIS Exchange Process OEM -> SUP (initial)' + + + + + + + + + + + + + + + + + + + + + + + + + + + + _RB77ZFyxEeumRtWSJE-orw + + + + + _RB77Y1yxEeumRtWSJE-orw + + + + + _RB77Y1yxEeumRtWSJE-orw + + + + + _RB77ZVyxEeumRtWSJE-orw + + + + + + + + + _RB77ZFyxEeumRtWSJE-orw + + + + + + + + + + + _RB77a1yxEeumRtWSJE-orw + + + Obj-01 + + + + + _RB77bFyxEeumRtWSJE-orw + + + + + _RB77bVyxEeumRtWSJE-orw + + + + + _RB77ZlyxEeumRtWSJE-orw + + + _RB77blyxEeumRtWSJE-orw + + + + + _RB77alyxEeumRtWSJE-orw + + + + + + + _RB77a1yxEeumRtWSJE-orw + + + Obj-03 + + + + + _RB77bFyxEeumRtWSJE-orw + + + + + _RB77bVyxEeumRtWSJE-orw + + + + + _RB77ZlyxEeumRtWSJE-orw + + + _RB77blyxEeumRtWSJE-orw + + + + + _RB77alyxEeumRtWSJE-orw + + + + + + + _RB77a1yxEeumRtWSJE-orw + + + Obj-04 + + + + + _RB77bFyxEeumRtWSJE-orw + + + + + _RB77bVyxEeumRtWSJE-orw + + + + + _RB77ZlyxEeumRtWSJE-orw + + + _RB77blyxEeumRtWSJE-orw + + + + + _RB77alyxEeumRtWSJE-orw + + + + + + + _RB77a1yxEeumRtWSJE-orw + + + Obj-07 + + + + + _RB77bFyxEeumRtWSJE-orw + + + + + _RB77bVyxEeumRtWSJE-orw + + + + + _RB77ZlyxEeumRtWSJE-orw + + + _RB77blyxEeumRtWSJE-orw + + + + + _RB77alyxEeumRtWSJE-orw + + + + + + + _RB77a1yxEeumRtWSJE-orw + + + Obj-08 + + + + + _RB77bFyxEeumRtWSJE-orw + + + + + _RB77bVyxEeumRtWSJE-orw + + + + + _RB77ZlyxEeumRtWSJE-orw + + + _RB77blyxEeumRtWSJE-orw + + + + + _RB77alyxEeumRtWSJE-orw + + + + + + + _RB77a1yxEeumRtWSJE-orw + + + Obj-09 + + + + + _RB77bFyxEeumRtWSJE-orw + + + + + _RB77bVyxEeumRtWSJE-orw + + + + + _RB77ZlyxEeumRtWSJE-orw + + + _RB77blyxEeumRtWSJE-orw + + + + + _RB77alyxEeumRtWSJE-orw + + + + + + + + + _RB77cFyxEeumRtWSJE-orw + + + Spec1 + + + + + _RB77b1yxEeumRtWSJE-orw + + + + + _RB77cVyxEeumRtWSJE-orw + + + + + _RB77eFyxEeumRtWSJE-orw + + + + + _RCCCAFyxEeumRtWSJE-orw + + + + + _RB77hlyxEeumRtWSJE-orw + + + + + _RB77lFyxEeumRtWSJE-orw + + + + + + + + + _RB77cFyxEeumRtWSJE-orw + + + Spec2 + + + + + _RB77b1yxEeumRtWSJE-orw + + + + + _RB77jVyxEeumRtWSJE-orw + + + + + + + + diff --git a/src/test/resources/corpus/org.eclipse.rmf/01_specRelationTest/sample.reqif b/src/test/resources/corpus/org.eclipse.rmf/01_specRelationTest/sample.reqif new file mode 100644 index 0000000..7ae26b5 --- /dev/null +++ b/src/test/resources/corpus/org.eclipse.rmf/01_specRelationTest/sample.reqif @@ -0,0 +1,109 @@ + + + + + Embedded OLE object with multiple representation forms. + 2005-05-23T12:00:00+02:00 + Manually written + Test data RIF72 + + + + + + + + + + + + + + + + + 3631dcd2-59d1-11da-beb2-6fbc179f63e3 + + + + + + + + 3631dcd2-59d1-11da-beb2-6fbc179f63e4 + + + + + + + + + + + + 356b02ec-59d1-11da-afa6-6b90abdfb5db + + + + + 356b02ec-59d1-11da-afa6-6b90abdfb5dc + + + + + 3631dcd2-59d1-11da-beb2-6fbc179f63e2 + + + + + + + 356b02ec-59d1-11da-afa6-6b90abdfb5db + + + + + 356b02ec-59d1-11da-afa6-6b90abdfb5dc + + + + + 3631dcd2-59d1-11da-beb2-6fbc179f63e2 + + + + + + + _WYTuPdaVEeCyV-qeruTLwA + + + 2c84e85a-59d1-11da-8ef5-afdbd01c7a79 + + + 2c84e85a-59d1-11da-8ef5-afdbd01c7a71 + + + + + + + + + + + 2c84e85a-59d1-11da-8ef5-afdbd01c7a71 + + + + + 2c84e85a-59d1-11da-8ef5-afdbd01c7a79 + + + + + + + + diff --git a/src/test/resources/corpus/org.eclipse.rmf/02_sample/sample.reqif b/src/test/resources/corpus/org.eclipse.rmf/02_sample/sample.reqif new file mode 100644 index 0000000..c5af8d4 --- /dev/null +++ b/src/test/resources/corpus/org.eclipse.rmf/02_sample/sample.reqif @@ -0,0 +1,95 @@ + + + + + Embedded OLE object with multiple representation forms. + 2005-05-23T12:00:00+02:00 + Manually written + Test data RIF72 + + + + + + + + + + + + + + + + + 3631dcd2-59d1-11da-beb2-6fbc179f63e3 + + + + + + + + 3631dcd2-59d1-11da-beb2-6fbc179f63e4 + + + + + + + + + + + 356b02ec-59d1-11da-afa6-6b90abdfb5db + + + + + 356b02ec-59d1-11da-afa6-6b90abdfb5dc + + + + + 3631dcd2-59d1-11da-beb2-6fbc179f63e2 + + + + + + + 356b02ec-59d1-11da-afa6-6b90abdfb5db + + + + + 356b02ec-59d1-11da-afa6-6b90abdfb5dc + + + + + 3631dcd2-59d1-11da-beb2-6fbc179f63e2 + + + + + + + + + + + 2c84e85a-59d1-11da-8ef5-afdbd01c7a71 + + + + + 2c84e85a-59d1-11da-8ef5-afdbd01c7a79 + + + + + + + + diff --git a/src/test/resources/full-feature.reqif b/src/test/resources/full-feature.reqif new file mode 100644 index 0000000..8d838c2 --- /dev/null +++ b/src/test/resources/full-feature.reqif @@ -0,0 +1,163 @@ + + + + + Exported for the round trip test + 2026-07-23T10:00:00Z + repo-42 + Some Tool + 1.0 + DOORS + All features + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dt-string + + + ad-textto be writtento be written + dt-xhtml + + + ad-count + dt-int + + + dt-real + + + ad-safety + dt-bool + + + dt-date + + + ad-statusev-open + dt-enum + + + dt-enum + + + + + + + dt-string + + + + + + + dt-string + + + + + + + + + + ad-id + + ad-text + The system shall boot. Diagram + The system shall boot. Diagram + + ad-count + ad-effort + ad-safety + ad-due + ad-statusev-done + ad-tagsev-openev-done + + st-req + + + + ad-id + ad-textA single paragraph. + ad-tags + + st-req + + + + + + ad-rel-note + + st-rel + so-2 + so-1 + + + + + + ad-spec-owner + + st-spec + + + + ad-text + ad-status + ad-text + + so-1 + + + so-2 + + + + + + + + + + st-group + spec-1 + spec-1 + sr-1 + + + + + + + + + +