Вот очень простой подход к переходу по дереву с использованием Java DOM API в Scala. REPL:
Сначала мы импортируем соответствующие пакеты и настроим наш конструктор документов и источник:
scala> import org.w3c.dom._
import org.w3c.dom._
scala> import javax.xml.parsers._
import javax.xml.parsers._
scala> val factory = DocumentBuilderFactory.newInstance()
factory: javax.xml.parsers.DocumentBuilderFactory = ...
scala> val builder = factory.newDocumentBuilder()
builder: javax.xml.parsers.DocumentBuilder = ...
scala> val source = new org.xml.sax.InputSource()
source: org.xml.sax.InputSource = org.xml.sax.InputSource@7ecec7c6
Теперь для разбора примера документа:
scala> val content = """<start>
<nodes>
<node><name>Whatever</name></node>
<node><name>Whatever 2</name></node>
</nodes>
</start>"""
content: java.lang.String = ...
scala> source.setCharacterStream(new java.io.StringReader(content))
scala> val document = builder.parse(source)
document: org.w3c.dom.Document = [#document: null]
Это очень простая функция, которая рекурсивно перемещает DOM к корню документа:
scala> def path: Node => String = {
| case document: Document => ""
| case node => path(node.getParentNode) + "/" + node.getNodeName
| }
path: org.w3c.dom.Node => String
И мы выбираем второй <name>
узел для проверки:
scala> val node = document.getElementsByTagName("name").item(1)
node: org.w3c.dom.Node = [name: null]
Мы получаем то, что ожидаем:
scala> path(node)
res1: String = /start/nodes/node/name
Нетрудно настроить функцию path
, чтобы избежать явной рекурсии или собрать больше информации по мере продвижения вверх по дереву - например, указывать положение при необходимости, чтобы избежать неоднозначности:
scala> def path(element: Element) = {
| def sameName(f: Node => Node)(n: Node) =
| Stream.iterate(n)(f).tail.takeWhile(_ != null).filter(
| _.getNodeName == n.getNodeName
| ).toList
| val preceding = sameName(_.getPreviousSibling) _
| val following = sameName(_.getNextSibling) _
| "/" + Stream.iterate[Node](element)(_.getParentNode).map {
| case _: Document => None
| case e: Element => Some { (preceding(e), following(e)) match {
| case (Nil, Nil) => e.getTagName
| case (els, _) => e.getTagName + "[" + (els.size + 1) + "]"
| }}
| }.takeWhile(_.isDefined).map(_.get).reverse.mkString("/")
| }
path: (element: org.w3c.dom.Element)java.lang.String
Обратите внимание, что я немного изменил тип, чтобы было ясно, что это даст нам только правильный путь XPath для элементов. Мы можем проверить:
scala> path(node.asInstanceOf[Element])
res13: java.lang.String = /start/nodes/node[2]/name
Это опять то, что мы ожидаем.