Scala - modifying nested elements in xml

后端 未结 7 541
太阳男子
太阳男子 2020-12-02 11:11

I\'m learning scala, and I\'m looking to update a nested node in some xml. I\'ve got something working but i\'m wondering if its the most elegant way.

I have some xm

相关标签:
7条回答
  • 2020-12-02 11:42

    I have since learned more and presented what I deem to be a superior solution in another answer. I have also fixed this one, as I noticed I was failing to account for the subnode restriction.

    Thanks for the question! I just learned some cool stuff when dealing with XML. Here is what you want:

    def updateVersion(node: Node): Node = {
      def updateNodes(ns: Seq[Node], mayChange: Boolean): Seq[Node] =
        for(subnode <- ns) yield subnode match {
          case <version>{ _ }</version> if mayChange => <version>2</version>
          case Elem(prefix, "subnode", attribs, scope, children @ _*) =>
            Elem(prefix, "subnode", attribs, scope, updateNodes(children, true) : _*)
          case Elem(prefix, label, attribs, scope, children @ _*) =>
            Elem(prefix, label, attribs, scope, updateNodes(children, mayChange) : _*)
          case other => other  // preserve text
        }
    
      updateNodes(node.theSeq, false)(0)
    }
    

    Now, explanation. First and last case statements should be obvious. The last one exists to catch those parts of an XML which are not elements. Or, in other words, text. Note in the first statement, though, the test against the flag to indicate whether version may be changed or not.

    The second and third case statements will use a pattern matcher against the object Elem. This will break an element into all its component parts. The last parameter, "children @ _*", will match children to a list of anything. Or, more specifically, a Seq[Node]. Then we reconstruct the element, with the parts we extracted, but pass the Seq[Node] to updateNodes, doing the recursion step. If we are matching against the element subnode, then we change the flag mayChange to true, enabling the change of the version.

    In the last line, we use node.theSeq to generate a Seq[Node] from Node, and (0) to get the first element of the Seq[Node] returned as result. Since updateNodes is essentially a map function (for ... yield is translated into map), we know the result will only have one element. We pass a false flag to ensure that no version will be changed unless a subnode element is an ancestor.

    There is a slightly different way of doing it, that's more powerful but a bit more verbose and obscure:

    def updateVersion(node: Node): Node = {
      def updateNodes(ns: Seq[Node], mayChange: Boolean): Seq[Node] =
        for(subnode <- ns) yield subnode match {
          case Elem(prefix, "version", attribs, scope, Text(_)) if mayChange => 
            Elem(prefix, "version", attribs, scope, Text("2"))
          case Elem(prefix, "subnode", attribs, scope, children @ _*) =>
            Elem(prefix, "subnode", attribs, scope, updateNodes(children, true) : _*)
          case Elem(prefix, label, attribs, scope, children @ _*) =>
            Elem(prefix, label, attribs, scope, updateNodes(children, mayChange) : _*)
          case other => other  // preserve text
        }
    
      updateNodes(node.theSeq, false)(0)
    }
    

    This version allows you to change any "version" tag, whatever it's prefix, attribs and scope.

    0 讨论(0)
  • 2020-12-02 11:55

    One approach would be lenses (e.g. scalaz's). See http://arosien.github.io/scalaz-base-talk-201208/#slide35 for a very clear presentation.

    0 讨论(0)
  • 2020-12-02 11:58

    I really don't know how this could be done elegantly. FWIW, I would go for a different approach: use a custom model class for the info you're handling, and have conversion to and from Xml for it. You're probably going to find it's a better way to handle the data, and it's even more succint.

    However there is a nice way to do it with Xml directly, I'd like to see it.

    0 讨论(0)
  • 2020-12-02 12:01

    I think the original logic is good. This is the same code with (shall I dare to say?) a more Scala-ish flavor:

    def updateVersion( node : Node ) : Node = {
       def updateElements( seq : Seq[Node]) : Seq[Node] = 
         for( subNode <- seq ) yield updateVersion( subNode )  
    
       node match {
         case <root>{ ch @ _* }</root> => <root>{ updateElements( ch ) }</root>
         case <subnode>{ ch @ _* }</subnode> => <subnode>{ updateElements( ch ) }</subnode>
         case <version>{ contents }</version> => <version>2</version>
         case other @ _ => other
       }
     }
    

    It looks more compact (but is actually the same :) )

    1. I got rid of all the unnecessary brackets
    2. If a bracket is needed, it starts in the same line
    3. updateElements just defines a var and returns it, so I got rid of that and returned the result directly

    if you want, you can get rid of the updateElements too. You want to apply the updateVersion to all the elements of the sequence. That's the map method. With that, you can rewrite the line

    case <subnode>{ ch @ _* }</subnode> => <subnode>{ updateElements( ch ) }</subnode>
    

    with

    case <subnode>{ ch @ _* }</subnode> => <subnode>{ ch.map(updateVersion (_)) }</subnode>
    

    As update version takes only 1 parameter I'm 99% sure you can omit it and write:

    case <subnode>{ ch @ _* }</subnode> => <subnode>{ ch.map(updateVersion) }</subnode>
    

    And end with:

    def updateVersion( node : Node ) : Node = node match {
             case <root>{ ch @ _* }</root> => <root>{ ch.map(updateVersion )}</root>
             case <subnode>{ ch @ _* }</subnode> => <subnode>{ ch.map(updateVersion ) }</subnode>
             case <version>{ contents }</version> => <version>2</version>
             case other @ _ => other
           }
    

    What do you think?

    0 讨论(0)
  • 2020-12-02 12:06

    Scales Xml provides tools for "in place" edits. Of course its all immutable but here's the solution in Scales:

    val subnodes = top(xml).\*("subnode"l).\*("version"l)
    val folded = foldPositions( subnodes )( p => 
      Replace( p.tree ~> "2"))
    

    The XPath like syntax is a Scales signature feature, the l after the string specifies it should have no namespace (local name only).

    foldPositions iterates over the resulting elements and transforms them, joining the results back together.

    0 讨论(0)
  • 2020-12-02 12:07

    You can use Lift's CSS Selector Transforms and write:

    "subnode" #> ("version *" #> 2)
    

    See http://stable.simply.liftweb.net/#sec:CSS-Selector-Transforms

    0 讨论(0)
提交回复
热议问题