2026-01-15 18:23:55 +01:00
|
|
|
"""
|
|
|
|
|
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
|
2026-03-14 19:37:05 +01:00
|
|
|
from xsl_dependencies import XslDependencyGraph
|
2026-01-15 18:23:55 +01:00
|
|
|
|
|
|
|
|
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
|
2026-03-14 19:37:05 +01:00
|
|
|
- self.xsl_dependency_graph: XslDependencyGraph für Import/Include-Erkennung
|
2026-01-15 18:23:55 +01:00
|
|
|
|
|
|
|
|
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:
|
2026-01-22 19:37:53 +01:00
|
|
|
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.",
|
|
|
|
|
)
|
2026-01-15 18:23:55 +01:00
|
|
|
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 = []
|
2026-01-22 19:37:53 +01:00
|
|
|
skipped_count = 0
|
2026-01-15 18:23:55 +01:00
|
|
|
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)
|
2026-01-22 19:37:53 +01:00
|
|
|
else:
|
|
|
|
|
skipped_count += 1
|
2026-01-15 18:23:55 +01:00
|
|
|
|
|
|
|
|
if not jobs:
|
2026-01-22 19:37:53 +01:00
|
|
|
QMessageBox.warning(
|
|
|
|
|
self,
|
|
|
|
|
"Fehler",
|
|
|
|
|
f"Konnte keine gültigen Transformations-Jobs erstellen.\n\n"
|
|
|
|
|
f"{skipped_count} XML-Datei(en) wurden übersprungen (fehlende Dateien).",
|
|
|
|
|
)
|
2026-01-15 18:23:55 +01:00
|
|
|
return
|
|
|
|
|
|
2026-01-22 19:37:53 +01:00
|
|
|
# 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.",
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-15 18:23:55 +01:00
|
|
|
# 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 = []
|
2026-01-22 19:37:53 +01:00
|
|
|
skipped_count = 0
|
2026-01-15 18:23:55 +01:00
|
|
|
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)
|
2026-01-22 19:37:53 +01:00
|
|
|
else:
|
|
|
|
|
skipped_count += 1
|
2026-01-15 18:23:55 +01:00
|
|
|
|
|
|
|
|
if not jobs:
|
2026-01-22 19:37:53 +01:00
|
|
|
QMessageBox.warning(
|
|
|
|
|
self,
|
|
|
|
|
"Fehler",
|
|
|
|
|
f"Konnte keine gültigen Transformations-Jobs erstellen.\n\n"
|
|
|
|
|
f"{skipped_count} XML-Datei(en) wurden übersprungen (fehlende Dateien).",
|
|
|
|
|
)
|
2026-01-15 18:23:55 +01:00
|
|
|
return
|
|
|
|
|
|
2026-01-22 19:37:53 +01:00
|
|
|
# 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.",
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-15 18:23:55 +01:00
|
|
|
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)}")
|
|
|
|
|
|
2026-03-09 20:11:58 +01:00
|
|
|
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
|
|
|
|
|
|
2026-01-15 18:23:55 +01:00
|
|
|
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
|
|
|
|
|
|
2026-03-09 20:11:58 +01:00
|
|
|
# Tool-Konfigurationen gecacht auflösen (einmalig pro Projekt)
|
|
|
|
|
java_vm, saxon_jar, apache_fop, diff_pdf, xsl_dir = self._get_cached_project_tools()
|
2026-01-15 18:23:55 +01:00
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
2026-01-22 19:37:53 +01:00
|
|
|
# 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
|
|
|
|
|
|
2026-01-15 18:23:55 +01:00
|
|
|
# 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:
|
2026-04-04 10:57:05 +02:00
|
|
|
# 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)
|
2026-01-15 18:23:55 +01:00
|
|
|
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}")
|
|
|
|
|
|
2026-03-14 19:37:05 +01:00
|
|
|
# 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()
|
|
|
|
|
|
2026-01-15 18:23:55 +01:00
|
|
|
# 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,
|
2026-03-14 19:37:05 +01:00
|
|
|
dependency_graph=self.xsl_dependency_graph,
|
2026-01-15 18:23:55 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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, 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}",
|
|
|
|
|
)
|
2026-01-25 15:23:32 +01:00
|
|
|
|
2026-03-08 20:21:02 +01:00
|
|
|
def _transform_all_xml_files(self, force: bool = False):
|
2026-01-25 15:23:32 +01:00
|
|
|
"""
|
|
|
|
|
Transformiert ALLE XML-Dateien in allen TreeNodes des TreeWidgets.
|
2026-03-08 20:21:02 +01:00
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
force: Wenn True, werden alle Dateien unabhängig vom Änderungsstatus neu transformiert.
|
2026-01-25 15:23:32 +01:00
|
|
|
"""
|
|
|
|
|
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
|
2026-03-08 20:21:02 +01:00
|
|
|
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
|
|
|
|
|
|
2026-01-25 15:23:32 +01:00
|
|
|
reply = QMessageBox.question(
|
|
|
|
|
self,
|
2026-03-08 20:21:02 +01:00
|
|
|
title,
|
|
|
|
|
message,
|
2026-01-25 15:23:32 +01:00
|
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
2026-03-08 20:21:02 +01:00
|
|
|
default_button,
|
2026-01-25 15:23:32 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if reply != QMessageBox.StandardButton.Yes:
|
2026-03-08 20:21:02 +01:00
|
|
|
logger.info(f"{'Force-' if force else ''}Transformation abgebrochen durch Benutzer")
|
2026-01-25 15:23:32 +01:00
|
|
|
return
|
|
|
|
|
|
2026-03-08 20:21:02 +01:00
|
|
|
logger.info(f"Starte {'Force-' if force else ''}Transformation für alle {len(all_jobs)} XML-Dateien")
|
2026-01-25 15:23:32 +01:00
|
|
|
|
|
|
|
|
# Starte Transformation in separatem Thread
|
2026-03-08 20:21:02 +01:00
|
|
|
self._start_transformation(all_jobs, force=force)
|
2026-01-25 15:23:32 +01:00
|
|
|
|
|
|
|
|
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):
|
2026-03-08 20:21:02 +01:00
|
|
|
"""Transformiert ALLE XML-Dateien im Force-Modus."""
|
|
|
|
|
self._transform_all_xml_files(force=True)
|