将自定义函数添加到 XPath

 

Prajakta Joshi
Microsoft Corporation

2002 年 10 月 8 日

总结:来宾作者 Prajakta Joshi 讨论如何使用 .NET Framework SDK 中的 System.Xml API 为 XPath 创建自定义函数。 主题包括向 XPath 1.0 添加扩展函数、期待 XPath 2.0 以及使用 XSLT 中的扩展函数。 ) (14 个打印页

扩展函数请求是 XMLXSL 公共新闻组上经常讨论的主题。 编写本文的动机源于观察有关本主题的大量用户帖子。

XPath 1.0 中的 XPath 表达式可以返回四种基本 XPath 数据类型之一:

  • String
  • Number
  • 布尔
  • Node-set

XSLT 变量在表达式语言中引入了其他类型, 即结果树片段

XPath 中 的核心函数库 和一 些特定于 XSLT 的其他函数 提供了用于操作 XPath 数据类型的基本工具。 快速浏览这些函数可发现,这不是满足所有用户需求的详尽集。

XPath 类型 函数
节点集 last () , position () , count () , id () , local-name () , namespace-uri () , name ()
字符串 string () 、concat () 、starts-with () 、contains () 、substring-before () 、substring-after () 、substring () 、string-length () 、normalize-space () 、translate ()
布尔 boolean () , not () , true () , false () , lang ()
Number number () , sum () , floor () , ceiling () , round ()
XSLT 1.0 新增内容 document () , key () , format-number () , current () , unparsed-entity-uri () , generate-id () , system-property ()

需要操作非 XPath 数据类型 ((如日期) )或使用 XPath 数据类型执行强大的/自定义数据操作的 XML 开发人员通常需要其他函数。 本文旨在概述如何使用 Microsoft .NET Framework SDK 中的 System.Xml API 为 XPath 实现自定义函数。

两个字符串的比较

当我编写我的第一个 XPath 查询时,我需要对两个字符串执行不区分大小写的比较。 我的books.xml如下所示:

<bookstore xmlns:my="urn:http//mycompany.com/">
   <book style="young adult">
      <title>Harry Potter and the Goblet of Fire</title>
      <author>
         <first-name>Mary</first-name>
         <last-name>Gradpre</last-name>
      </author>
      <my:price>8.99</my:price>
   </book>
   <book style="young fiction">
      <title>Lord of the Rings</title>
      <author>
         <first-name>J.</first-name>
         <last-name>Tolkien</last-name>
      </author>
      <my:price>22.50</my:price>
   </book>
</bookstore>

我在 XPath 1.0 字符串函数中查找了类似于 String.Compare () 的函数。 可在解决方案中使用的最接近的函数是 translate () 。 我在System.Xml中使用 XPath 类解决了以下代码段的问题 。XPath 命名空间:

using System;
using System.Xml;
using System.Xml.XPath;
public class sample
{
   public static void Main(string []args)
   {
      // Load source XML into XPathDocument.
      XPathDocument xd = new XPathDocument(args[0], XmlSpace.Preserve);
      
      // Create XPathNavigator from XPathDocument.
      XPathNavigator nav = xd.CreateNavigator();

      XPathExpression expr;
      expr = 
nav.Compile("/bookstore/book/title[translate(.,'abcdefghijklmnopqrstuvwxyz',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ') = 'HARRY POTTER AND THE GOBLET OF FIRE']");

      XPathNodeIterator iterator = nav.Select(expr);
      // Iterate through selected nodes.
      while (iterator.MoveNext())
      {
         Console.WriteLine("Book title: {0}", iterator.Current.Value);
      }
   }
}

此代码生成以下输出:

Book title: Harry Potter and the Goblet of Fire

解决方案有点长,但我解决了我的问题。 几天后编写 XPath 查询时,我想在正则表达式匹配项定义的位置将字符串拆分为子字符串数组。

<?xml version="1.0" encoding="utf-8" ?> 
<Books>
   <Book>
      <Title>Stephen Hawking's Universe: The Cosmos Explained</Title>
      <Authors>David Filkin, Stephen Hawking</Authors>
   </Book>
   <Book>
      <Title>Writing Secure Code</Title>
      <Authors>Michael Howard, David LeBlanc</Authors>
   </Book>
</Books>

我想从以逗号分隔的元素列表中<Authors>找出第 n 个作者的姓名 ,这是一个有点复杂的字符串操作问题。 我认为这是了解如何为 XPath 实现自定义扩展函数的好时机。

在 XPath 中实现扩展函数

我发现 XPath 1.0 建议没有定义扩展函数的机制。 但是,好消息是,我可以向 XPath 处理器提供自定义执行上下文,以解析 XPath 表达式中的用户定义的函数和变量。

下图说明了 XsltContext 类、IXsltContextFunction 接口以及解决方案中System.Xml.Xsl 命名空间中的 IXsltContextVariable 接口的作用。

图 1. XsltContext 的角色

解决方案中的关键步骤

  1. XPathExpression.SetContext (CustomContext) XPath 处理器 (XPathNavigator) 提供自定义上下文,用于解析用户定义的函数和变量。 从抽象类 XsltContext 派生的 CustomContext 实现了两个关键方法:ResolveFunction () ResolveVariable ()
  2. 当 XPathNavigator 在 XPathExpression 中看到用户定义的函数时,它会在自定义上下文中调用 ResolveFunction () 方法。 ResolveFunction () 返回相应的自定义函数,该函数派生自 IXsltContextFunction
  3. XPathNavigator 在运行时使用提供的参数对此自定义函数调用 Invoke () 方法。
  4. XPathNavigatorXPathExpression 中检测到用户定义的变量时,它会在自定义上下文中调用 ResolveVariable () 方法。 ResolveVariable () 返回从 IXsltContextVariable 派生的相应自定义变量。
  5. XPathNavigator 在运行时对此自定义变量调用 Evaluate () 方法。

我决定编写一个自定义 XPath 函数 Split () , 该函数的行为类似于 .NET SDK 中的 RegEx.Split () 方法。 以下是我把所有碎片放在一起的方式。

XsltContext 类的角色

首先,我实现了自定义 XsltContext ,以便为 XPath 处理器提供有关解析用户定义函数的必要信息。 ResolveFunctionResolveVariableXsltContext 类的两个关键方法,必须重写这些方法才能实现自定义解析。 这些函数在运行时由 XPathNavigator 调用,以解析对 XPath 查询表达式中用户定义的函数和变量的引用。

请注意,我在 CustomContext 类中封装了 XsltArgumentList 对象。 此对象是 XPath 表达式中变量的容器。

public class CustomContext : XsltContext
{
   // XsltArgumentList to store my user defined variables
   private XsltArgumentList m_ArgList;

   // Constructors 
   public CustomContext()
   {}

   public CustomContext(NameTable nt) : base(nt)
   {
   }

   public CustomContext(NameTable nt, XsltArgumentList argList) : base(nt)
   {
      m_ArgList = argList;
   }

   // Returns the XsltArgumentList that contains custom variable definitions.
   public XsltArgumentList ArgList
   {
      get
      { 
         return m_ArgList;
      }
   }

   // Function to resolve references to my custom functions.
   public override IXsltContextFunction ResolveFunction(string prefix, 
string name, XPathResultType[] ArgTypes)
   {
      XPathRegExExtensionFunction func = null;

      // Create an instance of appropriate extension function class.
      switch (name)
      {
         case "Split":
            // Usage 
            // myFunctions:Split(string source, string Regex_pattern, int n) returns string
            func = new XPathRegExExtensionFunction("Split", 3, 3, new 
XPathResultType[] {XPathResultType.String, XPathResultType.String, 
XPathResultType.Number}, XPathResultType.String);
            break;
         case "Replace":
            // Usage
            // myFunctions:Replace(string source, string Regex_pattern, 
string replacement_string) returns string
            func = new XPathRegExExtensionFunction("Replace", 3, 3, new 
XPathResultType[] {XPathResultType.String, XPathResultType.String, 
XPathResultType.String}, XPathResultType.String);
            break;
      }
      return func;
   }

   // Function to resolve references to my custom variables.
   public override IXsltContextVariable ResolveVariable(string prefix, string name)
   {
      // Create an instance of an XPathExtensionVariable.
      XPathExtensionVariable Var;
      Var = new XPathExtensionVariable(name);

      return Var;
   }

   public override int CompareDocument(string baseUri, string nextbaseUri)
   {
      return 0;
   }

   public override bool PreserveWhitespace(XPathNavigator node)
   {
      return true;
   }

   public override bool Whitespace
   {
      get
      {
         return true;
      }
   }
}

IXsltContextFunction 接口的角色

下一步是实现 IXsltContextFunction 接口以供 CustomContext 类使用。 此对象上的 Invoke () 方法在运行时由 XPathNavigator 使用提供的参数调用。

public class XPathRegExExtensionFunction : IXsltContextFunction
{
   private XPathResultType[] m_ArgTypes;
   private XPathResultType m_ReturnType;
   private string m_FunctionName;
   private int m_MinArgs;
   private int m_MaxArgs;

   // Methods to access the private fields.
   public int Minargs
   {
      get
      {
         return m_MinArgs;
      }
   }

   public int Maxargs
   {
      get
      {
         return m_MaxArgs;
      }
   }

   public XPathResultType[] ArgTypes
   {
      get
      {
         return m_ArgTypes;
      }
   }

   public XPathResultType ReturnType
   {
      get
      {
         return m_ReturnType;
      }
   }

   // Constructor
   public XPathRegExExtensionFunction(string name, int minArgs, int 
maxArgs, XPathResultType[] argTypes, XPathResultType returnType)
   {
      m_FunctionName = name;
      m_MinArgs = minArgs;
      m_MaxArgs = maxArgs;
      m_ArgTypes = argTypes;
      m_ReturnType = returnType;
   }

   // This method is invoked at run time to execute the user defined function.
   public object Invoke(XsltContext xsltContext, object[] args, 
XPathNavigator docContext)
   {
      Regex r;
      string str = null;

      // The two custom XPath extension functions
      switch (m_FunctionName)
      {
         case "Split":
                r = new Regex(args[1].ToString());
            string [] s1 = r.Split(args[0].ToString());
            int n = Convert.ToInt32(args[2]);
            if (s1.Length < n)
               str = "";
            else
               str = s1[n - 1];
            break;
         case "Replace":
            r = new Regex(args[1].ToString());
            string s2 = r.Replace(args[0].ToString(), args[2].ToString());
            str = s2;
            break;
      }
      return (object) str;
   } 
}

IXsltContextVariable 接口的角色

XPath 表达式可以包含用户定义的变量引用,例如:

XPathExpression expr1 = nav.Compile("myFunctions:Split(string(.), ',', $var)");

若要启用变量 $var的运行时解析,我需要实现 IXsltContextVariable 接口并重写 Evaluate () 方法,该方法在运行时由 XPathNavigator 调用。

public class XPathExtensionVariable : IXsltContextVariable
{
   // The name of the user-defined variable to resolve
   private string m_VarName;

   public XPathExtensionVariable(string VarName)
   {
      m_VarName = VarName;
   }

   // This method is invoked at run time to find the value of the user defined variable.
   public object Evaluate(XsltContext xsltContext)
   {
      XsltArgumentList vars = ((CustomContext) xsltContext).ArgList;
      return vars.GetParam(m_VarName, null);
   }

   public bool IsLocal
   {
      get
      {
         return false;
      }
   }

   public bool IsParam
   {
      get
      {
         return false;
      }
   }

   public XPathResultType VariableType
   {
      get
      {
         return XPathResultType.Any;
      }
   }
}

总结

最后,我使用了 XPathExpression.SetContext () 方法,向其传递了自定义上下文对象。 图 (1 汇总了解决方案中的所有步骤。 请注意, XsltContext 类继承自 XmlNamespaceManager,并使用 AddNamespace () 向集合添加了自定义命名空间。

using System;
using System.Xml;
using System.Xml.Xsl;
using System.Xml.XPath;
using System.Text.RegularExpressions;

public class sample
{
   public static void Main(string []argc)
   {
      // Load source XML into XPathDocument.
      XPathDocument doc = new XPathDocument("books.xml", XmlSpace.Preserve);

      // Create XPathNavigator from XPathDocument.
      XPathNavigator nav = doc.CreateNavigator();

      // Add user-defined variable to the XsltArgumentList.
      XsltArgumentList varList = new XsltArgumentList();
      varList.AddParam("var", "", 2);

      // Compile the XPathExpression.
      // Note that the compilation step only checks the query expression
      // for correct XPath syntax.
      // User defined functions and variables are not resolved.
      XPathExpression expr1 = nav.Compile("myFunctions:Split(string(.), ',', $var)");
   
      // Create an instance of a custom XsltContext object.
      CustomContext cntxt = new CustomContext(new NameTable(), varList);

      // Add a namespace definition for myFunctions prefix.
      cntxt.AddNamespace("myFunctions", "http://myXPathExtensionFunctions");

      // Associate the custom context with the XPathExpression object.
      expr1.SetContext(cntxt);

      XPathNodeIterator it = nav.Select("/Books/Book/Authors");
      while (it.MoveNext())
      {
         Console.WriteLine("Authors: {0}", it.Current.Value);
         Console.WriteLine("Second author: {0}", it.Current.Evaluate(expr1));
        }
   }
}

运行此代码生成了以下输出:

Authors: David Filkin, Stephen Hawking
Second author:  Stephen Hawking
Authors: Michael Howard, David LeBlanc
Second author:  David LeBlanc

扩展函数的其他用途

发现使用扩展函数的机制后,我在使用 XPath 编程时,在许多其他数据操作情况下都使用了扩展函数。 其他一些情况包括:

  • 操作日期: 将两个字符串作为日期进行比较是一个常见操作。 为此,我使用了 System.DateTime.Compare () 方法。
  • 操作数字:Math 类为常见数学运算提供了一组详尽的方法。 我使用了 Abs () 方法作为自定义函数。
  • 操作字符串:String 类为常见字符串操作提供了一组详尽的方法。 我发现 ToUpper () ToLower () 方法作为自定义函数非常有用。

这个列表确实是无穷无尽的。 根据你的特定情况,可以在自定义函数中使用任何 .NET 类。

未来方向:XPath 2.0

随着 W3C XML 架构数据类型与 XPath 2.0 的集成更加紧密, Xquery 1.0 和 XPath 2.0 函数和运算符 将为 XML 开发人员提供比 XPath 1.0 中当前更丰富的函数库。 这并不能完全消除 XPath 2.0 中对用户定义的函数的需求。 扩展机制对于 XML 的强大且可扩展的查询语言来说不可或缺。

XSLT 中的扩展函数

XSLT 1.0XslTransform 实现使用命名空间 urn:schemas-microsoft-com:xslt 作为扩展命名空间。 它内置了对 <msxsl:node-set> 扩展函数和 <msxsl:script> 扩展元素的支持。

可通过两种方法在 XSLT 中实现用户定义的函数:

  1. 样式表脚本
  2. 将扩展对象添加到 XsltArgumentList

深入阅读

Acknowledgement

我要感谢达雷·奥巴桑乔对本文的帮助。

Prajakta Joshi 是 Microsoft WebData XML 团队的成员,致力于System.Xml中的 XSLT 实现。.NET Framework的 Xsl 命名空间。

可随时在 GotDotNet 上的 Extreme XML 消息板上 发布有关本文的任何问题或评论。