二十、XML 和 JSON
在第 19 章中,我们学习了如何在我们的 Android 项目中包含外部库。Kotlin 的标准库中没有专门的 XML 和 JSON 处理类,所以为了完成 XML 和 JSON 相关的任务,我们使用适当的外部库,并以扩展函数的形式添加一些方便的函数。
注意
XML 和 JSON 都是结构化数据的格式规范。如果您的 Android 应用与外部世界通信以接收或发送标准化格式的数据,您将会经常使用它们。
本章假设您有一个示例应用,可以用来测试所提供的代码片段。使用你喜欢的任何应用或我们在本书中开发的应用之一。例如,您可以添加一些示例代码,为 activity 的onCreate()
函数内部的测试提供Log
输出,或者您可以使用一个使用 Android 测试方法的测试类。选择最适合您需求的方法。
XML 处理
XML 文件最简单的形式如下:
<?xml version="1.0" encoding="UTF-8"?>
<ProbeMsg>
<TimeStamp>2016-10-30T19:07:07Z</TimeStamp>
<ProbeId>1A6G</ProbeId>
<ProbeValue ScaleUnit="cm">37.4</ProbeValue>
<Meta>
<Generator>045</Generator>
<Priority>-3</Priority>
<Actor>P. Rosengaard</Actor>
</Meta>
</ProbeMsg>
注意
XML 允许更复杂的结构,如模式验证和名称空间。在本章中,我们只描述 XML 标签、属性和文本内容。您可以自由扩展本章中介绍的示例和实用程序函数,以包含这些扩展功能。
对于 XML 处理,使用以下范例之一或组合。
-
DOM 模型:完整的树处理:在文档对象模型(DOM)中,XML 数据被视为一个整体,由内存中的树状结构表示。
-
SAX:基于事件的处理:在这里,XML 文件被解析,每个元素或属性都会触发一个适当的事件。事件由回调函数接收,回调函数必须向 SAX 处理器注册。这种“告诉我你在做什么”的处理方式通常被称为推送解析。
-
StAX:基于流的处理:在这里,您执行诸如“给我下一个 XML 元素”之类的操作。与 SAX 不同,在 SAX 中我们有一个推送解析,对于 StAX,我们告诉解析器它必须做什么:“我告诉你你做什么。”这因此被称为拉解析。
在 Android 上,你通常处理小到中等大小的 XML 文件。因此,在本章中我们使用 DOM。对于读取,我们首先解析完整的 XML 文件,并将数据存储在内存中的 DOM 树中。在这里,像删除、更改或添加元素这样的操作很容易完成,并且发生在内存中;因此它们非常快。为了编写,我们从内存中取出完整的 DOM 树,并从中生成一个 XML 字符流,也许将结果写回到一个文件中。
对于 XML 处理,我们添加了 Java 参考实现 Xerces 作为外部库。在 Android Studio 中,打开模块的build.gradle
文件,在dependencies
部分添加:
implementation 'xerces:xercesImpl:2.12.0'
注意
Xerces 还实现了 SAX 和 StAX APIs,尽管我们将只使用它的 DOM 实现。
读取 XML 数据
借助 Xerces 实现,我们可以使用的 DOM 实现已经包含了读取 XML 元素所需的一切。然而,我们将添加几个扩展函数,大大提高 DOM API 的可用性。为此,创建一个包com.example.domext
,或者您也可以使用任何其他合适的包名。在这个包中添加一个 Kotlin 文件dom.kt
,其内容如下:
package com.example.domext
import org.apache.xerces.parsers.DOMParser
import org.w3c.dom.Document
import org.w3c.dom.Node
import org.xml.sax.InputSource
import java.io.StringReader
import java.io.StringWriter
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
fun parseXmlToDOM(s:String) : Document {
val parser: DOMParser = DOMParser()
return parser.let {
it.parse(InputSource(StringReader(s)))
it.document
}
}
fun Node.fetchChildren(withText:Boolean = false) =
(0..(this.childNodes.length - 1)).
map { this.childNodes.item(it) }.
filter { withText || it.nodeType != Node.TEXT_NODE }
fun Node.childCount() = fetchChildren().count()
fun Node.forEach(withText:Boolean = false,
f:(Node) -> Unit) {
fetchChildren(withText).forEach { f(it) }
}
operator fun Node.get(i:Int) = fetchChildren()[i]
operator fun Node.invoke(s:String): Node =
if(s.startsWith("@")) {
this.attributes.getNamedItem(s.substring(1))
}else{
this.childNodes.let { nl ->
val iter = object : Iterator<Node> {
var i: Int = 0
override fun next() = nl.item(i++)
override fun hasNext() = i < nl.length
}
iter.asSequence().find { it.nodeName == s }!!
}
}
operator fun Node.invoke(vararg s:String): Node =
s.fold(this, { acc, s1 -> acc(s1) })
fun Node.text() = this.firstChild.nodeValue
fun Node.name() = this.nodeName
fun Node.type() = this.nodeType
这些都是org.w3c.dom.Node
的包级函数和扩展函数,具有以下特点:
-
在 DOM API 中,树中的每个元素(例如,从本节开始的 XML 数据中的
ProbeValue
)都由一个Node
实例表示。 -
我们添加了一个
parseXmlToDOM(s:String)
包级函数,将 XML 字符串转换为Document
。 -
我们给
Node
添加了一个fetchChildren()
函数,它返回一个节点的所有非文本子节点,这些子节点忽略了文本元素。如果添加with- Text=true
作为参数,元素的文本节点会包含在子列表中,即使它们只包含空格和换行符。例如,在本节开头的 XML 数据中,节点Meta
有三个子节点:Generator
、Priority
和Actor
。使用withText=true
,它们之间的空格和换行符也将被返回。 -
我们给
Node
添加了一个childCount()
函数,它计算一个节点的子元素的数量,不考虑文本元素。官方的 DOM API 没有为此提供功能。 -
我们给
Node
添加了一个forEach()
函数,允许我们以 Kotlin 的方式遍历一个节点的子节点。最初的 DOM API 没有提供这样的迭代器,因为它只有函数和属性hasChild- Nodes()
、childNodes.length
和childNodes.item(index:Int)
来遍历子元素。如果添加withText=true
作为参数,元素的文本节点将包含在子列表中,即使它们只包含空格和换行符。 -
我们为
Node
添加了一个get(i:Int)
函数,以从元素中获取某个子元素,而不考虑文本节点。 -
我们重载了
Node
的invoke
运算符,属于括号()
。带有String
参数的第一个变量通过名称导航到一个孩子:node("cn")
=node
→名为“cn”的孩子如果参数以一个@
开始,属性被寻址:node("@an")
=node
→属性名为“an”在后一种情况下,您仍然需要调用text()
来获得字符串形式的属性值。 -
重载的
invoke
操作符的第二个变体允许我们指定几个字符串,从一个子元素导航到另一个子元素,等等。 -
我们向
Node
添加函数:首先,text()
获取元素的文本内容,然后name()
给出节点名,然后type()
计算节点类型(可能的值参见Node
类的常量属性)。
警告
为了简单起见,本节中显示的 DOM 处理代码片段没有以合理的方式处理异常。在将代码用于生产项目之前,必须添加适当的错误处理。
这个片段提供了如何使用 API 和扩展的例子。
import ...
import com.example.domext.*
...
val xml = """<?xml version="1.0" encoding="UTF-8"?>
<ProbeMsg>
<TimeStamp>2016-10-30T19:07:07Z</TimeStamp>
<ProbeId>1A6G</ProbeId>
<ProbeValue ScaleUnit="cm">37.4</ProbeValue>
<Meta>
<Generator>045</Generator>
<Priority>-3</Priority>
<Actor>P. Rosengaard</Actor>
</Meta>
</ProbeMsg>"""
try {
// Parse the complete XML document
val dom = parseXmlToDOM(xml)
// Access an element
val ts = dom("ProbeMsg")("TimeStamp").text()
Log.d("LOG", ts) // 2001-11-30T09:08:07Z
// Access an attribute
val uni = dom("ProbeMsg")("ProbeValue")("@ScaleUnit")
Log.d("LOG", uni.text()) // cm
// Simplified XML tree navigation
val uni2 = dom("ProbeMsg","ProbeValue","@ScaleUnit")
Log.d("LOG", uni2.text()) // cm
// Iterate through an element's children
dom("ProbeMsg")("Meta").forEach { n ->
Log.d("LOG", n.name() + ": " + n.text())
// Generator: 045
// Priority: -3
// Actor: P. Rosengaard
}
}catch(e:Exception) {
Log.e("LOG", "Cannot parse XML", e)
}
...
改变 XML 数据
一旦我们在内存中有了 XML 树的 DOM 表示,我们就可以添加元素了。虽然我们可以使用 DOM API 已经提供的函数,但是 Kotlin 允许我们提高表达能力。为此,将以下代码添加到我们的扩展文件dom.kt
(我不添加新的导入;按 Alt+Enter 让 Android Studio 帮你添加必要的导入):
fun prettyFormatXml(document:Document): String {
val format = OutputFormat(document).apply { lineWidth = 65
indenting = true
indent = 2
}
val out = StringWriter()
val serializer = XMLSerializer(out, format)
serializer.serialize(document)
return out.toString()
}
fun prettyFormatXml(unformattedXml: String) =
prettyFormatXml(parseXmlToDOM(unformattedXml))
fun Node.toXmlString():String {
val transformerFact = TransformerFactory.newInstance()
val transformer = transformerFact.newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
val source = DOMSource(this)
val writer = StringWriter()
val result = StreamResult(writer)
transformer.transform(source, result)
return writer.toString()
}
operator fun Node.plusAssign(child:Node) {
this.appendChild(child)
}
fun Node.addText(s:String): Node {
val doc = ownerDocument
val txt = doc.createTextNode(s)
appendChild(txt)
return this
}
fun Node.removeText() {
if(hasChildNodes() && firstChild.nodeType == Node.TEXT_NODE)
removeChild(firstChild)
}
fun Node.updateText(s:String) : Node { removeText()
return addText(s)
}
fun Node.addAttribute(name:String, value:String): Node {
(this as Element).setAttribute(name, value)
return this
}
fun Node.removeAttribute(name:String) {
this.attributes.removeNamedItem(name)
}
这是我们在这种情况下的描述
-
功能
prettyFormatXml( document: Document )
和prettyFormatXml( unformattedXml: String )
是主要用于诊断目的的实用功能。给定一个Document
或者一个无格式的 XML 字符串,它们创建一个漂亮的字符串。 -
扩展函数
Node.toXmlString()
从当前节点开始创建 XML 子树的字符串表示。如果对Document
这样做,整个 DOM 结构都将被转换。 -
我们重载
Node
的plusAssign
操作符(对应于+=
)来添加一个子节点。 -
我们为
Node
添加了一个addText()
扩展,用于向节点添加文本内容。 -
我们为
Node
添加了一个removeText()
扩展,用于从节点中删除文本内容。 -
我们为
Node
添加了一个updateText()
扩展,用于改变节点的文本内容。 -
我们为
Node
添加了一个addAttribute()
扩展,用于向节点添加属性。 -
我们为
Node
添加了一个removeAttribute()
扩展,用于从节点中删除属性。 -
我们为
Node
添加了一个updateAttribute()
扩展,用于改变节点的属性。
例如,这些函数的用例包括以下代码片段。首先,我们向给定的节点添加一个元素加属性:
val xml = """<?xml version="1.0" encoding="UTF-8"?>
<ProbeMsg>
<TimeStamp>2016-10-30T19:07:07Z</TimeStamp>
<ProbeId>1A6G</ProbeId>
<ProbeValue ScaleUnit="cm">37.4</ProbeValue>
<Meta>
<Generator>045</Generator>
<Priority>-3</Priority>
<Actor>P. Rosengaard</Actor>
</Meta>
</ProbeMsg>"""
try {
val dom = parseXmlToDOM(xml)
val msg = dom("ProbeMsg")
val meta = msg("Meta")
// Add a new element to "meta".
meta += dom.createElement("NewMeta").
addText("NewValue").
addAttribute("SomeAttr", "AttrVal")
Log.d("LOG", "\n\n" + prettyFormatXml(dom))
}catch(e:Exception) { Log.e("LOG", "XML Error", e)
}
为此,我们还使用了来自Document
类的createElement()
函数。最后,这段代码将修改后的 XML 写入日志控制台。
以下代码示例解释了如何更改和移除属性和元素:
val xml = """<?xml version="1.0" encoding="UTF-8"?>
<ProbeMsg>
<TimeStamp>2016-10-30T19:07:07Z</TimeStamp>
<ProbeId>1A6G</ProbeId>
<ProbeValue ScaleUnit="cm">37.4</ProbeValue>
<Meta>
<Generator>045</Generator>
<Priority>-3</Priority>
<Actor>P. Rosengaard</Actor>
</Meta>
</ProbeMsg>"""
try {
val dom = parseXmlToDOM(xml)
val msg = dom("ProbeMsg")
val ts = msg("TimeStamp")
val probeValue = msg("ProbeValue")
// Update an attribute and the text contents of
// an element.
probeValue.updateAttribute("ScaleUnit", "dm")
ts.updateText("1970-01-01T00:00:00Z")
Log.d("LOG", "\n\n" + prettyFormatXml(dom))
// Remove an attribute
probeValue.removeAttribute("ScaleUnit")
Log.d("LOG", "\n\n" + prettyFormatXml(dom))
// Removing a node means removing it from
// its parent node.
msg.removeChild(probeValue)
Log.d("LOG", "\n\n" + prettyFormatXml(dom))
}catch(e:Exception) {
Log.e("LOG", "XML Error", e)
}
创建新的 DOM
如果您需要从头开始编写 XML 文档的 DOM 表示,首先创建一个Document
实例。这个没有公共构造函数;相反,你应该写:
val doc = DocumentBuilderFactory.
newInstance().newDocumentBuilder().newDocument()
从这里,您可以像前面描述的那样添加元素。注意,要查看我们的prettyFormatXml()
实用函数的任何输出,您必须向doc
添加至少一个子元素。
练习 1
向dom.kt
文件添加一个createXmlDocument()
函数,以简化文档创建。
JSON 处理
JavaScript 对象表示法(JSON)是 XML 的小姐妹。与使用 XML 格式的数据相比,用 JSON 格式编写的数据需要更少的空间。此外,JSON 数据几乎自然地映射到浏览器环境中的 JavaScript 对象,因此 JSON 在最近几年获得了相当大的关注。
Kotlin 的标准库不知道如何处理 JSON 数据,所以,类似于 XML 处理,我们添加一个合适的外部库。从几种可能性来看,我们使用广泛采用的杰克逊图书馆。要将它添加到 Android 项目中,在模块的build.gradle
文件中的dependencies
部分添加
implementation
'com.fasterxml.jackson.core:jackson-core:2.9.8'
implementation
'com.fasterxml.jackson.core:jackson-databind:2.9.8'
(在两行上,删除换行符)。
JSON 处理有几种范例。最常用的是带有特定于 JSON 的对象的树状结构,以及带有各种半自动转换机制的 Kotlin 和 JSON 对象之间的映射。我们将映射方法留给您进一步的研究;它包含几个非常复杂的特性,主要是 JSON 集合映射。杰克逊的主页给了你更多的信息。相反,我们描述处理 JSON 数据的内存树表示的机制。
对于本节的其余部分,我们使用以下 JSON 数据来解释示例中使用的函数:
val json = """{
"id":27,
"name":"Roger Rabbit",
"permanent":true,
"address":{
"street":"El Camino Real",
"city":"New York",
"zipcode":95014
},
"phoneNumbers":[9945678, 123456781],
"role":"President"
}"""
JSON 助手函数
用于 JSON 处理的 Jackson 库包含了编写、更新和删除 JSON 元素所需的全部内容。这个库非常广泛,包含了大量的类和函数。为了简化开发并包含 Kotlin 好东西,我们使用了一些包级函数和扩展函数来提高 JSON 代码的可读性。这些最好放在某个包com.whatever.ext
中的 Kotlin 文件json.kt
中。
我们从导入开始,添加一个invoke
操作符,这样我们可以很容易地从一个节点获取一个子节点,并添加一个remove
和一个forEach
函数,用于删除一个节点并遍历节点的子节点:
import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.*
import java.io.ByteArrayOutputStream
import java.math.BigInteger
operator fun JsonNode.invoke(s:String) = this.get(s)
operator fun JsonNode.invoke(vararg s:String) =
s.fold(this, { acc, s -> acc(s) })
fun JsonNode.remove(name:String) {
val on = (this as? ObjectNode)?:
throw Exception("This is not an object node")
on.remove(name) }
fun JsonNode.forEach(iter: (JsonNode) -> Unit ) {
when(this) {
is ArrayNode -> this.forEach(iter)
is ObjectNode -> this.forEach(iter)
else -> throw Exception("Cannot iterate over " +
this::class)
}
}
接下来,我们为asText()
添加简单的别名text()
,以简化文本提取:
fun JsonNode.text() = this.asText()
另一个迭代器遍历对象节点的子节点。这一次,我们还会考虑孩子们的名字:
fun JsonNode.forEach(iter: (String, JsonNode) -> Unit ) {
if(this !is ObjectNode)
throw Exception(
"Cannot iterate (key,val) over " + this::class)
this.fields().forEach{
(name, value) -> iter(name, value) }
}
为了编写一个对象节点的子节点,我们定义了一个put()
函数,因此我们可以编写node.put( "childName", 42 )
:
// Works only if the node is an ObjectNode!
fun JsonNode.put(name:String, value:Any?) : JsonNode {
if(this !is ObjectNode)
throw Exception("Cannot put() on none-object node")
when(value) {
null -> this.putNull(name)
is Int -> this.put(name, value)
is Long -> this.put(name, value)
is Short -> this.put(name, value)
is Float -> this.put(name, value)
is Double -> this.put(name, value)
is Boolean -> this.put(name, value)
is String -> this.put(name, value)
is JsonNode -> this.put(name, value)
else -> throw Exception(
"Illegal value type: ${value::class}")
}
return this
}
为了向数组对象追加一个值,我们定义了一个add()
函数,它适用于各种类型:
// Add a value to an array, works only if this is an
// ArrayNode
fun JsonNode.add(value:Any?) : JsonNode {
if(this !is ArrayNode)
throw Exception("Cannot add() on none-array node")
when(value) {
null -> this.addNull()
is Int -> this.add(value)
is Long -> this.add(value)
is Float -> this.add(value)
is Double -> this.add(value)
is Boolean -> this.add(value)
is String -> this.add(value)
is JsonNode -> this.add(value)
else -> throw Exception(
"Illegal value type: ${value::class}")
}
return this
}
对于 JSON 对象创建,我们定义了各种createSomething()
样式的函数,并且我们还添加了几个类似 Kotlin 的构建函数:
// Node creators
fun createJsonTextNode(text:String) = TextNode.valueOf(text)
fun createJsonIntNode(i:Int) = IntNode.valueOf(i)
fun createJsonLongNode(l:Long) = LongNode.valueOf(l)
fun createJsonShortNode(s:Short) = ShortNode.valueOf(s)
fun createJsonFloatNode(f:Float) = FloatNode.valueOf(f)
fun createJsonDoubleNode(d:Double) = DoubleNode.valueOf(d)
fun createJsonBooleanNode(b:Boolean) = BooleanNode.valueOf(b)
fun createJsonBigIntegerNode(b: BigInteger) = BigIntegerNode.valueOf(b)
fun createJsonNullNode() = NullNode.instance
fun jsonObjectNodeOf(
children: Map<String,JsonNode> = HashMap()) :
ObjectNode {
return ObjectNode(JsonNodeFactory.instance, children)
}
fun jsonObjectNodeOf(
vararg children: Pair<String,Any?>) :
ObjectNode {
return children.fold(
ObjectNode(JsonNodeFactory.instance), { acc, v ->
acc.put(v.first, v.second)
acc
})
}
fun jsonArrayNodeOf(elements: Array<JsonNode> =
emptyArray()) : ArrayNode {
return ArrayNode(JsonNodeFactory.instance,
elements.asList())
}
fun jsonArrayNodeOf(elements: List<JsonNode> =
emptyList()) : ArrayNode {
return ArrayNode(JsonNodeFactory.instance,
elements)
}
fun jsonEmptyArrayNode() : ArrayNode {
return ArrayNode(JsonNodeFactory.instance)
}
fun jsonArrayNodeOf(vararg elements: Any?) : ArrayNode {
return elements.fold(
ArrayNode(JsonNodeFactory.instance), { acc, v ->
acc.add(v)
acc
})
}
扩展函数toPrettyString()
和toJsonString()
可以用来生成任何 JSON 节点的字符串表示:
// JSON output as pretty string
fun JsonNode.toPrettyString(
prettyPrinter:PrettyPrinter? =
DefaultPrettyPrinter()) : String {
var res:String? = null
ByteArrayOutputStream().use { os ->
val gen = JsonFactory().createGenerator(os).apply {
if(prettyPrinter != null) this.prettyPrinter = prettyPrinter
}
val mapper = ObjectMapper()
mapper.writeTree(gen, this)
res = String(os.toByteArray())
}
return res!!
}
// JSON output as simple string
fun JsonNode.toJsonString() : String =
toPrettyString(prettyPrinter = null)
所有这些扩展函数的主要思想是通过向基本节点类JsonNode
添加 JSON 对象相关和 JSON 数组相关的函数来提高简洁性,并在运行时执行类强制转换。虽然它使 JSON 代码更小,更有表现力,但运行时出现异常的风险也增加了。
读取和写入 JSON 数据
要读入 JSON 数据,您只需编写:
val json = ... // see section beginning
val mapper = ObjectMapper()
val root = mapper.readTree(json)
从这里我们可以研究 JSON 元素,遍历并获取 JSON 对象成员,并提取 JSON 数组元素:
try {
val json = ... // see section beginning
val mapper = ObjectMapper()
val root = mapper.readTree(json)
// see what we got
Log.d("LOG", root.toPrettyString())
// type of the node
Log.d("LOG", root.nodeType.toString())
// <- OBJECT
// is it a container?
Log.d("LOG", root.isContainerNode.toString())
// <- true
root.forEach { k,v ->
Log.d("LOG",
"Key:${k} -> val:${v} (${v.nodeType})")
Log.d("LOG",
" <- " + v::class.toString())
}
val phones = root("phoneNumbers")
phones.forEach { ph ->
Log.d("LOG", "Phone: " + ph.text())
}
Log.d("LOG", "Phone[0]: " + phones[0].text())
val street = root("address")("street").text()
Log.d("LOG", "Street: " + street)
Log.d("LOG", "Zip: " + root(“address”, “zipcode”).asInt())
}catch(e:Exception) {
Log.e("LOG", "JSON error", e)
}
以下代码片段展示了如何通过添加、更改或删除节点或 JSON 对象成员来改变 JSON 树。
// add it to the "try" statements from the
// last listing
// remove an entry
root("address").remove("zipcode")
Log.d("LOG", root.toPrettyString())
// update an entry
root("address").put("street", "Fake Street 42")
Log.d("LOG", root.toPrettyString())
root("address").put("country", createJsonTextNode("Argentina"))
Log.d("LOG", root.toPrettyString())
// create a new object node
root.put("obj", jsonObjectNodeOf(
"abc1" to 23,
"abc2" to "Hallo",
"someNull" to null
))
Log.d("LOG", root.toPrettyString())
// create a new array node
root.put("arr", jsonArrayNodeOf(
23,
null,
"Hallo"
))
Log.d("LOG", root.toPrettyString())
// write without spaces or line breaks
Log.d("LOG", root.toJsonString())
创建新的 JSON 树
要在内存中创建新的 JSON 树,可以使用:
val root = jsonObjectNodeOf()
从那里,您可以像前面描述的那样添加 JSON 元素。
练习 2
创建一个 JSON 文档,对应于:
{
"firstName": "Arthur",
"lastName": "Doyle",
"dateOfBirth": "03/04/1997",
"address": {
"streetAddress": "21 3rd Street",
"city": "New York",
"state": "NY",
"postalCode": "10021-1234"
},
"phoneNumbers": [
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "mobile",
"number": "123 456-7890"
}
],
"children": [],
"spouse": null
}
版权属于:月萌API www.moonapi.com,转载请注明出处