内容


不区分大小写的枚举

同时允许大小写字母的简单且自动化的解决方案

Comments

在 developerWorks,我们一直努力试着回答您的问题和满足您的需求。最近我收到下面这封来自衣阿华州得斯梅因斯(Des Moines)Tommy Jones 的信:

亲爱的 Regis:
请问在 XML Schema 中,有什么方法可以进行不区分大小写的枚举吗?如果一个元素的有效值是“red”、“blue”和“green”,我们想让用户对这些值可以使用任意的大写字母和小写字母组合。我们在 XML Schema 规范中找不到任何方法可以用来定义不区分大小写的枚举。您能帮帮我们吗?
祝安
衣阿华州得斯梅因斯 Tommy Jones

是这样的,Tommy,我这有好消息也有坏消息。坏消息是您不能用 XML Schema 做您想做的事;好消息是我们有一个自动化的解决方案,它遵循标准且相当简单,同时无需您做任何工作。

入门

首先,不能直接做您想做的事。解决这个问题的方法是把枚举转换成正则表达式。我们假设您的模式定义了下列数据类型:

<xsd:element name="favoriteColor">
  <xsd:simpleType>
    <xsd:restriction base="xsd:string">
      <xsd:enumeration value="red"/>
      <xsd:enumeration value="blue"/>
      <xsd:enumeration value="green"/>
    </xsd:restriction>
  </xsd:simpleType>
</xsd:element>

要进行不区分大小写的比较,需要把它转换成组合了所有有效值的正则表达式。例如,对于值 “blue” ”,要创建一个这样的正则表达式:“这是大写或小写字母 B,接着是大写或小写字母 L,接着是大写或小写字母 U,接着是大写或小写字母 E”。这意味着上面的枚举数据类型类似于这样:

<xsd:element name="favoriteColor">
  <xsd:simpleType>
    <xsd:restriction base="xsd:string">
      <xsd:pattern value="((B|b)(L|l)(U|u)(E|e)) | 
                          ((G|g)(R|r)(E|e)(E|e)(N|n)) | 
                          ((R|r)(E|e)(D|d))"/>
    </xsd:restriction>
  </xsd:simpleType>
</xsd:element>

这个正则表达式匹配 “blue”“BlUE”“bLUe” 和任何其它拼写出单词 blue 的大小写字母的组合(您也可以通过生成一组定义了所有大小写字母组合的 <xsd:enumeration> 元素来解决这个问题,但是那会比正则表达式大得多,当有效值是长字符串时尤其如此)。

更好的消息

因为 XML Schema 本身是 XML 文档,所以可以写一个将枚举标记转换成您刚刚看到的正则表达式的样式表。要这样做,需要查找所有基于 xsd:string 数据类型且包含了 <xsd:enumeration> 元素的 <xsd:restriction> 元素。您需要的就是一个复制了所有现有模式(您正在寻找的 <xsd:restriction> 元素除外)的样式表。然后您将添加一条规则,该规则定义怎样转换 <xsd:enumeration> 元素。

这里有一个样式表,定义了用于复制 XML 文档的基本规则。这将是用于源文档中所有内容的缺省规则;待会,将添加用于转换 <xsd:restriction> 元素的规则。

<?xml version="1.0" ?>
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <xsl:template match="*|@*|text()|comment()|processing-instruction()">
    <xsl:copy>
      <xsl:apply-templates select="*|@*|text()|comment()| 
                                   processing-instruction()" />
    </xsl:copy>
  </xsl:template>
  <!-- Add the stuff that handles the enumerations here. -->
</xsl:stylesheet>

现在您必须编写用于转换 <xsd:restriction> 元素的模板。下面是选择元素的 XPath 表达式:

<xsl:template match="xsd:restriction[@base='xsd:string']
                     [count(xsd:enumeration) > 0]">

如果不熟悉 XPath 语法,没关系,其实这告诉样式表处理器去选择所有具有 base='xsd:string' 属性且至少包含一个 <xsd:enumeration> 元素的 <xsd:restriction> 元素。对于 <xsd:restriction> 元素内的每个 <xsd:enumeration> 元素,遵循下面这个算法:

  1. 写入左括号。
  2. 写入每个字母的大小写值。
  3. 写入右括号。
  4. 如果这不是最后的 <xsd:enumeration> ,那么添加一条竖线。

那部分的样式表如下所示:

<xsl:template match="xsd:restriction[@base='xsd:string']
                     [count(xsd:enumeration) > 0]">
  <xsd:restriction base="xsd:string">
    <xsd:pattern>
      <xsl:attribute name="value">
        <xsl:for-each select="xsd:enumeration">
          <!-- Step 1. Write a left parenthesis -->
          <xsl:text>(</xsl:text>
          <!-- Step 2. Write the upper- and lowercase letters -->
          <!-- Step 3. Write a right parenthesis -->
          <xsl:text>)</xsl:text>
          <!-- Step 4. If this isn't the last enumeration, write -->
          <!-- a vertical bar -->
          <xsl:if test="not(position()=last())">
            <xsl:text>|</xsl:text>
          </xsl:if>
        </xsl:for-each>
      </xsl:attribute>
    </xsd:pattern>
  </xsd:restriction>
<xsl:template>

您可能已经注意到了,这一步骤跳过了写出每个字母的大小写值这艰难的一步。您可以使用 尾递归(tail recursion)和 XSLT translate() 函数来完成这一步。

尾递归是 XSLT 样式表中的常用技术。可以使用已命名的模板处理它;已命名的模板会调用它本身直到处理完字符串中所有的字母为止。该模板(在本示例中为 case-insensitive-pattern )接收两个参数:将被转换为正则表达式的字符串,以及在字符串中应该从哪开始的位置。下面是关于已命名的模板是怎样开始的:

  <xsl:template name="case-insensitive-pattern">
    <xsl:param name="string"/>
    <xsl:param name="index"/>

对于任意给定的字符串,正确值是由下列值并置而成的:

  1. 用( A|a )格式写的当前字母的值。
  2. 用( A|a )格式写的剩余字母的值(如果已没有字母,则该值为空;否则,递归调用该模板。要做到这一点,传递原始字符串并把开始位置加 1)。

创建代表以上两个值的两个变量,然后使用 <xsl:value-of> 元素输出它们的组合值。对于当前字母,输出左括号、该字母的大写值、竖线、该字母的小写值和右括号。以下是计算第一个变量的标记:

<xsl:variable name="current-letter">
  <!-- Write a left parenthesis -->
  <xsl:text>(</xsl:text>
  <!-- Convert the current letter to uppercase -->
  <xsl:value-of select="translate(substring($string, $index, 1), 
                                  'abcdefghijklmnopqrstuvwxyz', 
                                  'ABCDEFGHIJKLMNOPQRSTUVWXYZ')"/>
  <!-- Write a vertical bar -->
  <xsl:text>|</xsl:text>
  <!-- Convert the current letter to lowercase -->
  <xsl:value-of select="translate(substring($string, $index, 1), 
                                  'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 
                                  'abcdefghijklmnopqrstuvwxyz')"/>
  <!-- Write a right parenthesis -->
  <xsl:text>)</xsl:text>
</xsl:variable>

有关 XSLT translate() 函数的只言片语

在我们继续之前,有必要注意一下带有三个字符串参数的 XSLT translate() 函数。对于第一个字符串中的每个字符,出现在第二个字符串( 'abcde...' )中的任意一个字符都由第三个字符串( 'ABCDE...' )中相应的字母所替代。因此,如果第一个字符串是 bed ,那么该函数调用 translate('bed', 'abcde...', 'ABCDE...') ,返回 BED 。如果第一个字符串中的字符根本就没出现在第二个字符串中,它就不会改变。这意味着 translate('bed7', 'abcde...', 'ABCDE...') 返回 BED7 。如果需要,可以扩展函数调用中的字符串以包含西欧语言中使用的重音符(XSLT 规范警告: translate() 不能处理全世界所有语言的大小写转换,因此要知道这一点)。

现在计算所有剩余字母的值,这些字母都转换成( A|a )格式。如果当前字母的下标小于字符串的长度,则再次调用已命名的模板,传递原始字符串并把下标加 1。如果当前字母的下标和字符串的长度相等,那么这个变量是一个空字符串。

<xsl:variable name="remaining-letters">
  <!-- If $index is less than the length of the string, -->
  <!-- call the template again. -->
  <xsl:if test="$index < string-length($string)">
    <xsl:call-template name="case-insensitive-pattern">
      <!-- The string parameter doesn't change -->
      <xsl:with-param name="string" select="$string"/>
      <!-- Increment the index of the current letter by 1 -->
      <xsl:with-param name="index" select="$index + 1"/>
    </xsl:call-template>
  </xsl:if>
</xsl:variable>

最后,用 <xsl:value-of> 元素和 concat() 函数输出这两个变量的值。这相当于其它编程语言中的 return 语句。

<xsl:value-of select="concat($current-letter, $remaining-letters)"/>

因此,如果值 blue、redgreen 是有效的,则可以用样式表转换模式以生成一个新的模式。使用这个新的模式,值 BLUE、Blue、bLuEblUE 都是有效的。

示例

这有一个示例,演示了这个样式表是怎样工作的。可以使用模式来定义针对性别、婚姻状况和最喜欢颜色的枚举。以下是样本实例文档:

<?xml version="1.0"?>
<f:friend 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.ibm.com/developerWorks 
                    friend.xsd"
  xmlns:f="http://www.ibm.com/developerWorks">
  <f:name>
    <f:firstName>Jane</f:firstName>
    <f:lastName>Doe</f:lastName>
  </f:name>
  <f:gender>f</f:gender>
  <f:maritalStatus>married</f:maritalStatus>
  <f:favoriteColor>orange</f:favoriteColor>
</f:friend>

在这个示例中,包括了一小段的 Java 代码 - XMLValidator.java ,这段代码根据 XML 模式来验证 XML 文档。如果输入 java XMLValidator friend.xml ,则会看到下面这些消息:

> java XMLValidator friend.xml
Your document contains no errors!

在我们的样本文档中,值 fmarriedorange 都是区分大小写的;输入 FMarriedOrAnGE 都会引起错误。如果把这些不合法的值输入到 friend.xml ,会得到下面这些消息:

Error in friend.xml at line 10, column 25: cvc-type.3.1.3: The value 'F' of 
element 'f:gender' is not valid.
Error in friend.xml at line 11, column 45: cvc-type.3.1.3: The value 'Married' 
of element 'f:maritalStatus' is not valid.
Error in friend.xml at line 12, column 44: cvc-type.3.1.3: The value 'OrAnGE' 
of element 'f:favoriteColor' is not valid.

可以使用前面的 XSLT 样式表将原来的模式转换成新的模式文档。

> java org.apache.xalan.xslt.Process -in friend.xsd -xsl convert-enumerations.xsl 
-out insensitive-friend.xsd

接下来,将 XML 文档的根元素改成引用这个新的模式文件:

<f:friend 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.ibm.com/developerWorks 
                      insensitive-friend.xsd"
  xmlns:f="http://www.ibm.com/developerWorks">

如果现在对 XML 文档运行验证程序,会再次得到消息,说文档中不包含错误。文件 case-insensitive.zip 包含了您亲自试一试所需的所有代码和样本。

那好,Tommy,我希望这回答了您的问题。我们的解决方案相对要简单些,能自动工作,而且基于 XML 标准。

您自己还有问题吗?请随时发送电子邮件给我们,我们将在大量的业余时间里来设法回答这些问题。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=22309
ArticleTitle=不区分大小写的枚举
publish-date=10012002