4
votes

I have several XSL-transformations in a business process with Xalan where one of these steps produces XSL-Stylesheets with static Java extension functions.

I want to replace Xalan with Saxon (not only for performance issues but for the use of XSLT2 too).

I know what to change in XSL to get Saxon to use the functions. And it works very well (with Xalan-optimized XSL I get a speedup of 40 and only half use of RAM).

My problem is that these generated XSL-Stylesheets are "cached"/stored in huge amounts and it would be a mega pain (or impossible) to "refresh" them.

My question is if I can manage to get the XSLs to work with Saxon without changing them or preprocess the XML (modifying SAX-Parser or StringReplacing etc.)?

At the moment I need to change the namespace and the function calls because with Xalan I used the package in the namespace and Saxon (seems to) wants the class.

I have full control over the package and class structure and code of "de.server.macro".

(For testing) I'm using Saxon-9B but finally it would be Saxon-PE or Saxon-EE.

Here are my minimalized examples:

Xalan

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
  xmlns:macro="xalan://de.server.macro">

<xsl:template match="/">
    <output>
        <xsl:text>Hello World!</xsl:text>
        <mymacro>
            <xsl:variable name="foo">5</xsl:variable>
            <xsl:value-of select="macro:data.setVar('testdata', $foo)"/>
            <xsl:value-of select="macro:data.getVar('testdata')"/>
        </mymacro>
    </output>
</xsl:template>

Saxon

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
  xmlns:macro="de.server.macro.data">

<xsl:template match="/">
    <output>
        <xsl:text>Hello World!</xsl:text>
        <mymacro>
            <xsl:variable name="foo">5</xsl:variable>
            <xsl:value-of select="macro:setVar('testdata', $foo)"/>
            <xsl:value-of select="macro:getVar('testdata')"/>
        </mymacro>
    </output>
</xsl:template>

I know about http://xml.apache.org/xalan-j/extensions.html and http://www.saxonica.com/documentation9.5/extensibility/functions/staticmethods.html and several other sources (blog posts and books) about xslt-extensions but based on these informations it looks like the answer is "no".

but perhaps I miss something or some possible mapping mechanisms in Saxon to simulated the right behavior?

2

2 Answers

2
votes

I think there's a subset of formats that will work across Xalan and Saxon. If you use what Xalan calls a "class format" namespace, for example

xmlns:String="xalan://java.util.Hashtable"

then you should be able to call static methods as, for example

String:valueOf($x)

in either product; in the case of Saxon you will need to set the configuration property FeatureKeys.ALLOW_OLD_JAVA_URI_FORMAT

1
votes

I used the following solution for my (specific) problem: I post two variants because there are several differences between Saxon-B (v9.1.0.8) and Saxon-EE (v9.6.0.7).

Adding my special FunctionLibrary before all other existing FunctionLibrarys to ensure the default handling when my special case is not triggered:

Saxon-B

net.sf.saxon.TransformerFactoryImpl saxonFactory = (net.sf.saxon.TransformerFactoryImpl)tFactory;
FunctionLibraryList fll = new FunctionLibraryList();
fll.addFunctionLibrary( new OldXalanFunctionLibrary() );
fll.addFunctionLibrary( saxonFactory.getConfiguration().getExtensionBinder("java") );
saxonFactory.getConfiguration().setExtensionBinder("java", fll);

Saxon-EE

net.sf.saxon.TransformerFactoryImpl saxonFactory = (net.sf.saxon.TransformerFactoryImpl)tFactory;
FunctionLibraryList fll = new FunctionLibraryList(); 
fll.addFunctionLibrary( new OldXalanFunctionLibrary() );
ProfessionalConfiguration conf = (ProfessionalConfiguration)saxonFactory.getConfiguration();
fll.addFunctionLibrary( conf.getExtensionBinder("java") );
conf.setExtensionBinder("java", fll);

My special handling FunctionLibrary looks like:

Saxon-B

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;
import de.server.MacroClasses;
import net.sf.saxon.expr.Expression;
import net.sf.saxon.expr.StaticContext;
import net.sf.saxon.functions.ExtensionFunctionCall;
import net.sf.saxon.functions.FunctionLibrary;
import net.sf.saxon.om.StructuredQName;
import net.sf.saxon.trans.XPathException;


public class OldXalanFunctionLibrary implements FunctionLibrary {

    private static final long serialVersionUID = 2216303509238422532L;

    public OldXalanFunctionLibrary() {
    }

    @Override
    public boolean isAvailable(StructuredQName functionName, int arity) {
        String uri = functionName.getNamespaceURI();
        String local = functionName.getLocalName();
        if (uri.equals("xalan://de.server.macro") && (local.indexOf(".") > 0)) {
            Class c = MacroClasses.class;
            Method[] methods = c.getMethods();
            String searchName = local.substring(local.lastIndexOf(".")+1);  
            for (int i=0; i< methods.length; i++) {
                if (methods[i].getName().equals(searchName)) {
                    if (methods[i].getParameterTypes().length == arity) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    @Override
    public Expression bind(StructuredQName functionName, Expression[] staticArgs, StaticContext env) throws XPathException {
        String uri = functionName.getNamespaceURI();
        String local = functionName.getLocalName();

        if (uri.equals("xalan://de.server.macro") && (local.indexOf(".") > 0)) {
            Class c = MacroClasses.class;
            Method[] methods = c.getMethods();
            Method m = null;
            String searchName = local.substring(local.lastIndexOf(".")+1);  
            for (int i=0; i< methods.length; i++) {
                //String name = methods[i].getName();
                if (methods[i].getName().equals(searchName)) {
                    if (methods[i].getParameterTypes().length == staticArgs.length) {
                        m = methods[i];
                        break;
                    }
                }
            }

            AccessibleObject accessObj = (AccessibleObject)m;

            ExtensionFunctionCall fn;
            try {
                fn = (ExtensionFunctionCall)(c.newInstance());
            } catch (InstantiationException e) {
                throw new IllegalArgumentException(e.getMessage());
            } catch (IllegalAccessException e) {
                throw new IllegalArgumentException(e.getMessage());
            }

            fn.init(functionName, c, accessObj, env.getConfiguration());
            fn.setArguments(staticArgs);
            return fn;
        }
        return null;


    }

    @Override
    public FunctionLibrary copy() {
        OldXalanFunctionLibrary newLibrary = new OldXalanFunctionLibrary();
        return newLibrary;
    }

}

Saxon-EE

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;
import com.saxonica.expr.JavaExtensionFunctionCall;
import de.server.MacroClasses;
import net.sf.saxon.expr.Container;
import net.sf.saxon.expr.Expression;
import net.sf.saxon.expr.StaticContext;
import net.sf.saxon.functions.FunctionLibrary;
import net.sf.saxon.om.FunctionItem;
import net.sf.saxon.trans.SymbolicName;
import net.sf.saxon.trans.XPathException;

public class OldXalanFunctionLibrary implements FunctionLibrary {

    public OldXalanFunctionLibrary() {
    }

    @Override
    public boolean isAvailable(SymbolicName symName) {
        String uri = symName.getComponentName().getNamespaceBinding().getURI();
        String local = symName.getComponentName().getStructuredQName().getLocalPart();
        if (uri.equals("xalan://de.server.macro") && (local.indexOf(".") > 0)) {
            Class<MacroClasses> c = MacroClasses.class;
            Method[] methods = c.getMethods();
            String searchName = local.substring(local.lastIndexOf(".")+1);  
            for (int i=0; i< methods.length; i++) {
                if (methods[i].getName().equals(searchName)) {
                    if (methods[i].getParameterTypes().length == symName.getArity()) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    @Override
    public Expression bind(SymbolicName symName, Expression[] staticArgs, StaticContext context, Container cont) throws XPathException {
        String uri = symName.getComponentName().getNamespaceBinding().getURI();
        String local = symName.getComponentName().getStructuredQName().getLocalPart();

        if (uri.equals("xalan://de.server.macro") && (local.indexOf(".") > 0)) {
            Class<MacroClasses> c = MacroClasses.class;
            Method[] methods = c.getMethods();
            Method m = null;
            String searchName = local.substring(local.lastIndexOf(".")+1);  
            for (int i=0; i< methods.length; i++) {
                //String name = methods[i].getName();
                if (methods[i].getName().equals(searchName)) {
                    if (methods[i].getParameterTypes().length == symName.getArity()) {
                        m = methods[i];
                        break;
                    }
                }
            }

            AccessibleObject accessObj = (AccessibleObject)m;

            JavaExtensionFunctionCall fn;
            try {
                fn = (JavaExtensionFunctionCall)(c.newInstance());
            } catch (InstantiationException e) {
                throw new IllegalArgumentException(e.getMessage());
            } catch (IllegalAccessException e) {
                throw new IllegalArgumentException(e.getMessage());
            }

            fn.init(symName.getComponentName().getStructuredQName(), c, accessObj);
            fn.setArguments(staticArgs);
            return fn;
        }
        return null;
    }

    @Override
    public FunctionLibrary copy() {
        OldXalanFunctionLibrary newLibrary = new OldXalanFunctionLibrary();
        return newLibrary;
    }

    @Override
    public FunctionItem getFunctionItem(SymbolicName symName, StaticContext context, Container cont) throws XPathException {
        return null;
    }

}

My Wrapper-Class for calling my specific methods:

Saxon-B

package de.server;

import net.sf.saxon.functions.ExtensionFunctionCall;

import org.w3c.dom.Document;
import org.w3c.dom.Node;

import de.macro.Format;
import de.macro.TrimLine;
import de.macro.data;
import de.macro.exception.JavaStaticTransformationException;

public class MacroClasses extends ExtensionFunctionCall {

    // data
    public static Document getXmlDoc() {
        return data.getXmlDoc();
    }

    public static void setVar(String name, String value) {
        data.setVar(name, value);
    }

    public static void setVar(String name, String value, String context) {
        data.setVar(name, value, context);
    }

    public static Node getVar(String name) {
        return data.getVar(name);
    }

    public static Node getVar(String name, String context) {
        return data.getVar(name, context);
    }

    public static void flush() {
        data.flush();
    }

    public static String countContextItems() {
        return data.countContextItems();
    }

    public static String getContextsAsString() {
        return data.getContextsAsString();
    }

    public static String countAllItems() {
        return data.countAllItems();
    }

    public static void setOutdatedTime(long aInterval) {
        data.setOutdatedTime(aInterval);
    }

    // Format
     public static String format(String aVarName, String aDataType, String aInputString, String aFormatType, String aPrecision) throws Exception {
         return Format.format(aVarName, aDataType, aInputString, aFormatType, aPrecision);
     }

     public static String format(String aVarName, String aDataType, String aInputString) throws Exception {
         return Format.format(aVarName, aDataType, aInputString);
     }

     public static String format(String aVarName, String aDataType, String aInputString, String aSwitchSign,
             String aFormatType, String aPrecision) throws Exception {
         return Format.format(aVarName, aDataType, aInputString, aSwitchSign, aFormatType, aPrecision);
     }

     public static String convertToEnglish(String aInputString) {
         return Format.convertToEnglish(aInputString);
     }

     public static String convertLastMonthsLast(String aInputString) {
         return Format.convertLastMonthsLast(aInputString);
     }

     public static String getCountSelected(String aVarName, String aValue)
             throws JavaStaticTransformationException {
         return Format.getCountSelected(aVarName, aValue);
     }

     public static String fill(String aVarName, String aInputString, String aFillChar, String aLength,
             String aDirection) throws JavaStaticTransformationException {
         return Format.fill(aVarName, aInputString, aFillChar, aLength, aDirection);
     }

     // TrimLine
     public static String process(String aInput, int aMaxLineLength) throws Exception {
         return TrimLine.process(aInput, aMaxLineLength);
     }
}

Saxon-EE

package de.server;

import org.w3c.dom.Document;
import org.w3c.dom.Node;

import com.saxonica.expr.JavaExtensionFunctionCall;

import de.macro.Format;
import de.macro.TrimLine;
import de.macro.data;
import de.macro.exception.JavaStaticTransformationException;

public class MacroClasses extends JavaExtensionFunctionCall {

    // data
    public static Document getXmlDoc() {
        return data.getXmlDoc();
    }

    private static String convert4setVar(Object value) throws Exception {
        String setValue;

        if (value instanceof String)
        {
            setValue = (String)value;
        }
        else if (value instanceof net.sf.saxon.value.TextFragmentValue)
        {
            net.sf.saxon.value.TextFragmentValue newTextValue = (net.sf.saxon.value.TextFragmentValue)value;
            setValue = newTextValue.getStringValue();
        }
        else if (value instanceof Integer)
        {
            setValue = Integer.toString((Integer)value);
        }
        else if (value instanceof Double)
        {
            setValue = Double.toString((Double)value);
        }
        else if (value instanceof java.math.BigInteger)
        {
            java.math.BigInteger newIntegerValue = (java.math.BigInteger)value;
            setValue = newIntegerValue.toString();
        }
        else
        {
            throw new Exception("Type for data.setVar not implemented: " + value.getClass().getName());
        }
        return setValue;
    }

    public static void setVar(String name, Object value) throws Exception {
        data.setVar(name, convert4setVar(value));
    }

    public static void setVar(String name, String value, String context) throws Exception {
        data.setVar(name, convert4setVar(value), context);
    }

    public static Node getVar(String name) {
        return data.getVar(name);
    }

    public static Node getVar(String name, String context) {
        return data.getVar(name, context);
    }

    public static void flush() {
        data.flush();
    }

    public static String countContextItems() {
        return data.countContextItems();
    }

    public static String getContextsAsString() {
        return data.getContextsAsString();
    }

    public static String countAllItems() {
        return data.countAllItems();
    }

    public static void setOutdatedTime(long aInterval) {
        data.setOutdatedTime(aInterval);
    }

    // Format
     public static String format(String aVarName, String aDataType, String aInputString, String aFormatType, String aPrecision) throws Exception {
         return Format.format(aVarName, aDataType, aInputString, aFormatType, aPrecision);
     }

     public static String format(String aVarName, String aDataType, String aInputString) throws Exception {
         return Format.format(aVarName, aDataType, aInputString);
     }

     public static String format(String aVarName, String aDataType, String aInputString, String aSwitchSign,
             String aFormatType, String aPrecision) throws Exception {
         return Format.format(aVarName, aDataType, aInputString, aSwitchSign, aFormatType, aPrecision);
     }

     public static String convertToEnglish(String aInputString) {
         return Format.convertToEnglish(aInputString);
     }

     public static String convertLastMonthsLast(String aInputString) {
         return Format.convertLastMonthsLast(aInputString);
     }

     public static String getCountSelected(String aVarName, String aValue)
             throws JavaStaticTransformationException {
         return Format.getCountSelected(aVarName, aValue);
     }

     public static String fill(String aVarName, String aInputString, String aFillChar, String aLength,
             String aDirection) throws JavaStaticTransformationException {
         return Format.fill(aVarName, aInputString, aFillChar, aLength, aDirection);
     }

     // TrimLine
     public static String process(String aInput, int aMaxLineLength) throws Exception {
         return TrimLine.process(aInput, aMaxLineLength);
     }

}

Now I don't need to modify any (cached/saved) XSLT-Stylesheets and it only catches the specific cases. So I can use the normal Saxon-Style calls for new generated Stylesheets but I'm downward compatible. And I hope I have no significant performance loss (my tests confirm this estimation till now).