21

I have an XML file that I would like to map some attributes of in with a script. For example:

<a>
  <b attr1 = "100" attr2 = "50"/>
</a>

might have attributes scaled by a factor of two:

<a>
  <b attr1 = "200" attr2 = "100"/>
</a>

This page has a suggestion for adding attributes but doesn't detail a way to map a current attribute with a function (this way would make that very hard): http://www.scalaclass.com/book/export/html/1

What I've come up with is to manually create the XML (non-scala) linked-list... something like:

// a typical match case for running thru XML elements:
case  Elem(prefix, e, attributes, scope, children @ _*) => {
 var newAttribs = attributes
 for(attr <- newAttribs)  attr.key match {
  case "attr1" => newAttribs = attribs.append(new UnprefixedAttribute("attr1", (attr.value.head.text.toFloat * 2.0f).toString, attr.next))
  case "attr2" => newAttribs = attribs.append(new UnprefixedAttribute("attr2", (attr.value.head.text.toFloat * 2.0f).toString, attr.next))
  case _ =>
 }
 Elem(prefix, e, newAttribs, scope, updateSubNode(children) : _*)  // set new attribs and process the child elements
}

Its hideous, wordy, and needlessly re-orders the attributes in the output, which is bad for my current project due to some bad client code. Is there a scala-esque way to do this?

2
  • 14
    I'm shocked at how lousy the library seems to be in this regard. Commented Apr 3, 2010 at 0:32
  • Many good answers here. Also see stackoverflow.com/a/23092226/35274 Commented Aug 4, 2016 at 21:06

5 Answers 5

15
+150

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
Sign up to request clarification or add additional context in comments.

3 Comments

The design of the library does seem to have made some strange choices. You've come up with something more versatile than I did so... points for that.
I tried this out in Scala 2.9.1. A few minor things: the .toSeq wrapping the RewriteRule seems to be redundant, as a Node is a Seq[Node]. Also the attributes end up reversed.
Here's my fix: def unchainMetaData(m: MetaData): Iterable[GenAttr] = m.flatMap(decomposeMetaData).toList.reverse
12

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)

Comments

9

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>

Comments

1

With the help of Scalate's Scuery and its CSS3 selectors and transforms:

def modAttr(name: String, fn: Option[String] => Option[String])(node: Node) = node match {
  case e: Elem =>
    fn(e.attribute(name).map(_.toString))
      .map { newVal => e % Attribute(name, Text(newVal), e.attributes.remove(name)) }
      .getOrElse(e)
}

$("#foo > div[bar]")(modAttr("bar", _ => Some("hello")))

— this transforms e.g. this

<div id="foo"><div bar="..."/></div>

into

<div id="foo"><div bar="hello"/></div>`

3 Comments

Scuery now lives on github
Thanks for that, good news also (means it's alive); updated the answer.
...too bad the issue tracker stayed on Assembla — it could have been migrated to Github as well!
0

I found it easier to create a separate XML snippet and merge. This code fragment also demonstrates removing elements, adding extra elements and using variables in an XML literal:

val alt = orig.copy(
  child = orig.child.flatMap {
    case b: Elem if b.label == "b" =>
      val attr2Value = "100"
      val x = <x attr1="200" attr2={attr2Value}/>  //////////////////// Snippet
      Some(b.copy(attributes = b.attributes.append(x.attributes)))

    // Will remove any <remove-me some-attrib="specific value"/> elems
    case removeMe: Elem if isElem(removeMe, "remove-me", "some-attrib" -> "specific value") => 
      None

    case keep => Some(keep)
  }
    ++
      <added-elem name="..."/>

// Tests whether the given element has the given label
private def isElem(elem: Elem, desiredLabel: String, attribValue: (String, String)): Boolean = {
  elem.label == desiredLabel && elem.attribute(attribValue._1).exists(_.text == attribValue._2)
}

For other new-comers to Scala XML, you'll also need to add a separate Scala module to use XML in scala code.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.