1
0
mirror of https://github.com/alexandrev/xslt-lab.git synced 2026-09-21 13:03:15 +00:00
This commit is contained in:
2025-09-23 16:09:29 +02:00
parent 33048ee9a2
commit 39d6767ac5
6 changed files with 773 additions and 52 deletions
+200
View File
@@ -0,0 +1,200 @@
package com.xsltplayground;
import com.xsltplayground.ext.CustomFunctions;
import net.sf.saxon.s9api.*;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import java.io.File;
import java.nio.file.Files;
import java.util.LinkedHashMap;
import java.util.Map;
public class Runner {
public static void main(String[] args) {
try {
Map<String, String> rawParams = new LinkedHashMap<>();
Map<String, String> fileParams = new LinkedHashMap<>();
String sourcePath = null;
String xslPath = null;
String outPath = null;
boolean trace = false;
boolean cliTrace = false; // -T flag (parsed but not used directly)
String traceOutPath = null;
for (String a : args) {
if (a.startsWith("-s:")) {
sourcePath = a.substring(3);
} else if (a.startsWith("-xsl:")) {
xslPath = a.substring(5);
} else if (a.startsWith("-o:")) {
outPath = a.substring(3);
} else if (a.startsWith("+")) {
// +name=/path/to/file (file-based param, typically XML)
int eq = a.indexOf('=');
if (eq > 1) {
String name = a.substring(1, eq);
String path = a.substring(eq + 1);
fileParams.put(name, path);
}
} else if (a.equals("-trace")) {
trace = true;
} else if (a.equals("-T")) {
cliTrace = true;
} else if (a.startsWith("-traceout:")) {
traceOutPath = a.substring("-traceout:".length());
} else if (a.contains("=")) {
int eq = a.indexOf('=');
String name = a.substring(0, eq);
String val = a.substring(eq + 1);
rawParams.put(name, val);
}
}
if (xslPath == null || outPath == null) {
System.err.println("Missing -xsl: or -o: arguments");
System.exit(2);
}
Processor proc = new Processor(false);
CustomFunctions.registerAll(proc);
XsltCompiler compiler = proc.newXsltCompiler();
XsltExecutable exec;
if (trace) {
// Transform the stylesheet to inject xsl:message after each xsl:variable.
// Messages are written in a block form so backend can capture multiline XML:
// TRACE_VAR_START|name \n [serialized value or string value] \n TRACE_VAR_END
Processor iproc = new Processor(false);
XsltCompiler icomp = iproc.newXsltCompiler();
String instrumenter =
"<xsl:stylesheet version=\"2.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">" +
" <xsl:output method=\"xml\"/>" +
" <xsl:strip-space elements=\"*\"/>" +
" <xsl:template match=\"@*|node()\">" +
" <xsl:copy>" +
" <xsl:apply-templates select=\"@*|node()\"/>" +
" </xsl:copy>" +
" </xsl:template>" +
// Instrument top-level variables
" <xsl:template match=\"/xsl:stylesheet/xsl:variable | /xsl:transform/xsl:variable\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">" +
" <xsl:copy>" +
" <xsl:apply-templates select=\"@*|node()\"/>" +
" </xsl:copy>" +
" <xsl:message>" +
" <xsl:text>TRACE_VAR_START|</xsl:text>" +
" <xsl:value-of select=\"@name\"/>" +
" <xsl:text>&#10;</xsl:text>" +
" <xsl:element name=\"xsl:copy-of\" namespace=\"http://www.w3.org/1999/XSL/Transform\">" +
" <xsl:attribute name=\"select\">$<xsl:value-of select=\"@name\"/></xsl:attribute>" +
" </xsl:element>" +
" <xsl:text>&#10;TRACE_VAR_END</xsl:text>" +
" </xsl:message>" +
" </xsl:template>" +
// Instrument non-top-level variables
" <xsl:template match=\"xsl:variable\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">" +
" <xsl:copy>" +
" <xsl:apply-templates select=\"@*|node()\"/>" +
" </xsl:copy>" +
" <xsl:message>" +
" <xsl:text>TRACE_VAR_START|</xsl:text>" +
" <xsl:value-of select=\"@name\"/>" +
" <xsl:text>&#10;</xsl:text>" +
" <xsl:element name=\"xsl:copy-of\" namespace=\"http://www.w3.org/1999/XSL/Transform\">" +
" <xsl:attribute name=\"select\">$<xsl:value-of select=\"@name\"/></xsl:attribute>" +
" </xsl:element>" +
" <xsl:text>&#10;TRACE_VAR_END</xsl:text>" +
" </xsl:message>" +
" </xsl:template>" +
"</xsl:stylesheet>";
XsltExecutable instExec = icomp.compile(new StreamSource(new java.io.StringReader(instrumenter)));
XsltTransformer instTr = instExec.load();
XdmNode styleDoc = iproc.newDocumentBuilder().build(new StreamSource(new File(xslPath)));
instTr.setInitialContextNode(styleDoc);
java.io.StringWriter sw = new java.io.StringWriter();
Serializer out = iproc.newSerializer(sw);
instTr.setDestination(out);
instTr.transform();
String instrumented = sw.toString();
exec = compiler.compile(new StreamSource(new java.io.StringReader(instrumented)));
} else {
exec = compiler.compile(new StreamSource(new File(xslPath)));
}
XsltTransformer transformer = exec.load();
// If a trace output path is provided, tee System.err to that file so that
// xsl:message (and other diagnostics) also land in the trace file.
java.io.PrintStream originalErr = System.err;
java.io.FileOutputStream traceFos = null;
java.io.PrintStream tracePs = null;
java.io.PrintStream teeErr = null;
if (traceOutPath != null && !traceOutPath.isEmpty()) {
try {
traceFos = new java.io.FileOutputStream(new File(traceOutPath), true);
tracePs = new java.io.PrintStream(traceFos, true, "UTF-8");
final java.io.PrintStream err1 = originalErr;
final java.io.PrintStream err2 = tracePs;
java.io.OutputStream tee = new java.io.OutputStream() {
@Override public void write(int b) throws java.io.IOException { err1.write(b); err2.write(b); }
@Override public void write(byte[] b) throws java.io.IOException { err1.write(b); err2.write(b); }
@Override public void write(byte[] b, int off, int len) throws java.io.IOException { err1.write(b, off, len); err2.write(b, off, len); }
@Override public void flush() throws java.io.IOException { err1.flush(); err2.flush(); }
@Override public void close() throws java.io.IOException { err2.close(); err1.flush(); }
};
teeErr = new java.io.PrintStream(tee, true, "UTF-8");
System.setErr(teeErr);
} catch (Exception e) {
// If we fail to set up the trace file, continue without it
e.printStackTrace(originalErr);
}
}
if (sourcePath != null && !sourcePath.isEmpty()) {
Source src = new StreamSource(new File(sourcePath));
XdmNode doc = proc.newDocumentBuilder().build(src);
transformer.setInitialContextNode(doc);
}
// Parameters as strings
for (Map.Entry<String, String> e : rawParams.entrySet()) {
transformer.setParameter(new QName(e.getKey()), new XdmAtomicValue(e.getValue()));
}
// File parameters (parse as XML if it looks like XML; else pass as string)
for (Map.Entry<String, String> e : fileParams.entrySet()) {
String name = e.getKey();
String path = e.getValue();
String content = new String(Files.readAllBytes(new File(path).toPath()))
.trim();
if (content.startsWith("<")) {
XdmNode node = proc.newDocumentBuilder().build(new StreamSource(new File(path)));
transformer.setParameter(new QName(name), node);
} else {
transformer.setParameter(new QName(name), new XdmAtomicValue(content));
}
}
Serializer ser = proc.newSerializer(new File(outPath));
transformer.setDestination(ser);
try {
transformer.transform();
} finally {
// Restore System.err and close the trace stream if used
if (teeErr != null) {
System.setErr(originalErr);
try { teeErr.flush(); } catch (Exception ignored) {}
}
if (tracePs != null) {
try { tracePs.flush(); } catch (Exception ignored) {}
try { tracePs.close(); } catch (Exception ignored) {}
}
if (traceFos != null) {
try { traceFos.close(); } catch (Exception ignored) {}
}
}
} catch (Exception e) {
e.printStackTrace(System.err);
System.exit(1);
}
}
}
@@ -382,4 +382,233 @@ public class CustomFunctions {
public static boolean xor(boolean a, boolean b) {
return a ^ b;
}
// ---------------------------------------------------------------------
// Saxon HE integration
// ---------------------------------------------------------------------
// Saxon-HE doesn't support reflexive java: calls. To keep XSLT unchanged
// (e.g., xmlns:java="java:com.xsltplayground.ext.CustomFunctions" and
// java:uuid()), register integrated extension functions programmatically.
// Call CustomFunctions.registerAll(processor) once during initialization.
public static void registerAll(net.sf.saxon.s9api.Processor processor) {
final String namespace = "java:" + CustomFunctions.class.getName();
java.lang.reflect.Method[] methods = CustomFunctions.class.getDeclaredMethods();
for (java.lang.reflect.Method m : methods) {
int mods = m.getModifiers();
if (!java.lang.reflect.Modifier.isStatic(mods) || !java.lang.reflect.Modifier.isPublic(mods)) {
continue;
}
if (m.getName().equals("registerAll")) {
continue;
}
String camel = m.getName();
String kebab = camelToKebab(camel);
if (m.isVarArgs()) {
int fixed = m.getParameterCount() - 1;
int maxVarargs = 8;
for (int extra = 0; extra <= maxVarargs; extra++) {
int arity = fixed + (extra == 0 ? 1 : extra);
processor.registerExtensionFunction(new SaxonDynamicFunction(namespace, camel, m, arity));
if (!kebab.equals(camel)) {
processor.registerExtensionFunction(new SaxonDynamicFunction(namespace, kebab, m, arity));
}
}
} else {
int arity = m.getParameterCount();
processor.registerExtensionFunction(new SaxonDynamicFunction(namespace, camel, m, arity));
if (!kebab.equals(camel)) {
processor.registerExtensionFunction(new SaxonDynamicFunction(namespace, kebab, m, arity));
}
}
}
// Back-compat alias: support XSLT calls named "parse-dateTime"
// in addition to the canonical kebab-case "parse-date-time".
try {
java.lang.reflect.Method pdt = CustomFunctions.class.getDeclaredMethod("parseDateTime", String.class, String.class);
processor.registerExtensionFunction(new SaxonDynamicFunction(namespace, "parse-dateTime", pdt, 2));
} catch (NoSuchMethodException ignore) {
}
}
private static String camelToKebab(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isUpperCase(c)) {
if (i > 0) sb.append('-');
sb.append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
private static final class SaxonDynamicFunction implements net.sf.saxon.s9api.ExtensionFunction {
private final String namespace;
private final String localName;
private final java.lang.reflect.Method method;
private final int arity;
SaxonDynamicFunction(String namespace, String localName, java.lang.reflect.Method method, int arity) {
this.namespace = namespace;
this.localName = localName;
this.method = method;
this.arity = arity;
}
@Override
public net.sf.saxon.s9api.QName getName() {
return new net.sf.saxon.s9api.QName(namespace, localName);
}
@Override
public net.sf.saxon.s9api.SequenceType[] getArgumentTypes() {
net.sf.saxon.s9api.SequenceType any = net.sf.saxon.s9api.SequenceType
.makeSequenceType(net.sf.saxon.s9api.ItemType.ANY_ITEM,
net.sf.saxon.s9api.OccurrenceIndicator.ZERO_OR_MORE);
net.sf.saxon.s9api.SequenceType[] arr = new net.sf.saxon.s9api.SequenceType[arity];
for (int i = 0; i < arity; i++) arr[i] = any;
return arr;
}
@Override
public net.sf.saxon.s9api.SequenceType getResultType() {
// Be permissive on result typing as well
return net.sf.saxon.s9api.SequenceType
.makeSequenceType(net.sf.saxon.s9api.ItemType.ANY_ITEM,
net.sf.saxon.s9api.OccurrenceIndicator.ZERO_OR_MORE);
}
@Override
public net.sf.saxon.s9api.XdmValue call(net.sf.saxon.s9api.XdmValue[] arguments) throws net.sf.saxon.s9api.SaxonApiException {
try {
Object[] args = buildJavaArgs(arguments);
Object result = method.invoke(null, args);
return toXdmValue(result);
} catch (java.lang.reflect.InvocationTargetException ite) {
Throwable cause = ite.getTargetException() != null ? ite.getTargetException() : ite;
throw new net.sf.saxon.s9api.SaxonApiException("Error in extension function '" + method.getName() + "': " + cause.getMessage(), cause);
} catch (Exception e) {
throw new net.sf.saxon.s9api.SaxonApiException("Failed to invoke extension function '" + method.getName() + "'", e);
}
}
private Object[] buildJavaArgs(net.sf.saxon.s9api.XdmValue[] arguments) throws Exception {
Class<?>[] params = method.getParameterTypes();
boolean isVarArgs = method.isVarArgs();
if (!isVarArgs) {
Object[] out = new Object[params.length];
for (int i = 0; i < params.length; i++) {
out[i] = fromXdm(arguments[i], params[i]);
}
return out;
} else {
int fixed = params.length - 1;
Object[] out = new Object[params.length];
for (int i = 0; i < fixed; i++) {
out[i] = fromXdm(arguments[i], params[i]);
}
// Collect remaining args into the varargs array
Class<?> comp = params[params.length - 1].getComponentType();
int varCount = Math.max(0, arguments.length - fixed);
Object varArray = java.lang.reflect.Array.newInstance(comp, varCount);
for (int i = 0; i < varCount; i++) {
Object v = fromXdm(arguments[fixed + i], comp);
java.lang.reflect.Array.set(varArray, i, v);
}
out[out.length - 1] = varArray;
return out;
}
}
// Type mapping helpers removed; using ANY_SEQUENCE for resilience across versions.
private static net.sf.saxon.s9api.XdmValue toXdmValue(Object result) {
if (result == null) return net.sf.saxon.s9api.XdmEmptySequence.getInstance();
if (result instanceof net.sf.saxon.s9api.XdmValue) return (net.sf.saxon.s9api.XdmValue) result;
if (result instanceof String) return new net.sf.saxon.s9api.XdmAtomicValue((String) result);
if (result instanceof Integer) return new net.sf.saxon.s9api.XdmAtomicValue(((Integer) result).longValue());
if (result instanceof Long) return new net.sf.saxon.s9api.XdmAtomicValue(((Long) result).longValue());
if (result instanceof Double) return new net.sf.saxon.s9api.XdmAtomicValue(((Double) result).doubleValue());
if (result instanceof Boolean) return new net.sf.saxon.s9api.XdmAtomicValue(((Boolean) result).booleanValue());
if (result instanceof org.w3c.dom.Node) {
// Serialize DOM node to string for portability
try {
javax.xml.transform.TransformerFactory tf = javax.xml.transform.TransformerFactory.newInstance();
javax.xml.transform.Transformer t = tf.newTransformer();
java.io.StringWriter sw = new java.io.StringWriter();
t.transform(new javax.xml.transform.dom.DOMSource((org.w3c.dom.Node) result), new javax.xml.transform.stream.StreamResult(sw));
return new net.sf.saxon.s9api.XdmAtomicValue(sw.toString());
} catch (Exception e) {
return new net.sf.saxon.s9api.XdmAtomicValue("");
}
}
// Generic fallback to string
return new net.sf.saxon.s9api.XdmAtomicValue(String.valueOf(result));
}
private static Object fromXdm(net.sf.saxon.s9api.XdmValue value, Class<?> target) throws Exception {
if (target == String.class) {
if (value == null || value.size() == 0) return "";
for (net.sf.saxon.s9api.XdmItem it : value) { return it.getStringValue(); }
return "";
}
if (target == int.class || target == Integer.class) {
if (value == null || value.size() == 0) return (target == Integer.class ? null : 0);
String s = ""; for (net.sf.saxon.s9api.XdmItem it : value) { s = it.getStringValue(); break; }
return Integer.parseInt(s);
}
if (target == long.class || target == Long.class) {
if (value == null || value.size() == 0) return (target == Long.class ? null : 0L);
String s = ""; for (net.sf.saxon.s9api.XdmItem it : value) { s = it.getStringValue(); break; }
return Long.parseLong(s);
}
if (target == double.class || target == Double.class) {
if (value == null || value.size() == 0) return (target == Double.class ? null : 0d);
String s = ""; for (net.sf.saxon.s9api.XdmItem it : value) { s = it.getStringValue(); break; }
return Double.parseDouble(s);
}
if (target == boolean.class || target == Boolean.class) {
if (value == null || value.size() == 0) return (target == Boolean.class ? null : false);
String s = ""; for (net.sf.saxon.s9api.XdmItem it : value) { s = it.getStringValue(); break; }
return Boolean.parseBoolean(s);
}
if (target == org.w3c.dom.Node.class) {
if (value == null || value.size() == 0) return null;
if (value instanceof net.sf.saxon.s9api.XdmNode) {
javax.xml.transform.Source src = ((net.sf.saxon.s9api.XdmNode) value).asSource();
javax.xml.transform.TransformerFactory tf = javax.xml.transform.TransformerFactory.newInstance();
javax.xml.transform.Transformer tr = tf.newTransformer();
javax.xml.transform.dom.DOMResult res = new javax.xml.transform.dom.DOMResult();
tr.transform(src, res);
return res.getNode();
}
// Fallback: try to parse string as XML
String xml = ""; for (net.sf.saxon.s9api.XdmItem it : value) { xml = it.getStringValue(); break; }
javax.xml.parsers.DocumentBuilderFactory dbf = javax.xml.parsers.DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
return dbf.newDocumentBuilder().parse(new org.xml.sax.InputSource(new java.io.StringReader(xml)));
}
if (target.isArray() && target.getComponentType() == String.class) {
// Expect a sequence of strings in a single argument
if (value == null || value.size() == 0) return new String[0];
java.util.List<String> items = new java.util.ArrayList<>();
for (net.sf.saxon.s9api.XdmItem it : value) {
items.add(it.getStringValue());
}
return items.toArray(new String[0]);
}
// Default to string conversion
if (value == null || value.size() == 0) return null;
for (net.sf.saxon.s9api.XdmItem it : value) { return it.getStringValue(); }
return null;
}
}
}