135 lines
6.2 KiB
Markdown
135 lines
6.2 KiB
Markdown
|
|
---
|
|||
|
|
type: Workflow
|
|||
|
|
title: Transformations-Pipeline
|
|||
|
|
description: Die dreistufige Pipeline XML→FO→PDF→Diff, Worker-Pool-Architektur, Entscheidungslogik für Neutransformation und XSL-Abhängigkeitsgraph.
|
|||
|
|
tags: [transformation, saxon, fop, diff-pdf, worker-pool, pipeline]
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Transformations-Pipeline
|
|||
|
|
|
|||
|
|
## Übersicht
|
|||
|
|
|
|||
|
|
Die Kern-Pipeline von DocuMentor besteht aus drei Stufen:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
XML ──[Saxon XSLT]──→ FO ──[Apache FOP]──→ PDF ──[diff-pdf]──→ Diff-PDF
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
1. **XML → FO**: XSLT-Transformation mit Saxon-HE (XSLT 1.0 via JAXP oder 2.0/3.0 via s9api)
|
|||
|
|
2. **FO → PDF**: PDF-Generierung aus XSL-FO mit Apache FOP
|
|||
|
|
3. **PDF → Diff**: Vergleich von Referenz- und Neu-PDF mit diff-pdf
|
|||
|
|
|
|||
|
|
Die Implementierung befindet sich in `src/transform.py` (`TransformationJob`-Klasse).
|
|||
|
|
|
|||
|
|
## TransformationJob
|
|||
|
|
|
|||
|
|
`TransformationJob` repräsentiert einen einzelnen Transformations-Job. Beim Initialisieren werden folgende Ausgabeverzeichnisse im Projektordner erstellt:
|
|||
|
|
|
|||
|
|
| Verzeichnis | Inhalt |
|
|||
|
|
|-------------|--------|
|
|||
|
|
| `new/` | Neu generierte PDFs und temporäre FO-Dateien |
|
|||
|
|
| `ref/` | Referenz-PDFs (vor Änderung) |
|
|||
|
|
| `diff/` | Diff-PDFs vom Vergleich |
|
|||
|
|
|
|||
|
|
Dateinamen folgen dem Schema `{xml_stem}_xsl_{xsl_id_tuple}.pdf` – z.B. `zeugnis_xsl_1_2_3.pdf`.
|
|||
|
|
|
|||
|
|
### Classpath-Caching
|
|||
|
|
|
|||
|
|
`TransformationJob` hat einen klassenweiten Cache (`_classpath_cache: dict[Path, str]`) für Saxon-Classpaths. Beim ersten Job wird der Classpath aus allen JARs im Saxon-Verzeichnis (+ `lib/`-Unterordner) gebaut und für nachfolgende Jobs wiederverwendet.
|
|||
|
|
|
|||
|
|
## Entscheidungslogik: Wann wird transformiert?
|
|||
|
|
|
|||
|
|
`is_up_to_date()` prüft, ob eine Neutransformation nötig ist. Eine Transformation gilt als **aktuell**, wenn:
|
|||
|
|
|
|||
|
|
1. Das `new/`-PDF existiert **und**
|
|||
|
|
2. Das `new/`-PDF neuer ist als:
|
|||
|
|
- Die XML-Eingabedatei
|
|||
|
|
- Die XSL-Stylesheet-Datei
|
|||
|
|
- **Alle transitiv importierten/inkludierten XSL-Dateien** (über `XslDependencyGraph`)
|
|||
|
|
|
|||
|
|
Wenn `force=True` übergeben wird (Menü "Alle neu transformieren"), wird `is_up_to_date()` übersprungen.
|
|||
|
|
|
|||
|
|
Diese Logik stellt sicher, dass Änderungen in importierten XSL-Dateien – die durch `<xsl:import/>`/`<xsl:include/>` transitiv abhängig sind – automatisch erkannt werden. Siehe auch `docs/Transformation ohne Force - Entscheidungslogik`.
|
|||
|
|
|
|||
|
|
## Worker-Pool-Architektur
|
|||
|
|
|
|||
|
|
Um den JVM-Startup-Overhead zu eliminieren, verwendet DocuMentor persistente JVM-Worker-Pools. Die Basisimplementierung ist `BaseWorkerPool` (`src/worker_pool_base.py`).
|
|||
|
|
|
|||
|
|
### Saxon-Worker-Pool
|
|||
|
|
|
|||
|
|
Zwei Varianten, ausgewählt über `AppSettings.saxon_xslt_version`:
|
|||
|
|
|
|||
|
|
| Variante | Klasse | API | XSLT-Version |
|
|||
|
|
|----------|--------|-----|--------------|
|
|||
|
|
| JAXP | `SaxonWorkerPool` (`src/saxon_pool.py`) | `javax.xml.transform` | 1.0 |
|
|||
|
|
| s9api | `SaxonWorkerPoolS9Api` (`src/saxon_pool_s9api.py`) | `net.sf.saxon.s9api` | 2.0/3.0 |
|
|||
|
|
|
|||
|
|
Beide Varianten:
|
|||
|
|
- Starten N Worker-JVM-Prozesse (konfigurierbar über `AppSettings.max_workers`, Standard: 8)
|
|||
|
|
- Kompilieren Java-Worker-Code zur Laufzeit (Java-Sourcecode als String- Konstante in der Python-Datei)
|
|||
|
|
- Jeder Worker liest Jobs von stdin, schreibt Ergebnisse nach stdout, Logs nach stderr
|
|||
|
|
- Stylesheet-Caching im Worker (HashMap) für wiederholte Transformationen
|
|||
|
|
- Kommunikation über tab-getrennte Zeilen via stdin/stdout
|
|||
|
|
- Fallback auf direkten subprocess-Aufruf, wenn der Pool nicht verfügbar ist
|
|||
|
|
|
|||
|
|
### FOP-Worker-Pool
|
|||
|
|
|
|||
|
|
`FopWorkerPool` (`src/fop_pool.py`) analog zum Saxon-Pool, aber für FO→PDF-Transformation:
|
|||
|
|
- `FopFactory` wird einmal pro Worker erstellt und wiederverwendet
|
|||
|
|
- Unterstützt optionale FOP-Konfigurationsdatei (`fop.xconf`)
|
|||
|
|
- Fallback auf subprocess (`fop.cmd`/`fop`)
|
|||
|
|
|
|||
|
|
### Lebenszyklus
|
|||
|
|
|
|||
|
|
Der Lebenszyklus wird vom `WorkerPoolMixin` (`src/ui/mixins/worker_pool.py`) gesteuert:
|
|||
|
|
|
|||
|
|
1. **Initialisierung** bei Projektwechsel: `_initialize_saxon_worker_pool()` / `_initialize_fop_worker_pool()`
|
|||
|
|
2. **Shutdown** bei Projektwechsel oder Anwendungsende: `_shutdown_saxon_worker_pool()` / `_shutdown_fop_worker_pool()`
|
|||
|
|
3. Globale Referenz über `set_saxon_worker_pool()` / `get_saxon_worker_pool()` in `src/transform.py`
|
|||
|
|
|
|||
|
|
Da DocuMentor permanent im Hintergrund läuft, ist sparsamer RAM-Umgang wichtig: Worker-Pools werden nach Verwendung heruntergefahren.
|
|||
|
|
|
|||
|
|
### Performance-Metriken
|
|||
|
|
|
|||
|
|
`WorkerPoolMetrics` (`src/worker_metrics.py`) sammelt:
|
|||
|
|
- Kompilierungszeit
|
|||
|
|
- Worker-Start-Zeiten
|
|||
|
|
- RAM-Verbrauch pro Worker (vor/nach Transformation, via `psutil`)
|
|||
|
|
- XSL-Kompilierungszeiten (nur Saxon)
|
|||
|
|
|
|||
|
|
Metriken werden im `WorkerPoolMetricsDialog` (`src/ui/WorkerPoolMetricsDialog.py`) angezeigt.
|
|||
|
|
|
|||
|
|
## XSL-Abhängigkeitsgraph
|
|||
|
|
|
|||
|
|
`XslDependencyGraph` (`src/xsl_dependencies.py`) ist für zwei Bereiche relevant:
|
|||
|
|
|
|||
|
|
1. **Entscheidungslogik**: `is_up_to_date()` nutzt `get_dependencies()`, um transitiv importierte XSL-Dateien auf Änderungen zu prüfen
|
|||
|
|
2. **Visualisierung**: Der Dialog `XslDependencyDialog` (`src/ui/XslDependencyDialog.py`) stellt den Graph mit vis.js dar
|
|||
|
|
|
|||
|
|
Der Graph wird über Datei-mtime automatisch invalidiert – bei Änderung einer XSL-Datei wird deren Eintrag neu geparst. Das Parsing verwendet lxml mit dem XSL-Namespace `http://www.w3.org/1999/XSL/Transform`.
|
|||
|
|
|
|||
|
|
## Transformations-Ausführung
|
|||
|
|
|
|||
|
|
Die `TransformationMixin` (`src/ui/mixins/transformation.py`) orchestriert die Ausführung:
|
|||
|
|
|
|||
|
|
1. Sammelt alle zu transformierenden Jobs aus dem Baum
|
|||
|
|
2. Sammelt XSLT-Parameter (projektweit + knotenspezifisch, über `_collect_parent_params()`)
|
|||
|
|
3. Startet `TransformationThread` (QThread) mit der Job-Liste
|
|||
|
|
4. Zeigt Fortschrittsbalken in der Statusbar an
|
|||
|
|
5. Aktualisiert Diff-Icons im Baum nach Abschluss
|
|||
|
|
|
|||
|
|
### "Accept Changes"
|
|||
|
|
|
|||
|
|
Nach Begutachtung der Diff-PDF kann der Benutzer Änderungen akzeptieren: Das `new/`-PDF wird zum `ref/`-PDF kopiert, und der Diff wird gelöscht.
|
|||
|
|
|
|||
|
|
## Datenbank-Integration
|
|||
|
|
|
|||
|
|
Über das `DatabaseMixin` (`src/ui/mixins/database.py`) können XML/XSL-Zuordnungen aus einer PostgreSQL-Datenbank geladen werden:
|
|||
|
|
|
|||
|
|
1. `DatabaseQueryThread` führt SQL asynchron via ConnectorX aus
|
|||
|
|
2. Ergebnis wird als Polars DataFrame sortiert nach `reporttyp_bez`, `report_bez`, `repfile_bez`
|
|||
|
|
3. DataFrame wird in `TreeNode`/`XslFile`-Struktur konvertiert und in den Baum geladen
|
|||
|
|
4. `obsolete_detector` erkennt veraltete Einträge, die nicht mehr in der DB vorhanden sind
|
|||
|
|
|
|||
|
|
Siehe [Datenmodelle & Konfiguration](../data-models.md) für das `PostgreSqlDb`-Modell und SSL-Modi.
|