mirror of
https://github.com/alexandrev/xslt-lab.git
synced 2026-09-21 13:03:15 +00:00
+1
This commit is contained in:
@@ -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> </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> 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> </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> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user