org.json4s.Xml.scala Maven / Gradle / Ivy
/*
* Copyright 2009-2010 WorldWide Conferencing, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.json4s
/** Functions to convert between JSON and XML.
*/
object Xml {
import scala.xml._
/** Convert given XML to JSON.
*
* Following rules are used in conversion.
*
* - XML leaf element is converted to JSON string
* - XML parent element is converted to JSON object and its children to JSON fields
* - XML elements with same name at same level are converted to JSON array
* - XML attributes are converted to JSON fields
*
*
* Example:
* scala> val xml =
* <users>
* <user>
* <id>1</id>
* <name>Harry</name>
* </user>
* <user>
* <id>2</id>
* <name>David</name>
* </user>
* </users>
*
* scala> val json = toJson(xml)
* scala> pretty(render(json))
*
* {
* "users":{
* "user":[{
* "id":"1",
* "name":"Harry"
* },{
* "id":"2",
* "name":"David"
* }]
* }
* }
*
*
* Now, the above example has two problems. First, the id is converted to 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 JSON array
* because there's more than one user-element in XML. Therefore a structurally equivalent
* XML document which happens to have just one user-element will generate a JSON document
* without JSON array. This is rarely a desired outcome. These both problems can be fixed
* by following map function.
*
* json map {
* 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((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(v, x :: xs), 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))
}
}
/** Convert given JSON to XML.
*
* 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 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 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 (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 JDecimal(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 (n, v) => toXml(n, v) }
case x => toXml("root", x)
}
}
private[json4s] class XmlNode(name: String, children: Seq[Node]) extends Elem(null, name, xml.Null, TopScope, children :_*)
private[json4s] class XmlElem(name: String, value: String) extends Elem(null, name, xml.Null, TopScope, Text(value))
}