---
# System prepended metadata

title: XML 處理 - SAX、DOM、PULL
tags: [網路開發, Web]

---

---
title: 'XML 處理 - SAX、DOM、PULL'
disqus: kyleAlien
---

XML 處理 - SAX、DOM、PULL
===

## Overview of Content

如有引用參考請詳註出處，感謝 :cat:

[TOC]

## XML 概述

XML（`eXtensible Markup Language`）可拓展標示語言，又稱為可拓展標記語言，**它與 HTML 一樣都是「通用標記語言」**（`SGML`, `Standard Generalized Markup Language`）

:::info
* 其實這種「標記」語言並不算程式語言，因為它無法通過圖靈測試；**主要算是 Open stardard 的結構領域，用來做不同裝置之間的通訊**
    
    > XML 不依賴任何的平台，它是普遍公開的通訊協定

    ```mermaid
    graph LR;

    iOS --> XML 
    Android --> XML --> MacOS
    ```
:::

* **XML 是 W3C 制定的，它的特性如下**：
    
    1. 主要目的在「**簡單並清晰的 ++展示（傳遞）數據++**」，它並不在意數據的索引、排序、查找… 等等的效率，其目的是為了展示，所以相對的也比較耗費空間
    
        > 好的展示數據是為了給人們觀看

    2. **重新定義 SGML** 的內部值、參數，只留下常用的功能，像是保留 **SGML 的結構化功能！**
        
        * XML 的標記完全自由定義，不受約束（以下只有第一行為必須，其他標記可以用中文）

            ```xml=
            <?xml version="1.0" encoding="UTF-8"?>
            <學生資訊>

                <名子>Kyle</名子>
                <學號>456789</學號>
                <性別>Man</性別>

            </學生資訊>
            ```
        
        * 對於**大小寫敏感**
        
        * **標記成對出現**，每一行都需要有**結束標記的符號** `</>`


### XML 結構

* XML 擁有結構化的特性，開發時常見的 `pom.xml` 檔案就是 xml 結構… 範例如下

    ```xml=
    <?xml version="1.0" encoding="UTF-8"?>
    <project
        xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>com.example</groupId>
        <artifactId>JavaEEHelloWorld</artifactId>
        <version>1.0-SNAPSHOT</version>
        <name>JavaEEHelloWorld</name>
        <packaging>war</packaging>
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.target>11</maven.compiler.target>
            <maven.compiler.source>11</maven.compiler.source>
            <junit.version>5.9.2</junit.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>jakarta.servlet</groupId>
                <artifactId>jakarta.servlet-api</artifactId>
                <version>5.0.0</version>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter-api</artifactId>
                <version>${junit.version}</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter-engine</artifactId>
                <version>${junit.version}</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>commons-lang</groupId>
                <artifactId>commons-lang</artifactId>
                <version>2.5</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.1</version>
            </dependency>
        </dependencies>
        <build>
            <resources>
                <resource>
                    <directory>src/main/resources</directory>
                </resource>
            </resources>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-war-plugin</artifactId>
                    <version>3.3.2</version>
                </plugin>
            </plugins>
        </build>
    </project>
    ```

### 解析 XML 數據的方式

* 解析 XML 數據的方式有不同種，每種都有其特性、優缺點、解析 XML 的思考方式，像是 `SAX`、`DOM`、`PULL`
    
* 語法分析器一般來說會有兩種分析想法，然而 **想法並沒有對錯（哪個更好）**，只有在正確的情況下使用對的方式才是最好的
    
    * 基於「**物件**」的界面，DOM 就是基於物件去開發
    
    * 基於「**事件**」的界面，SAX 就是基於事件去開發
    
        > ![image](https://hackmd.io/_uploads/SJLWk86w6.png)

## SAX 解析

**SAX 全名為 `Simple API for XML`**，它是指一套 API，同時它也是一個開源套件

> 最早由 Dacid Megginson 使用 Java 語言開發

:::danger
* **SAX 的不同之處**

    它不同於其他大多數 XML 標準，**SAX 沒有語言開發商必須遵守的標準 SAX 參考版本**，因此 SAX 的不同實現可能採用區別很大的界面
:::

### SAX 原理 - 事件驅動

* SAX 軟體設計是「**事件驅動型**」：
    
    簡單來說就是，它會邊依照順序讀取 XML 並 即時的反應給使用 SAX 的程序員
  
    ```mermaid
    graph LR

    程序員 --> |1. 使用 SAX| 解析_XML

    解析_XML --> |解析| 數據 -.-> |2. 即時透過 XML 界面反應| 程序員
    ```  
    :::success
    * SAX 是透過標準的解析 XML 的界面，即時反饋給使用者，這個解析 XML 的界面是標準界面（`Standard Interface`），不會改變
    
    * SAX 也不會在記憶體中幫我們建立物件
    :::

* 當掃描到文檔、元素的開始與結束都會透過 XML 的標準驅動 API 反饋給使用者，而 **SAX 大多數都有實現 5 種類型的事件**

    > SAX 使用的事件驅動與用戶操作無關，事件只會與 XML 中的元素有關（元素驅動事件）

    1. **文檔**（`Document`）的開始、結束

    2. 每次觸發 XML 新的 **元素開始、結束**（標籤）
    
    3. **MetaData** 通常有單獨的事件處理
    
    4. 當處理文檔中的 **合法元素** `DTD`、`Schema`… 時，產生對應的事件
        
        > DTD（`Document Type Definition`）是一種用來定義 XML 文件結構和合法元素的規範… 像是 `CDATA`、`ID`、`ID`、`IDREF`、`IDREFS`、`NMTOKEN`、`NMTOKENS`、`ENTITY`、`ENTITIES`、`NOTATION`、`DOCTYPE`
        
        以下範例說明著
        
        * bookstore 元素是一個包含至少一個 book 元素的元素
        
        * book 元素包含 `title`、`author` 和 `price` 元素
        
        * `title`、`author` 和 `price` 元素都包含字符數據（`PCDATA`）
        ```xml=
        <!-- XML 文件（example.xml） -->
        <!DOCTYPE bookstore [
            <!ELEMENT bookstore (book+)>
            <!ELEMENT book (title, author, price)>
            <!ELEMENT title (#PCDATA)>
            <!ELEMENT author (#PCDATA)>
            <!ELEMENT price (#PCDATA)>
        ]>

        <bookstore>
            <book>
                <title>Introduction to XML</title>
                <author>John Doe</author>
                <price>29.95</price>
            </book>
            <book>
                <title>Java Programming</title>
                <author>Jane Smith</author>
                <price>39.99</price>
            </book>
        </bookstore>
        ```
        
    5. 每當產生錯誤事件時，會透過界面通知開發者


### SAX - XML 解析界面、類

> `org.xml.sax` 包 是 JavaSE 使用 SAX 的方式來解析 XML

* SAX 使用的常見 **XML 解析 ++界面++** 如下表

    | XML 解析界面 | 概述 | 補充 |
    | - | - | - |
    | `ContentHandler` | **文檔本身** 相關的事件（像是開始、結束） | 其中也包括元素（`Element`）監聽 |
    | `EntityResolver` | **實體關聯** 的事件 | 較少使用 |
    | `DTDHandler` | XML 中的 **標準事件** | 然而它定義的事件不夠完整，要完整 DTD 事件的話，可以使用 `DeclHandler` 界面 |
    | `ErrorHandler` | **錯誤事件** | 定義三種級別 ^1.^ warning（非 XML 規範）、^2.^ error（XML 定義的錯誤）、^3.^ fatalError（致命錯誤） |

    大多數時候都會使用 `DefaultHandler` 類（有大部分的界面）

    > ![image](https://hackmd.io/_uploads/H1IyfPpw6.png)


* SAX 使用的常見 **XML 解析 ++類++** 如下表

    | XML 解析類 | 概述 | 補充 |
    | - | - | - |
    | `XMLReader` | 該類可以用 `parse` 方法，來啟動 XML 語法分析器，它可以接受 ^1.^ **文件名**、^2.^ **URL**、^3.^ **InputSource** | 我們可以以設定 XML 解析監聽器… |
    |  | `setFeature`（Boolean）用來控制語法分析器的工作 |  |
    |  | `setProperty`（物件） 用來控制語法分析器的工作 |  |
    | `XMLReaderFactory` | 用來創建 XML 語法分析器物件 | 可創建 ^1.^ **系統預設**、^2.^ **指定類型**（透過設定類路徑） |
    | `InputSource` | 控制 SAX 如何讀取文件 |  |
    | `SAXException` | SAX 定義的大多數方法都會拋出 `SAXException` |  |


### SAX 解析 XML 範例

* 目標 XML 如下

    ```xml=
    <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE bookstore [
        <!ELEMENT bookstore (book+)>
        <!ELEMENT book (title, author, price)>
        <!ELEMENT title (#PCDATA)>
        <!ELEMENT author (#PCDATA)>
        <!ELEMENT price (#PCDATA)>
        ]>

    <bookstore>
        <book>
            <title>Introduction to XML</title>
            <author>John Doe</author>
            <price>29.95</price>
        </book>
        <book>
            <title>Java Programming</title>
            <author>Jane Smith</author>
            <price>39.99</price>
        </book>
    </bookstore>
    ```

* 使用 SAX API 來解析 XML 數據，並保存在記憶體中

    ```kotlin=
    data class BookInformation(var bookTitle: String = "",
                               var authorName: String = "",
                               var bookPrice: Float = -1F) {
        companion object {
            const val XML_KEY_BOOK = "book"
            const val XML_KEY_TITLE = "title"
            const val XML_KEY_AUTHOR = "author"
            const val XML_KEY_PRICE = "price"
        }
    }

    class BookXmlSaxHandler: DefaultHandler() {

        lateinit var bookList: MutableList<BookInformation>

        var curBookInfo: BookInformation? = null
        lateinit var curTag: String

        override fun startDocument() {
            bookList = mutableListOf()

            println("---- Document start ----")
        }

        override fun startElement(
            uri: String?,               // 名稱空間
            localName: String?,         // 本地名，不帶前綴
            qName: String?,             // 帶前綴的限定名
            attributes: Attributes?
        ) {
            if (qName == null || attributes == null) {
                return
            }

            when(qName) {
                BookInformation.XML_KEY_BOOK -> {
                    println("create book instance")
                    curBookInfo = BookInformation()
                }
                BookInformation.XML_KEY_TITLE,
                BookInformation.XML_KEY_AUTHOR,
                BookInformation.XML_KEY_PRICE -> {
                    curTag = qName
                }

            }

        }

        override fun characters(ch: CharArray?, start: Int, length: Int) {
            if (ch == null) {
                return
            }
            val data = String(ch, start, length)

            when(curTag) {
                BookInformation.XML_KEY_TITLE -> {
                    curBookInfo!!.bookTitle = data
                }
                BookInformation.XML_KEY_AUTHOR -> {
                    curBookInfo!!.authorName = data
                }
                BookInformation.XML_KEY_PRICE -> {
                    curBookInfo!!.bookPrice = data.toFloat()
                }
            }
        }

        override fun endElement(uri: String?, localName: String?, qName: String?) {

            if (BookInformation.XML_KEY_BOOK == qName) {
                if (curBookInfo != null) {
                    bookList.add(curBookInfo!!)
                }
                curBookInfo = null
                println("create book set to null")
            }
        }

        override fun endDocument() {
            println("---- Document end ----")

            bookList.forEach {
                println(it)
            }
        }
    }
    ```

* 測試使用 SAX API 解析 XML：

    ```kotlin=
    fun main() {
        val xmlFile = File("/home/alien/AndroidStudioProjects/Network/app/src/main/res/xml/books.xml")

        val bookXmlHandler = BookXmlSaxHandler()

        // 創建 XMLReader
        val xmlReader: XMLReader = XMLReaderFactory.createXMLReader().apply {
            // 設置其中的 content Handler
            contentHandler = bookXmlHandler
        }

        val fileInputStream = FileInputStream(xmlFile)
        try {
            xmlReader.parse(InputSource(fileInputStream))
        } finally {
            fileInputStream.close()
        }

    }
    ```
    > ![image](https://hackmd.io/_uploads/BJjZi5pva.png)


## DOM 解析

DOM（`Document Object Model`）提供了一個方便的編程界面，因此可以被視為一種官方的W3C標準

W3C（`World Wide Web Consortium`）制定了DOM的標準，並且 **DOM 在瀏覽器中被廣泛使用**，**它允許 JavaScript 通過瀏覽器直接操作 HTML 和 XML 文檔**


### DOM 原理 - JavaScript

* DOM 會將 XML 完全解析完成後，在記憶體中建立一個 XML 副本（**又稱之為物件樹**），並且該副本中的所有元素皆以「物件的形式」儲存

    ```mermaid
    graph LR
    

    程序員 --> |1. 使用 DOM| 解析_XML

    解析_XML --> |解析完成| 記憶體

    subgraph 記憶體
    XML_對應的物件樹
    end
    
    程序員 --> |2. 取物件| XML_對應的物件樹
    ```

    :::warning
    * DOM 會佔用，記憶體空間（建立 XML 對應的物件樹）；但相對的… DOM 會比 SAX 還方便使用
    :::

* **DOM & JavaScript 的關係**

    早期人們會把 DOM 視為是一種讓 JavaScript 在遊覽器間可移植的方法！但其實 DOM 的應用是遠遠超出這個範圍的
    
    * DOM 技術使的用戶界面可以「**動態變化**」，大大增加了交互性

        > 像是 JavaScript 可以透過它動態操作 HTML 的元素的顯示、隱藏

    * JavaScript 就是利用 DOM 來取得 HTML 文件的操縱入口，並獲得 HTML 對應的物件


### DOM 規範結構

* 根據 W3C DOM 規範，DOM 解析 HTML、XML 這兩類標記語言的界面…  DOM 會將整個文件映射為一個由「層次節點」組成的物件
    
    * **Node 節點**
        
        根據 DOM 規範，HTML 文檔中的每一個成份都是節點；有分為「**文檔節點**」、「**元素節點**」、「**屬性節點**」、「**文本節點**」、「**註釋節點**」
        
        > 除了 文檔節點 之外，所有節點都有父節點
        
        ```xml=
        <!DOCTYPE html>
        <html>

        <head>
            <title>Title</title>
        </head>

        <body>
            <form method="post"
                  enctype="multipart/form-data"
                  action="upload.handle">
                <label>
                    <input type="file" name="upload" value="upload"/>
                </label>
                <button>Upload</button>
            </form>
        </body>

        </html>
        ```

        ```mermaid
        graph TB;
        
        doc(HTML 文檔節點) --> html(< html > 元素節點)
        
        html --> header(< header > 元素節點)
        header --> titlew(< title > 元素節點)
        titlew --> titleContext(Title 文本節點)
        
        
        html --> body(< body > 元素節點)
        body --> form(< form > 元素節點)
        form --> formContext1(method 文本節點)
        form --> formContext2(enctype 文本節點)
        form --> formContext3(action 文本節點)
        form --> label(< label > 元素節點)
        label --> input(< input > 元素節點)
        form --> button(< button > 元素節點)
        ```
    

* **DOM API 的分級**（共有 3 個分級）
    
    :::success
    * **Level 0 DOM / 0 級 DOM 是什麼**？

        在遊覽器正式規範 DOM 標準之前就存在的規範，它並非有文件的規範，所以是非正規的稱之為 DOM 0，正確一點應該稱其為 BOM（`Brower Object Model`）
    :::

    1. **1 級 DOM**：
    
        **由 `DOM 核心` 與 `DOM HTML` 兩個模塊組層**
        
        * DOM 核心能映射以「**XML 為基礎**」的文檔結構為物件，核心 API 會將文件中所有的內容都視為節點，再依照類型區分不同型態

        * DOM HTML（其實就是拓展核心）則通過添加 HTML 專用的物件與函數對 DOM 核心進行拓展，專門用來操作 HTML
            
            > DOM HTML 在瀏覽器開發中較為常見，像是 `HTMLHeadElement` 元素就提供專屬 HTML 操作的 API
        
            ```mermaid
            graph TB

            d1(Level 1 DOM)
            
            dc(DOM 核心)
            dh(DOM Html)
            
            d1 --> dc
            d1 --> dh
            ```

    2. **2 級 DOM**：
        
        鑑於 1 級 DOM 僅以映射文檔結構到記憶體中（物件樹），2 級 DOM 通過對於源有的 DOM 拓展，**針對 DOM 界面增加**
        
        * DOM 視圖：描述跟蹤一個文檔的各種視圖（使用 CSS 樣式設計文檔前後的差異）

        * DOM 事件：對於用戶界面事件的反應（像是滑鼠點擊），對這一系列的事件進行標準化
            
            > 以往尚未標準化時稱之為 **基本事件模型（`Basic Event Model`）** 或是傳統模型（`Traditional model`）
        
        * DOM 樣式：基於 CSS 樣式的反應
        
        * DOM 遍歷：遍歷、操作文檔物件
        
        * 支持 XML 的命名空間

            :::info
            * **XML 的命名空間**？
                
                在 XML 中，命名空間（`Namespace`）是一種用來「區分元素、屬性名稱的機制」，以「**避免名稱衝突**」的問題
                
                **命名空間在 XML 中通常以一個 URI**（`Uniform Resource Identifier`）或 URL 來標識

                > 實際上這個 URL 並不要求是一個可訪問的網址

                ```xml=
                <!-- 定義 books 的命名空間-->
                <root xmlns:books="http://example.com/books">
                    <!-- 使用命名空間 + 標記 -->
                    <books:book>
                        <books:title>Introduction to XML</books:title>
                        <books:author>John Doe</books:author>
                        <books:price>29.95</books:price>
                    </books:book>
                    
                    <!-- 無命名空間，這個 book 跟上面的 books:book 是不同標記 -->
                    <book>
                        <title>The Great Novel</books:title>
                        <author>Jane Smith</books:author>
                        <price>19.99</books:price>
                    </book>
                </root>
                ```
            :::

        
    3. **3 級 DOM**：

        對上一級 DOM 拓展，引入「統一載入」、「保存文件」和「文檔驗證方法」
        
        DOM 核心再次拓展，支持 XML 1.0 的所有設計，像是 `XML Infoset`、`XPath`、`XML Base`… 等等


### DOM 解析 XML 範例

* 目標 XML 如下

    ```xml=
    <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE bookstore [
        <!ELEMENT bookstore (book+)>
        <!ELEMENT book (title, author, price)>
        <!ELEMENT title (#PCDATA)>
        <!ELEMENT author (#PCDATA)>
        <!ELEMENT price (#PCDATA)>
        ]>

    <bookstore>
        <book>
            <title>Introduction to XML</title>
            <author>John Doe</author>
            <price>29.95</price>
        </book>
        <book>
            <title>Java Programming</title>
            <author>Jane Smith</author>
            <price>39.99</price>
        </book>
    </bookstore>
    ```

* 使用 Javax 提供的 DOM API 來解析 XML 數據，使用 DOM API 的特點在於… 它會一次幫你處理好所有的物件映射關係（以 Node 的方式，儲存在記憶體中）
    
    這樣我們在使用上就不用特別關心 XML 資料流的處理

    ```kotlin=
    data class BookInformation(var bookTitle: String = "",
                               var authorName: String = "",
                               var bookPrice: Float = -1F) {
        companion object {
            const val XML_KEY_BOOK = "book"
            const val XML_KEY_TITLE = "title"
            const val XML_KEY_AUTHOR = "author"
            const val XML_KEY_PRICE = "price"
        }
    } 

    class DomXml {

        fun parseByDom(inputStream: InputStream) {
            val list = mutableListOf<BookInformation>()
            val docBuilder = DocumentBuilderFactory.newInstance().run {
                newDocumentBuilder()
            }

            val doc = docBuilder.parse(inputStream)
            val bookList = doc.documentElement.getElementsByTagName(BookInformation.XML_KEY_BOOK)

            for (i in 0 until bookList.length) {
                val element = bookList.item(i) as Element

                val bookInformation = BookInformation()

                val bookDetail = element.childNodes
                for (j in 0 until bookDetail.length) {
                    val bookItem = bookDetail.item(j)

                    when(bookItem.nodeName) {
                        BookInformation.XML_KEY_TITLE -> {
                            bookInformation.bookTitle = bookItem.textContent
                        }
                        BookInformation.XML_KEY_AUTHOR -> {
                            bookInformation.authorName = bookItem.textContent
                        }
                        BookInformation.XML_KEY_PRICE -> {
                            bookInformation.bookPrice = bookItem.textContent.toFloat()
                        }

                    }

                }
                list.add(bookInformation)
            }

            list.forEach {
                println(it)
            }
        }

    }
    ```
    
* 測試使用 DOM API 解析 XML：

    ```kotlin=
    fun main() {
        val xmlFile = File("/home/alien/AndroidStudioProjects/Network/app/src/main/res/xml/books.xml")

        val inputStream = FileInputStream(xmlFile)
        try {
            DomXml().parseByDom(inputStream)
        } finally {
            inputStream.close()
        }

    }
    ```
    > ![image](https://hackmd.io/_uploads/SywVchTvT.png)


### DOM 創建 XML

* DOM 與 SAX 的令一個不同點在於，它也有提供對應創建（也可以覆蓋）XML 的對應 API；以下我們修改 DOM Parse XML 的範例
    
    1. 首先我們先 Parse XML 得到物件樹，保存在記憶體 `List<BookInformation>` 內，並返回給 DOM 使用者
        
        :::info
        * 這裡返回的並不是 DOM 在記憶體內建構的 Node 節點，而是應用 DOM Parse 後，自己建立的物件
        :::
        
        ```kotlin=
        class DomXml {
            
            private lateinit var curDocument: Document

            fun parseByDom(inputStream: InputStream): List<BookInformation> {
                val list = mutableListOf<BookInformation>()
                val docBuilder = DocumentBuilderFactory.newInstance().run {
                    newDocumentBuilder()
                }

                val doc = docBuilder.parse(inputStream).apply {
                    curDocument = this
                }
                val bookList = doc.documentElement.getElementsByTagName(BookInformation.XML_KEY_BOOK)

                for (i in 0 until bookList.length) {
                    val element = bookList.item(i) as Element

                    val bookInformation = BookInformation()

                    val bookDetail = element.childNodes
                    for (j in 0 until bookDetail.length) {
                        val bookItem = bookDetail.item(j)

                        when(bookItem.nodeName) {
                            BookInformation.XML_KEY_TITLE -> {
                                bookInformation.bookTitle = bookItem.textContent
                            }
                            BookInformation.XML_KEY_AUTHOR -> {
                                bookInformation.authorName = bookItem.textContent
                            }
                            BookInformation.XML_KEY_PRICE -> {
                                bookInformation.bookPrice = bookItem.textContent.toFloat()
                            }

                        }

                    }
                    list.add(bookInformation)
                }

                list.forEach {
                    println(it)
                }

                return list
            }
        }
        ```
    
    2. 再來撰寫一個透過 DOM 提供的 API，來將改變後的數據寫入到新 XML 中…

        將使用者設定的新數據，寫入到 DOM 的 Node 節點之中
        
        > 透過 `element`#`getElementsByTagName` 取得節點，並修改節點數據

        ```kotlin=
        class DomXml {

            fun updateXmlPrice(book: BookInformation, outputStream: FileOutputStream) {
                val bookList = curDocument.documentElement.getElementsByTagName(BookInformation.XML_KEY_BOOK)

                // 找到要修改的書籍
                for (i in 0 until bookList.length) {
                    val element = bookList.item(i) as Element
                    val title = element.getElementsByTagName(BookInformation.XML_KEY_TITLE).item(0).textContent

                    if (book.bookTitle != title) {
                        continue
                    }
                    // 取得 price 節點
                    val prizeNode = element.getElementsByTagName(BookInformation.XML_KEY_PRICE).item(0)
                    // 修改價格
                    prizeNode.textContent = book.bookPrice.toString()

                    // 將修改後的 XML 寫回文件
                    val transformerFactory = TransformerFactory.newInstance()

                    transformerFactory.newTransformer().apply {
                        // 設定縮排
                        setOutputProperty(OutputKeys.INDENT, "yes")

                        val streamResult = StreamResult(outputStream)
                        // 文件節點
                        val domSource = DOMSource(curDocument)
                        // 將修改後的物件，寫入目標文件
                        transform(domSource, streamResult)
                    }

                    println("XML updated successfully.")
                    break
                }
            }
        }
        ```

    * 使用 DOM 寫 XML 範例
        
        ```kotlin=
        fun main() {
            val xmlFile = File("/home/alien/AndroidStudioProjects/Network/app/src/main/res/xml/books.xml")
            val xmlRefactorFile = File("/home/alien/AndroidStudioProjects/Network/app/src/main/res/xml/books_refactor.xml")

            val inputStream = FileInputStream(xmlFile)
            val outputStream = FileOutputStream(xmlRefactorFile)
            try {
                val domXml = DomXml()

                val list = domXml.parseByDom(inputStream).apply {
                    this[0].bookPrice = 100.876f
                }

                domXml.updateXmlPrice(list[0], outputStream)
            } finally {
                inputStream.close()
            }

        }
        ```

        > ![image](https://hackmd.io/_uploads/B1xP7FRPa.png)


## PULL 解析

如果是使用 Android 平台開發，除了 SAX、DOM 之外… Android 平台原生還提供了一個內置的 PULL 解析器！同樣可以用來解析 XML 文件

### PULL 原理

* PULL 採用與 SAX 類似的解析方式
    
    **它也是採用「事件驅動」的方式解**析 XML，當遇到元素時會產生事件並驅動，讓使用者透過界面接收到事件


### PULL 解析 XML

* 目標 XML 如下（需要將檔案放置在 `assets` 目錄下，之後會利用 Android Context 取得目標檔案）

    ```xml=
    <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE bookstore [
        <!ELEMENT bookstore (book+)>
        <!ELEMENT book (title, author, price)>
        <!ELEMENT title (#PCDATA)>
        <!ELEMENT author (#PCDATA)>
        <!ELEMENT price (#PCDATA)>
        ]>

    <bookstore>
        <book>
            <title>Introduction to XML</title>
            <author>John Doe</author>
            <price>29.95</price>
        </book>
        <book>
            <title>Java Programming</title>
            <author>Jane Smith</author>
            <price>39.99</price>
        </book>
    </bookstore>
    ```
    
    > ![image](https://hackmd.io/_uploads/SJTtJ9RP6.png)

    
* 以下使用 Android 系統內提供的 XmlPullParser 來解析 XML 檔案

    ```kotlin=
    class PullXml {
        fun parseByAndroidPull(inputStream: InputStream): List<BookInformation> {
            val list = mutableListOf<BookInformation>()

            var curBookInformation: BookInformation? = null

            val xmlPullParser: XmlPullParser = Xml.newPullParser().apply {
                // 設定 input stream 以及解碼方式 
                setInput(inputStream, Charsets.UTF_8.name())
            }

            var event = xmlPullParser.eventType
            while (event != XmlPullParser.END_DOCUMENT) {

                if (event == XmlPullParser.START_TAG) {
                    when(xmlPullParser.name) {
                        BookInformation.XML_KEY_BOOK -> {
                            curBookInformation = BookInformation()
                        }
                        BookInformation.XML_KEY_TITLE -> {
                            curBookInformation!!.bookTitle = xmlPullParser.nextText()
                        }
                        BookInformation.XML_KEY_AUTHOR -> {
                            curBookInformation!!.authorName = xmlPullParser.nextText()
                        }
                        BookInformation.XML_KEY_PRICE -> {
                            curBookInformation!!.bookPrice = xmlPullParser.nextText().toFloat()
                        }
                    }
                } else if (event == XmlPullParser.END_TAG) {
                    if (curBookInformation != null) {
                        list.add(curBookInformation)
                        curBookInformation = null
                    }
                }

                event = xmlPullParser.next()
            }

            return list
        }

    }
    ```

    使用 Android 的機器測試（整合測試）才能測試 Pull 的 Parse
    
    :::warning
    * Android XmlPullParser 服務必須運行在 Android Runtime 中
        
        > ![image](https://hackmd.io/_uploads/H1-Xx5Rva.png)
    :::

    ```kotlin=
    @RunWith(AndroidJUnit4::class)
    class ExampleInstrumentedTest {

        @Test
        fun pullWithAndroidPull() {
            // Context of the app under test.
            val appContext = InstrumentationRegistry.getInstrumentation().targetContext

            val inputStream = appContext.resources.assets.open("books.xml")
            try {
                val pullXml = PullXml()

                val list = pullXml.parseByAndroidPull(inputStream)

                list.forEach {
                    println("---- $it")
                }

            } finally {
                inputStream.close()
            }
        }

    }
    ```
    > ![image](https://hackmd.io/_uploads/HyvJxqAvp.png)

### PULL 創建 XML

* Pull 雖然同 SAX 一樣不會在記憶體中建構物件樹，不過我們仍可以自己透過在記憶體中建立並輸出到目標檔案中（以下方法同樣也可以使用在 SAX）

    ```kotlin=
    fun createByAndroidPull(outputStream: OutputStream, list: List<BookInformation>) {
        val xmlNameSpace = null
        val encodeType = Charsets.UTF_8.name()

        val xmlSerializer = Xml.newSerializer().apply {
            setOutput(outputStream, encodeType)
            setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
        }

        xmlSerializer.startDocument(encodeType, true)
        xmlSerializer.startTag(xmlNameSpace, "bookstore")
        list.forEach {
            xmlSerializer.startTag(xmlNameSpace, BookInformation.XML_KEY_BOOK)

            xmlSerializer.startTag(xmlNameSpace, BookInformation.XML_KEY_TITLE)
            xmlSerializer.text(it.bookTitle)
            xmlSerializer.endTag(xmlNameSpace, BookInformation.XML_KEY_TITLE)

            // Author
            xmlSerializer.startTag(xmlNameSpace, BookInformation.XML_KEY_AUTHOR)
            xmlSerializer.text(it.authorName)
            xmlSerializer.endTag(xmlNameSpace, BookInformation.XML_KEY_AUTHOR)

            // Price
            xmlSerializer.startTag(xmlNameSpace, BookInformation.XML_KEY_PRICE)
            xmlSerializer.text(it.bookPrice.toString())
            xmlSerializer.endTag(xmlNameSpace, BookInformation.XML_KEY_PRICE)


            xmlSerializer.endTag(xmlNameSpace, BookInformation.XML_KEY_BOOK)
        }
        xmlSerializer.endTag(xmlNameSpace, "bookstore")
        xmlSerializer.endDocument()

    }
    ```
    
    :::info
    * 如果是建立 attribute 的話，那數值會被寫在 tag 中

        ```kotlin=
        xmlSerializer.attribute(xmlNameSpace, BookInformation.XML_KEY_AUTHOR, it.authorName)
        ```
        
        建立出來的數據就會寫在 tag 中（如下）
        ```xml=
        <?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
        <bookstore>
            <book title="Introduction to XML" 
                  author="Kyle" 
                  price="29.95" />
            
            <book title="Java Programming" 
                  author="Jane Smith" 
                  price="39.99" />
        </bookstore>
        ```
    :::
    
    測試程式
    
    > 以下測試運行在 Android App 中（非整合測試）

    ```kotlin=
    fun testAndroidPull() {
        val appContext = this

        val inputStream = appContext.resources.assets.open("books.xml")
        // 開啟或創建文件在內部存儲空間
        val fileOutputStream: FileOutputStream = appContext.openFileOutput("hello.xml", Context.MODE_PRIVATE)
        try {
            val pullXml = PullXml()

            val list = pullXml.parseByAndroidPull(inputStream).apply {
                // 修改數據
                this[0].authorName = "Kyle"
            }

            pullXml.createByAndroidPull(fileOutputStream, list)

        } finally {
            inputStream.close()
            fileOutputStream.close()
        }
    }
    ```

    > ![image](https://hackmd.io/_uploads/HyfOF50vT.png)


## 其他


### SAX vs DOM

* SAX、DOM 兩者都是對於解析 XML　的方案，不過特點、解決思路都不盡相同，兩者的比較表如下…

    | ＼ | SAX | DOM |
    | - | - | - |
    | XML 物件建立 | 手動分析、建立 | 自動分析、建立 |
    | XML 的限制 | 可以讀取不限大小 XML 檔 | 由於會在內存中自動幫我們建立物件樹，所以 XML 大小不能太大 |
    | XML 讀取 | 以順序方式讀取（不能回頭） | 建立完物件樹後，可隨意讀取 |
    | XML 修改 | 只能讀取 XML，不能修改！ | 可以透過對物件樹的修改，來改動到 XML 檔案 |
    | 技術的特點 | 較為自由（有很多地方可以設定），但相對的也較為麻煩 | 易於操作、開發 |






## Appendix & FAQ

:::info
:::

###### tags: `網路開發`
