Tuesday, January 09, 2007

Try/catch in XSLT 2.0

I have experimented a little bit with Saxon B 8.8.0.4j to implement a try/catch instruction in XSLT. The overall idea is simple: an element error-safe (the name is maybe not very nice, but I didn't find anything else now) contains an element try then a suite of 1 or more elements catch. try and catch both contain any sequence constructor. catch has an optional @errors attribute that is a space-separated list of QNames, representing error names:

<error-safe>
  <try>
    <xsl:any-sequence-constructor/>
  </try>
  <catch errors="err:ERRNAME">
    <xsl:any-sequence-constructor/>
  </catch>
  <catch>
    <xsl:any-sequence-constructor/>
  </catch>
<error-safe>

These are just some tests, and there is still a lot of things to specify, but this first implementation is quite usable I think and was incredibly easy to write with Saxon 8.8. Thanks Mike (as well for your help today)! Below are the four Java files: Exslt2InstructionFactory.java, ErrorSafe.java, Try.java and Catch.java.

package org.fgeorges.exslt2.saxon;

import net.sf.saxon.style.ExtensionElementFactory;

public class Exslt2InstructionFactory
    implements ExtensionElementFactory
{
    public Class getExtensionClass(String localname) {
        if ( localname.equals("error-safe") ) {
            return ErrorSafe.class;
        }
        if ( localname.equals("try") ) {
            return Try.class;
        }
        if ( localname.equals("catch") ) {
            return Catch.class;
        }
        return null;
    }
}
package org.fgeorges.exslt2.saxon;

import java.util.Map;
import java.util.HashMap;

import net.sf.saxon.expr.Expression;
import net.sf.saxon.expr.SimpleExpression;
import net.sf.saxon.expr.XPathContext;
import net.sf.saxon.instruct.Executable;
import net.sf.saxon.om.Axis;
import net.sf.saxon.om.AxisIterator;
import net.sf.saxon.om.NodeInfo;
import net.sf.saxon.style.ExtensionInstruction;
import net.sf.saxon.trans.DynamicError;
import net.sf.saxon.trans.XPathException;

public class ErrorSafe
    extends ExtensionInstruction
{
    public Expression compile(Executable exec)
        throws XPathException
    {
        Map<String, Catch> handlers = new HashMap<String, Catch>();
        AxisIterator       kids     = iterateAxis(Axis.CHILD);
        Expression         tryExpr  = null;

        while ( true ) {
            NodeInfo curr = (NodeInfo) kids.next();
            if (curr == null) {
                break;
            }
            else if ( curr instanceof Try ) {
                tryExpr = ((Try) curr).compile(exec);
            }
            else if ( curr instanceof Catch ) {
                Catch    c      = (Catch) curr;
                String[] errors = c.getErrors();
                if ( null == errors ) {
                    handlers.put(null, c);
                }
                else {
                    for ( int i = 0; i < errors.length; ++i ) {
                        handlers.put(errors[i], c);
                    }
                }
                c.compile(exec);
            }
            else {
                // TODO: Report a compilation error!
            }
        }

        return new ErrorSafeExpression(tryExpr, handlers);
    }

    public void prepareAttributes() {
    }
}

class ErrorSafeExpression
    extends SimpleExpression
{
    public ErrorSafeExpression(Expression expr, Map<String, Catch> handlers) {
        myTry            = expr;
        myHandlers       = handlers;
        myDefaultHandler = handlers.get(null);
    }

    public void process(XPathContext ctxt)
        throws XPathException
    {
        try {
            myTry.process(ctxt);
        }
        catch ( DynamicError err ) {
            String error   = '{' + err.getErrorCodeNamespace() + '}' + err.getErrorCodeLocalPart();
            Catch  handler = myHandlers.get(error);
            if ( handler != null ) {
                handler.handle(err, ctxt);
            }
            else if ( null != myDefaultHandler ) {
                myDefaultHandler.handle(err, ctxt);
            }
            else {
                throw err;
            }
        }
    }

    private Expression         myTry            = null;
    private Catch              myDefaultHandler = null;
    private Map<String, Catch> myHandlers       = null;
}
package org.fgeorges.exslt2.saxon;

import net.sf.saxon.expr.Expression;
import net.sf.saxon.instruct.Executable;
import net.sf.saxon.om.Axis;
import net.sf.saxon.style.ExtensionInstruction;
import net.sf.saxon.trans.XPathException;

public class Try
    extends ExtensionInstruction
{
    public Expression compile(Executable exec)
        throws XPathException
    {
        return compileSequenceConstructor(exec, iterateAxis(Axis.CHILD), true);
    }

    public void prepareAttributes() {
    }
}
package org.fgeorges.exslt2.saxon;

import java.util.ArrayList;

import net.sf.saxon.expr.Expression;
import net.sf.saxon.expr.SimpleExpression;
import net.sf.saxon.expr.XPathContext;
import net.sf.saxon.instruct.Executable;
import net.sf.saxon.om.Axis;
import net.sf.saxon.om.AttributeCollection;
import net.sf.saxon.style.ExtensionInstruction;
import net.sf.saxon.trans.DynamicError;
import net.sf.saxon.trans.XPathException;

public class Catch
    extends ExtensionInstruction
{
    public Expression compile(Executable exec)
        throws XPathException
    {
        myAction = compileSequenceConstructor(exec, iterateAxis(Axis.CHILD), true);
        return myAction;
    }

    public void prepareAttributes()
        throws XPathException
    {
        String              errors = null;
        AttributeCollection atts   = getAttributeList();

        for ( int a = 0; a < atts.getLength(); ++a ) {
            if ( "".equals(atts.getPrefix(a)) && "errors".equals(atts.getLocalName(a)) ) {
                errors = atts.getValue(a);
                break;
            }
        }

        if ( errors != null ) {
            String[] qnames = errors.split(" +");
            myErrors = new String[qnames.length];
            for ( int i = qnames.length; i > 0; --i ) {
                String qn     = qnames[i - 1];
                if ( ! "".equals(qn) ) {
                    int    colons = qn.indexOf(':');
                    String prefix = qn.substring(0, colons);
                    String local  = qn.substring(colons + 1);
                    myErrors[i - 1] =
                        '{' + getNamespaceResolver().getURIForPrefix(prefix, false) + '}' + local;
                }
            }
        }
    }

    public String[] getErrors() {
        return myErrors;
    }

    public void handle(DynamicError err, XPathContext ctxt)
        throws XPathException
    {
        myAction.process(ctxt);
    }

    private String[]   myErrors = null;
    private Expression myAction = null;
}

Here is an simple complete example in XSLT using the above classes, error-safe.xsl:

<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
               xmlns:err="http://www.w3.org/2005/xqt-errors"
               xmlns:xs="http://www.w3.org/2001/XMLSchema"
               xmlns:ex="java:/org.fgeorges.exslt2.saxon.Exslt2InstructionFactory"
               xmlns:my="my:error-safe.xsl"
               exclude-result-prefixes="err my xs"
               extension-element-prefixes="ex"
               version="2.0">

  <xsl:output indent="yes"/>

  <xsl:template name="main">
    <root>
      <first>
        <xsl:call-template name="first-test"/>
      </first>
      <second>
        <xsl:call-template name="second-test"/>
      </second>
    </root>
  </xsl:template>

  <xsl:template name="first-test">
    <ex:error-safe>
      <ex:try>
        <xsl:sequence select="1 div 0"/>
        <ok/>
      </ex:try>
      <ex:catch errors="err:FOAR0001">
        <div-by-0/>
      </ex:catch>
    </ex:error-safe>
  </xsl:template>

  <xsl:template name="second-test">
    <ex:error-safe>
      <ex:try>
        <xsl:sequence select="error(xs:QName('my:err0001'), 'Error message!')"/>
        <ok/>
      </ex:try>
      <ex:catch errors="my:err0001 my:err0003">
        <handle-1/>
      </ex:catch>
      <ex:catch errors="my:err0002">
        <handle-2/>
      </ex:catch>
      <ex:catch>
        <handle-all/>
      </ex:catch>
    </ex:error-safe>
  </xsl:template>

</xsl:transform>

Run with the right incantation, for example the following, with the script described here: saxon --b --add-cp=fgeorges.jar -it main error-safe.xsl (if the classes are compiled in fgeorges.jar), this produces:

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <first>
      <div-by-0/>
   </first>
   <second>
      <handle-1/>
   </second>
</root>

Extension functions could now be added to provide the name and message of the currently trapped error within a catch element.

Labels:

2 Comments:

Anonymous Anonymous said...

I don't think much of the over-heavy syntax - drop the try - it's not needed.

Colin Adams

06:57  
Blogger Florent Georges said...

Hi Colin

Glad to see you here :-)

You are right, the ex:try wrapper element is not needed. Actually, in my verry first try, I had no ex:error-safe element, but an ex:try element with a sequence constructor as content, and ex:catch elements at the end of the sequence constructor:

<ex:try>
< ... >
<ex:catch/>
</ex:try>

But I deliberately changed to:

<ex:error-safe>
<ex:try/>
<ex:catch/>
</ex:error-safe>

because I think it is more in the spirit of the XSLT 2.0 PR (think for example about xsl:choose). I don't say it is better, I'm just saying it is more the way XSLT itself is defined.

Thanks for your comments. Regards,

--drkm

11:02  

Post a Comment

<< Home