Files
xsl-validator/src/ui/mixins/transformation.py
T

868 lines
38 KiB
Python
Raw Normal View History

"""
TransformationMixin - Mixin für XSL-Transformationen.
Dieses Mixin enthält alle Methoden zur Durchführung und Verwaltung von
XSL-Transformationen für das MainWindow.
"""
import logging
from copy import deepcopy
from pathlib import Path
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QMessageBox, QProgressBar, QTreeWidgetItem
from conf import app_settings, TreeNode, XslFile, XmlFile
from transform import TransformationJob
from ui.threads import TransformationThread
logger = logging.getLogger(__name__)
class TransformationMixin:
"""
Mixin für XSL-Transformationen.
Dieses Mixin wird von MainWindow verwendet und erwartet folgende Attribute:
- self.project: Das aktuelle Projekt
- self.ui: Das UI-Objekt mit treeWidget, statusBar
- self.transformation_thread: Thread für Transformationen
- self.transformation_progress_bar: QProgressBar für Transformation-Fortschritt
- self.xml_item_map: Mapping von xml_path|xsl_id zu TreeWidgetItems
- self.last_saxon_metrics: Gecachte Saxon-Worker-Pool-Metriken
- self.last_fop_metrics: Gecachte FOP-Worker-Pool-Metriken
Erwartet folgende Methoden von anderen Mixins:
- self._initialize_saxon_worker_pool(): Von WorkerPoolMixin
- self._initialize_fop_worker_pool(): Von WorkerPoolMixin
- self._shutdown_saxon_worker_pool(): Von WorkerPoolMixin
- self._shutdown_fop_worker_pool(): Von WorkerPoolMixin
- self._create_centered_progress_bar(): Von TreeManagerMixin
- self._create_centered_diff_icon(): Von TreeManagerMixin
- self._collect_parent_params(): Von TreeManagerMixin
- self._update_diff_icons_for_existing_pdfs(): Von MainWindow
"""
def _show_transformation_progress_bar(self, total_jobs: int):
"""
Zeigt einen Progressbar in der Statusbar für Transformationen.
Args:
total_jobs: Gesamtanzahl der Transformations-Jobs
"""
if self.transformation_progress_bar is None:
self.transformation_progress_bar = QProgressBar()
self.transformation_progress_bar.setMaximumHeight(20)
self.transformation_progress_bar.setMaximumWidth(300)
self.transformation_progress_bar.setMinimum(0)
self.transformation_progress_bar.setMaximum(total_jobs)
self.transformation_progress_bar.setValue(0)
self.transformation_progress_bar.setFormat("%v/%m Jobs")
# Füge Progressbar zur Statusbar hinzu
self.statusBar().addPermanentWidget(self.transformation_progress_bar)
self.transformation_progress_bar.show()
def _hide_transformation_progress_bar(self):
"""Versteckt und entfernt den Transformation-Progressbar aus der Statusbar."""
if self.transformation_progress_bar:
self.statusBar().removeWidget(self.transformation_progress_bar)
self.transformation_progress_bar.hide()
def _update_transformation_progress(self):
"""Aktualisiert den Transformation-Progressbar um einen Schritt."""
if self.transformation_progress_bar:
current_value = self.transformation_progress_bar.value()
self.transformation_progress_bar.setValue(current_value + 1)
def _transform_xml_file(self, item: QTreeWidgetItem, force: bool = False):
"""
Transformiert eine einzelne XML-Datei.
Args:
item: Das TreeWidgetItem der XML-Datei
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
"""
try:
# Hole XslFile vom Parent-Item
parent_item = item.parent()
if not parent_item:
logger.error("XML-Datei hat kein Parent-Item (XslFile)")
QMessageBox.warning(self, "Fehler", "XML-Datei hat keine zugeordnete XSL-Datei")
return
xsl_file_obj = parent_item.data(0, Qt.ItemDataRole.UserRole)
if not isinstance(xsl_file_obj, XslFile):
logger.error(f"Parent-Item ist kein XslFile: {type(xsl_file_obj)}")
QMessageBox.warning(self, "Fehler", "Konnte XSL-Datei nicht ermitteln")
return
# Hole XmlFile-Objekt
xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
if not isinstance(xml_file_obj, XmlFile):
logger.error(f"Item ist kein XmlFile: {type(xml_file_obj)}")
QMessageBox.warning(self, "Fehler", "Konnte XML-Datei nicht ermitteln")
return
# Erstelle TransformationJob mit TreeWidgetItem-Kontext für Parameter-Sammlung
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, parent_item)
if not job:
QMessageBox.warning(
self,
"Fehler",
"Transformation kann nicht gestartet werden.\n\n"
"Bitte überprüfen Sie, ob XML- und XSL-Dateien existieren.\n"
"Details finden Sie im Log.",
)
return
# Starte Transformation in separatem Thread
self._start_transformation([job], force=force)
except Exception as e:
logger.error(f"Fehler beim Transformieren der XML-Datei: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")
def _transform_xsl_file(self, item: QTreeWidgetItem, force: bool = False):
"""
Transformiert alle XML-Dateien einer XSL-Datei.
Args:
item: Das TreeWidgetItem der XSL-Datei
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
"""
try:
# Hole XslFile-Objekt
xsl_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
if not isinstance(xsl_file_obj, XslFile):
logger.error(f"Item ist kein XslFile: {type(xsl_file_obj)}")
QMessageBox.warning(self, "Fehler", "Konnte XSL-Datei nicht ermitteln")
return
# Prüfe ob XML-Dateien vorhanden sind
if not xsl_file_obj.xmls:
QMessageBox.information(self, "Info", "Keine XML-Dateien zugeordnet")
return
# Erstelle TransformationJobs für alle XML-Dateien
jobs = []
skipped_count = 0
for xml_file_obj in xsl_file_obj.xmls:
# Übergebe das XslFile-TreeWidgetItem für Parameter-Sammlung
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, item)
if job:
jobs.append(job)
else:
skipped_count += 1
if not jobs:
QMessageBox.warning(
self,
"Fehler",
f"Konnte keine gültigen Transformations-Jobs erstellen.\n\n"
f"{skipped_count} XML-Datei(en) wurden übersprungen (fehlende Dateien).",
)
return
# Informiere Benutzer wenn einige Jobs übersprungen wurden
if skipped_count > 0:
logger.warning(f"{skipped_count} von {len(xsl_file_obj.xmls)} Jobs übersprungen (fehlende Dateien)")
QMessageBox.warning(
self,
"Warnung",
f"{skipped_count} von {len(xsl_file_obj.xmls)} XML-Datei(en) werden übersprungen "
f"(fehlende XML- oder XSL-Dateien).\n\n"
f"{len(jobs)} Job(s) werden ausgeführt.",
)
# Starte Transformation in separatem Thread
self._start_transformation(jobs, force=force)
except Exception as e:
logger.error(f"Fehler beim Transformieren der XSL-Datei: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")
def _count_diff_pdfs_under_node(self, node: TreeNode | XslFile, node_item: QTreeWidgetItem) -> int:
"""
Zählt die Anzahl der existierenden Diff-PDFs unter einem Knoten.
Args:
node: TreeNode oder XslFile Objekt
node_item: Das TreeWidgetItem des Knotens
Returns:
int: Anzahl der existierenden Diff-PDF-Dateien
"""
count = 0
if isinstance(node, XslFile):
# Für XslFile: Zähle Diff-PDFs für jede XML-Datei
if not self.project:
return 0
diff_dir = self.project.project_dir / "diff"
xsl_id_str = "_".join(str(x) for x in node.id) if node.id else ""
for xml_file_obj in node.xmls:
xml_stem = xml_file_obj.xml.stem
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
diff_pdf_path = diff_dir / pdf_basename
if diff_pdf_path.exists():
count += 1
elif isinstance(node, TreeNode):
# Für TreeNode: Rekursiv alle Kinder durchgehen
for i in range(node_item.childCount()):
child_item = node_item.child(i)
child_node = child_item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(child_node, (XslFile, TreeNode)):
count += self._count_diff_pdfs_under_node(child_node, child_item)
return count
def _update_diff_pdf_counts_recursive(self, tree_item: QTreeWidgetItem):
"""
Aktualisiert rekursiv die Diff-PDF-Anzahl in Spalte 2 für alle TreeNode und XslFile Items.
Args:
tree_item: Das TreeWidgetItem (kann Root oder beliebiger Knoten sein)
"""
node = tree_item.data(0, Qt.ItemDataRole.UserRole)
# Aktualisiere nur für TreeNode und XslFile, nicht für XmlFile
if isinstance(node, (TreeNode, XslFile)):
count = self._count_diff_pdfs_under_node(node, tree_item)
tree_item.setText(2, str(count) if count > 0 else "")
# Rekursiv für alle Kinder
for i in range(tree_item.childCount()):
child_item = tree_item.child(i)
self._update_diff_pdf_counts_recursive(child_item)
def _update_all_diff_pdf_counts(self):
"""
Aktualisiert die Diff-PDF-Anzahl für alle Knoten im TreeWidget.
"""
root = self.ui.treeWidget.invisibleRootItem()
for i in range(root.childCount()):
self._update_diff_pdf_counts_recursive(root.child(i))
def _has_xml_files_recursive(self, node: TreeNode) -> bool:
"""
Prüft rekursiv, ob unter einem TreeNode mindestens eine XML-Datei vorhanden ist.
Args:
node: Der TreeNode
Returns:
bool: True wenn mindestens eine XML-Datei gefunden wurde
"""
if not hasattr(node, "children") or not node.children:
return False
for child in node.children:
if isinstance(child, XslFile):
if child.xmls:
return True
elif isinstance(child, TreeNode):
if self._has_xml_files_recursive(child):
return True
return False
def _collect_all_xsl_xml_pairs_recursive(
self, tree_node: TreeNode, tree_item: QTreeWidgetItem
) -> list[tuple[XslFile, XmlFile, QTreeWidgetItem]]:
"""
Sammelt rekursiv alle (XslFile, XmlFile, XslFileItem) Tupel unter einem TreeNode.
Args:
tree_node: Der TreeNode
tree_item: Das TreeWidgetItem des TreeNode
Returns:
list: Liste von (XslFile, XmlFile, XslFileItem) Tupeln
"""
pairs = []
if not hasattr(tree_node, "children") or not tree_node.children:
return pairs
# Durchlaufe alle Kinder des TreeNode
for i in range(tree_item.childCount()):
child_item = tree_item.child(i)
child_node = child_item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(child_node, XslFile):
# XslFile gefunden - sammle alle XML-Dateien
for xml_file_obj in child_node.xmls:
pairs.append((child_node, xml_file_obj, child_item))
elif isinstance(child_node, TreeNode):
# Rekursiv in Unterknoten suchen
pairs.extend(self._collect_all_xsl_xml_pairs_recursive(child_node, child_item))
return pairs
def _transform_tree_node(self, item: QTreeWidgetItem, force: bool = False):
"""
Transformiert alle XML-Dateien unter einem TreeNode (rekursiv).
Args:
item: Das TreeWidgetItem des TreeNode
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
"""
try:
# Hole TreeNode-Objekt
tree_node_obj = item.data(0, Qt.ItemDataRole.UserRole)
if not isinstance(tree_node_obj, TreeNode):
logger.error(f"Item ist kein TreeNode: {type(tree_node_obj)}")
QMessageBox.warning(self, "Fehler", "Konnte TreeNode nicht ermitteln")
return
# Prüfe ob XML-Dateien vorhanden sind
if not self._has_xml_files_recursive(tree_node_obj):
QMessageBox.information(self, "Info", "Keine XML-Dateien unter diesem Knoten gefunden")
return
# Sammle alle XSL/XML-Paare rekursiv
xsl_xml_pairs = self._collect_all_xsl_xml_pairs_recursive(tree_node_obj, item)
if not xsl_xml_pairs:
QMessageBox.information(self, "Info", "Keine XML-Dateien gefunden")
return
# Erstelle TransformationJobs für alle XML-Dateien
jobs = []
skipped_count = 0
for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
# Übergebe das XslFile-TreeWidgetItem für Parameter-Sammlung
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, xsl_file_item)
if job:
jobs.append(job)
else:
skipped_count += 1
if not jobs:
QMessageBox.warning(
self,
"Fehler",
f"Konnte keine gültigen Transformations-Jobs erstellen.\n\n"
f"{skipped_count} XML-Datei(en) wurden übersprungen (fehlende Dateien).",
)
return
# Informiere Benutzer wenn einige Jobs übersprungen wurden
if skipped_count > 0:
logger.warning(f"{skipped_count} von {len(xsl_xml_pairs)} Jobs übersprungen (fehlende Dateien)")
QMessageBox.warning(
self,
"Warnung",
f"{skipped_count} von {len(xsl_xml_pairs)} XML-Datei(en) werden übersprungen "
f"(fehlende XML- oder XSL-Dateien).\n\n"
f"{len(jobs)} Job(s) werden ausgeführt.",
)
logger.info(f"Starte Transformation für {len(jobs)} XML-Dateien unter TreeNode '{tree_node_obj.bez}'")
# Starte Transformation in separatem Thread
self._start_transformation(jobs, force=force)
except Exception as e:
logger.error(f"Fehler beim Transformieren des TreeNode: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")
def _create_transformation_job(
self, xsl_file_obj: XslFile, xml_file_obj: XmlFile, xsl_file_item: QTreeWidgetItem | None = None
) -> TransformationJob | None:
"""
Erstellt einen TransformationJob für eine XML/XSL-Kombination.
Args:
xsl_file_obj: Das XslFile-Objekt
xml_file_obj: Das XmlFile-Objekt
xsl_file_item: Optional das TreeWidgetItem des XslFile für hierarchische Parameter-Sammlung
Returns:
TransformationJob oder None bei Fehler
"""
try:
if not self.project:
QMessageBox.warning(self, "Fehler", "Kein Projekt geöffnet")
return None
# Hole Tool-Konfigurationen aus app_settings
java_vm = next((jvm for jvm in app_settings.java_vms if jvm.id == self.project.java_vm_id), None)
saxon_jar = next((sj for sj in app_settings.saxon_jars if sj.id == self.project.saxon_jar_id), None)
apache_fop = next((af for af in app_settings.apache_fops if af.id == self.project.apache_fop_id), None)
diff_pdf = next((dp for dp in app_settings.diff_pdfs if dp.id == self.project.diff_pdf_id), None)
xsl_dir = next((xd for xd in app_settings.xsl_dirs if xd.id == self.project.xsl_dir_id), None)
# Prüfe ob alle Konfigurationen vorhanden sind
if not all([java_vm, saxon_jar, apache_fop, diff_pdf, xsl_dir]):
missing = []
if not java_vm:
missing.append("Java VM")
if not saxon_jar:
missing.append("Saxon JAR")
if not apache_fop:
missing.append("Apache FOP")
if not diff_pdf:
missing.append("diff-pdf")
if not xsl_dir:
missing.append("XSL-Verzeichnis")
QMessageBox.warning(
self, "Fehlende Konfiguration", f"Folgende Konfigurationen fehlen: {', '.join(missing)}"
)
return None
# Zusätzliche Sicherheitsprüfung für path_to_binary_file Attribute
if java_vm is None or not hasattr(java_vm, "path_to_binary_file") or java_vm.path_to_binary_file is None:
QMessageBox.warning(self, "Konfigurationsfehler", "Java VM Pfad ist nicht konfiguriert")
return None
if saxon_jar is None or not hasattr(saxon_jar, "path_to_jar_file") or saxon_jar.path_to_jar_file is None:
QMessageBox.warning(self, "Konfigurationsfehler", "Saxon JAR Pfad ist nicht konfiguriert")
return None
if apache_fop is None or not hasattr(apache_fop, "path_to_dir") or apache_fop.path_to_dir is None:
QMessageBox.warning(self, "Konfigurationsfehler", "Apache FOP Pfad ist nicht konfiguriert")
return None
if diff_pdf is None or not hasattr(diff_pdf, "path_to_binary_file") or diff_pdf.path_to_binary_file is None:
QMessageBox.warning(self, "Konfigurationsfehler", "diff-pdf Pfad ist nicht konfiguriert")
return None
if xsl_dir is None or not hasattr(xsl_dir, "path_to_root_dir") or xsl_dir.path_to_root_dir is None:
QMessageBox.warning(self, "Konfigurationsfehler", "XSL-Verzeichnis Pfad ist nicht konfiguriert")
return None
# Erstelle absoluten Pfad zur XSL-Datei
xsl_file_abs = xsl_dir.path_to_root_dir / xsl_file_obj.xsl_file
# Prüfe ob XSL-Datei existiert
if not xsl_file_abs.exists():
error_msg = f"XSL-Datei nicht gefunden: {xsl_file_abs}"
logger.error(error_msg)
# Kein MessageBox hier - wird in der aufrufenden Funktion behandelt
return None
# Erstelle absoluten Pfad zur XML-Datei
xml_file_abs = self.project.project_dir / xml_file_obj.xml
# Prüfe ob XML-Datei existiert
if not xml_file_abs.exists():
error_msg = f"XML-Datei nicht gefunden: {xml_file_abs}"
logger.error(error_msg)
# Kein MessageBox hier - wird in der aufrufenden Funktion behandelt
return None
# Sammle XSLT-Parameter hierarchisch (TreeNode-Eltern → XslFile)
xslt_params = {}
# 1. Sammle Parameter von übergeordneten TreeNodes (falls TreeWidgetItem verfügbar)
if xsl_file_item is not None:
parent_params = self._collect_parent_params(xsl_file_item)
xslt_params.update(parent_params)
logger.debug(f"Hierarchische Parameter gesammelt: {parent_params}")
else:
logger.warning(
"Kein TreeWidgetItem-Kontext verfügbar - "
"übergeordnete TreeNode-Parameter werden nicht berücksichtigt"
)
# 2. Überschreibe mit XslFile-eigenen Parametern (höchste Priorität)
xslt_params.update(xsl_file_obj.xslt_params)
logger.info(f"Finale XSLT-Parameter für {xml_file_obj.xml} mit {xsl_file_obj.bez}: {xslt_params}")
# Erstelle TransformationJob
job = TransformationJob(
project_dir=self.project.project_dir,
xml_file=xml_file_obj.xml,
xsl_file=xsl_file_abs,
xslt_params=xslt_params,
java_vm_path=java_vm.path_to_binary_file,
saxon_jar_path=saxon_jar.path_to_jar_file,
apache_fop_dir=apache_fop.path_to_dir,
diff_pdf_path=diff_pdf.path_to_binary_file,
diff_pdf_params=diff_pdf.default_params,
xsl_id=xsl_file_obj.id,
fop_config_dir=self.project.fop_config_dir,
)
return job
except Exception as e:
logger.error(f"Fehler beim Erstellen des TransformationJobs: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Erstellen des Jobs: {str(e)}")
return None
def _start_transformation(self, jobs: list[TransformationJob], force: bool = False):
"""
Startet die Transformation in einem separaten Thread.
Args:
jobs: Liste der TransformationJobs
force: Wenn True, werden alle Jobs ausgeführt (ignoriert Up-to-Date)
"""
try:
# Prüfe ob bereits ein Thread läuft
if self.transformation_thread and self.transformation_thread.isRunning():
QMessageBox.warning(self, "Warnung", "Es läuft bereits eine Transformation")
return
# Erstelle und konfiguriere Thread
self.transformation_thread = TransformationThread(jobs, force=force, max_workers=app_settings.max_workers)
# Verbinde Signale
self.transformation_thread.job_started.connect(self._on_transformation_job_started)
self.transformation_thread.job_finished.connect(self._on_transformation_job_finished)
self.transformation_thread.job_error.connect(self._on_transformation_job_error)
self.transformation_thread.all_jobs_finished.connect(self._on_all_transformations_finished)
# Zeige Progressbar
self._show_transformation_progress_bar(len(jobs))
# Initialisiere Worker-Pools (lazy loading - nur wenn benötigt)
self._initialize_saxon_worker_pool()
self._initialize_fop_worker_pool()
# Erfasse RAM-Verbrauch vor Transformation
import transform
if transform._saxon_worker_pool:
transform._saxon_worker_pool.capture_ram_before_transform()
if transform._fop_worker_pool:
transform._fop_worker_pool.capture_ram_before_transform()
# Starte Thread
self.transformation_thread.start()
logger.info(f"Transformation von {len(jobs)} Job(s) gestartet (force={force})")
self.statusBar().showMessage(f"Transformation von {len(jobs)} Job(s) gestartet...")
except Exception as e:
logger.error(f"Fehler beim Starten der Transformation: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Starten: {str(e)}")
def _expand_tree_item_parents(self, item: QTreeWidgetItem):
"""
Öffnet alle Eltern-Knoten eines Tree-Items rekursiv.
Args:
item: Das Tree-Item, dessen Eltern geöffnet werden sollen
"""
if item is None:
return
# Rekursiv alle Eltern öffnen
parent = item.parent()
while parent is not None:
parent.setExpanded(True)
parent = parent.parent()
def _on_transformation_job_started(self, xml_file_name: str, xsl_id_str: str):
"""
Signal-Handler: Ein Job wurde gestartet.
Args:
xml_file_name: Name der XML-Datei
xsl_id_str: XSL-ID als String (z.B. "2002_1_128")
"""
logger.info(f"Transformation gestartet: {xml_file_name} (XSL-ID: {xsl_id_str})")
self.statusBar().showMessage(f"Transformiere: {xml_file_name}")
# Progress Bar anzeigen
map_key = f"{xml_file_name}|{xsl_id_str}"
if map_key not in self.xml_item_map and self.xml_item_map:
# Zeige erste Keys zur Diagnose
list(self.xml_item_map.keys())[:3]
logger.info(f"Suche TreeWidget-Item für: '{map_key}'")
logger.info(f"Map hat {len(self.xml_item_map)} Einträge")
tree_item = self.xml_item_map.get(map_key)
if tree_item:
# Öffne alle Eltern-Knoten, damit der Benutzer den Fortschritt sehen kann
self._expand_tree_item_parents(tree_item)
# Scrolle zum Item, damit es sichtbar ist
self.ui.treeWidget.scrollToItem(tree_item)
# Entferne vorhandenes Widget (falls Icon vorhanden)
self.ui.treeWidget.removeItemWidget(tree_item, 2)
# Erstelle und setze Progress Bar
progress_widget, progress_bar = self._create_centered_progress_bar()
self.ui.treeWidget.setItemWidget(tree_item, 2, progress_widget)
logger.debug(f"Progress Bar für {xml_file_name} gesetzt und Eltern-Knoten geöffnet")
else:
logger.warning(f"Kein TreeWidget-Item für {xml_file_name} gefunden")
def _on_transformation_job_finished(self, result: dict):
"""
Signal-Handler: Ein Job wurde abgeschlossen.
Args:
result: Ergebnis-Dictionary
"""
# Aktualisiere Transformation-Progressbar
self._update_transformation_progress()
xml_file = result.get("xml_file", "?")
success = result.get("success", False)
duration = result.get("duration", 0)
if success:
logger.info(f"Transformation erfolgreich: {xml_file} ({duration:.2f}s)")
pdfs_identical = result.get("pdfs_identical", False)
if pdfs_identical:
self.statusBar().showMessage(f"{xml_file} - PDFs identisch ({duration:.2f}s)", 3000)
else:
self.statusBar().showMessage(f"{xml_file} - Unterschiede gefunden ({duration:.2f}s)", 3000)
else:
logger.error(f"Transformation fehlgeschlagen: {xml_file}")
# Zeige Fehlerdetails an
steps = result.get("steps", {})
error_msgs = []
for step_name, step_info in steps.items():
if not step_info.get("success", True):
error_msgs.append(f"{step_name}: {step_info.get('message', 'Unbekannter Fehler')}")
error_text = "\n".join(error_msgs) if error_msgs else "Unbekannter Fehler"
QMessageBox.critical(
self, "Transformation fehlgeschlagen", f"XML-Datei: {xml_file}\n\nFehler:\n{error_text}"
)
# Update Widget in Spalte 2: Entferne Progress Bar, zeige Icon wenn Diff-PDF existiert
xml_file_str = result.get("xml_file", "")
xsl_id = result.get("xsl_id", None)
xsl_id_str = "_".join(str(x) for x in xsl_id) if xsl_id else ""
map_key = f"{xml_file_str}|{xsl_id_str}"
diff_pdf_str = result.get("diff_pdf", None)
tree_item = self.xml_item_map.get(map_key)
if tree_item:
# Entferne Progress Bar
self.ui.treeWidget.removeItemWidget(tree_item, 2)
# Wenn Diff-PDF existiert, zeige Icon
if diff_pdf_str and Path(diff_pdf_str).exists():
xml_file_path = Path(xml_file_str)
icon_widget = self._create_centered_diff_icon(xml_file_path, xsl_id_str)
self.ui.treeWidget.setItemWidget(tree_item, 2, icon_widget)
logger.debug(f"Diff-Icon für {xml_file_str} gesetzt")
else:
logger.debug(f"Keine Diff-PDF für {xml_file_str}, kein Icon gesetzt")
def _on_transformation_job_error(self, xml_file_name: str, xsl_id_str: str, error_message: str):
"""
Signal-Handler: Ein Job ist mit einem Fehler abgebrochen.
Args:
xml_file_name: Name der XML-Datei
xsl_id_str: XSL-ID als String
error_message: Fehlermeldung
"""
# Aktualisiere Transformation-Progressbar
self._update_transformation_progress()
logger.error(f"Transformation-Fehler bei {xml_file_name} (XSL-ID: {xsl_id_str}): {error_message}")
QMessageBox.critical(self, "Fehler", f"Fehler bei {xml_file_name}:\n{error_message}")
# Entferne Progress Bar bei Fehler
map_key = f"{xml_file_name}|{xsl_id_str}"
tree_item = self.xml_item_map.get(map_key)
if tree_item:
self.ui.treeWidget.removeItemWidget(tree_item, 2)
logger.debug(f"Progress Bar für {map_key} entfernt (Fehler)")
def _on_all_transformations_finished(self, successful_count: int, total_count: int, total_duration: float):
"""
Signal-Handler: Alle Jobs wurden abgeschlossen.
Args:
successful_count: Anzahl erfolgreicher Jobs
total_count: Gesamtanzahl der Jobs
total_duration: Gesamtdauer aller Transformationen in Sekunden
"""
logger.info(
f"Alle Transformationen abgeschlossen: {successful_count}/{total_count} erfolgreich ({total_duration:.2f}s)"
)
# Erfasse RAM-Verbrauch nach Transformation
import transform
if transform._saxon_worker_pool:
transform._saxon_worker_pool.capture_ram_after_transform()
# Speichere Metriken vor Shutdown (für späteren Zugriff im Dialog)
self.last_saxon_metrics = deepcopy(transform._saxon_worker_pool.metrics)
logger.debug("Saxon Worker-Pool Metriken gespeichert")
if transform._fop_worker_pool:
transform._fop_worker_pool.capture_ram_after_transform()
# Speichere Metriken vor Shutdown (für späteren Zugriff im Dialog)
self.last_fop_metrics = deepcopy(transform._fop_worker_pool.metrics)
logger.debug("FOP Worker-Pool Metriken gespeichert")
# Beende Worker-Pools (RAM-Optimierung - Pools werden bei nächster Transformation neu gestartet)
self._shutdown_saxon_worker_pool()
self._shutdown_fop_worker_pool()
# Verstecke Transformation-Progressbar
self._hide_transformation_progress_bar()
# Aktualisiere Diff-PDF-Anzahl und Icons in allen Knoten
self._update_all_diff_pdf_counts()
self._update_diff_icons_for_existing_pdfs()
# Formatiere Dauer für Anzeige
duration_str = f"{total_duration:.2f}s"
if successful_count == total_count:
self.statusBar().showMessage(f"✓ Alle {total_count} Transformationen erfolgreich ({duration_str})", 5000)
QMessageBox.information(
self,
"Abgeschlossen",
f"Alle {total_count} Transformationen wurden erfolgreich abgeschlossen.\n\nGesamtdauer: {duration_str}",
)
else:
failed_count = total_count - successful_count
self.statusBar().showMessage(
f"{successful_count}/{total_count} erfolgreich, {failed_count} fehlgeschlagen ({duration_str})", 5000
)
QMessageBox.warning(
self,
"Abgeschlossen mit Fehlern",
f"{successful_count} von {total_count} Transformationen erfolgreich\n{failed_count} fehlgeschlagen\n\nGesamtdauer: {duration_str}",
)
def _transform_all_xml_files(self):
"""
Transformiert ALLE XML-Dateien in allen TreeNodes des TreeWidgets.
Nur Dateien, die nicht up-to-date sind, werden transformiert.
"""
try:
if not self.project or not self.pdf_project:
QMessageBox.warning(self, "Fehler", "Kein Projekt geladen")
return
# Sammle alle XSL/XML-Paare aus allen Root-Nodes
all_jobs = []
root = self.ui.treeWidget.invisibleRootItem()
for i in range(root.childCount()):
root_item = root.child(i)
root_node = root_item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(root_node, TreeNode):
# Sammle alle XSL/XML-Paare rekursiv
xsl_xml_pairs = self._collect_all_xsl_xml_pairs_recursive(root_node, root_item)
# Erstelle TransformationJobs
for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, xsl_file_item)
if job:
all_jobs.append(job)
elif isinstance(root_node, XslFile):
# Direkt XslFile als Root-Element
for xml_file_obj in root_node.xmls:
job = self._create_transformation_job(root_node, xml_file_obj, root_item)
if job:
all_jobs.append(job)
if not all_jobs:
QMessageBox.information(self, "Info", "Keine XML-Dateien zum Transformieren gefunden")
return
# Frage Benutzer um Bestätigung
reply = QMessageBox.question(
self,
"Alle XML-Dateien transformieren",
f"Möchten Sie wirklich alle {len(all_jobs)} XML-Dateien transformieren?\n\n"
f"Nur Dateien mit Änderungen werden verarbeitet.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.Yes,
)
if reply != QMessageBox.StandardButton.Yes:
logger.info("Transformation abgebrochen durch Benutzer")
return
logger.info(f"Starte Transformation für alle {len(all_jobs)} XML-Dateien (nicht-force)")
# Starte Transformation in separatem Thread
self._start_transformation(all_jobs, force=False)
except Exception as e:
logger.error(f"Fehler beim Transformieren aller XML-Dateien: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Starten der Transformation: {str(e)}")
def _transform_all_xml_files_force(self):
"""
Transformiert ALLE XML-Dateien in allen TreeNodes des TreeWidgets (force).
Alle Dateien werden unabhängig vom Änderungsstatus neu transformiert.
"""
try:
if not self.project or not self.pdf_project:
QMessageBox.warning(self, "Fehler", "Kein Projekt geladen")
return
# Sammle alle XSL/XML-Paare aus allen Root-Nodes
all_jobs = []
root = self.ui.treeWidget.invisibleRootItem()
for i in range(root.childCount()):
root_item = root.child(i)
root_node = root_item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(root_node, TreeNode):
# Sammle alle XSL/XML-Paare rekursiv
xsl_xml_pairs = self._collect_all_xsl_xml_pairs_recursive(root_node, root_item)
# Erstelle TransformationJobs
for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, xsl_file_item)
if job:
all_jobs.append(job)
elif isinstance(root_node, XslFile):
# Direkt XslFile als Root-Element
for xml_file_obj in root_node.xmls:
job = self._create_transformation_job(root_node, xml_file_obj, root_item)
if job:
all_jobs.append(job)
if not all_jobs:
QMessageBox.information(self, "Info", "Keine XML-Dateien zum Transformieren gefunden")
return
# Frage Benutzer um Bestätigung (mit Warnung wegen force)
reply = QMessageBox.question(
self,
"Alle XML-Dateien neu transformieren (force)",
f"Möchten Sie wirklich ALLE {len(all_jobs)} XML-Dateien NEU transformieren?\n\n"
f"⚠ WARNUNG: Im Force-Modus werden alle Dateien unabhängig von ihrem Status neu verarbeitet!\n"
f"Dies kann längere Zeit in Anspruch nehmen.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
logger.info("Force-Transformation abgebrochen durch Benutzer")
return
logger.info(f"Starte Force-Transformation für alle {len(all_jobs)} XML-Dateien")
# Starte Transformation in separatem Thread (mit force=True)
self._start_transformation(all_jobs, force=True)
except Exception as e:
logger.error(f"Fehler beim Force-Transformieren aller XML-Dateien: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Starten der Force-Transformation: {str(e)}")