diff --git a/backend/Dockerfile b/backend/Dockerfile index 8e9be3f4..d5661be1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -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"] diff --git a/backend/ext/com/xsltplayground/Saxon2Daemon.java b/backend/ext/com/xsltplayground/Saxon2Daemon.java new file mode 100644 index 00000000..e1821d53 --- /dev/null +++ b/backend/ext/com/xsltplayground/Saxon2Daemon.java @@ -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 = + "" + + ""; + String warmupXml = ""; + 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 params = jsonObjectToMap(req, "parameters"); + Map 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 e : params.entrySet()) { + transformer.setParameter(new QName(e.getKey()), new XdmAtomicValue(e.getValue())); + } + + for (Map.Entry 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 jsonObjectToMap(JsonObject req, String key) { + Map result = new LinkedHashMap<>(); + if (req.has(key) && req.get(key).isJsonObject()) { + for (Map.Entry e : req.getAsJsonObject(key).entrySet()) { + result.put(e.getKey(), e.getValue().getAsString()); + } + } + return result; + } + } +} diff --git a/backend/ext/com/xsltplayground/XalanDaemon.java b/backend/ext/com/xsltplayground/XalanDaemon.java new file mode 100644 index 00000000..8ed1a23a --- /dev/null +++ b/backend/ext/com/xsltplayground/XalanDaemon.java @@ -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( + "" + + ""))); + } 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 params = jsonObjectToMap(req, "parameters"); + Map 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 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 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 : ""; + 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 jsonObjectToMap(JsonObject req, String key) { + Map result = new LinkedHashMap<>(); + if (req.has(key) && req.get(key).isJsonObject()) { + for (Map.Entry e : req.getAsJsonObject(key).entrySet()) { + result.put(e.getKey(), e.getValue().getAsString()); + } + } + return result; + } + } +} diff --git a/backend/src/main.go b/backend/src/main.go index 5bd41656..2ea0cdc6 100644 --- a/backend/src/main.go +++ b/backend/src/main.go @@ -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 } diff --git a/backend/start.sh b/backend/start.sh index 57adfe9f..15d71262 100644 --- a/backend/start.sh +++ b/backend/start.sh @@ -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