xmlscalascala-2.10scala-xml

Add or append new element to XML file in Scala instead of replacing it


My scala code currently ends up replacing an entire section of my xml file with the new tag that I'm adding. I want it to only add the tag once as a child of ClientConfig but it replaces all the tags present in this section with itself.

val data = XML.load(file)
val p = new XMLPrettyPrinter(2)
val tryingtoAdd = addNewEntry(data,host,env)
p.write(tryingtoAdd)(System.out)

where host=bob and env=flat are previously defined and addNewEntry is defined as follows

 private def isCorrectLocation(parent: Elem, node: Elem, host: String): Boolean = {
    parent.label == "ClientConfig" && node.label == "host"
  }

  def addNewEntry(elem:Elem, host: String, env: String): Elem ={
    val toAdd = <host name={host} env={env} />
    def addNew(current: Elem): Elem = current.copy(
      child = current.child.map {
        case e: Elem if isCorrectLocation(current, e, host) ⇒ toAdd
        case e: Elem ⇒ addNew(e)
        case other ⇒ other
      }
    )
    addNew(elem)
  }

The xml it produces is

<ClientConfig>
    <host name="bob" env="flat"/>
    <host name="bob" env="flat"/>
    <host name="bob" env="flat"/>
    <host name="bob" env="flat"/>
</ClientConfig>

where instead I want it to just append it as a single child of ClientConfig such as this where the last three children were already present in the file

<ClientConfig>
    <host name="bob" env="flat"/>
    <host name="george" env="flat"/>
    <host name="alice" env="flat"/>
    <host name="bernice" env="flat"/>
</ClientConfig>

What do i do? For example python has a simple insert method


Solution

  • In your case, when the pattern match goes to

    case e: Elem if isCorrectLocation(current, e, host) => toAdd
    

    The toAdd method will use the host, env you pass in addNewEntry(data, host, env). bob for host, flat for env. So, toAdd will always return <host name="bob" env="flat"/>.

    Assume you have the client.xml like this:

       <Root>
         <ServerConfig>
           <host name="allen" env="flat"/>
         </ServerConfig>
         <ClientConfig>
           <host name="george" env="flat"/>
           <host name="alice" env="flat"/>
           <host name="bernice" env="flat"/>
        </ClientConfig>
       </Root>
    

    The following code is how I try to get it done.

        def toBeAddedEntry(name: String, env: String) = <host name={ name } env={ env } />
        def addNewEntry(originalXML: Elem, name: String, env: String) = {
          originalXML match {
             case e @ Elem(_, _, _, _, configs @ _*) => {
                val changedNodes = configs.map {
                    case <ClientConfig>{ innerConfigs @ _* }</ClientConfig> => {
                        <ClientConfig> { toBeAddedEntry(name, env) ++ innerConfigs }</ClientConfig>
                    }
                    case other => other
                 }
                e.copy(child = changedNodes)
             }
             case _ => originalXML
         }
       }   
    
        val originalXML = XML.load("client.xml")
        val printer = new scala.xml.PrettyPrinter(80,5)
        println(printer.format(addNewEntry(originalXML, "bob", "flat")))
    
    
        // result
        <Root>
          <ServerConfig>
            <host env="flat" name="allen"/>
         </ServerConfig>
         <ClientConfig>
           <host name="bob" env="flat"/>
           <host env="flat" name="george"/>
           <host env="flat" name="alice"/>
           <host env="flat" name="bernice"/>
        </ClientConfig>
       </Root>
    

    Besides, I notice one thing during the process. XML.load actually reverses attribute order, maybe it's irrelevant for solving your problem but just adding it here in case you need it.