Как отслеживать исходную строку (местоположение) элемента XML?

Я предполагаю, что, вероятно, нет удовлетворительного ответа на этот вопрос, но я все равно прошу, если я что-то пропустил.

В принципе, я хочу узнать строку в исходном документе, из которого возник какой-либо элемент XML, с учетом экземпляра элемента. Я хочу, чтобы это было только для улучшения диагностических сообщений об ошибках - XML ​​является частью файла конфигурации, и если с ним что-то не так, я хочу, чтобы он мог указать читателю сообщения об ошибке точно в нужное место в документе XML поэтому он может исправить ошибку.

Я понимаю, что стандартная поддержка XML Scala, вероятно, не имеет встроенной функции, подобной этой. В конце концов, было бы расточительно аннотировать каждый экземпляр NodeSeq такой информацией, и не каждый XML-элемент даже имел исходный документ, с которого он был разобран. Мне кажется, что стандартный Scala синтаксический анализатор XML выводит данные строки, а затем нет способа получить его.

Но переключение на другую структуру XML не является вариантом. Добавление другой зависимости библиотеки "только" для улучшения сообщений об ошибках диагностики представляется мне неуместным. Кроме того, несмотря на некоторые недостатки, мне очень нравится встроенная поддержка сопоставления шаблонов для XML.

Моя единственная надежда заключается в том, что вы можете показать мне способ изменить или подклассифицировать стандартный парсер XML Scala так, чтобы создаваемые им узлы были аннотированы с номером строки источника. Может быть, для этого может быть создан специальный подкласс NodeSeq. Или, может быть, только Atom можно подклассифицировать, потому что NodeSeq слишком динамичен? Я не знаю.

В любом случае, мои надежды близки к нулю. Я не думаю, что в парсере есть место, где мы можем подключиться, чтобы изменить способ создания узлов, и что в этом месте доступна информация о линии. Тем не менее, я задаюсь вопросом, почему я раньше этого не задавал. Пожалуйста, укажите мне оригинал, если это дубликат.

Ответ 1

Я понятия не имел, как это сделать, но Pangea показал мне путь. Во-первых, давайте создадим черту для обработки местоположения:

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)
    }
}

Затем создайте наш собственный загрузчик, переопределяющий XMLLoader adapter, чтобы включить наш признак:

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 извлекает информацию о своей строке и номере из исключения, кстати. Для наших целей нам также нужны внешние исключения (я предполагаю).

Теперь, чтобы изменить само создание node, просмотрите абстрактные методы 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 выскочить из этого стека в var, используемый createNode.

Хороший вопрос. Я многому научился!: -)

Ответ 2

I видеть, что scala внутренне использует SAX для синтаксического анализа. SAX позволяет вам установить Locator на ContentHandler, который можно использовать для извлечения текущее местоположение, где произошла ошибка. Я не уверен, как вы можете использовать внутреннюю работу Scala. Вот одна статья. Я обнаружил, что может помочь, если это выполнимо.

Ответ 3

Я ничего не знаю о Scala, но та же проблема появляется в других средах. Например, преобразование XML отправляет свои результаты вниз по SAX-конвейеру в валидатор, и когда валидатор пытается найти номера строк для своих ошибок проверки, они исчезли. Или XML, о котором идет речь, никогда не был сериализован или проанализирован и поэтому никогда не имел номеров строк.

Один из способов решения проблемы - генерировать (человекочитаемые) выражения XPath, чтобы сказать, где произошла ошибка. Они не так просты в использовании, как номера строк, но они намного лучше, чем ничего: они однозначно идентифицируют node, и их часто довольно легко интерпретировать для людей (особенно если они имеют редактор XML).

Например, этот шаблон XSLT от Ken Holman (я думаю), используемый Schematron, генерирует выражение XPath для описания местоположения/идентичности контекста node:

<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 в своем сценарии, но вы можете применить тот же принцип с любыми инструментами, которые у вас есть.

Ответ 4

Хотя вы указали, что не хотите использовать другую библиотеку или фреймворк, стоит отметить, что все хорошие синтаксические анализаторы потоков Java (Xerces для Sax, Woodstox и Aalto для Stax) делают информацию о местоположении доступной для всех событий/токенов, которые они служить.

Хотя эта информация не всегда сохраняется абстракциями более высокого уровня, такими как деревья DOM (из-за необходимости дополнительного хранилища, производительность не является большой проблемой, поскольку информация о местоположении всегда отслеживается, поскольку она необходима для сообщения об ошибках в любом случае), это может быть легко или, по крайней мере, можно исправить.