How to change attribute on Scala XML Element How to change attribute on Scala XML Element xml xml

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:

  1. Attribute doesn't have a useful copy method, and
  2. Mapping over a MetaData yields an Iterable[MetaData] instead of a MetaData so even something as simple as elem.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>