2010-04-02 9 views
20

Ho un file XML che vorrei mappare alcuni attributi di dentro con uno script. Ad esempio:Come modificare l'attributo su Scala XML Element

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

sarebbe potuto attributi scalato di un fattore di due:

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

Questa pagina ha un suggerimento per aggiunta attributi ma non riporta in dettaglio un modo per mappare un attributo corrente con una funzione (in questo modo farebbe che molto difficile): http://www.scalaclass.com/book/export/html/1

quello che è venuta in mente è quello di creare manualmente l'XML (non Scala) lista concatenata ... qualcosa di simile:

// 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 
} 

È orribile, prolisso e ri-ordina inutilmente gli attributi nell'output, il che è negativo per il mio progetto corrente a causa di un codice client errato. Esiste un modo scala-esque per fare questo?

+13

Sono scioccato da quanto pessima sia la biblioteca in questo senso. –

+0

Molte buone risposte qui. Vedi anche http://stackoverflow.com/a/23092226/35274 – Philippe

risposta

15

Ok, miglior sforzo, Scala 2.8. Abbiamo bisogno di ricostruire gli attributi, il che significa che dobbiamo decompongarli correttamente.Creiamo una funzione per questo:

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)) 
} 

Quindi, diamo decompongono gli attributi concatenati in una sequenza:

def unchainMetaData(m: MetaData): Iterable[GenAttr] = 
    m flatMap (decomposeMetaData) 

A questo punto, si può facilmente manipolare questa lista:

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 
} 

Ora, ricollegalo:

def chainMetaData(l: Iterable[GenAttr]): MetaData = l match { 
    case Nil => Null 
    case head :: tail => head.copy(next = chainMetaData(tail)).toMetaData 
} 
.210

Ora, dobbiamo solo creare una funzione di prendersi cura di queste cose:

def mapMetaData(m: MetaData)(f: GenAttr => GenAttr): MetaData = 
    chainMetaData(unchainMetaData(m).map(f)) 

in modo che possiamo usare in questo modo:

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) 

che alla fine permettono di fare la traduzione che si voleva:

rt.transform(<a><b attr1="100" attr2="50"></b></a>) 

Tutto questo potrebbe essere semplificata se:

  • Abilità effetti definito prefisso chiave e il valore, con un prefisso facoltativo
  • Abilità era una sequenza, non una catena
  • Abilità aveva una mappa, le macro, mapValues ​​
  • Elem aveva un mapAttribute
+1

Il design della libreria sembra aver fatto delle scelte strane. Hai trovato qualcosa di più versatile di quello che ho fatto ... punti per questo. – Dave

+1

Ho provato questo in Scala 2.9.1. Alcune cose minori: il .toSeq che avvolge il RewriteRule sembra essere ridondante, come un Nodo è un Seq [Nodo]. Anche gli attributi finiscono invertiti. –

+1

Ecco la mia correzione: def unchainMetaData (m: MetaData): Iterable [GenAttr] = m.flatMap (decomposeMetaData) .toList.reverse –

8

Quindi, se fossi nella tua posizione, penso che quello che avevo molta voglia di scrivere è qualcosa di simile:

case elem: Elem => elem.copy(attributes= 
    for (attr <- elem.attributes) yield attr match { 
    case [email protected]("attr1", _, _) => 
     attr.copy(value=attr.value.text.toInt * 2) 
    case [email protected]("attr2", _, _) => 
     attr.copy(value=attr.value.text.toInt * -1) 
    case other => other 
    } 
) 

ci sono due ragioni per cui questo won 'T lavorare fuori dalla scatola:

  1. Attribute non hai un metodo utile copy, e
  2. Mapping nel corso di un MetaData produce una Iterable[MetaData] invece di un MetaData quindi, anche qualcosa di semplice come elem.copy(attributes=elem.attributes.map(x => x)) fallirà.

Per fissare il primo problema, useremo un implicito per aggiungere un metodo di copia migliore per 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) 
} 

Non può essere nominato dal copy un metodo con questo nome esiste già, quindi lo chiameremo semplicemente goodcopy. (Inoltre, se crei valori che sono Seq[Node] invece di cose che dovrebbero essere convertite in stringhe, potresti fare un po 'più attenzione con value, ma per i nostri scopi attuali non è necessario.)

Per risolvere il secondo problema, useremo un implicito per spiegare come creare un MetaData da un Iterable[MetaData]:

implicit def iterableToMetaData(items: Iterable[MetaData]): MetaData = { 
    items match { 
    case Nil => Null 
    case head :: tail => head.copy(next=iterableToMetaData(tail)) 
    } 
} 

Quindi è possibile scrivere codice molto simile a quello che ho proposto all'inizio:

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 [email protected]("attr1", _, _) => 
    |  attr.goodcopy(value=attr.value.text.toInt * 2) 
    |  case [email protected]("attr2", _, _) => 
    |  attr.goodcopy(value=attr.value.text.toInt * -1) 
    |  case other => other 
    | } 
    |) 
res1: scala.xml.Elem = <b attr1="200" attr2="-50"></b> 
11

Questo è come si può fare usando 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) 
1

Con l'aiuto di Scalate's Scuery e dei suoi selettori CSS3 e trasforma:

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"))) 

- questo trasforma per esempio questo

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

in

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

Scuery ora vive [su github] (http://scalate.github.io/scalate/documentation /scuery.html) – millhouse

+0

Grazie anche a ciò, buone notizie (significa che è vivo); aggiornato la risposta. –

+0

... peccato che il tracker dei problemi sia rimasto su Assembla - potrebbe essere stato migrato anche su Github! –

0

ho trovato più facile per creare un frammento XML separato e si fondono. Questo frammento di codice dimostra anche la rimozione di elementi, l'aggiunta di elementi aggiuntivi e utilizzando le variabili in un letterale XML:

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) 
} 

Per gli altri nuovi arrivati ​​a Scala XML, avrete anche bisogno di aggiungere un separate Scala module da utilizzare XML in codice Scala .