How to change attribute on Scala XML Element
Ok, best effort, Scala 2.8. We need to reconstruct attributes, which means we have to decompose them correctly. Let's create a function for that:
import scala.xml._case class GenAttr(pre: Option[String], key: String, value: Seq[Node], next: MetaData) { def toMetaData = Attribute(pre, key, value, next)}def decomposeMetaData(m: MetaData): Option[GenAttr] = m match { case Null => None case PrefixedAttribute(pre, key, value, next) => Some(GenAttr(Some(pre), key, value, next)) case UnprefixedAttribute(key, value, next) => Some(GenAttr(None, key, value, next))}
Next, let's decompose the chained attributes into a sequence:
def unchainMetaData(m: MetaData): Iterable[GenAttr] = m flatMap (decomposeMetaData)
At this point, we can easily manipulate this list:
def doubleValues(l: Iterable[GenAttr]) = l map { case g @ GenAttr(_, _, Text(v), _) if v matches "\\d+" => g.copy(value = Text(v.toInt * 2 toString)) case other => other}
Now, chain it back again:
def chainMetaData(l: Iterable[GenAttr]): MetaData = l match { case Nil => Null case head :: tail => head.copy(next = chainMetaData(tail)).toMetaData}
Now, we only have to create a function to take care of these things:
def mapMetaData(m: MetaData)(f: GenAttr => GenAttr): MetaData = chainMetaData(unchainMetaData(m).map(f))
So we can use it like this:
import scala.xml.transform._val attribs = Set("attr1", "attr2")val rr = new RewriteRule { override def transform(n: Node): Seq[Node] = (n match { case e: Elem => e.copy(attributes = mapMetaData(e.attributes) { case g @ GenAttr(_, key, Text(v), _) if attribs contains key => g.copy(value = Text(v.toInt * 2 toString)) case other => other }) case other => other }).toSeq}val rt = new RuleTransformer(rr)
Which finally let you do the translation you wanted:
rt.transform(<a><b attr1="100" attr2="50"></b></a>)
All of this could be simplified if:
- Attribute actually defined prefix, key and value, with an optional prefix
- Attribute was a sequence, not a chain
- Attribute had a map, mapKeys, mapValues
- Elem had a mapAttribute
This is how you can do it using Scala 2.10:
import scala.xml._import scala.xml.transform._val xml1 = <a><b attr1="100" attr2="50"></b></a>val rule1 = new RewriteRule { override def transform(n: Node) = n match { case e @ <b>{_*}</b> => e.asInstanceOf[Elem] % Attribute(null, "attr1", "200", Attribute(null, "attr2", "100", Null)) case _ => n }}val xml2 = new RuleTransformer(rule1).transform(xml1)
So if I were in your position, I think what I'd really want to be writing is something like:
case elem: Elem => elem.copy(attributes= for (attr <- elem.attributes) yield attr match { case attr@Attribute("attr1", _, _) => attr.copy(value=attr.value.text.toInt * 2) case attr@Attribute("attr2", _, _) => attr.copy(value=attr.value.text.toInt * -1) case other => other })
There are two reasons this won't work out of the box:
Attribute
doesn't have a usefulcopy
method, and- Mapping over a
MetaData
yields anIterable[MetaData]
instead of aMetaData
so even something as simple aselem.copy(attributes=elem.attributes.map(x => x))
will fail.
To fix the first problem, we'll use an implicit to add a better copy method to Attribute
:
implicit def addGoodCopyToAttribute(attr: Attribute) = new { def goodcopy(key: String = attr.key, value: Any = attr.value): Attribute = Attribute(attr.pre, key, Text(value.toString), attr.next)}
It can't be named copy
since a method with that name already exists, so we'll just call it goodcopy
. (Also, if you're ever creating values that are Seq[Node]
instead of things that should be converted to strings, you could be a little more careful with value
, but for our current purposes it's not necessary.)
To fix the second problem, we'll use an implicit to explain how to create a MetaData
from an Iterable[MetaData]
:
implicit def iterableToMetaData(items: Iterable[MetaData]): MetaData = { items match { case Nil => Null case head :: tail => head.copy(next=iterableToMetaData(tail)) }}
Then you can write code pretty much like what I proposed at the beginning:
scala> val elem = <b attr1 = "100" attr2 = "50"/>elem: scala.xml.Elem = <b attr1="100" attr2="50"></b>scala> elem.copy(attributes= | for (attr <- elem.attributes) yield attr match { | case attr@Attribute("attr1", _, _) => | attr.goodcopy(value=attr.value.text.toInt * 2) | case attr@Attribute("attr2", _, _) => | attr.goodcopy(value=attr.value.text.toInt * -1) | case other => other | } | )res1: scala.xml.Elem = <b attr1="200" attr2="-50"></b>