""" 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 from xsl_dependencies import XslDependencyGraph 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 - self.xsl_dependency_graph: XslDependencyGraph für Import/Include-Erkennung 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 1 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(1, 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 _get_cached_project_tools(self): """ Gibt die aufgelösten Tool-Konfigurationen des aktuellen Projekts zurück (gecacht). Der Cache wird invalidiert, sobald sich die Projekt-ID ändert. """ project_id = self.project.id if self.project else None if getattr(self, "_tool_cache_project_id", None) != project_id: self._tool_cache_project_id = project_id self._tool_cache = ( next((jvm for jvm in app_settings.java_vms if jvm.id == self.project.java_vm_id), None), next((sj for sj in app_settings.saxon_jars if sj.id == self.project.saxon_jar_id), None), next((af for af in app_settings.apache_fops if af.id == self.project.apache_fop_id), None), next((dp for dp in app_settings.diff_pdfs if dp.id == self.project.diff_pdf_id), None), next((xd for xd in app_settings.xsl_dirs if xd.id == self.project.xsl_dir_id), None), ) return self._tool_cache 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 # Tool-Konfigurationen gecacht auflösen (einmalig pro Projekt) java_vm, saxon_jar, apache_fop, diff_pdf, xsl_dir = self._get_cached_project_tools() # 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: # Ohne TreeWidgetItem-Kontext: nur Projekt-Parameter als Basis if hasattr(self, "project") and self.project and self.project.xslt_params: xslt_params.update(self.project.xslt_params) 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}") # Initialisiere XSL-Abhängigkeitsgraph (lazy, einmalig pro Mixin-Instanz) if not hasattr(self, "xsl_dependency_graph") or self.xsl_dependency_graph is None: self.xsl_dependency_graph = XslDependencyGraph() # 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, dependency_graph=self.xsl_dependency_graph, ) 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}" 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, 1) # Erstelle und setze Progress Bar progress_widget, progress_bar = self._create_centered_progress_bar() self.ui.treeWidget.setItemWidget(tree_item, 1, 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 1: 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, 1) # 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, 1, 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, 1) 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, force: bool = False): """ Transformiert ALLE XML-Dateien in allen TreeNodes des TreeWidgets. Args: force: Wenn True, werden alle Dateien 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 if force: title = "Alle XML-Dateien neu transformieren (force)" message = ( 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." ) default_button = QMessageBox.StandardButton.No else: title = "Alle XML-Dateien transformieren" message = ( f"Möchten Sie wirklich alle {len(all_jobs)} XML-Dateien transformieren?\n\n" f"Nur Dateien mit Änderungen werden verarbeitet." ) default_button = QMessageBox.StandardButton.Yes reply = QMessageBox.question( self, title, message, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, default_button, ) if reply != QMessageBox.StandardButton.Yes: logger.info(f"{'Force-' if force else ''}Transformation abgebrochen durch Benutzer") return logger.info(f"Starte {'Force-' if force else ''}Transformation für alle {len(all_jobs)} XML-Dateien") # Starte Transformation in separatem Thread self._start_transformation(all_jobs, force=force) 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 im Force-Modus.""" self._transform_all_xml_files(force=True)