1
0
mirror of https://github.com/alexandrev/xslt-lab.git synced 2026-09-14 09:03:15 +00:00

Add per-version XSLT engines: XSLTC (1.0), Saxon 9 (2.0), Saxon 12 (3.0)

Each XSLT version now runs against its own true processor rather than
everything going through Saxon 12 in backwards-compatible mode:
- version=1.0 → port 8082, JDK built-in XSLTC (proper XSLT 1.0 validator)
- version=2.0 → port 8083, Saxon HE 9.9.1.8 (true XSLT 2.0 semantics)
- version=3.0 → port 8081, Saxon HE 12.5 (unchanged)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-05-12 09:26:50 +02:00
parent 2bd3e94bb9
commit 49c9281910
5 changed files with 434 additions and 45 deletions
+63 -27
View File
@@ -1,57 +1,93 @@
# Etapa de build
# Build stage
FROM golang:1.23-alpine AS builder
# Instalar dependencias necesarias
RUN apk add --no-cache git openjdk17 curl
# Establecer directorio de trabajo
# ── Go binary ────────────────────────────────────────────────────────────────
WORKDIR /app/src
# Copiar los archivos
COPY src/ .
RUN go mod tidy && go build -o server .
# Compilar el binario estático
RUN go mod tidy && \
go build -o server .
# Build extension functions jar + SaxonDaemon
# ── Java daemons ─────────────────────────────────────────────────────────────
WORKDIR /app/ext
COPY ext/ .
RUN mkdir -p /tmp/saxon && \
curl -L -o /tmp/saxon/saxon-he.jar https://repo1.maven.org/maven2/net/sf/saxon/Saxon-HE/12.5/Saxon-HE-12.5.jar && \
curl -L -o /tmp/saxon/gson.jar https://repo1.maven.org/maven2/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar && \
javac -cp "/tmp/saxon/saxon-he.jar:/tmp/saxon/gson.jar" \
# Download JARs for Saxon 12 (XSLT 3.0)
RUN mkdir -p /tmp/saxon12 && \
curl -L -o /tmp/saxon12/saxon-he.jar \
https://repo1.maven.org/maven2/net/sf/saxon/Saxon-HE/12.5/Saxon-HE-12.5.jar && \
curl -L -o /tmp/saxon12/gson.jar \
https://repo1.maven.org/maven2/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar
# Download JARs for Saxon 9 (XSLT 2.0)
RUN mkdir -p /tmp/saxon9 && \
curl -L -o /tmp/saxon9/saxon-he.jar \
https://repo1.maven.org/maven2/net/sf/saxon/Saxon-HE/9.9.1.8/Saxon-HE-9.9.1.8.jar && \
curl -L -o /tmp/saxon9/gson.jar \
https://repo1.maven.org/maven2/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar
# Compile SaxonDaemon + Runner + CustomFunctions against Saxon 12
RUN mkdir -p /tmp/classes12 && \
javac -cp "/tmp/saxon12/saxon-he.jar:/tmp/saxon12/gson.jar" \
-d /tmp/classes12 \
com/xsltplayground/ext/CustomFunctions.java \
com/xsltplayground/Runner.java \
com/xsltplayground/SaxonDaemon.java && \
jar cf custom-functions.jar com
jar cf /tmp/custom-functions-12.jar -C /tmp/classes12 .
# Compile Saxon2Daemon + Runner + CustomFunctions against Saxon 9
RUN mkdir -p /tmp/classes9 && \
javac -cp "/tmp/saxon9/saxon-he.jar:/tmp/saxon9/gson.jar" \
-d /tmp/classes9 \
com/xsltplayground/ext/CustomFunctions.java \
com/xsltplayground/Runner.java \
com/xsltplayground/Saxon2Daemon.java && \
jar cf /tmp/custom-functions-9.jar -C /tmp/classes9 .
# Compile XalanDaemon — JDK built-in XSLT 1.0 processor, only gson needed
RUN mkdir -p /tmp/classesxalan && \
javac -cp "/tmp/saxon12/gson.jar" \
-d /tmp/classesxalan \
com/xsltplayground/XalanDaemon.java && \
jar cf /tmp/custom-functions-xalan.jar -C /tmp/classesxalan .
WORKDIR /app/src
# Etapa final (runtime)
# Runtime stage
FROM alpine:latest
# Instalar Java y wget (para el health-check en start.sh)
RUN apk add --no-cache openjdk17-jre curl wget
# Crear directorio de trabajo
WORKDIR /app
# Copiar binario, configuración y script de arranque
COPY --from=builder /app/src/server .
COPY app.config .
COPY start.sh .
RUN chmod +x start.sh
# Crear carpeta para Saxon y descargar JARs
RUN mkdir -p /opt/saxon && \
curl -L -o /opt/saxon/saxon-he.jar https://repo1.maven.org/maven2/net/sf/saxon/Saxon-HE/12.5/Saxon-HE-12.5.jar && \
curl -L -o /opt/saxon/xmlresolver.jar https://repo1.maven.org/maven2/org/xmlresolver/xmlresolver/4.5.0/xmlresolver-4.5.0.jar && \
curl -L -o /opt/saxon/gson.jar https://repo1.maven.org/maven2/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar
# Saxon 12 (XSLT 3.0) — port 8081
RUN mkdir -p /opt/saxon12 && \
curl -L -o /opt/saxon12/saxon-he.jar \
https://repo1.maven.org/maven2/net/sf/saxon/Saxon-HE/12.5/Saxon-HE-12.5.jar && \
curl -L -o /opt/saxon12/xmlresolver.jar \
https://repo1.maven.org/maven2/org/xmlresolver/xmlresolver/4.5.0/xmlresolver-4.5.0.jar && \
curl -L -o /opt/saxon12/gson.jar \
https://repo1.maven.org/maven2/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar
COPY --from=builder /tmp/custom-functions-12.jar /opt/saxon12/
# Extension functions + daemon jar
COPY --from=builder /app/ext/custom-functions.jar /opt/saxon/
# Saxon 9 (XSLT 2.0) — port 8083
RUN mkdir -p /opt/saxon9 && \
curl -L -o /opt/saxon9/saxon-he.jar \
https://repo1.maven.org/maven2/net/sf/saxon/Saxon-HE/9.9.1.8/Saxon-HE-9.9.1.8.jar && \
curl -L -o /opt/saxon9/gson.jar \
https://repo1.maven.org/maven2/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar
COPY --from=builder /tmp/custom-functions-9.jar /opt/saxon9/
# XSLTC / JDK built-in (XSLT 1.0) — port 8082
RUN mkdir -p /opt/xalan && \
curl -L -o /opt/xalan/gson.jar \
https://repo1.maven.org/maven2/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar
COPY --from=builder /tmp/custom-functions-xalan.jar /opt/xalan/
# Exponer el puerto del API y arrancar con el script
EXPOSE 8000
CMD ["./start.sh"]
@@ -0,0 +1,183 @@
package com.xsltplayground;
import com.google.gson.*;
import com.sun.net.httpserver.*;
import com.xsltplayground.ext.CustomFunctions;
import net.sf.saxon.lib.FeatureKeys;
import net.sf.saxon.s9api.*;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
/**
* XSLT 2.0 daemon using Saxon HE 9.x.
* Runs on port 8083. Compiled and run against Saxon 9 JARs.
*/
public class Saxon2Daemon {
static final Processor PROCESSOR;
static final Processor TRACE_PROCESSOR;
private static final Gson GSON = new Gson();
static {
PROCESSOR = new Processor(false);
CustomFunctions.registerAll(PROCESSOR);
TRACE_PROCESSOR = new Processor(false);
TRACE_PROCESSOR.setConfigurationProperty(FeatureKeys.OPTIMIZATION_LEVEL, "0");
CustomFunctions.registerAll(TRACE_PROCESSOR);
String warmupXslt =
"<xsl:stylesheet version='2.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>" +
"<xsl:template match='/'><out/></xsl:template></xsl:stylesheet>";
String warmupXml = "<root/>";
for (Processor proc : new Processor[]{PROCESSOR, TRACE_PROCESSOR}) {
try {
XsltCompiler c = proc.newXsltCompiler();
XsltExecutable exec = c.compile(new StreamSource(new StringReader(warmupXslt)));
XsltTransformer t = exec.load();
XdmNode doc = proc.newDocumentBuilder()
.build(new StreamSource(new StringReader(warmupXml)));
t.setInitialContextNode(doc);
Serializer ser = proc.newSerializer(new StringWriter());
t.setDestination(ser);
t.transform();
} catch (Exception e) {
System.err.println("Saxon2Daemon warm-up warning: " + e.getMessage());
}
}
System.out.println("Saxon2Daemon: warm-up complete.");
}
public static void main(String[] args) throws Exception {
int port = 8083;
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 32);
server.createContext("/transform", new TransformHandler());
server.createContext("/health", exchange -> {
byte[] resp = "ok".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/plain");
exchange.sendResponseHeaders(200, resp.length);
try (OutputStream os = exchange.getResponseBody()) { os.write(resp); }
});
int threads = Math.max(2, Runtime.getRuntime().availableProcessors());
server.setExecutor(Executors.newFixedThreadPool(threads));
server.start();
System.out.println("Saxon2Daemon ready on :" + port + " (threads=" + threads + ")");
}
static class TransformHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if (!exchange.getRequestMethod().equalsIgnoreCase("POST")) {
exchange.sendResponseHeaders(405, -1);
exchange.close();
return;
}
String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
JsonObject response = new JsonObject();
int status = 200;
try {
JsonObject req = GSON.fromJson(body, JsonObject.class);
String xslt = req.has("xslt") ? req.get("xslt").getAsString() : "";
String source = req.has("source") ? req.get("source").getAsString() : "";
boolean trace = req.has("trace") && req.get("trace").getAsBoolean();
Map<String, String> params = jsonObjectToMap(req, "parameters");
Map<String, String> fileParams = jsonObjectToMap(req, "fileParameters");
Processor proc = trace ? TRACE_PROCESSOR : PROCESSOR;
ByteArrayOutputStream traceBuf = new ByteArrayOutputStream();
PrintStream traceSink = new PrintStream(traceBuf, true, StandardCharsets.UTF_8);
XsltCompiler compiler = proc.newXsltCompiler();
compiler.setErrorReporter(
new Runner.DeduplicatingErrorReporter(compiler.getErrorReporter()));
boolean instrumentationEnabled = false;
if (trace) {
instrumentationEnabled = Runner.enableCompileWithTracing(compiler);
}
XsltExecutable exec;
try {
exec = compiler.compile(new StreamSource(new StringReader(xslt)));
} catch (SaxonApiException e) {
if (trace && instrumentationEnabled) {
compiler = proc.newXsltCompiler();
compiler.setErrorReporter(
new Runner.DeduplicatingErrorReporter(compiler.getErrorReporter()));
exec = compiler.compile(new StreamSource(new StringReader(xslt)));
} else {
throw e;
}
}
XsltTransformer transformer = exec.load();
if (trace) {
Runner.attachTraceListener(proc, transformer, traceSink);
}
if (source != null && !source.isEmpty()) {
XdmNode doc = proc.newDocumentBuilder()
.build(new StreamSource(new StringReader(source)));
transformer.setInitialContextNode(doc);
}
for (Map.Entry<String, String> e : params.entrySet()) {
transformer.setParameter(new QName(e.getKey()), new XdmAtomicValue(e.getValue()));
}
for (Map.Entry<String, String> e : fileParams.entrySet()) {
String val = e.getValue().trim();
if (val.startsWith("<")) {
XdmNode node = proc.newDocumentBuilder()
.build(new StreamSource(new StringReader(val)));
transformer.setParameter(new QName(e.getKey()), node);
} else {
transformer.setParameter(new QName(e.getKey()), new XdmAtomicValue(val));
}
}
StringWriter resultWriter = new StringWriter();
Serializer ser = proc.newSerializer(resultWriter);
transformer.setDestination(ser);
transformer.transform();
traceSink.flush();
response.addProperty("result", resultWriter.toString());
response.addProperty("traceText", trace ? traceBuf.toString(StandardCharsets.UTF_8) : "");
} catch (SaxonApiException e) {
response.addProperty("error", e.getMessage() != null ? e.getMessage() : e.toString());
status = 400;
} catch (Exception e) {
response.addProperty("error", e.toString());
status = 500;
}
byte[] respBytes = GSON.toJson(response).getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(status, respBytes.length);
try (OutputStream os = exchange.getResponseBody()) { os.write(respBytes); }
}
private Map<String, String> jsonObjectToMap(JsonObject req, String key) {
Map<String, String> result = new LinkedHashMap<>();
if (req.has(key) && req.get(key).isJsonObject()) {
for (Map.Entry<String, JsonElement> e : req.getAsJsonObject(key).entrySet()) {
result.put(e.getKey(), e.getValue().getAsString());
}
}
return result;
}
}
}
@@ -0,0 +1,142 @@
package com.xsltplayground;
import com.google.gson.*;
import com.sun.net.httpserver.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
/**
* XSLT 1.0 daemon using the JDK's built-in XSLT processor (XSLTC / Xalan-J).
* Runs on port 8082. No Saxon dependency.
*/
public class XalanDaemon {
private static final Gson GSON = new Gson();
private static final int PORT = 8082;
static {
// Force JDK built-in XSLTC (not Saxon, which might be on the classpath)
System.setProperty("javax.xml.transform.TransformerFactory",
"com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl");
// Warm up
try {
TransformerFactory.newInstance().newTemplates(new StreamSource(new StringReader(
"<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>" +
"<xsl:template match='/'><out/></xsl:template></xsl:stylesheet>")));
} catch (Exception e) {
System.err.println("XalanDaemon warm-up warning: " + e.getMessage());
}
System.out.println("XalanDaemon: warm-up complete.");
}
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", PORT), 32);
server.createContext("/transform", new TransformHandler());
server.createContext("/health", exchange -> {
byte[] resp = "ok".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/plain");
exchange.sendResponseHeaders(200, resp.length);
try (OutputStream os = exchange.getResponseBody()) { os.write(resp); }
});
int threads = Math.max(2, Runtime.getRuntime().availableProcessors());
server.setExecutor(Executors.newFixedThreadPool(threads));
server.start();
System.out.println("XalanDaemon ready on :" + PORT + " (threads=" + threads + ")");
}
static class TransformHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if (!exchange.getRequestMethod().equalsIgnoreCase("POST")) {
exchange.sendResponseHeaders(405, -1);
exchange.close();
return;
}
String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
JsonObject response = new JsonObject();
int status = 200;
try {
JsonObject req = GSON.fromJson(body, JsonObject.class);
String xslt = req.has("xslt") ? req.get("xslt").getAsString() : "";
String source = req.has("source") ? req.get("source").getAsString() : "";
Map<String, String> params = jsonObjectToMap(req, "parameters");
Map<String, String> fileParams = jsonObjectToMap(req, "fileParameters");
// Collect warnings
StringBuilder warnings = new StringBuilder();
ErrorListener errorListener = new ErrorListener() {
@Override public void warning(TransformerException e) {
warnings.append("Warning: ").append(e.getMessage()).append("\n");
}
@Override public void error(TransformerException e) throws TransformerException { throw e; }
@Override public void fatalError(TransformerException e) throws TransformerException { throw e; }
};
TransformerFactory factory = TransformerFactory.newInstance();
factory.setErrorListener(errorListener);
Templates templates = factory.newTemplates(new StreamSource(new StringReader(xslt)));
Transformer transformer = templates.newTransformer();
transformer.setErrorListener(errorListener);
for (Map.Entry<String, String> e : params.entrySet()) {
transformer.setParameter(e.getKey(), e.getValue());
}
// File params: XSLT 1.0 doesn't support node-typed params natively — pass as string
for (Map.Entry<String, String> e : fileParams.entrySet()) {
transformer.setParameter(e.getKey(), e.getValue());
}
// XSLT 1.0 always requires a source document (no xsl:initial-template)
String src = (source != null && !source.isEmpty()) ? source : "<root/>";
StringWriter resultWriter = new StringWriter();
transformer.transform(new StreamSource(new StringReader(src)), new StreamResult(resultWriter));
response.addProperty("result", resultWriter.toString());
response.addProperty("traceText", warnings.toString());
} catch (TransformerException e) {
response.addProperty("error", formatError(e));
status = 400;
} catch (Exception e) {
response.addProperty("error", e.getMessage() != null ? e.getMessage() : e.toString());
status = 500;
}
byte[] respBytes = GSON.toJson(response).getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(status, respBytes.length);
try (OutputStream os = exchange.getResponseBody()) { os.write(respBytes); }
}
private static String formatError(TransformerException e) {
SourceLocator loc = e.getLocator();
String msg = e.getMessage() != null ? e.getMessage() : e.toString();
if (loc != null && loc.getLineNumber() > 0) {
return "Error at line " + loc.getLineNumber() +
", column " + loc.getColumnNumber() + ": " + msg;
}
return msg;
}
private Map<String, String> jsonObjectToMap(JsonObject req, String key) {
Map<String, String> result = new LinkedHashMap<>();
if (req.has(key) && req.get(key).isJsonObject()) {
for (Map.Entry<String, JsonElement> e : req.getAsJsonObject(key).entrySet()) {
result.put(e.getKey(), e.getValue().getAsString());
}
}
return result;
}
}
}
+10 -2
View File
@@ -218,10 +218,18 @@ func main() {
return
}
daemonPort := "8081" // Saxon 12 — XSLT 3.0 (default)
switch req.Version {
case "1.0":
daemonPort = "8082" // XSLTC (JDK) — true XSLT 1.0
case "2.0":
daemonPort = "8083" // Saxon 9 — true XSLT 2.0
}
start := time.Now()
httpClient := &http.Client{Timeout: 10 * time.Second}
resp, err := httpClient.Post(
"http://127.0.0.1:8081/transform",
"http://127.0.0.1:"+daemonPort+"/transform",
"application/json",
bytes.NewReader(daemonBody),
)
@@ -250,7 +258,7 @@ func main() {
}
if daemonResp.Error != "" {
log.Printf("saxon error after %dms: %s", duration, daemonResp.Error)
log.Printf("transform error after %dms: %s", duration, daemonResp.Error)
c.JSON(http.StatusBadRequest, gin.H{"error": daemonResp.Error})
return
}
+36 -16
View File
@@ -1,27 +1,47 @@
#!/bin/sh
set -e
# Start Saxon daemon in background
# ── Saxon 12 — XSLT 3.0 (port 8081) ─────────────────────────────────────────
java \
-Xms64m -Xmx256m \
-XX:+UseSerialGC \
-cp '/opt/saxon/*' \
-cp '/opt/saxon12/*' \
com.xsltplayground.SaxonDaemon &
DAEMON_PID=$!
# ── XSLTC / JDK — XSLT 1.0 (port 8082) ──────────────────────────────────────
java \
-Xms32m -Xmx128m \
-XX:+UseSerialGC \
-cp '/opt/xalan/*' \
com.xsltplayground.XalanDaemon &
# Wait until the daemon responds on /health (max 30s)
echo "Waiting for SaxonDaemon..."
TRIES=0
until wget -qO- http://127.0.0.1:8081/health > /dev/null 2>&1; do
TRIES=$((TRIES + 1))
if [ $TRIES -ge 60 ]; then
echo "SaxonDaemon did not start in time" >&2
exit 1
fi
sleep 0.5
done
echo "SaxonDaemon ready after ${TRIES} probes."
# ── Saxon 9 — XSLT 2.0 (port 8083) ──────────────────────────────────────────
java \
-Xms32m -Xmx128m \
-XX:+UseSerialGC \
-cp '/opt/saxon9/*' \
com.xsltplayground.Saxon2Daemon &
# Start Go server in foreground
# ── Wait for all three daemons ────────────────────────────────────────────────
wait_for() {
PORT=$1
NAME=$2
echo "Waiting for $NAME on :$PORT..."
TRIES=0
until wget -qO- "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; do
TRIES=$((TRIES + 1))
if [ $TRIES -ge 60 ]; then
echo "$NAME did not start in time" >&2
exit 1
fi
sleep 0.5
done
echo "$NAME ready after ${TRIES} probes."
}
wait_for 8081 SaxonDaemon
wait_for 8082 XalanDaemon
wait_for 8083 Saxon2Daemon
# ── Go server in foreground ───────────────────────────────────────────────────
exec ./server