All Downloads are FREE. Search and download functionalities are using the official Maven repository.

olon.json.Xml.scala Maven / Gradle / Ivy

The newest version!
package olon
package json

/** Functions to convert between JSON and XML.
  */
object Xml {
  import scala.xml._

  /** Converts the given XML to JSON.
    *
    * The following rules are used in the conversion:
    *
    *   - an XML leaf element is converted to a JSON string
    *   - an XML parent element is converted to a JSON object and its children
    *     to JSON fields
    *   - XML elements with the same name at the same level are converted to a
    *     JSON array
    *   - XML attributes are converted to JSON fields
    *
    * For example:
    * {{{
    * scala> val xml =
    *     
    *       
    *         1
    *         Harry
    *       
    *       
    *         2
    *         David
    *       
    *     
    *
    * scala> val json = toJson(xml)
    * scala> prettyRender(json)
    *
    * {
    *   "users":{
    *     "user":[{
    *       "id":"1",
    *       "name":"Harry"
    *     },{
    *       "id":"2",
    *       "name":"David"
    *     }]
    *   }
    * }
    * }}}
    *
    * Now, the above example has two problems. First, the id is converted to a
    * `String` while we might want it as an `Int`. This is easy to fix by
    * mapping `JString(s)` to `JInt(s.toInt)`. The second problem is more
    * subtle: the conversion function decides to use a JSON array because
    * there's more than one `user` element in the XML. Therefore a structurally
    * equivalent XML document which happens to have just one `user` element will
    * generate a JSON document without a JSON array. This is rarely a desired
    * outcome. Both of these problems can be fixed by the following `map`
    * invocation:
    *
    * {{{
    * json mapField {
    *   case JField("id", JString(s)) => JField("id", JInt(s.toInt))
    *   case JField("user", x: JObject) => JField("user", JArray(x :: Nil))
    *   case x => x
    * }
    * }}}
    */
  def toJson(xml: NodeSeq): JValue = {
    def empty_?(node: Node) = node.child.isEmpty

    /* Checks if given node is leaf element. For instance these are considered leafs:
     * bar, { doSomething() }, etc.
     */
    def leaf_?(node: Node) = {
      def descendant(n: Node): List[Node] = n match {
        case g: Group => g.nodes.toList.flatMap(x => x :: descendant(x))
        case _        => n.child.toList.flatMap { x => x :: descendant(x) }
      }

      !descendant(node).find(_.isInstanceOf[Elem]).isDefined
    }

    def array_?(nodeNames: Seq[String]) =
      nodeNames.size != 1 && nodeNames.toList.distinct.size == 1
    def directChildren(n: Node): NodeSeq =
      n.child.filter(c => c.isInstanceOf[Elem])
    def nameOf(n: Node) =
      (if (n.prefix ne null) n.prefix + ":" else "") + n.label
    def buildAttrs(n: Node) =
      n.attributes.map((a: MetaData) => (a.key, XValue(a.value.text))).toList

    sealed trait XElem
    case class XValue(value: String) extends XElem
    case class XLeaf(value: (String, XElem), attrs: List[(String, XValue)])
        extends XElem
    case class XNode(fields: List[(String, XElem)]) extends XElem
    case class XArray(elems: List[XElem]) extends XElem

    def toJValue(x: XElem): JValue = x match {
      case XValue(s) => JString(s)
      case XLeaf((name, value), attrs) =>
        (value, attrs) match {
          case (_, Nil)         => toJValue(value)
          case (XValue(""), xs) => JObject(mkFields(xs))
          case (_, xs) => JObject(JField(name, toJValue(value)) :: mkFields(xs))
        }
      case XNode(xs)     => JObject(mkFields(xs))
      case XArray(elems) => JArray(elems.map(toJValue))
    }

    def mkFields(xs: List[(String, XElem)]) =
      xs.flatMap { case (name, value) =>
        (value, toJValue(value)) match {
          // This special case is needed to flatten nested objects which resulted from
          // XML attributes. Flattening keeps transformation more predicatable.
          // x -> {"a":{"foo":{"foo":"x","id":"1"}}} vs
          // x -> {"a":{"foo":"x","id":"1"}}
          case (XLeaf(_, _ :: _), o: JObject) => o.obj
          case (_, json)                      => JField(name, json) :: Nil
        }
      }

    def buildNodes(xml: NodeSeq): List[XElem] = xml match {
      case n: Node =>
        if (empty_?(n)) XLeaf((nameOf(n), XValue("")), buildAttrs(n)) :: Nil
        else if (leaf_?(n))
          XLeaf((nameOf(n), XValue(n.text)), buildAttrs(n)) :: Nil
        else {
          val children = directChildren(n)
          XNode(
            buildAttrs(n) ::: children
              .map(nameOf)
              .toList
              .zip(buildNodes(children))
          ) :: Nil
        }
      case nodes: NodeSeq =>
        val allLabels = nodes.map(_.label)
        if (array_?(allLabels)) {
          val arr = XArray(nodes.toList.flatMap { n =>
            if (leaf_?(n) && n.attributes.length == 0) XValue(n.text) :: Nil
            else buildNodes(n)
          })
          XLeaf((allLabels(0), arr), Nil) :: Nil
        } else nodes.toList.flatMap(buildNodes)
    }

    buildNodes(xml) match {
      case List(x @ XLeaf(_, _ :: _)) => toJValue(x)
      case List(x) => JObject(JField(nameOf(xml.head), toJValue(x)) :: Nil)
      case x       => JArray(x.map(toJValue))
    }
  }

  /** Converts the given JSON to XML.
    *
    * The following rules are used in conversion:
    *
    *   - JSON primitives are converted to XML leaf elements
    *   - JSON objects are converted to XML elements
    *   - JSON arrays are recursively converted to XML elements
    *
    * Use the `map` function to preprocess JSON before conversion to adjust the
    * end result. For instance a common conversion is to encode arrays as comma
    * separated Strings since XML does not have an array type:
    *
    * {{{
    * toXml(json map {
    *   case JField("nums",JArray(ns)) => JField("nums",JString(ns.map(_.values).mkString(",")))
    *   case x => x
    * })
    * }}}
    */
  def toXml(json: JValue): NodeSeq = {
    def toXml(name: String, json: JValue): NodeSeq = json match {
      case JObject(fields) =>
        new XmlNode(name, fields flatMap { case JField(n, v) => toXml(n, v) })
      case JArray(xs) => xs flatMap { v => toXml(name, v) }
      case JInt(x)    => new XmlElem(name, x.toString)
      case JDouble(x) => new XmlElem(name, x.toString)
      case JString(x) => new XmlElem(name, x)
      case JBool(x)   => new XmlElem(name, x.toString)
      case JNull      => new XmlElem(name, "null")
      case JNothing   => Text("")
    }

    json match {
      case JObject(fields) =>
        fields flatMap { case JField(name, value) => toXml(name, value) }
      case x => toXml("root", x)
    }
  }

  // SCALA3 using `x*` instead of `_*`
  private[json] class XmlNode(name: String, children: Seq[Node])
      extends Elem(null, name, scala.xml.Null, TopScope, true, children*)

  private[json] class XmlElem(name: String, value: String)
      extends Elem(null, name, scala.xml.Null, TopScope, true, Text(value))
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy