关闭

使用 XSLT 和 Java 扩展验证 XML 文档中的复杂约束

1358人阅读 评论(0) 收藏 举报

Peter Heneback (peter.heneback@uk.ibm.com), 顾问, IBM

2006 年 6 月 05 日

 

基于语法的验证语言,例如 XML Schema 和 DTD,可以很好地确保 XML 文档遵从定义良好的消息结构。这样可确保接收 XML 消息的应用程序能够正确地处理接收到的 XML 消息,但是不能保证包含在消息中的数据是有效的。例如,基于语法的验证语言的这些局限性意味着必须使用不同的方法来验证变量和外部数据集上的同现约束(co-occurrence constraints)和其他约束。

在很多情况下,用 XML Schema 或 DTD 不能实现的验证逻辑被并入到应用程序代码中。这种解决方案比较容易实现,但是得到的实现常常不够灵活。本文首先调查研究 Schematron 这种解决上述问题的方法,接着指出这种方法的一些缺点。然后,本文探索一种使用 W3C 标准组件并结合 Java 扩展和开放源代码 XSLT 处理器的方法。

 

常被推荐的用于补充 XML Schema 的方案是使用 Schematron。Schematron 是基于规则的语言,使用 XPath 来表达关于 XML 实例文档中的内容的断言。这是通过用一个基本样式表转换 Schematron 模式,将该模式转换成一个 XSLT 样式表来完成的。转换的结果是一个 XML 格式的报告,其中包含关于哪些断言失败的详细信息,并附有注释。然而,Schematron 不大适用于定义结构,因为用 Schematron 定义结构很快就会变得吃力起来。因此,仍然需要首先用 XML Schema 对文档进行验证,但是 XML Schema 和 Schematron 一起可以满足大多数应用程序的验证需求。实际上,在 XML Schema 语法中,由于 Schematron 断言处在不同的名称空间里,两者通常可以包括在同一个文件中,并且分别作为文档验证过程的一部分。

图 1 描绘了一个很常见的应用场景的逻辑处理步骤。XSLT 首先验证传入的 XML 实例文档,然后在应用程序本身对它进行处理或者它被发送到外部应用程序之前对它进行转换。从图中很容易看出,当结合使用 XML Schema 和 Schematron 第一次执行验证,然后使用一个 XSLT 样式表转换验证过的文档时,这一操作变得复杂起来。可以通过将模式分离,提前转换 Schematron 模式来缩短该过程,但是这仍然需要运行两次 XSLT 处理器,并且需要解析和检查由 Schematron 转换产生的验证报告。



使用 Schematron 验证 XML 数据

 

  • 在使用 Schematron 模式验证 XML 文档之前,至少需要使用基本样式表对该模式进行一次转换。如果 Schematron 语法包括在 XML Schema 中,那么还需要进一步的转换。
  • Schematron 目前不能直接将报告返回给应用程序。在进行验证之后,需要手动地或自动地对报告加以处理。

 

在本节中,我将介绍 XSLT 加 Java 扩展这种补充基于语法的 XML Schema 验证的方法,这是一种基于规则的方法。我将带您亲历一组简单的示例。至于如何使用 XML Schema 实现 XML 验证器(validator),本文不作介绍,因为在 developerWorks 上的很多文章和教程上对此作了大量的阐述(参见 参考资料)。

图 2 展示了与 图 1 相同的场景,不过这次使用 XSLT 加 Java 扩展在一步内执行验证和转换。这一次不再产生一个报告,然后又必须单独处理该报告,而是直接将验证失败以一个 ValidationException 对象的形式返回给应用程序,其中 ValidationException 类扩展了 Exception 类。除了减少文档经过 XSLT 处理器的次数以外,在第一次验证失败时就会停止转换,从而防止对无效数据的不必要的处理。



使用 XSLT 和 Java 扩展进行验证

 

为了演示如何使用 XSLT 和 Java 进行验证,我使用一个简单的登记表的转换作为示例输入,该登记表包含雇员详细信息,例如姓名、电话号码、称呼和性别。稍后可以看到,称呼和性别信息在同现约束的中央。清单 1 包含 XML 输入文档的一部分。



<input:staff xmlns:input="cross-field-validation-namespace">

...

  <input:employee id="1234A">
    <input:first_name>Julia</input:first_name>
    <input:last_name>Smith</input:last_name>
    <input:title>Mrs</input:title>
    <input:gender>F</input:gender>
    <input:telephones>
      <input:mobile preferred="false">0770-555 1231</input:mobile>
      <input:mobile preferred="true">0771-555 1232</input:mobile>
      <input:home preferred="false">0207-555 1233</input:home>
    </input:telephones>
  </input:employee>

...

</input:staff>

这个例子中的转换简单地将雇员登记表中的姓和名合并在一起。该转换还提取那些 preferred 属性被设置为 true 的电话号码,如 清单 2 所示。



<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="1.0" xmlns:xalan="http://xml.apache.org/xslt"
  xmlns:input="cross-field-validation-namespace">

  <xsl:template match="/input:staff">
    <phones>
        <xsl:apply-templates select="input:employee" />
    </phones>
  </xsl:template>

  <xsl:template match="input:employee">
    <employee>
      <name>
        <xsl:value-of select="concat(input:first_name,' ',input:last_name)" />
      </name>
      <tel>
        <xsl:value-of select="input:telephones/*[@preferred = 'true']" />
      </tel>
    </employee>
  </xsl:template>

</xsl:stylesheet>

清单 3 显示了通过用 清单 2 中的 XSLT 转换输入文档而得到的输出文档。



<phones xmlns:input="cross-field-validation-namespace"
  xmlns:exception="xfield.exception.ValidationExceptionThrower"
  xmlns:xalan="http://xml.apache.org/xslt">
  <employee>
    <name>Julia Smith</name>
    <tel>0771-555 1232</tel>
  </employee>
  <employee>
    <name>John Smith</name>
    <tel>0207-555 1236</tel>
  </employee>
  <employee>
    <name>Jenny Smith</name>
    <tel>0770-555 1237</tel>
  </employee>
</phones>

 

为了通过 XSLT 处理器将验证错误返回给应用程序,只需要两个非常简单的 Java 类。为此,必须创建 ValidationException 类和 ValidationExceptionThrower 类。ValidationException 类是标准 Java Exception 类的一个简单的扩展,它使应用程序可以将验证错误与处理器抛出的其他异常区分开来。当 ValidationExceptionThrower 类的 throwException 方法被调用时,该类简单地抛出 ValidationException。为此另外还需要一个类。当在 XSLT 中使用 Java 扩展时,不能使用常规的 Java throw 语法抛出异常。只能使用用于 XSLT 的 Java 扩展来创建对象和调用方法。清单 45 展示了 Java 扩展所需的两个类的完整源代码。



package xfield.exception;

public class ValidationException extends Exception{

  public ValidationException(String sMsg)
  {
    super(sMsg);
  } // end constructor

} // end class




package xfield.exception;

public class ValidationExceptionThrower {

  public ValidationExceptionThrower()
  {
    // Emtpy
  } // end constructor

  public void throwException(String sMessage) throws Exception
  {
    throw new ValidationException(sMessage);
  } // end throwException

} // end class

 

如前所述,本文使用称呼和性别作为一个常见同现约束的例子。您需要检查称呼和性别元素是否匹配。例如,如果 title 被设为 Mr,那么 gender 应该被设置为表示男性的 M。但是,要验证 title 和 gender 元素是否分别包含有效的值,最好的做法是使用 XML Schema 中的枚举。

为了进行验证,将转换代码放入到 <choose/> 子句的 <otherwise/> 块中,并检查 <when/> 块中的 test 语句。如果 test 语句的值等于 false,也就是说验证成功,那么就会执行转换代码。如果 test 语句的值等于 true,那么就会使用一个 Java 扩展在 ValidationExceptionThrower 类的一个实例上调用 throwException()。注意,在 XSLT 的根标记中附加了一个带前缀 exception 的名称空间,其中包含类名和包名。然后这个前缀被像一个对象名那样用来调用 throwException() 方法,调用时以一个错误字符串作为参数。回到 ValidationExceptionThrower 类的 Java 代码上来,很容易看出一个包含 XSLT 中的错误字符串的 ValidationException 是如何被抛出的。

为了检查进一步的状况,添加所需的 <when/> 语句,并添加适当的错误消息作为这些方法的参数。



<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="1.0" xmlns:xalan="http://xml.apache.org/xslt"
  xmlns:exception="xfield.exception.ValidationExceptionThrower"
  xmlns:input="cross-field-validation-namespace">

...

<xsl:template match="input:employee">
  <xsl:choose>
    <xsl:when test="input:gender = 'M' and input:title != 'Mr' 
                 or input:gender = 'F' and input:title = 'Mr'">
      <xsl:value-of
        select="exception:throwException('Gender and title do not match')"/>
    </xsl:when>
    <xsl:otherwise>
       <employee>
         <name>
           <xsl:value-of select="concat(input:first_name,' ',input:last_name)"/>
         </name>
         <tel>
           <xsl:value-of select="input:telephones/*[@preferred = 'true']"/>
         </tel>
       </employee>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

</xsl:stylesheet>

清单 7 所示,添加 XPath 表达式到异常消息将产生更详细的对验证失败的描述,这样大大方便了发现问题。



<xsl:when test="input:gender = 'M' and input:title != 'Mr' 
          or input:gender = 'F' and input:title = 'Mr'">
  <xsl:value-of select="exception:throwException(
                  concat('Gender and title do not match for employee ',
                          input:first_name,' ',input:last_name))" />
</xsl:when>

 

如果需要将转换和验证逻辑分离开来,那么可以使用一个标准的 <include/> 指令来引用验证检查,并将它们与转换代码一起执行。清单 8 展示了转换代码,其中 <include/> 标记引用了文件 validate.xsl。还应注意添加的对名为 validate 的模板的调用。这个调用应该与被包括的文件中包含条件检查的模板的名称相匹配,如 清单 9 所示,这一点很重要。



<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="1.0" xmlns:xalan="http://xml.apache.org/xslt"
  xmlns:input="cross-field-validation-namespace">

  <xsl:include href="validate.xsl" />

  <xsl:template match="/">
    <xsl:call-template name="validate" />
    <phones>
      <xsl:apply-templates />
    </phones>
  </xsl:template>

  <xsl:template match="input:staff/input:employee">
    <employee>
      <name>
        <xsl:value-of
          select="concat(input:first_name,' ',input:last_name)" />
      </name>
      <tel>
        <xsl:value-of
          select="input:telephones/*[@preferred = 'true']" />
      </tel>
    </employee>
  </xsl:template>

</xsl:stylesheet>



<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="1.0" xmlns:xalan="http://xml.apache.org/xslt"
  xmlns:exception="xfield.exception.ValidationExceptionThrower"
  xmlns:input="cross-field-validation-namespace">

  <xsl:template name="validate">
    <xsl:for-each select="/input:staff/input:employee">
      <xsl:if
        test="input:gender = 'M' and input:title != 'Mr' 
               or input:gender = 'F' and input:title = 'Mr'">
        <xsl:value-of
          select="exception:throwException('Gender and title do not match')" />
      </xsl:if>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

 

XML Schema 可以使用枚举将传入的数据与一组预先决定的可接受的值进行比较。但是,如果引用数据是变量,且本身包含关系,那么 XML Schema 就无法对它进行验证。这个例子表明,使用 XSLT 和 Java 扩展的解决方案可以满足这种场景,并且可以对变量和外部 XML 数据集进行验证。注意,Schematron 也提供了这种功能,因为被转换的模式使用 XSLT 来执行验证。



<roles>
...
  <employee id="1234A" role="A"/>
  <employee id="1234D" role="A"/>
  <employee id="1234C" role="X"/>
  <employee id="1234B" role="Z"/>
  <employee id="1234X" role="Z"/>
...    
</roles>

这个例子中的外部引用数据是一组雇员,这些雇员有相关联的角色。检查传入的 XML 文档中的所有雇员 ID 是否都在引用数据集中。为此,使用 document() 函数将一个外部 XML 文档装载到一个变量中。检查是否至少有一个具有当前 ID 的雇员在 reference 变量中,以断言该雇员号是有效的。



<xsl:variable name="reference" select="document('reference.xml')" />

<xsl:template name="validate">
  <xsl:for-each select="/input:staff/input:employee">

    <xsl:if test="input:gender = 'M' and input:title != 'Mr' 
           or input:gender = 'F' and input:title = 'Mr'">
      <xsl:value-of
        select="exception:throwException(concat('Gender and title do not match 
               for employee ',input:first_name,' ',input:last_name))" />
    </xsl:if>

    <xsl:variable name="current_id" select="@id" />

    <xsl:if
      test="count($reference/roles/employee[@id = $current_id]) = 0">
      <xsl:value-of
        select="exception:throwException(concat('Invalid employee ID: ',$current_id))" />
    </xsl:if>

  </xsl:for-each>

</xsl:template>

 

总而言之,当处理同现约束,或者需要一个 XML 报告工具时,Schematron 是 XML Schema 的一个很好的补充。但是当性能更为重要时,尤其是当验证之后要进行转换时,XSLT 加 Java 扩展是一种更紧凑的解决方案。

:点击下载源代码

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:1582031次
    • 积分:21408
    • 等级:
    • 排名:第343名
    • 原创:434篇
    • 转载:209篇
    • 译文:1篇
    • 评论:992条
    文章分类
    最新评论
    数据库