6

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?

parsa
  • 2,628
  • 3
  • 34
  • 44

4 Answers4

3

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.

Knut Arne Vedaa
  • 15,372
  • 11
  • 48
  • 59
  • Very nice. I would say the 'inside out' or 'outside in' depends on the order of the 'case's. – parsa Jan 14 '11 at 06:24
  • 2
    No, it's because that RuleTransformer starts its transformation with each innermost element. (Or so it seems, I haven't looked at the source code, just the behaviour.) If you change the order of cases in this particular example and e.g. put the second case at the top, the "bar" will never be matched. – Knut Arne Vedaa Jan 14 '11 at 10:23
1

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
       }
parsa
  • 2,628
  • 3
  • 34
  • 44
  • It might not be a good practice to include irrelevant elements in the transformation. – Knut Arne Vedaa Jan 12 '11 at 22:25
  • @Knut: I was wondering if I can extend the first two cases to any tag, but I failed: `case Elem(prefix, tag, attribs, scope, content) => Elem(prefix, tag, attribs, scope, content.map(updateVersion ))` gives compiler error since last argument of Elem should be a Node not a NodeSeq. Maybe I should iterate the NodeSeq. – parsa Jan 13 '11 at 02:24
1

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.

Knut Arne Vedaa
  • 15,372
  • 11
  • 48
  • 59
0

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
       }
     }
   }
Daniel C. Sobral
  • 295,120
  • 86
  • 501
  • 681
  • This doesn't work. 1) You need to cast elem to Elem before invoking copy or %. The compiler complains that copy is not defined on Node. 2) children is undefined, and what you seem to be trying to do is not necessary for having it work recursively. If you remove the copy and the mid case, it works identical to OP's. – Knut Arne Vedaa Jan 12 '11 at 22:35
  • @Knut My mistake. The `@`, not the `children`. The `children` thing is Scala's library mistake, that my brain refuses to accept. :-) – Daniel C. Sobral Jan 13 '11 at 15:13
  • You need to do "this transform elem.child". Still, this is actually a combination of the two different techniques mentioned (inside-out / outside-in). It works (although it triggers the bug described in my answer), but it transforms the entire tree twice, which is not necessesary. (As far as I can see, I could be wrong, it has happened before.) – Knut Arne Vedaa Jan 13 '11 at 18:45