diff --git a/src/conf.py b/src/conf.py
index 0a8a913..6bde665 100644
--- a/src/conf.py
+++ b/src/conf.py
@@ -73,6 +73,12 @@ class SSLMode(str, Enum):
VERIFY_FULL = "verify-full"
+class XsltVersion(str, Enum):
+ """XSLT-Version für Saxon-Transformationen."""
+ XSLT_1_0 = "1.0" # JAXP API (nur XSLT 1.0)
+ XSLT_2_0_3_0 = "2.0/3.0" # s9api (XSLT 2.0 und 3.0)
+
+
class PostgreSqlDb(BaseModel):
id: int
name: str
@@ -144,6 +150,7 @@ class AppSettings(BaseSettings):
theme: str | None = None
max_workers: int = 8 # Anzahl paralleler Worker für Transformationen (Standard: 8)
use_saxon_worker_pool: bool = True # SaxonWorkerPool aktivieren (schneller, benötigt JDK)
+ saxon_xslt_version: XsltVersion = XsltVersion.XSLT_2_0_3_0 # XSLT-Version für Saxon (Standard: 2.0/3.0 mit s9api)
use_fop_worker_pool: bool = True # FopWorkerPool aktivieren (schneller, benötigt JDK)
# UI-Zustand
diff --git a/src/saxon_pool_s9api.py b/src/saxon_pool_s9api.py
new file mode 100644
index 0000000..010a064
--- /dev/null
+++ b/src/saxon_pool_s9api.py
@@ -0,0 +1,443 @@
+"""
+Saxon Worker Pool (s9api) - Persistente JVM-Prozesse für XSLT 2.0/3.0 Transformationen.
+
+Diese Variante verwendet die Saxon s9api API anstatt JAXP und ist für XSLT 2.0 und 3.0 geeignet.
+Eliminiert JVM-Startup-Overhead durch Vorinitialisierung von N Worker-Prozessen.
+Jeder Worker läuft als Daemon und verarbeitet mehrere Transformationen nacheinander.
+"""
+
+import logging
+import subprocess
+import threading
+from pathlib import Path
+from queue import Queue
+from typing import Optional
+import tempfile
+
+logger = logging.getLogger(__name__)
+
+# Java-Worker-Code für s9api (wird zur Laufzeit kompiliert)
+SAXON_S9API_WORKER_JAVA = """
+import net.sf.saxon.s9api.*;
+import javax.xml.transform.stream.StreamSource;
+import java.io.*;
+
+public class SaxonS9ApiWorker {
+ public static void main(String[] args) {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
+ String line;
+
+ // Create Processor once and reuse (equivalent to TransformerFactory)
+ Processor processor = new Processor(false);
+
+ System.err.println("SaxonS9ApiWorker started and ready (using s9api for XSLT 2.0/3.0)");
+ System.err.flush();
+
+ try {
+ while ((line = reader.readLine()) != null) {
+ System.err.println("DEBUG: Received line: " + line.substring(0, Math.min(100, line.length())));
+ System.err.flush();
+
+ if ("EXIT".equals(line.trim())) {
+ System.err.println("SaxonS9ApiWorker exiting");
+ break;
+ }
+
+ try {
+ // Parse job
+ System.err.println("DEBUG: Parsing job...");
+ System.err.flush();
+
+ String[] parts = line.split("\\\\t");
+ System.err.println("DEBUG: Parts count: " + parts.length);
+ System.err.flush();
+
+ if (parts.length < 3) {
+ System.out.println("ERROR: Invalid job format");
+ System.out.flush();
+ continue;
+ }
+
+ String sourceXml = parts[0];
+ String xslStylesheet = parts[1];
+ String outputFo = parts[2];
+
+ System.err.println("DEBUG: Compiling stylesheet...");
+ System.err.flush();
+
+ // Compile stylesheet
+ XsltCompiler compiler = processor.newXsltCompiler();
+ XsltExecutable executable = compiler.compile(new StreamSource(new File(xslStylesheet)));
+
+ System.err.println("DEBUG: Creating transformer...");
+ System.err.flush();
+
+ // Create transformer
+ XsltTransformer transformer = executable.load();
+
+ // Set source
+ transformer.setSource(new StreamSource(new File(sourceXml)));
+
+ // Set destination
+ Serializer serializer = processor.newSerializer(new File(outputFo));
+ transformer.setDestination(serializer);
+
+ // Set parameters if present
+ if (parts.length > 3 && !parts[3].isEmpty()) {
+ String[] params = parts[3].split("\\\\|\\\\|\\\\|");
+ for (String param : params) {
+ if (!param.isEmpty() && param.contains("=")) {
+ String[] kv = param.split("=", 2);
+ transformer.setParameter(new QName(kv[0]), new XdmAtomicValue(kv[1]));
+ System.err.println("DEBUG: Set parameter: " + kv[0] + " = " + kv[1]);
+ }
+ }
+ System.err.flush();
+ }
+
+ System.err.println("DEBUG: Running transformation...");
+ System.err.flush();
+
+ // Transform
+ transformer.transform();
+
+ System.err.println("DEBUG: Transformation completed");
+ System.err.flush();
+
+ System.out.println("OK");
+ System.out.flush();
+
+ } catch (SaxonApiException e) {
+ System.err.println("DEBUG: SaxonApiException: " + e.getClass().getName());
+ System.err.flush();
+ e.printStackTrace(System.err);
+
+ String errorMsg = e.getMessage();
+ if (errorMsg == null || errorMsg.isEmpty()) {
+ errorMsg = e.getClass().getSimpleName();
+ }
+ System.out.println("ERROR: " + errorMsg);
+ System.out.flush();
+
+ } catch (Exception e) {
+ System.err.println("DEBUG: Job processing exception: " + e.getClass().getName());
+ System.err.flush();
+ e.printStackTrace(System.err);
+ System.out.println("ERROR: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getName()));
+ System.out.flush();
+ }
+ }
+ } catch (IOException e) {
+ System.err.println("SaxonS9ApiWorker I/O error: " + e.getMessage());
+ e.printStackTrace(System.err);
+ }
+ }
+}
+"""
+
+
+class SaxonWorkerPoolS9Api:
+ """
+ Pool von lang-laufenden JVM-Prozessen für Saxon-Transformationen mit s9api.
+
+ Diese Variante verwendet die Saxon s9api API anstatt JAXP und unterstützt
+ vollständig XSLT 2.0 und 3.0 Transformationen.
+ """
+
+ def __init__(
+ self,
+ num_workers: int,
+ java_vm_path: Path,
+ saxon_jar_path: Path,
+ classpath_cache: dict[Path, str],
+ log_dir: Optional[Path] = None,
+ ):
+ """
+ Initialisiert den Saxon-Worker-Pool mit s9api.
+
+ Args:
+ num_workers: Anzahl der Worker-Prozesse
+ java_vm_path: Pfad zur Java VM Binary
+ saxon_jar_path: Pfad zur Saxon JAR-Datei
+ classpath_cache: Cache für Saxon-Classpaths
+ log_dir: Optionales Verzeichnis für Worker-Logs (Standard: temp_dir/temp)
+ """
+ self.num_workers = num_workers
+ self.java_vm_path = java_vm_path
+ self.saxon_jar_path = saxon_jar_path
+ self.classpath_cache = classpath_cache
+ self.log_dir = log_dir
+
+ # Worker-Prozesse und Queues
+ self.workers: list[subprocess.Popen] = []
+ self.job_queue: Queue = Queue()
+ self.result_queue: Queue = Queue()
+ self.worker_locks: list[threading.Lock] = []
+
+ # Temporäres Verzeichnis für kompilierte Java-Klasse
+ self.temp_dir: Optional[Path] = None
+ self.worker_class_path: Optional[Path] = None
+ self.worker_log_dir: Optional[Path] = None
+
+ # Initialisierung
+ self._compile_worker_class()
+ self._start_workers()
+
+ logger.info(f"SaxonWorkerPoolS9Api initialisiert mit {num_workers} Workern (XSLT 2.0/3.0)")
+
+ def _compile_worker_class(self):
+ """Kompiliert die SaxonS9ApiWorker-Java-Klasse."""
+ try:
+ # Erstelle temporäres Verzeichnis
+ self.temp_dir = Path(tempfile.mkdtemp(prefix="saxon_s9api_worker_"))
+
+ # Schreibe Java-Quellcode
+ java_file = self.temp_dir / "SaxonS9ApiWorker.java"
+ java_file.write_text(SAXON_S9API_WORKER_JAVA, encoding="utf-8")
+
+ # Hole Classpath
+ saxon_dir = self.saxon_jar_path.parent
+ if saxon_dir in self.classpath_cache:
+ classpath = self.classpath_cache[saxon_dir]
+ else:
+ # Fallback: Baue Classpath neu
+ import glob
+ import sys
+
+ all_jars = glob.glob(str(saxon_dir / "*.jar"))
+ lib_dir = saxon_dir / "lib"
+ if lib_dir.exists():
+ all_jars.extend(glob.glob(str(lib_dir / "*.jar")))
+
+ classpath_separator = ";" if sys.platform == "win32" else ":"
+ classpath = classpath_separator.join(all_jars)
+
+ # Kompiliere Java-Klasse
+ javac_cmd = [str(self.java_vm_path).replace("java", "javac"), "-cp", classpath, str(java_file)]
+
+ logger.debug(f"Kompiliere SaxonS9ApiWorker: {' '.join(javac_cmd)}")
+
+ result = subprocess.run(javac_cmd, capture_output=True, text=True, timeout=30)
+
+ if result.returncode != 0:
+ raise RuntimeError(f"Java-Kompilierung fehlgeschlagen: {result.stderr}")
+
+ self.worker_class_path = self.temp_dir
+
+ logger.info(f"SaxonS9ApiWorker erfolgreich kompiliert: {self.temp_dir}")
+
+ except Exception as e:
+ logger.error(f"Fehler beim Kompilieren von SaxonS9ApiWorker: {e}")
+ raise
+
+ def _start_workers(self):
+ """Startet N Worker-Prozesse."""
+ # Hole Classpath
+ saxon_dir = self.saxon_jar_path.parent
+ if saxon_dir in self.classpath_cache:
+ classpath = self.classpath_cache[saxon_dir]
+ else:
+ # Fallback: Baue Classpath neu (sollte nicht nötig sein, aber zur Sicherheit)
+ import glob
+ import sys
+
+ all_jars = glob.glob(str(saxon_dir / "*.jar"))
+ lib_dir = saxon_dir / "lib"
+ if lib_dir.exists():
+ all_jars.extend(glob.glob(str(lib_dir / "*.jar")))
+
+ classpath_separator = ";" if sys.platform == "win32" else ":"
+ classpath = classpath_separator.join(all_jars)
+
+ # Cache für zukünftige Verwendung
+ self.classpath_cache[saxon_dir] = classpath
+ logger.debug(f"Classpath für {saxon_dir} neu erstellt und gecacht")
+
+ # Füge Worker-Classpath hinzu
+ import sys
+
+ classpath_separator = ";" if sys.platform == "win32" else ":"
+ full_classpath = str(self.worker_class_path) + classpath_separator + classpath
+
+ logger.debug(f"S9Api Worker Classpath: {full_classpath[:200]}...")
+
+ # Bestimme Log-Verzeichnis
+ self.worker_log_dir = self.log_dir if self.log_dir else self.temp_dir
+ if self.log_dir:
+ self.worker_log_dir.mkdir(parents=True, exist_ok=True)
+
+ for i in range(self.num_workers):
+ try:
+ # Starte JVM-Prozess mit SaxonS9ApiWorker
+ cmd = [str(self.java_vm_path), "-cp", full_classpath, "SaxonS9ApiWorker"]
+
+ # Öffne stderr-Log-Datei für diesen Worker
+ stderr_log = self.worker_log_dir / f"s9api_worker_{i}_stderr.log"
+ stderr_file = open(stderr_log, "w", encoding="utf-8")
+
+ process = subprocess.Popen(
+ cmd,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=stderr_file, # Redirect stderr to file
+ text=True,
+ bufsize=1, # Line buffered
+ )
+
+ self.workers.append(process)
+ self.worker_locks.append(threading.Lock())
+
+ logger.debug(f"S9Api Worker {i} gestartet (PID: {process.pid}, stderr: {stderr_log})")
+
+ # Warte kurz damit Worker initialisieren kann
+ import time
+
+ time.sleep(0.1)
+
+ # Prüfe ob Worker noch läuft
+ if process.poll() is not None:
+ # Worker ist bereits beendet - Fehler!
+ stderr_file.close()
+ with open(stderr_log, "r") as f:
+ stderr_content = f.read()
+ raise RuntimeError(
+ f"S9Api Worker {i} ist sofort beendet (Exit Code: {process.returncode})\nstderr:\n{stderr_content}"
+ )
+
+ except Exception as e:
+ logger.error(f"Fehler beim Starten von S9Api Worker {i}: {e}")
+ raise
+
+ logger.info(f"{len(self.workers)} Saxon-S9Api-Worker erfolgreich gestartet")
+
+ def transform(
+ self, source_xml: Path, xsl_stylesheet: Path, output_fo: Path, xslt_params: dict[str, str]
+ ) -> tuple[bool, str]:
+ """
+ Führt eine XSLT-Transformation mit einem Worker aus dem Pool aus.
+
+ Args:
+ source_xml: Pfad zur XML-Eingabedatei
+ xsl_stylesheet: Pfad zur XSL-Stylesheet-Datei
+ output_fo: Pfad zur FO-Ausgabedatei
+ xslt_params: Dictionary mit XSLT-Parametern
+
+ Returns:
+ tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
+ """
+ # Finde freien Worker
+ worker_idx = None
+ for i, lock in enumerate(self.worker_locks):
+ if lock.acquire(blocking=False):
+ worker_idx = i
+ break
+
+ if worker_idx is None:
+ # Kein freier Worker, warte auf ersten verfügbaren
+ for i, lock in enumerate(self.worker_locks):
+ lock.acquire()
+ worker_idx = i
+ break
+
+ try:
+ worker = self.workers[worker_idx]
+
+ # Prüfe ob Worker noch läuft
+ if worker.poll() is not None:
+ # Worker ist tot!
+ stderr_log = self.worker_log_dir / f"s9api_worker_{worker_idx}_stderr.log"
+ try:
+ with open(stderr_log, "r") as f:
+ stderr_content = f.read()
+ error_msg = (
+ f"S9Api Worker {worker_idx} ist beendet (Exit: {worker.returncode})\nstderr:\n{stderr_content}"
+ )
+ except Exception:
+ error_msg = f"S9Api Worker {worker_idx} ist beendet (Exit: {worker.returncode})"
+ logger.error(error_msg)
+ return False, error_msg
+
+ # Formatiere Parameter
+ params_str = "|||".join([f"{key}={value}" for key, value in xslt_params.items()])
+
+ # Erstelle Job-String (Tab-separated)
+ job = f"{source_xml}\t{xsl_stylesheet}\t{output_fo}\t{params_str}\n"
+
+ logger.debug(f"Sende Job an S9Api Worker {worker_idx}: {source_xml.name}")
+
+ # Sende Job an Worker
+ worker.stdin.write(job)
+ worker.stdin.flush()
+
+ # Warte auf Antwort
+ response = worker.stdout.readline().strip()
+
+ logger.debug(f"S9Api Worker {worker_idx} Antwort: '{response}'")
+
+ if response == "OK":
+ return True, "Erfolgreich"
+ elif response.startswith("ERROR:"):
+ error_msg = response[6:].strip()
+ return False, f"Saxon-Fehler (s9api): {error_msg}"
+ else:
+ # Leere Antwort bedeutet Worker ist crashed
+ if not response:
+ stderr_log = self.worker_log_dir / f"s9api_worker_{worker_idx}_stderr.log"
+ try:
+ with open(stderr_log, "r") as f:
+ stderr_content = f.read()[-500:] # Letzte 500 Zeichen
+ return False, f"S9Api Worker {worker_idx} crashed (keine Antwort)\nstderr:\n{stderr_content}"
+ except Exception:
+ return False, f"S9Api Worker {worker_idx} crashed (keine Antwort)"
+ return False, f"Unerwartete Antwort: {response}"
+
+ except Exception as e:
+ logger.error(f"Fehler bei S9Api Worker {worker_idx}: {e}")
+ return False, f"Worker-Fehler: {str(e)}"
+
+ finally:
+ # Gebe Worker-Lock frei
+ self.worker_locks[worker_idx].release()
+
+ def shutdown(self):
+ """Beendet alle Worker-Prozesse sauber."""
+ logger.info("Beende Saxon-S9Api-Worker-Pool...")
+
+ for i, worker in enumerate(self.workers):
+ try:
+ # Sende EXIT-Befehl
+ if worker.stdin and not worker.stdin.closed:
+ worker.stdin.write("EXIT\n")
+ worker.stdin.flush()
+
+ # Warte auf Beendigung (max 2 Sekunden)
+ worker.wait(timeout=2)
+ logger.debug(f"S9Api Worker {i} beendet")
+
+ except subprocess.TimeoutExpired:
+ # Force kill falls nötig
+ worker.kill()
+ logger.warning(f"S9Api Worker {i} musste gekillt werden")
+
+ except Exception as e:
+ logger.error(f"Fehler beim Beenden von S9Api Worker {i}: {e}")
+
+ # Lösche temporäres Verzeichnis
+ if self.temp_dir and self.temp_dir.exists():
+ try:
+ import shutil
+
+ shutil.rmtree(self.temp_dir)
+ logger.debug(f"Temporäres Verzeichnis gelöscht: {self.temp_dir}")
+ except Exception as e:
+ logger.warning(f"Konnte temporäres Verzeichnis nicht löschen: {e}")
+
+ logger.info("Saxon-S9Api-Worker-Pool beendet")
+
+ def __enter__(self):
+ """Context manager entry."""
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """Context manager exit."""
+ self.shutdown()
diff --git a/src/transform.py b/src/transform.py
index e1ae286..8c8d29a 100644
--- a/src/transform.py
+++ b/src/transform.py
@@ -15,18 +15,20 @@ from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from saxon_pool import SaxonWorkerPool
+ from saxon_pool_s9api import SaxonWorkerPoolS9Api
from fop_pool import FopWorkerPool
logger = logging.getLogger(__name__)
# Globaler Saxon-Worker-Pool (wird von MainWindow initialisiert)
-_saxon_worker_pool: Optional["SaxonWorkerPool"] = None
+# Kann entweder JAXP oder s9api Variante sein
+_saxon_worker_pool: Optional["SaxonWorkerPool | SaxonWorkerPoolS9Api"] = None
# Globaler FOP-Worker-Pool (wird von MainWindow initialisiert)
_fop_worker_pool: Optional["FopWorkerPool"] = None
-def set_saxon_worker_pool(pool: Optional["SaxonWorkerPool"]):
+def set_saxon_worker_pool(pool: Optional["SaxonWorkerPool | SaxonWorkerPoolS9Api"]):
"""Setzt den globalen Saxon-Worker-Pool."""
global _saxon_worker_pool
_saxon_worker_pool = pool
diff --git a/src/ui/AppSettings.py b/src/ui/AppSettings.py
index beda06b..a149476 100644
--- a/src/ui/AppSettings.py
+++ b/src/ui/AppSettings.py
@@ -10,7 +10,7 @@ from ui.ApacheFopConfigDialog import ApacheFopConfigDialog
from ui.XslDirConfigDialog import XslDirConfigDialog
from ui.PostgreSqlConfigDialog import PostgreSqlConfigDialog
from ui.PdfProject import PdfProjectDlg
-from conf import AppSettings, JavaVm, DiffPdf, SaxonJar, ApacheFop, XslDir, Project, PostgreSqlDb
+from conf import AppSettings, JavaVm, DiffPdf, SaxonJar, ApacheFop, XslDir, Project, PostgreSqlDb, XsltVersion
class AppSettingsDlg(QDialog):
@@ -282,6 +282,12 @@ class AppSettingsDlg(QDialog):
# SaxonWorkerPool-Checkbox setzen
self.ui.checkBoxUseSaxonPool.setChecked(self.settings.use_saxon_worker_pool)
+ # XSLT-Version ComboBox setzen
+ if self.settings.saxon_xslt_version == XsltVersion.XSLT_1_0:
+ self.ui.comboBoxXsltVersion.setCurrentIndex(0)
+ else: # XSLT_2_0_3_0
+ self.ui.comboBoxXsltVersion.setCurrentIndex(1)
+
# FopWorkerPool-Checkbox setzen
self.ui.checkBoxUseFopPool.setChecked(self.settings.use_fop_worker_pool)
@@ -740,6 +746,13 @@ class AppSettingsDlg(QDialog):
# Performance-Einstellungen übernehmen
self.settings.max_workers = self.ui.spinBoxWorkerCount.value()
self.settings.use_saxon_worker_pool = self.ui.checkBoxUseSaxonPool.isChecked()
+
+ # XSLT-Version übernehmen
+ if self.ui.comboBoxXsltVersion.currentIndex() == 0:
+ self.settings.saxon_xslt_version = XsltVersion.XSLT_1_0
+ else:
+ self.settings.saxon_xslt_version = XsltVersion.XSLT_2_0_3_0
+
self.settings.use_fop_worker_pool = self.ui.checkBoxUseFopPool.isChecked()
self.settings.save()
diff --git a/src/ui/AppSettings.ui b/src/ui/AppSettings.ui
index f4d11fe..cec08c7 100644
--- a/src/ui/AppSettings.ui
+++ b/src/ui/AppSettings.ui
@@ -7,7 +7,7 @@
0
0
833
- 446
+ 526
@@ -20,7 +20,7 @@
true
- 0
+ 7
Qt::TextElideMode::ElideRight
@@ -580,6 +580,55 @@ Deaktivieren Sie diese Option, wenn:
+ -
+
+
-
+
+
+ XSLT-Version:
+
+
+
+ -
+
+
+ Wählen Sie die XSLT-Version für Saxon-Transformationen:
+
+XSLT 1.0 (JAXP): Verwendet die JAXP Transformer API
+• Nur für XSLT 1.0 vollständig spezifiziert
+• Kann bei XSLT 2.0/3.0 zu fehlerhaften Ausgaben führen
+
+XSLT 2.0/3.0 (s9api): Verwendet die Saxon s9api
+• Vollständige Unterstützung für XSLT 2.0 und 3.0
+• Empfohlen für moderne XSLT-Stylesheets
+
+
-
+
+ XSLT 1.0 (JAXP)
+
+
+ -
+
+ XSLT 2.0/3.0 (s9api) - Empfohlen
+
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
-
@@ -629,6 +678,19 @@ Deaktivieren Sie diese Option, wenn:
+ -
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
-
diff --git a/src/ui/AppSettings_ui.py b/src/ui/AppSettings_ui.py
index f87de87..72ae429 100644
--- a/src/ui/AppSettings_ui.py
+++ b/src/ui/AppSettings_ui.py
@@ -15,17 +15,17 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
-from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QDialog,
- QDialogButtonBox, QFrame, QGroupBox, QHBoxLayout,
- QHeaderView, QLabel, QPushButton, QSizePolicy,
- QSpacerItem, QSpinBox, QTabWidget, QTableWidget,
- QTableWidgetItem, QVBoxLayout, QWidget)
+from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox,
+ QDialog, QDialogButtonBox, QFrame, QGroupBox,
+ QHBoxLayout, QHeaderView, QLabel, QPushButton,
+ QSizePolicy, QSpacerItem, QSpinBox, QTabWidget,
+ QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget)
class Ui_Dialog(object):
def setupUi(self, Dialog):
if not Dialog.objectName():
Dialog.setObjectName(u"Dialog")
- Dialog.resize(833, 446)
+ Dialog.resize(833, 526)
self.verticalLayout = QVBoxLayout(Dialog)
self.verticalLayout.setObjectName(u"verticalLayout")
self.tabSettings = QTabWidget(Dialog)
@@ -336,6 +336,27 @@ class Ui_Dialog(object):
self.verticalLayout_11.addWidget(self.checkBoxUseSaxonPool)
+ self.horizontalLayoutXsltVersion = QHBoxLayout()
+ self.horizontalLayoutXsltVersion.setObjectName(u"horizontalLayoutXsltVersion")
+ self.labelXsltVersion = QLabel(self.groupBoxSaxonPool)
+ self.labelXsltVersion.setObjectName(u"labelXsltVersion")
+
+ self.horizontalLayoutXsltVersion.addWidget(self.labelXsltVersion)
+
+ self.comboBoxXsltVersion = QComboBox(self.groupBoxSaxonPool)
+ self.comboBoxXsltVersion.addItem("")
+ self.comboBoxXsltVersion.addItem("")
+ self.comboBoxXsltVersion.setObjectName(u"comboBoxXsltVersion")
+
+ self.horizontalLayoutXsltVersion.addWidget(self.comboBoxXsltVersion)
+
+ self.horizontalSpacerXsltVersion = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
+
+ self.horizontalLayoutXsltVersion.addItem(self.horizontalSpacerXsltVersion)
+
+
+ self.verticalLayout_11.addLayout(self.horizontalLayoutXsltVersion)
+
self.labelSaxonPoolInfo = QLabel(self.groupBoxSaxonPool)
self.labelSaxonPoolInfo.setObjectName(u"labelSaxonPoolInfo")
self.labelSaxonPoolInfo.setWordWrap(True)
@@ -363,6 +384,10 @@ class Ui_Dialog(object):
self.verticalLayout_9.addWidget(self.groupBoxFopPool)
+ self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
+
+ self.verticalLayout_9.addItem(self.verticalSpacer)
+
self.label = QLabel(self.tabPerformance)
self.label.setObjectName(u"label")
self.label.setMouseTracking(True)
@@ -392,7 +417,7 @@ class Ui_Dialog(object):
self.buttonBox.accepted.connect(Dialog.accept)
self.buttonBox.rejected.connect(Dialog.reject)
- self.tabSettings.setCurrentIndex(0)
+ self.tabSettings.setCurrentIndex(7)
QMetaObject.connectSlotsByName(Dialog)
@@ -438,6 +463,21 @@ class Ui_Dialog(object):
"\u2022 Sie die Funktion testen m\u00f6chten", None))
#endif // QT_CONFIG(tooltip)
self.checkBoxUseSaxonPool.setText(QCoreApplication.translate("Dialog", u"SaxonWorkerPool verwenden (empfohlen)", None))
+ self.labelXsltVersion.setText(QCoreApplication.translate("Dialog", u"XSLT-Version:", None))
+ self.comboBoxXsltVersion.setItemText(0, QCoreApplication.translate("Dialog", u"XSLT 1.0 (JAXP)", None))
+ self.comboBoxXsltVersion.setItemText(1, QCoreApplication.translate("Dialog", u"XSLT 2.0/3.0 (s9api) - Empfohlen", None))
+
+#if QT_CONFIG(tooltip)
+ self.comboBoxXsltVersion.setToolTip(QCoreApplication.translate("Dialog", u"W\u00e4hlen Sie die XSLT-Version f\u00fcr Saxon-Transformationen:\n"
+"\n"
+"XSLT 1.0 (JAXP): Verwendet die JAXP Transformer API\n"
+"\u2022 Nur f\u00fcr XSLT 1.0 vollst\u00e4ndig spezifiziert\n"
+"\u2022 Kann bei XSLT 2.0/3.0 zu fehlerhaften Ausgaben f\u00fchren\n"
+"\n"
+"XSLT 2.0/3.0 (s9api): Verwendet die Saxon s9api\n"
+"\u2022 Vollst\u00e4ndige Unterst\u00fctzung f\u00fcr XSLT 2.0 und 3.0\n"
+"\u2022 Empfohlen f\u00fcr moderne XSLT-Stylesheets", None))
+#endif // QT_CONFIG(tooltip)
self.labelSaxonPoolInfo.setText(QCoreApplication.translate("Dialog", u"Hinweis: SaxonWorkerPool ben\u00f6tigt ein JDK (Java Development Kit).
Mit JRE allein werden Transformationen im Fallback-Modus ausgef\u00fchrt.", None))
self.groupBoxFopPool.setTitle(QCoreApplication.translate("Dialog", u"FopWorkerPool Einstellungen", None))
#if QT_CONFIG(tooltip)
diff --git a/src/ui/MainWindow.py b/src/ui/MainWindow.py
index b0cf1c1..3506cbf 100644
--- a/src/ui/MainWindow.py
+++ b/src/ui/MainWindow.py
@@ -28,9 +28,10 @@ from ui.PdfProject import PdfProjectDlg
from ui.TreeNodeEditDialog import TreeNodeEditDialog
from ui.XslFileEditDialog import XslFileEditDialog
from ui.XmlToXslAssignDialog import XmlToXslAssignDialog
-from conf import app_settings, Project, ProjectData, TreeNode, XslFile, XmlFile
+from conf import app_settings, Project, ProjectData, TreeNode, XslFile, XmlFile, XsltVersion
from transform import TransformationJob, set_saxon_worker_pool
from saxon_pool import SaxonWorkerPool
+from saxon_pool_s9api import SaxonWorkerPoolS9Api
from pathlib import Path
@@ -727,22 +728,37 @@ class MainWindow(QMainWindow):
logger.warning("Java VM oder Saxon JAR nicht gefunden, Pool nicht initialisiert")
return
- # Erstelle Worker-Pool
+ # Erstelle Worker-Pool (wähle richtige Variante basierend auf XSLT-Version)
num_workers = app_settings.max_workers
log_dir = self.project.project_dir / "temp"
- pool = SaxonWorkerPool(
- num_workers=num_workers,
- java_vm_path=java_vm.path_to_binary_file,
- saxon_jar_path=saxon_jar.path_to_jar_file,
- classpath_cache=TransformationJob._classpath_cache,
- log_dir=log_dir,
- )
+
+ # Wähle die richtige Worker-Pool-Implementierung
+ if app_settings.saxon_xslt_version == XsltVersion.XSLT_1_0:
+ # JAXP-basierte Variante für XSLT 1.0
+ pool = SaxonWorkerPool(
+ num_workers=num_workers,
+ java_vm_path=java_vm.path_to_binary_file,
+ saxon_jar_path=saxon_jar.path_to_jar_file,
+ classpath_cache=TransformationJob._classpath_cache,
+ log_dir=log_dir,
+ )
+ pool_type = "JAXP (XSLT 1.0)"
+ else:
+ # s9api-basierte Variante für XSLT 2.0/3.0
+ pool = SaxonWorkerPoolS9Api(
+ num_workers=num_workers,
+ java_vm_path=java_vm.path_to_binary_file,
+ saxon_jar_path=saxon_jar.path_to_jar_file,
+ classpath_cache=TransformationJob._classpath_cache,
+ log_dir=log_dir,
+ )
+ pool_type = "s9api (XSLT 2.0/3.0)"
# Setze globalen Pool
set_saxon_worker_pool(pool)
logger.info(
- f"Saxon-Worker-Pool initialisiert: {num_workers} Worker "
+ f"Saxon-Worker-Pool initialisiert: {num_workers} Worker mit {pool_type} "
f"(erwartet: {num_workers}x schneller für Saxon-Transformationen)"
)