How to change attribute on Scala XML Element

后端 未结 5 790
渐次进展
渐次进展 2020-12-13 04:41

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


  


        
相关标签:
5条回答
  • 2020-12-13 05:13

    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>
    
    0 讨论(0)
  • 2020-12-13 05:14

    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)
    
    0 讨论(0)
  • 2020-12-13 05:17

    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.

    0 讨论(0)
  • 2020-12-13 05:23

    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>`
    
    0 讨论(0)
  • 2020-12-13 05:35

    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
    0 讨论(0)
提交回复
热议问题