JavaRush/Courses/Kotlin SELF/DOM parsing: Document, Element, attributes, and textConte...

DOM parsing: Document, Element, attributes, and textContent

Open

1. Introduction

If you’ve ever seen code that “parses XML” using substringBefore("</tag>"), then you know two facts: such code sometimes even works… and that’s exactly why it’s especially dangerous. DOM parsing is the approach where XML becomes a tree of objects in memory. We stop catching corners with strings and start “walking the structure” like folders in a file explorer.

DOM is handy when the document isn’t huge and we need to freely extract different pieces of data. Yes, DOM loads the entire XML into memory (that’s the price of convenience), but after that we can grab the root element, inspect attributes, find all <expense>, read the text inside <amount>, and so on.

And a nice bonus: Kotlin on the JVM can easily call Java APIs, and a DOM parser is a classic Java tool. So we’ll use ready-made, time-tested libraries instead of reinventing the wheel out of split() and hope. Kotlin is explicitly positioned as a language that interoperates with Java, so “calling the Java DOM API from Kotlin” is a normal real-world scenario.

Mini DOM parsing pipeline

Before we dive into the details, let’s put together the big picture. In the DOM approach there are usually two stages: first we parse XML into a Document, and then we extract data from that tree.

Here’s a simple flowchart (yes, programmers draw arrows so they cry less during debugging):

flowchart TD
    A[XML as String / file] --> B["DocumentBuilder.parse(...)"]
    B --> C[Document]
    C --> D[documentElement: Element]
    D --> E[Find elements / read attributes / textContent]
    E --> F[Your Kotlin objects / strings / numbers]

In this lecture, we’re interested in everything up to block E inclusive: how to correctly get a Document, how to safely retrieve an Element, and how to read attributes and text.

2. DOM model: Document, Element, NodeList

Before writing code, it’s useful to get the terms straight. DOM is the “Document Object Model”— a document model as a tree. The top of the tree is the document itself; below are elements (tags); inside elements are other elements and text.

So your brain doesn’t try to memorize everything at once, here’s a simple matching picture: XML → tree → nodes.

Below is a small correspondence table so you don’t mix up what’s an “XML concept” and what’s a “DOM class”:

In XML In DOM (JVM) What it means in code
The whole document
org.w3c.dom.Document
A “box” that contains the whole tree
Tag <user>...</user>
org.w3c.dom.Element
An element node: it has a tag name, attributes, and content
Attribute id="42"
Element.getAttribute("id")
Fetch the attribute’s string value
A set of same-type tags <item>...</item>
org.w3c.dom.NodeList
A DOM-world collection: length + index access
Text inside a tag
Element.textContent
All text inside the element (often with spaces/newlines)

And now an important “gotcha” for beginners: NodeList is not a List. It doesn’t have map, forEach, indices, and other Kotlin collection niceties. In the DOM world you’ll often have to live “the old way”: 0 until nodes.length and nodes.item(i).

3. Getting a Document from XML

XML string → Document

To get a Document, we need a DOM parser. On Java/JVM it’s created via the DocumentBuilderFactory factory, which produces a DocumentBuilder. And DocumentBuilder already knows how to parse(...).

Let’s start with the minimal example: the XML is directly in a Kotlin string.

import javax.xml.parsers.DocumentBuilderFactory

fun main() {
    val xml = """<user id="42"><name>Ann</name></user>"""

    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    println(doc.documentElement.tagName) // user
}

Notice byteInputStream(). The parser typically works with byte streams (InputStream), while a string is just convenience for us. We turn the string into a byte stream and feed it to the parser.

There’s an important practical idea here: XML is text, but the parser reads bytes. In real projects you’ll think a lot about encodings, but for this lecture it’s enough to understand one simple rule: “the parser takes an InputStream, not ‘just a string’.”

Root element: documentElement

Once we have the Document, we want to know: “what’s inside?” The starting point is almost always the same: the root element.

doc.documentElement returns an Element. That is the root of the tree (the “top tag” that wraps everything else).

import javax.xml.parsers.DocumentBuilderFactory

fun main() {
    val xml = """<user id="42"><name>Ann</name></user>"""

    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    val root = doc.documentElement
    println(root.tagName) // user
}

Why does this matter? Because later you’ll often read “contextually”: first confirm the root is <expenses>, and only then search inside it for <expense>. This makes the code more predictable (and slightly less magic that you accidentally smuggle in yourself).

XML from a file: parse(File)

When XML comes not as a string but as a file (the classic “report export” case), you may want to read it directly. A DOM parser can parse a file without an intermediate readText(), meaning you don’t have to read the file into a string first.

Minimal example: “file → Document”:

import java.io.File
import javax.xml.parsers.DocumentBuilderFactory

fun parseXmlFile(path: String) =
    DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(File(path))

Usage:

import org.w3c.dom.Element

fun main() {
    val doc = parseXmlFile("expenses.xml")
    val root = doc.documentElement

    println(root.tagName) // expenses
    val first = root.getElementsByTagName("expense").item(0) as? Element
    println(first?.getAttribute("id")) // e.g.: e1
}

Why might this be more convenient? Because parse(File) opens the stream and reads the data itself. But if you do File(path).readText() and then parse(...), you first create a big string in memory, and then convert it back to bytes. For small files it’s not a tragedy, but the habit of “not copying big data unnecessarily” is usually a good one.

Parsing errors and try/catch

A DOM parser is fairly strict: if the XML isn’t well-formed, it throws an exception. And that’s normal: it’s not obligated to guess what you meant when you forgot to close a tag. (Though it would be cool if it also brought tea and nodded sympathetically.)

Wrapping parsing in try/catch is a good practice, especially in CLI apps where you want to keep working instead of crashing.

import javax.xml.parsers.DocumentBuilderFactory

fun main() {
    val brokenXml = "<user><name>Ann</user>" // name tag not closed

    try {
        DocumentBuilderFactory.newInstance()
            .newDocumentBuilder()
            .parse(brokenXml.byteInputStream())

        println("Parsed OK")
    } catch (e: Exception) {
        println("XML parse failed: ${e.message}")
    }
}

Here we catch Exception as broadly as possible (that’s fine for a learning example). In production code, people often catch more specific types, but for now the key idea is: “parsing is a potentially failing operation.”

4. Reading data from the DOM: attributes, elements, text

Attributes: getAttribute(...)

Attributes in XML look like id="42". In DOM they’re read like this:

import javax.xml.parsers.DocumentBuilderFactory

fun main() {
    val xml = """<user id="42"></user>"""

    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    val id = doc.documentElement.getAttribute("id")
    println(id) // 42
}

And here’s where the “fun” starts: if the attribute is missing, many people expect null. But getAttribute(...) usually returns an empty string.

import javax.xml.parsers.DocumentBuilderFactory

fun main() {
    val xml = """<user></user>"""

    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    val id = doc.documentElement.getAttribute("id")
    println("id='$id'") // id=''
}

This means “attribute exists but empty” and “attribute does not exist” look the same to you—both yield "". So in real code you almost always add an extra check:

val id = root.getAttribute("id").trim()
val idOrNull = id.takeIf { it.isNotEmpty() }

We use trim() because sometimes an attribute may contain spaces (and you don’t want to happily accept " " as a meaningful value).

Finding elements: getElementsByTagName(...)

When we have the root, the next typical step is to find nested elements. The simplest way: getElementsByTagName("name"). It returns a NodeList.

Let’s make a mini example:

import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Element

fun main() {
    val xml = """<user><name>Ann</name></user>"""

    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    val nodes = doc.getElementsByTagName("name")
    println(nodes.length) // 1

    val nameEl = nodes.item(0) as Element
    println(nameEl.tagName) // name
}

There are several nuances here right away:

First, nodes.length is the number of elements. If length == 0, then item(0) will return null, and casting with as Element will crash the program. So the rule is simple: check first, then access.

Second, getElementsByTagName searches the entire subtree, not only direct children. That is, if you have <user><profile><name>...</name></profile></user>, then user.getElementsByTagName("name") will find <name> inside <profile>. Sometimes that’s handy, sometimes it’s surprising. For now, just remember: the search is “deep.”

To avoid crashing on an empty result, you can use a safe cast as? and a null check:

import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Element

fun main() {
    val xml = """<user></user>"""

    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    val nameEl = doc.getElementsByTagName("name").item(0) as? Element
    println(nameEl?.tagName) // null
}

Text inside a tag: textContent

Now to the best part: how to extract the value inside <name>Ann</name>.

In DOM this is done via textContent:

import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Element

fun main() {
    val xml = """<user><name>Ann</name></user>"""

    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    val nameEl = doc.getElementsByTagName("name").item(0) as Element
    println(nameEl.textContent) // Ann
}

Now a real-life example: XML is often nicely formatted with newlines and indentation. For a human, that’s great; for the parser, it’s also text.

import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Element

fun main() {
    val xml = """
        <user>
            <name>
                Ann
            </name>
        </user>
    """.trimIndent()

    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    val nameEl = doc.getElementsByTagName("name").item(0) as Element
    println(nameEl.textContent)          // "\n                Ann\n            "
    println(nameEl.textContent.trim())   // Ann
}

That’s why trim() next to textContent isn’t “just in case,” but almost standard hygiene.

One more nuance: textContent returns all text inside the element, including the text of nested elements. For example:

import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Element

fun main() {
    val xml = """<msg>Hello <b>World</b>!</msg>"""

    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    val msgEl = doc.documentElement
    println(msgEl.textContent.trim()) // Hello World!
}

That’s normal; it’s just important to understand: textContent means “everything textual inside,” not “only the direct text child.”

A bit of DOM hygiene to make the code calmer

When you start writing DOM code, a common temptation appears: everywhere do item(0) as Element and trust that the world is kind. The world isn’t kind. It just hasn’t shown you an XML yet where <amount> is missing, or someone wrote <Amount> (a different, case-sensitive universe).

So a good style is to extract small helper functions that hide checks, trim(), and safe casts. We’ll do this in the next section— that’s where firstTagText will appear, and the main parsing code will become noticeably easier to read, while checks will be collected in one place.

Another useful technique is to normalize strings immediately when reading: trim(), sometimes lowercase() for categories (if your format allows it), empty checks. DOM parsing almost always goes hand-in-hand with “data preparation,” otherwise you’ll get category = " food " and then wonder why your groupings don’t work.

5. Example: importing expenses from XML into a list

So the examples don’t live in a vacuum, let’s embed DOM reading into logic we already know: we have a console expense-tracking app, and suddenly accounting (or a bank) provides an export… in XML. Because “that’s how it’s historically done.” Don’t ask—this is like “why does the office printer only work if you ask it nicely.”

Let the XML look like this:

<expenses>
  <expense id="e1">
    <amount>120.50</amount>
    <category>food</category>
    <comment>Lunch</comment>
  </expense>
</expenses>

For simplicity, let’s define an expense model (if you already have one in your project—use it; here the important part is not the model, but DOM extraction):

data class Expense(
    val id: String,
    val amount: Double,
    val category: String,
    val comment: String
)

Now let’s make a small helper: “get the text of the first tag inside parent.” Note: we return String? because the element may be missing.

import org.w3c.dom.Element

fun firstTagText(parent: Element, tag: String): String? {
    val node = parent.getElementsByTagName(tag).item(0) as? Element ?: return null
    return node.textContent.trim()
}

And now the main function: parse XML and get a list of Expense.

import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Element

fun parseExpenses(xml: String): List<Expense> {
    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    val nodes = doc.getElementsByTagName("expense")
    val result = mutableListOf<Expense>()

    for (i in 0 until nodes.length) {
        val expenseEl = nodes.item(i) as? Element ?: continue

        val id = expenseEl.getAttribute("id").trim()
        val amount = firstTagText(expenseEl, "amount")?.toDoubleOrNull()
        val category = firstTagText(expenseEl, "category")
        val comment = firstTagText(expenseEl, "comment") ?: ""

        if (id.isNotEmpty() && amount != null && category != null) {
            result.add(Expense(id, amount, category, comment))
        }
    }

    return result
}

This intentionally uses the most down-to-earth techniques: toDoubleOrNull(), null checks, trim(). We’re not building a pretty error system yet—we just need to learn how to read DOM carefully without crashing, because input data is almost always “sometimes fine, sometimes… well, you know.”

Let’s test with a mini input:

fun main() {
    val xml = """
        <expenses>
            <expense id="e1">
                <amount>120.50</amount>
                <category>food</category>
                <comment>Lunch</comment>
            </expense>
            <expense id="e2">
                <amount>999</amount>
                <category>tech</category>
                <comment>Keyboard</comment>
            </expense>
        </expenses>
    """.trimIndent()

    val items = parseExpenses(xml)
    println(items.size)        // 2
    println(items.first().id)  // e1
}

6. Common mistakes in DOM parsing

Mistake #1: calling item(0) without checking length.
This is the most common cause of unexpected crashes. In XML, an element may be missing: an older format version, a different supplier, or simply human error. If you do nodes.item(0) as Element, then on an empty NodeList item(0) returns null, and then either the cast fails, or you get an NPE a bit later. The fix is a habit: “check first, then access,” or use as? and an early return.

Mistake #2: expecting null from getAttribute(...).
Beginners often write val id = el.getAttribute("id") ?: ..., and then wonder why ?: doesn’t trigger. getAttribute usually returns an empty string, and that’s a separate logic branch. As a result, without trim() and isNotEmpty() you can “accept” a missing attribute as “well, looks like there is a value.”

Mistake #3: forgetting trim() on textContent and getting “invisible” spaces.
XML formatting (indentation, line breaks) is also text. So textContent may contain "\n 120.50\n" instead of "120.50". If you then do toDouble() (or string comparison), you’ll get errors where everything “looks pretty.” In learning examples it feels minor; in real projects it turns into hours of debugging “why doesn’t the number parse, it’s a number.”

Mistake #4: thinking getElementsByTagName only searches among direct children.
This method searches the entire subtree, and sometimes that leads to unexpected results: you wanted to find <amount> specifically inside a particular <expense>, but you found all <amount> from nested blocks (or from elsewhere if the structure is more complex). On small documents it’s not obvious, and then suddenly “the first <amount>” isn’t the one you meant. At this stage, discipline helps: search from the right element (context) and keep the XML structure as clear as possible.

1
Task
Kotlin SELF,  50 level,  1 lecture
Locked
User passport
User passport
1
Task
Kotlin SELF,  50 level,  1 lecture
Locked
Missing id
Missing id
1
Task
Kotlin SELF,  50 level,  1 lecture
Locked
Book title
Book title
1
Task
Kotlin SELF,  50 level,  1 lecture
Locked
Product showcase
Product showcase
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet