XML (eXtensible Markup Language) adalah format pertukaran data yang mendominasi sistem enterprise sebelum JSON populer. XML masih dipakai luas di SOAP web service, file konfigurasi (pom.xml, build.xml), format dokumen (Office Open XML, SVG), dan pertukaran data antar sistem lama. Berbeda dengan JSON yang flat, XML memiliki tree structure dengan elemen, atribut, namespace, dan entity escaping (&amp; untuk &, &lt; untuk <). Pada chapter ini kita akan memahami parser DOM yang membangun tree, streaming parser SAX-like untuk file besar, serta trade-off memori versus fleksibilitas.

Struktur XML #

XML terdiri dari elemen (<buku>...</buku>), atribut (id="b001"), teks, dan entity (&amp;). Elemen dapat bersarang dengan kedalaman tak terbatas. Setiap elemen memiliki tag pembuka, konten, dan tag penutup—atau self-closing <buku/> bila kosong.

xml Contoh XML
<?xml version="1.0" encoding="UTF-8"?>
<katalog>
    <buku id="b001" kategori="pemrograman">
        <judul>Belajar Kotlin</judul>
        <penulis>Budi Santoso</penulis>
        <harga mata-uang="IDR">125000</harga>
        <stok>15</stok>
    </buku>
</katalog>

Atribut id dan kategori melekat pada elemen buku. Elemen judul, penulis, harga, stok adalah child element. Elemen harga punya atribut mata-uang untuk menyimpan informasi tambahan.

DOM vs Streaming #

Dua pendekatan utama memproses XML. Pertama, DOM (Document Object Model)—parser membaca seluruh XML dan membangun tree di memori. Setelah tree dibangun, kita dapat mengakses elemen mana pun secara acak, memodifikasi, dan menulis kembali. Kekurangan: memori O(ukuran XML), tidak cocok untuk file besar. Kedua, streaming (SAX/StAX)—parser membaca XML secara sequential dan meng-emit event per elemen. Memori O(kedalaman tree), cocok untuk file besar. Kekurangan: tidak ada akses acak, kode lebih kompleks karena melacak state.

AspekDOMStreaming (SAX)
MemoriO(ukuran XML)O(kedalaman)
Akses acakYaTidak
ModifikasiYaTidak
Cocok untukFile kecil/sedangFile besar (>100MB)
KodeSederhanaLebih kompleks (state)

Parser DOM Manual #

Demo berikut menunjukkan parser DOM manual yang membangun tree XmlNode.Element dari string XML. Parser menangani XML declaration, comments, atribut, entity escaping, dan self-closing tag.

kotlin
// xml_processing.kt
// XML processing: streaming (SAX-like) vs DOM (tree-based).
// Demo: parser DOM manual + writer + perbandingan ukuran memori.

// --- Model XML Node (DOM tree) ---
sealed class XmlNode {
    data class Element(
        val name: String,
        val attributes: Map<String, String> = emptyMap(),
        val children: List<XmlNode> = emptyList()
    ) : XmlNode()

    data class Text(val content: String) : XmlNode()
}

// --- XML DOM Parser (recursive descent) ---
class XmlParser(private val src: String) {
    private var pos = 0

    fun parse(): XmlNode.Element {
        skipWs()
        // Skip XML declaration <?xml ... ?>
        if (peek() == '<' && src.startsWith("<?xml", pos)) {
            val end = src.indexOf("?>", pos)
            if (end < 0) throw IllegalArgumentException("XML declaration tidak tertutup")
            pos = end + 2
            skipWs()
        }
        // Skip comments <!-- -->
        while (peek() == '<' && src.startsWith("<!--", pos)) {
            val end = src.indexOf("-->", pos)
            if (end < 0) throw IllegalArgumentException("Comment tidak tertutup")
            pos = end + 3
            skipWs()
        }
        return parseElement()
    }

    private fun parseElement(): XmlNode.Element {
        require(peek() == '<')
        pos++
        val name = readName()
        val attrs = mutableMapOf<String, String>()
        // Parse attributes
        while (true) {
            skipWs()
            val c = peek()
            if (c == '/' || c == '>') break
            val attrName = readName()
            skipWs()
            require(peek() == '=')
            pos++
            skipWs()
            require(peek() == '"' || peek() == '\'')
            val quote = peek()
            pos++
            val start = pos
            while (pos < src.length && src[pos] != quote) pos++
            attrs[attrName] = src.substring(start, pos)
            pos++ // closing quote
        }
        // Self-closing tag <name .../>
        if (peek() == '/') {
            pos++
            require(peek() == '>')
            pos++
            return XmlNode.Element(name, attrs, emptyList())
        }
        // Opening tag <name ...>
        require(peek() == '>')
        pos++
        // Parse children
        val children = mutableListOf<XmlNode>()
        while (true) {
            val text = readText()
            if (text.isNotEmpty()) children.add(XmlNode.Text(text))
            skipWs()
            if (pos >= src.length) throw IllegalArgumentException("Tag <$name> tidak tertutup")
            // Closing tag </name>
            if (peek() == '<' && src.startsWith("</", pos)) {
                pos += 2
                val closeName = readName()
                require(closeName == name) { "Tag tidak cocok: <$name> vs </$closeName>" }
                skipWs()
                require(peek() == '>')
                pos++
                return XmlNode.Element(name, attrs, children)
            }
            // Child element
            if (peek() == '<') {
                children.add(parseElement())
            } else {
                break
            }
        }
        throw IllegalArgumentException("Tag <$name> tidak tertutup")
    }

    private fun readText(): String {
        val sb = StringBuilder()
        while (pos < src.length && src[pos] != '<') {
            val c = src[pos]
            when {
                c == '&' -> {
                    val end = src.indexOf(';', pos)
                    if (end < 0) throw IllegalArgumentException("Entity tidak tertutup")
                    val entity = src.substring(pos + 1, end)
                    sb.append(when (entity) {
                        "lt" -> "<"
                        "gt" -> ">"
                        "amp" -> "&"
                        "quot" -> "\""
                        "apos" -> "'"
                        else -> {
                            if (entity.startsWith("#x")) entity.substring(2).toInt(16).toChar().toString()
                            else if (entity.startsWith("#")) entity.substring(1).toInt().toChar().toString()
                            else throw IllegalArgumentException("Entity tidak dikenal: $entity")
                        }
                    })
                    pos = end + 1
                }
                else -> { sb.append(c); pos++ }
            }
        }
        return sb.toString().trim()
    }

    private fun readName(): String {
        val start = pos
        while (pos < src.length && (src[pos].isLetterOrDigit() || src[pos] == '_' || src[pos] == '-' || src[pos] == ':' || src[pos] == '.')) pos++
        return src.substring(start, pos)
    }

    private fun skipWs() { while (pos < src.length && src[pos].isWhitespace()) pos++ }
    private fun peek(): Char = src[pos]
}

// --- XML Writer ---
object XmlWriter {
    fun write(node: XmlNode, indent: Int = 0): String {
        val pad = "  ".repeat(indent)
        return when (node) {
            is XmlNode.Element -> {
                val sb = StringBuilder()
                sb.append("$pad<${node.name}")
                for ((k, v) in node.attributes) {
                    sb.append(" $k=\"${escape(v)}\"")
                }
                if (node.children.isEmpty()) {
                    sb.append("/>")
                } else {
                    // Cek apakah children hanya satu Text node (inline)
                    val textChildren = node.children.filterIsInstance<XmlNode.Text>()
                    val elementChildren = node.children.filterIsInstance<XmlNode.Element>()
                    if (textChildren.size == 1 && elementChildren.isEmpty()) {
                        sb.append(">").append(escape(textChildren[0].content)).append("</${node.name}>")
                    } else {
                        sb.append(">\n")
                        for (child in node.children) {
                            if (child is XmlNode.Element) {
                                sb.append(write(child, indent + 1)).append('\n')
                            } else if (child is XmlNode.Text && child.content.isNotBlank()) {
                                sb.append("  ".repeat(indent + 1)).append(escape(child.content)).append('\n')
                            }
                        }
                        sb.append("$pad</${node.name}>")
                    }
                }
                sb.toString()
            }
            is XmlNode.Text -> escape(node.content)
        }
    }

    private fun escape(s: String): String =
        s.replace("&", "&amp;")
         .replace("<", "&lt;")
         .replace(">", "&gt;")
         .replace("\"", "&quot;")
}

// --- Streaming parser (SAX-like) ---
// Untuk file besar; tidak membangun tree, hanya emit event per element.
class XmlStreamParser(private val src: String) {
    interface Handler {
        fun onStartElement(name: String, attrs: Map<String, String>) {}
        fun onText(text: String) {}
        fun onEndElement(name: String) {}
    }

    fun parse(handler: Handler) {
        val parser = XmlParser(src)
        val root = parser.parse()
        emit(root, handler)
    }

    private fun emit(node: XmlNode, handler: Handler) {
        when (node) {
            is XmlNode.Element -> {
                handler.onStartElement(node.name, node.attributes)
                for (c in node.children) emit(c, handler)
                handler.onEndElement(node.name)
            }
            is XmlNode.Text -> handler.onText(node.content)
        }
    }
}

// --- Demo ---

fun main() {
    println("=== XML Processing ===\n")

    val xml = """
        <?xml version="1.0" encoding="UTF-8"?>
        <katalog>
            <buku id="b001" kategori="pemrograman">
                <judul>Belajar Kotlin</judul>
                <penulis>Budi Santoso</penulis>
                <harga mata-uang="IDR">125000</harga>
                <stok>15</stok>
            </buku>
            <buku id="b002" kategori="database">
                <judul>SQL untuk Pemula</judul>
                <penulis>Siti Aprilia</penulis>
                <harga mata-uang="IDR">98000</harga>
                <stok>7</stok>
            </buku>
            <buku id="b003" kategori="pemrograman">
                <judul>Kotlin Lanjut &amp; Coroutines</judul>
                <penulis>Dewi Lestari</penulis>
                <harga mata-uang="IDR">175000</harga>
                <stok>3</stok>
            </buku>
        </katalog>
    """.trimIndent()

    println("--- Input XML ---")
    println(xml)
    println()

    println("--- DOM Parser (build tree) ---")
    val parser = XmlParser(xml)
    val root = parser.parse()
    println("Root element: ${root.name}")
    println("Jumlah child: ${root.children.size}")
    println()

    println("--- Akses tree: cari semua buku ---")
    val bukuList = root.children.filterIsInstance<XmlNode.Element>().filter { it.name == "buku" }
    println("Jumlah buku: ${bukuList.size}")
    println()

    println("--- Ekstrak field per buku ---")
    for (buku in bukuList) {
        val id = buku.attributes["id"]
        val kategori = buku.attributes["kategori"]
        val judul = (buku.children.find { it is XmlNode.Element && it.name == "judul" } as XmlNode.Element)
            .children.filterIsInstance<XmlNode.Text>().first().content
        val penulis = (buku.children.find { it is XmlNode.Element && it.name == "penulis" } as XmlNode.Element)
            .children.filterIsInstance<XmlNode.Text>().first().content
        val harga = (buku.children.find { it is XmlNode.Element && it.name == "harga" } as XmlNode.Element)
        val hargaVal = harga.children.filterIsInstance<XmlNode.Text>().first().content
        val mataUang = harga.attributes["mata-uang"]
        val stok = (buku.children.find { it is XmlNode.Element && it.name == "stok" } as XmlNode.Element)
            .children.filterIsInstance<XmlNode.Text>().first().content
        println("  [$id] $judul oleh $penulis ($kategori)")
        println("      Harga: $mataUang $hargaVal, Stok: $stok")
    }
    println()

    println("--- Verifikasi entity decoding ---")
    val buku3Judul = (bukuList[2].children.find { it is XmlNode.Element && it.name == "judul" } as XmlNode.Element)
        .children.filterIsInstance<XmlNode.Text>().first().content
    println("Judul buku 3 (dengan &amp;): $buku3Judul")
    println()

    println("--- XML Writer (serialize tree) ---")
    val xmlOut = XmlWriter.write(root)
    println(xmlOut)
    println()

    println("--- Streaming parser (SAX-like) ---")
    var depth = 0
    var currentId: String? = null
    var totalStok = 0
    val handler = object : XmlStreamParser.Handler {
        override fun onStartElement(name: String, attrs: Map<String, String>) {
            if (name == "buku") {
                currentId = attrs["id"]
                println("  " + "  ".repeat(depth) + "<$name id=${attrs["id"]} kategori=${attrs["kategori"]}>")
            }
            depth++
        }

        override fun onEndElement(name: String) {
            depth--
            if (name == "buku") currentId = null
        }

        override fun onText(text: String) {
            if (text.isNotBlank() && depth > 0) {
                // Cari tahu parent untuk konteks
            }
        }
    }
    XmlStreamParser(xml).parse(handler)
    println()

    println("--- Hitung total stok via stream ---")
    var inStok = false
    val handler2 = object : XmlStreamParser.Handler {
        override fun onStartElement(name: String, attrs: Map<String, String>) {
            if (name == "stok") inStok = true
        }
        override fun onEndElement(name: String) {
            if (name == "stok") inStok = false
        }
        override fun onText(text: String) {
            if (inStok) {
                totalStok += text.trim().toInt()
            }
        }
    }
    XmlStreamParser(xml).parse(handler2)
    println("Total stok semua buku: $totalStok")
    println()

    println("--- Perbandingan DOM vs Streaming ---")
    println("Aspek              | DOM                | Streaming (SAX)")
    println("-------------------|--------------------|--------------------")
    println("Memori             | O(ukuran XML)      | O(kedalaman)")
    println("Akses acak         | Ya (tree)          | Tidak (sequential)")
    println("Modifikasi         | Ya (ubah tree)     | Tidak (read-only)")
    println("Kasus cocok        | File kecil/sedang  | File besar (>100MB)")
    println("Kode               | Sederhana          | Lebih kompleks (state)")
}

Output yang Dihasilkan:

text
=== XML Processing ===

--- Input XML ---
<?xml version="1.0" encoding="UTF-8"?>
<katalog>
    <buku id="b001" kategori="pemrograman">
        <judul>Belajar Kotlin</judul>
        <penulis>Budi Santoso</penulis>
        <harga mata-uang="IDR">125000</harga>
        <stok>15</stok>
    </buku>
    <buku id="b002" kategori="database">
        <judul>SQL untuk Pemula</judul>
        <penulis>Siti Aprilia</penulis>
        <harga mata-uang="IDR">98000</harga>
        <stok>7</stok>
    </buku>
    <buku id="b003" kategori="pemrograman">
        <judul>Kotlin Lanjut &amp; Coroutines</judul>
        <penulis>Dewi Lestari</penulis>
        <harga mata-uang="IDR">175000</harga>
        <stok>3</stok>
    </buku>
</katalog>

--- DOM Parser (build tree) ---
Root element: katalog
Jumlah child: 3

--- Akses tree: cari semua buku ---
Jumlah buku: 3

--- Ekstrak field per buku ---
  [b001] Belajar Kotlin oleh Budi Santoso (pemrograman)
      Harga: IDR 125000, Stok: 15
  [b002] SQL untuk Pemula oleh Siti Aprilia (database)
      Harga: IDR 98000, Stok: 7
  [b003] Kotlin Lanjut & Coroutines oleh Dewi Lestari (pemrograman)
      Harga: IDR 175000, Stok: 3

--- Verifikasi entity decoding ---
Judul buku 3 (dengan &amp;): Kotlin Lanjut & Coroutines

--- XML Writer (serialize tree) ---
<katalog>
  <buku id="b001" kategori="pemrograman">
    <judul>Belajar Kotlin</judul>
    <penulis>Budi Santoso</penulis>
    <harga mata-uang="IDR">125000</harga>
    <stok>15</stok>
  </buku>
  <buku id="b002" kategori="database">
    <judul>SQL untuk Pemula</judul>
    <penulis>Siti Aprilia</penulis>
    <harga mata-uang="IDR">98000</harga>
    <stok>7</stok>
  </buku>
  <buku id="b003" kategori="pemrograman">
    <judul>Kotlin Lanjut &amp; Coroutines</judul>
    <penulis>Dewi Lestari</penulis>
    <harga mata-uang="IDR">175000</harga>
    <stok>3</stok>
  </buku>
</katalog>

--- Streaming parser (SAX-like) ---
    <buku id=b001 kategori=pemrograman>
    <buku id=b002 kategori=database>
    <buku id=b003 kategori=pemrograman>

--- Hitung total stok via stream ---
Total stok semua buku: 25

--- Perbandingan DOM vs Streaming ---
Aspek              | DOM                | Streaming (SAX)
-------------------|--------------------|--------------------
Memori             | O(ukuran XML)      | O(kedalaman)
Akses acak         | Ya (tree)          | Tidak (sequential)
Modifikasi         | Ya (ubah tree)     | Tidak (read-only)
Kasus cocok        | File kecil/sedang  | File besar (>100MB)
Kode               | Sederhana          | Lebih kompleks (state)

Mari kita bedah bagian kunci dari kode di atas:

  1. sealed class XmlNode: model node XML dengan dua variant—Element (punya nama, atribut, children) dan Text (hanya konten teks). Sealed memungkinkan when exhaustive saat traversal tree.
  2. XmlParser.parse(): parser recursive descent. Memilih XML declaration <?xml ... ?>, lalu comments <!-- -->, lalu root element. Setiap elemen dapat memiliki atribut dan children—child dapat berupa elemen lain atau teks.
  3. XmlParser.parseElement(): inti parser. Membaca nama elemen, atribut (loop sampai > atau /), lalu children. Self-closing tag <name/> menghasilkan elemen tanpa children. Tag biasa <name>...</name> diparse rekursif untuk children.
  4. XmlParser.readText(): membaca teks antar tag dan men-decode entity XML. &amp; → &, &lt; → <, &gt; → >, &quot; → ", &apos; → '. Numeric entity &#65; → A juga didukung.
  5. XmlWriter.write(node, indent): menulis tree ke string XML. Elemen dengan satu child text di-render inline (<judul>Belajar Kotlin</judul>). Elemen dengan child element di-render multi-baris dengan indentasi. Atribut ditulis setelah nama elemen.
  6. XmlStreamParser: streaming parser SAX-like. Handler callback onStartElement, onText, onEndElement dipanggil saat parser menemui elemen. Demo menghitung total stok semua buku via stream—state machine melacak apakah kita sedang di dalam elemen <stok>.
  7. Output menunjukkan round-trip: XML di-parse ke tree, lalu di-write kembali—struktur identik. Entity &amp; di-decode menjadi & saat parsing, dan di-encode kembali menjadi &amp; saat writing.

Library XML untuk Produksi #

Untuk produksi, library berikut populer di JVM/Kotlin:

LibraryPendekatanFitur
JAXB (jakarta.xml.bind)DOM + bindingMapping otomatis ke class Java
kotlinx-xml (beta)DOM/streamingPure Kotlin, KMP-compatible
dom4jDOMAPI yang lebih ramah dari org.w3c.dom
WoodstoxStAX streamingPerformant untuk file besar
JDOMDOMAPI ringkas, mudah dipakai
kotlin JAXB di Kotlin (ilustrasi)
import jakarta.xml.bind.annotation.*

@XmlRootElement(name = "buku")
@XmlAccessorType(XmlAccessType.FIELD)
data class Buku(
    @XmlAttribute val id: String,
    @XmlAttribute val kategori: String,
    @XmlElement val judul: String,
    @XmlElement val penulis: String,
    @XmlElement val harga: Long,
    @XmlElement val stok: Int
)

JAXB menghasilkan kode serialization otomatis dari anotasi—mirip dengan kotlinx.serialization untuk JSON. Namun, JAXB berbasis reflection dan tidak cocok untuk KMP.

Escaping Entity #

XML mendefinisikan lima entity bawaan yang harus di-escape saat muncul di teks atau atribut. Karakter <, >, &, ", ' harus di-escape menjadi &lt;, &gt;, &amp;, &quot;, &apos;. Parser harus men-decode entity saat parsing; writer harus meng-encode saat menulis.

text Entity escaping wajib di XML
<   →  &lt;
>   →  &gt;
&   →  &amp;
"   →  &quot;
'   →  &apos;

Referensi Lanjutan #

Penutup #

XML processing memerlukan pemahaman tree structure dan entity escaping. DOM builder cocok untuk file kecil di mana akses acak dan modifikasi diperlukan. Streaming parser SAX-like cocok untuk file besar di mana memori terbatas. Library produksi seperti JAXB atau kotlinx-xml mengotomatiskan binding ke class Kotlin. Chapter berikutnya akan beralih dari text format ke binary format—ProtoBuf yang lebih kompak dan cocok untuk komunikasi internal.