Scala Создание XML: добавление дочерних элементов в существующие узлы

У меня есть XML Node, с которым я хочу добавить детей со временем:

val root: Node = <model></model>

Но я не вижу таких методов, как addChild(), поскольку я хотел бы написать что-то по строкам:

def addToModel() = {
    root.addChild(<subsection>content</subsection>)
}

Итак, после одного вызова этого метода корневой xml будет:

<model><subsection>content</subsection></model>

Единственный класс, который я вижу, который имеет возможность добавить Node, - это NodeBuffer. Я что-то не хватает здесь?

Ответ 1

Начните с этого:

def addChild(n: Node, newChild: Node) = n match {
  case Elem(prefix, label, attribs, scope, child @ _*) =>
    Elem(prefix, label, attribs, scope, child ++ newChild : _*)
  case _ => error("Can only add children to elements!")
}

Метод ++ работает здесь, потому что child является Seq[Node], а newChild является Node, который расширяет NodeSeq, который расширяет Seq[Node].

Теперь это ничего не меняет, потому что XML в Scala является неизменным. Он будет производить новый node с необходимыми изменениями. Единственная стоимость - это создание нового объекта Elem, а также создание нового Seq для детей. Дети node, сами по себе, не копируются, просто упоминаются, что не вызывает проблем, потому что они неизменяемы.

Однако, если вы добавляете дочерние элементы в путь node вниз по иерархии XML, все становится сложнее. Один из способов - использовать молнии, например, описанные в в этом блоге.

Однако вы можете использовать scala.xml.transform с правилом, которое изменит конкретный node, чтобы добавить нового ребенка. Сначала напишите новый класс трансформатора:

class AddChildrenTo(label: String, newChild: Node) extends RewriteRule {
  override def transform(n: Node) = n match {
    case n @ Elem(_, `label`, _, _, _*) => addChild(n, newChild)
    case other => other
  }
}

Затем используйте его следующим образом:

val newXML = new RuleTransformer(new AddChildrenTo(parentName, newChild)).transform(oldXML).head

В Scala 2.7 замените head на first.

Пример Scala 2.7:

scala> val oldXML = <root><parent/></root>
oldXML: scala.xml.Elem = <root><parent></parent></root>

scala> val parentName = "parent"
parentName: java.lang.String = parent

scala> val newChild = <child/>
newChild: scala.xml.Elem = <child></child>

scala>     val newXML = new RuleTransformer(new AddChildrenTo(parentName, newChild)).transform(oldXML).first
newXML: scala.xml.Node = <root><parent><child></child></parent></root>

Вы можете сделать более сложным, чтобы получить правильный элемент, если только родительский недостаточно. Однако, если вам нужно добавить ребенка к родительскому лицу с общим именем определенного индекса, вам, вероятно, придется идти по пути молнии.

Например, если у вас есть <books><book/><book/></books>, и вы хотите добавить <author/> ко второму, это будет сложно сделать с трансформатором правил. Вам понадобится RewriteRule против books, который затем получит его child (который действительно должен был быть назван children), найдите в нем n thbook, добавьте новый ребенок к этому, а затем перекомпонуйте детей и создайте новый node. Возможность, но молнии могут быть проще, если вам нужно сделать это слишком много.

Ответ 2

В Scala узлы xml неизменяемы, но могут сделать это:

var root = <model/>

def addToModel(child:Node) = {
  root = root match {
    case <model>{[email protected] _*}</model> => <model>{children ++ child}</model>
    case other => other
  }
}

addToModel(<subsection>content</subsection>)

Он перезаписывает новый xml, создавая копию старого и добавляя ваш node в качестве дочернего.

Изменить: Брайан предоставил больше информации, и я решил, что будет отличаться.

Чтобы добавить ребенка к произвольному node в 2.8, вы можете сделать:

def add(n:Node,c:Node):Node = n match { case e:Elem => e.copy(child=e.child++c) }

Это вернет новую копию родительского node с добавленным потомком. Предполагая, что вы уложили ваши дочерние узлы по мере их появления:

scala> val stack = new Stack[Node]()
stack: scala.collection.mutable.Stack[scala.xml.Node] = Stack()

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

stack.foldRight(<parent/>:Node){(c:Node,n:Node) => add(n,c)}

Я понятия не имею о влиянии производительности на использование Stack и foldRight, поэтому, в зависимости от того, сколько у вас детей было уложено, вам может понадобиться возиться... Тогда вам может понадобиться также вызвать stack.clear. Надеюсь, это позаботится об неизменном характере Node, но также и о вашем процессе по мере необходимости.

Ответ 3

Так как scala 2.10.0 конструктор экземпляра Elem изменился, если вы хотите использовать наивное решение, написанное @Daniel C. Sobral, оно должно быть:

xmlSrc match {
  case xml.Elem(prefix, label, attribs, scope, child @ _*) =>
       xml.Elem(prefix, label, attribs, scope, child.isEmpty, child ++ ballot : _*)
  case _ => throw new RuntimeException
}

Для меня это работает очень хорошо.

Ответ 4

В обычном режиме Scala все экземпляры Node, Elem и т.д. неизменяемы. Вы можете работать по-другому:

  scala> val child = <child>foo</child>
  child: scala.xml.Elem = <child>foo</child>

  scala> val root = <root>{child}</root>
  root: scala.xml.Elem = <root><child>foo</child></root>

Подробнее см. http://sites.google.com/site/burakemir/scalaxbook.docbk.html.

Ответ 5

Так как XML immutable, вы должны создать новый каждый раз, когда хотите добавить node, вы можете использовать Pattern matching, чтобы добавить новый node:

    var root: Node = <model></model>
    def addToModel(newNode: Node) = root match {
       //match all the node from your model
       // and make a new one, appending old nodes and the new one
        case <model>{[email protected]_*}</model> => root = <model>{oldNodes}{newNode}</model>
    }
    addToModel(<subsection>content</subsection>)

Ответ 6

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

Сохраните свой подраздел, если хотите, когда вам нужен XML, оберните его все вместе.

  val subsections : List[Elem]

  def wrapInModel(f : => Elem) = {
    <model>{f}</model>
  }

  wrapInModel(subsections)

или

  def wrapInModel(f : => Elem) = {
    <model>{f}</model>
  }
  wrapInModel(<subsection>content</subsection>)

Ответ 7

Масштабы Xml позволяют легко менять место с помощью сгибания по XPaths, добавление детей к определенному югу node подходит прямо к этому подходу.

Подробнее см. In-Place Transformations.

Ответ 8

ваше определение корня на самом деле является объектом Elem, подклассом node, поэтому, если вы отбросите ненужную Node типизацию (которая скрывает ее реализацию), вы можете на самом деле сделать с ним ++, так как класс Elem имеет это метод.

val root = <model/>
val myChild = <myChild/>
root.copy(child = root.child ++ myChild)

scala ev:

root: scala.xml.Elem = <model/>
myChild: scala.xml.Elem = <mychild/>
res2: scala.xml.Elem = <model><mychild/></model>

Так как каждый Elem и каждый Node является NodeSeq, вы можете добавить их довольно эффективно, даже если то, что вы добавляете, является неизвестной последовательностью:

val root = <model/>
//some node sequence of unknown subtype or structure
val children: scala.xml.NodeSeq = <node1><node2/></node1><node3/> 
root.copy(child = root.child ++ children)

scala ev:

root: scala.xml.Elem = <model/>
children: scala.xml.NodeSeq = NodeSeq(<node1><node2/></node1>, <node3/>)
res6: scala.xml.Elem = <model><node1><node2/></node1><node3/></model>

Ответ 9

Я реализую свой метод appendChild следующим образом:

  def appendChild(elem: Node, child: Node, names: String) = {
    appendChild(elem, child, names.split("/"))
  }

  private def appendChild(elem: Node, child: Node, names: Array[String]) = {
    var seq = elem.child.diff(elem \ names.head)
    if (names.length == 1)
      for (re <- elem \ names.head)
        seq = seq ++ re.asInstanceOf[Elem].copy(child = re.child ++ child)
    else
      for (subElem <- elem \ names.head)
        seq = seq ++ appendChild(subElem, child, names.tail)
    elem.asInstanceOf[Elem].copy(child = seq)
  }

Метод добавляет дочерние элементы к вашим узлам рекурсивным образом. В выражении "if" он просто вызывает метод "copy" класса Elem для создания новых экземпляров затронутых детей (это может быть множественное число). Затем в выражении "else" рекурсивные вызовы метода "appendChild" проверяют, что полученный XML будет восстановлен. До "if-else" есть последовательность, которая построена из детей без влияния. В конце нам нужно скопировать эту последовательность в исходный элемент.

val baz = <a><z x="1"/><b><z x="2"/><c><z x="3"/></c><z x="4"/></b></a>
println("Before: \n" + XmlPrettyPrinter.format(baz.toString()))

val res = appendChild(baz, <y x="5"/>, "b/c/z")
println("After: \n" + XmlPrettyPrinter.format(res.toString()))

Результаты:

Before: 
<a>
  <z x="1"/>
  <b>
    <z x="2"/>
    <c>
      <z x="3"/>
    </c>
    <z x="4"/>
  </b>
</a>

After: 
<a>
  <z x="1"/>
  <b>
    <z x="2"/>
    <z x="4"/>
    <c>
      <z x="3">
        <y x="5"/>
      </z>
    </c>
  </b>
</a>