10

我认为这个问题可能没有令人满意的答案,但无论如何我都会问它,以防我错过了什么。

基本上,在给定元素实例的情况下,我想找出某个 XML 元素源自的源文档中的哪一行。我希望这只是为了更好地诊断错误消息 - XML 是配置文件的一部分,如果它有问题,我希望能够将错误消息的阅读器指向 XML 文档中的正确位置这样他就可以纠正错误。

我知道标准的 Scala XML 支持可能没有这样的内置功能。毕竟,NodeSeq用这样的信息注释每个实例是很浪费的,而且不是每个 XML 元素甚至都有一个源文档来解析它。在我看来,标准的 Scala XML 解析器丢弃了行信息,后来没有办法检索它。

但是切换到另一个 XML 框架不是一种选择。为了更好的诊断错误消息而“仅”添加另一个库依赖项对我来说似乎不合适。此外,尽管有一些缺点,但我真的很喜欢 XML 的内置模式匹配支持。

我唯一的希望是您可以向我展示一种更改或子类化标准 Scala XML 解析器的方法,以便它生成的节点将使用源代码行的编号进行注释。也许NodeSeq可以为此创建一个特殊的子类。或者也许只能Atom因为NodeSeq太动态而被子类化?我不知道。

无论如何,我的希望几乎为零。我不认为在解析器中有一个地方可以用来改变节点的创建方式,并且在那个地方可以使用行信息。不过,我想知道为什么我以前没有发现这个问题。如果这是重复的,请指出原件。

4

4 回答 4

11

我不知道该怎么做,但Pangea 向我展示了方法。首先,让我们创建一个 trait 来处理位置:

import org.xml.sax.{helpers, Locator, SAXParseException}
trait WithLocation extends helpers.DefaultHandler {
    var locator: org.xml.sax.Locator = _
    def printLocation(msg: String) {
        println("%s at line %d, column %d" format (msg, locator.getLineNumber, locator.getColumnNumber))
    }

    // Get location
    abstract override def setDocumentLocator(locator: Locator) {
        this.locator = locator
        super.setDocumentLocator(locator)
    }

    // Display location messages
    abstract override def warning(e: SAXParseException) {
        printLocation("warning")
        super.warning(e)
    }
    abstract override def error(e: SAXParseException) {
        printLocation("error")
        super.error(e)
    }
    abstract override def fatalError(e: SAXParseException) {
        printLocation("fatal error")
        super.fatalError(e)
    }
}

接下来,让我们创建自己的 loader 覆盖XMLLoaderadapter包含我们的 trait:

import scala.xml.{factory, parsing, Elem}
object MyLoader extends factory.XMLLoader[Elem] {
    override def adapter = new parsing.NoBindingFactoryAdapter with WithLocation
}

这就是它的全部!该对象XML几乎没有增加XMLLoader- 基本上,save方法。如果您觉得需要完全替换,您可能想查看它的源代码。但这只是当您想自己处理所有这些时,因为 Scala 已经具有产生错误的特性:

object MyLoader extends factory.XMLLoader[Elem] {
    override def adapter = new parsing.NoBindingFactoryAdapter with parsing.ConsoleErrorHandler
}

顺便说一下,该ConsoleErrorHandler特征从异常中提取其行和编号信息。出于我们的目的,我们也需要异常之外的位置(我假设)。

现在,要修改节点创建本身,请查看scala.xml.factory.FactoryAdapter抽象方法。我已经确定了createNode,但我在NoBindingFactoryAdapter级别上覆盖,因为它返回Elem而不是Node,这使我能够添加属性。所以:

import org.xml.sax.Locator
import scala.xml._
import parsing.NoBindingFactoryAdapter
trait WithLocation extends NoBindingFactoryAdapter {
    var locator: org.xml.sax.Locator = _

    // Get location
    abstract override def setDocumentLocator(locator: Locator) {
        this.locator = locator
        super.setDocumentLocator(locator)
    }

    abstract override def createNode(pre: String, label: String, attrs: MetaData, scope: NamespaceBinding, children: List[Node]): Elem = (
        super.createNode(pre, label, attrs, scope, children) 
        % Attribute("line", Text(locator.getLineNumber.toString), Null) 
        % Attribute("column", Text(locator.getColumnNumber.toString), Null)
    )
}

object MyLoader extends factory.XMLLoader[Elem] {
    // Keeping ConsoleErrorHandler for good measure
    override def adapter = new parsing.NoBindingFactoryAdapter with parsing.ConsoleErrorHandler with WithLocation
}

结果:

scala> MyLoader.loadString("<a><b/></a>")
res4: scala.xml.Elem = <a line="1" column="12"><b line="1" column="8"></b></a>

请注意,它获得了最后一个位置,即结束标记处的位置。这是可以通过覆盖startElement来改进的一件事,以跟踪每个元素在堆栈中的开始位置,并endElement从该堆栈弹出到varused by createNode

好问题。我学到了很多!:-)

于 2010-12-15T12:07:15.983 回答
4

看到 scala 在内部使用 SAX 进行解析。SAX 允许您在ContentHandler上设置一个定位器,它可用于检索发生错误的当前位置。不过,我不确定如何利用 Scala 的内部工作原理。这是我发现的一篇文章,可能有助于了解这是否可行。

于 2010-12-15T04:52:53.153 回答
2

我对 Scala 一无所知,但在其他环境中也会出现同样的问题。例如,XML 转换将其结果通过 SAX 管道发送到验证器,当验证器尝试为其验证错误查找行号时,它们就消失了。或者有问题的 XML 从未被序列化或解析,因此从未有行号。

解决该问题的一种方法是生成(人类可读的)XPath 表达式来说明错误发生的位置。这些不像行号那样易于使用,但它们总比没有好:它们唯一地标识一个节点,并且它们通常很容易被人类解释(特别是如果他们有一个 XML 编辑器)。

例如,Schematron 使用的 Ken Holman(我认为)的这个 XSLT 模板生成了一个 XPath 表达式来描述上下文节点的位置/身份:

<xsl:template match="node() | @*" mode="schematron-get-full-path-2">
   <!--report the element hierarchy-->
   <xsl:for-each select="ancestor-or-self::*">
      <xsl:text>/</xsl:text>
      <xsl:value-of select="name(.)"/>
      <xsl:if test="preceding-sibling::*[name(.)=name(current())]">
         <xsl:text>[</xsl:text>
         <xsl:value-of
            select="count(preceding-sibling::*[name(.)=name(current())])+1"/>
         <xsl:text>]</xsl:text>
      </xsl:if>
   </xsl:for-each>
   <!--report the attribute-->
   <xsl:if test="not(self::*)">
      <xsl:text/>/@<xsl:value-of select="name(.)"/>
   </xsl:if>
</xsl:template>

我不知道您是否可以在您的场景中使用 XSLT,但您可以将相同的原则应用于您可用的任何工具。

于 2010-12-15T04:25:25.307 回答
2

尽管您表示您不想使用不同的库或框架,但值得注意的是,所有优秀的 Java 流解析器(Sax 的 Xerces、Woodstox 和 Stax 的 Aalto)确实为它们所服务的所有事件/令牌提供了位置信息。

尽管此信息并不总是由 DOM 树等更高级别的抽象保留(由于需要额外的存储;性能不是大问题,因为始终跟踪位置信息,因为无论如何都需要它来进行错误报告),但这可能很容易或至少可以修复。

于 2010-12-15T06:53:36.887 回答