parsa
parsa

Reputation: 2668

Scala: modify a NodeSeq

I have a NodeSeq like this:

<foo>
<baz><bar key1="value1" key2="value2">foobar</bar></baz>
Blah blah blah
<bar key1="value3">barfoo</bar>
</foo>

I want to add a new attribute to all bars' attributes. I'm currently doing:

   val rule = new RewriteRule() {
     override def transform(node: Node): Seq[Node] = {
       node match {
          case Elem(prefix, "bar", attribs, scope, content@_*)  => Elem(prefix, "bar", attribs append Attribute(None, "newKey", Text("newValue"), scala.xml.Null) , scope, content:_*)
          case other => other
       }
     }
   }

But the problem is that it only works on 1 node. I want it to recursively work on all nodes, and if I call the transform inside a for loop, I can't replace them with new values since they become immutable. How can I solve this?

Upvotes: 6

Views: 5865

Answers (4)

Daniel C. Sobral
Daniel C. Sobral

Reputation: 297155

Try

 val rule = new RewriteRule() {
     override def transform(node: Node): Seq[Node] = {
       node match {
          case elem : Elem if elem.label == "bar"  => 
              (elem copy (child = this transform child)) % Attribute(None, "newKey", Text("newValue"), scala.xml.Null)
          case elem : Elem => elem copy (child = this transform child)
          case other => other
       }
     }
   }

Upvotes: 0

Knut Arne Vedaa
Knut Arne Vedaa

Reputation: 15742

Here is a simplified version of your own solution (using Daniel's variant of the matching logic):

def updateBar(node: Node): Node = node match {
    case elem @ Elem(_, "bar", _, _, child @ _*) => elem.asInstanceOf[Elem] % Attribute(None, "newKey", Text("newValue"), Null) copy(child = child map updateBar)
    case elem @ Elem(_, _, _, _, child @ _*) => elem.asInstanceOf[Elem].copy(child = child map updateBar)
    case other => other
}

Note that the major differences between this and your original code is that this one processes the nodes from the outside in, as shown here where I've added some print statements as in my first answer:

scala> updateBar(<foo><bar>blabla</bar></foo>)
processing '<foo><bar>blabla</bar></foo>'
processing '<bar>blabla</bar>'
processing 'blabla'
result: 'blabla'
result: '<bar newKey="newValue">blabla</bar>'
result: '<foo><bar newKey="newValue">blabla</bar></foo>'
res1: scala.xml.Node = <foo><bar newKey="newValue">blabla</bar></foo>

While your original code works from the inside out (simplified example):

scala> xf { <a><b><c/></b></a> }
transforming '<c></c>'
result: '<c></c>'
transforming '<b><c></c></b>'
result: '<b><c></c></b>'
transforming '<a><b><c></c></b></a>'
result: '<a><b><c></c></b></a>'
res4: scala.xml.Node = <a><b><c></c></b></a>

There are probably cases where these two techniques will yield different results.

The other difference is that the matching code is slightly more verbose: you need one case for the actual transformation of the relevant element, and one case for recursively processing the subnodes. The example shown could probably be refactored a bit, though.

Upvotes: 3

Knut Arne Vedaa
Knut Arne Vedaa

Reputation: 15742

Your original code seems to be correct. The problem is not that it does not work recursively (it does), but a weird issue that occurs when there is exactly one existing attribute.

Look at the following, which is basically identical to your code except I've added some print statements for debugging:

val rule = new RewriteRule() {
   override def transform(node: Node): Seq[Node] = {
        println("transforming '" + node + "'")
        val result = node match {
            case elem @ Elem(prefix, label @ "bar", attribs, scope, children @ _*)  => 
                Elem(prefix, label, attribs append Attribute(None, "newKey", Text("newValue"), Null), scope, children: _*)          
            case other => other
        } 
        println("result: '" + result + "'")
        result
   }
}

object xf extends RuleTransformer(rule) 

Now we test it:

scala> xf { <bar/> }
transforming '<bar></bar>'
result: '<bar newKey="newValue"></bar>'
transforming '<bar></bar>'
result: '<bar newKey="newValue"></bar>'
res0: scala.xml.Node = <bar newKey="newValue"></bar>

We see that for an element without attributes, the transformation results in the new attribute being added, and the returned result is correct as well. (I don't know why the transformation occurs twice.)

However, when there is an existing attribute:

scala> xf { <bar key1="value1"/> }
transforming '<bar key1="value1"></bar>'
result: '<bar key1="value1" newKey="newValue"></bar>'
res1: scala.xml.Node = <bar key1="value1"></bar>

The result of the transformation is correct, but it is not propagated to the final result!

But when there are two (or more) existing attributes, everything is fine:

scala> xf { <bar key1="value1" key2="value2"/> }
transforming '<bar key1="value1" key2="value2"></bar>'
result: '<bar key2="value2" key1="value1" newKey="newValue"></bar>'
transforming '<bar key1="value1" key2="value2"></bar>'
result: '<bar key2="value2" key1="value1" newKey="newValue"></bar>'
res2: scala.xml.Node = <bar key2="value2" key1="value1" newKey="newValue"></bar>

I'm tempted to believe this is a bug in the library.

Upvotes: 1

parsa
parsa

Reputation: 2668

This bad boy did the job:

def updateVersion( node : Node ) : Node = node match {
         case <foo>{ ch @ _* }</foo> => <foo>{ ch.map(updateVersion )}</foo>
         case <baz>{ ch @ _* }</baz> => <baz>{ ch.map(updateVersion ) }</baz>
         case Elem(prefix, "bar", attribs, scope, content@_*)  => Elem(prefix, "bar", attribs append Attribute(None, "key3", Text("value3"), scala.xml.Null) , scope, content:_*)
         case other @ _ => other
       }

Upvotes: 1

Related Questions