All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gitlab.mvysny.konsumexml.Konsumer.kt Maven / Gradle / Ivy

The newest version!
package com.gitlab.mvysny.konsumexml

import com.gitlab.mvysny.konsumexml.stax.Location
import com.gitlab.mvysny.konsumexml.stax.StaxEventType
import com.gitlab.mvysny.konsumexml.stax.StaxParser
import java.io.Closeable
import javax.xml.namespace.QName

@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
public annotation class KonsumerDsl

/**
 * The settings for [Konsumer]. Mutable, can be changed during XML parsing.
 * @property failOnUnconsumedAttributes defaults to false. If true, any unconsumed attributes (attributes not queried by
 * via [AttributeKonsumer.getValue], with the exception of `xml:` and `xmlns:` attributes) will fail the parsing. Enabling this
 * allows stricter validation since unknown attributes will be discovered and reported.
 */
public data class KonsumerSettings(var failOnUnconsumedAttributes: Boolean = false) {
    public fun newAttributeKonsumer(stax: StaxParser): AttributeKonsumer {
        var k: AttributeKonsumer = StaxAttributeKonsumer(stax)
        if (failOnUnconsumedAttributes) {
            k = AttributeKonsumerWatchdog(k, stax)
        }
        return k
    }
}

/**
 * Upon calling a method, say [child], konsumer reads (consumes) next event from [reader] and matches it to the expected content.
 *
 * All methods throw [KonsumerException] on any parsing error, [javax.xml.stream.XMLStreamException] on any I/O errors and XML parsing errors.
 *
 * It is not necessary to [close] child konsumers - in fact you must not done so. See [close] for more info.
 * @property elementName the name of the current element we're in (the contents of which we're consuming). If we haven't consumed root element yet then this is null.
 */
@KonsumerDsl
public class Konsumer(private val reader: StaxReader,
                      public val elementName: QName?,
                      public val settings: KonsumerSettings) : Closeable {
    /**
     * If true then this konsumer has finished reading the contents of its element. This konsumer can no longer be used
     * anymore; the parent konsumer is now allowed to continue consuming.
     */
    public var isFinished: Boolean = false
        private set

    /**
     * The name of the current element we're in (the contents of which we're consuming). If we haven't consumed root element yet then this fails.
     *
     * Equals to [elementName].
     */
    public val name: QName
        get() = elementName ?: throw KonsumerException(location, this.elementName, "Expected to learn name but I'm not currently in an element")

    /**
     * Returns [QName.localPart] out of [name].
     */
    public val localName: String
        get() = name.localPart

    /**
     * Returns the [QName.tagName] out of [name]. The tag name is a [name] but it includes a prefix, e.g. `atom:link`.
     * Fails if [name] is null.
     */
    public val tagName: String
        get() = name.tagName

    private var currentChildKonsumer: Konsumer? = null

    // lazy, so that settings can be changed beforehand
    private val attributesLazy: AttributeKonsumer by lazy(LazyThreadSafetyMode.NONE) { if (elementName == null) NullElementKonsumer(reader.stax) else settings.newAttributeKonsumer(reader.stax) }

    /**
     * Allows access to the attributes of the element [elementName]. Only accessible in the beginning, before any of the element
     * contents are enumerated. Accessing attributes later on will cause [IllegalStateException] to be thrown.
     *
     * By default unconsumed attributes (attributes not queried by via [AttributeKonsumer.getValue], with the exception of `xml:` and `xmlns:` attributes) will not fail the parsing;
     * however this can be changed by changing the [KonsumerSettings.failOnUnconsumedAttributes] setting.
     */
    public val attributes: AttributeKonsumer
        get() {
            checkNotFinished()
            checkNoChildKonsumer()
            return attributesLazy
        }

    /**
     * The current location in the XML file.
     */
    public val location: Location get() = reader.stax.location

    private fun checkNotFinished() {
        check(!isFinished) { "finished - cannot be used anymore" }
    }

    private fun checkNoChildKonsumer() {
        check(currentChildKonsumer?.isFinished != false) { "A child konsumer of '${currentChildKonsumer!!.elementName?.getFullName()}' is ongoing, cannot use this consumer of '${elementName?.getFullName()}' until the child konsumer finishes" }
    }

    /**
     * Checks that the current element local name (tag name without any namespace prefix) is [localName].
     * @throws KonsumerException if the current element is named differently.
     */
    @KonsumerDsl
    public fun checkCurrent(localName: String) {
        if (this.elementName == null) {
            throw KonsumerException(location, this.elementName, "Expected current element '$localName' but I'm not currently in an element")
        }
        if (this.elementName.localPart != localName) {
            throw KonsumerException(location, this.elementName, "Expected current element '$localName' but I'm in '${this.elementName}'")
        }
    }

    /**
     * Expects the next element is of given [localName], and consumes it. Runs a child consumer which
     * is responsible for consuming of the contents of this element. This consumer cannot be used while the child consumer
     * haven't finished consuming of the tokens.
     *
     * The function fails if any non-whitespace text is encountered. Use [text] to
     * read any text nodes for elements with mixed contents.
     *
     * @throws KonsumerException if the next element is of different name, or there is no next element (the current element ends),
     * or on end-of-stream.
     */
    @KonsumerDsl
    public fun  child(localName: String, block: Konsumer.() -> T): T =
            child(Names.of(localName), block)

    /**
     * Expects the next element is of given [names], and consumes it. Runs a child consumer which
     * is responsible for consuming of the contents of this element. This consumer cannot be used while the child consumer
     * haven't finished consuming of the tokens.
     *
     * The function fails if any non-whitespace text is encountered. Use [text] to
     * read any text nodes for elements with mixed contents.
     *
     * @throws KonsumerException if the next element is of different name, or there is no next element (the current element ends),
     * or on end-of-stream.
     */
    @KonsumerDsl
    public fun  child(names: Names, block: Konsumer.() -> T): T {
        val childKonsumer: Konsumer = nextElement(names, true)!!
        val result: T = childKonsumer.runProtected(block)
        childKonsumer.finish()
        return result
    }

    /**
     * Catches any exceptions thrown by [block] and wraps them in [KonsumerException],
     * which will contain the exact location of the problematic place.
     */
    private fun  runProtected(block: Konsumer.() -> T): T = try {
        block()
    } catch (e: Exception) {
        if (e is KonsumerException) throw e
        throw KonsumerException(location, elementName, e.message ?: "", e)
    }

    /**
     * Expects the next element is of given [localName], and consumes it; does nothing and returns `null` if there is no such next element.
     * If there is, runs a child consumer which
     * is responsible for consuming of the contents of this element. This consumer cannot be used while the child consumer
     * haven't finished consuming of the tokens.
     *
     * The function fails if any non-whitespace text is encountered. Use [text] to
     * read any text nodes for elements with mixed contents.
     *
     * @throws KonsumerException if the next element is of different name, or there is no next element (the current element ends),
     * or on end-of-stream.
     */
    @KonsumerDsl
    public fun  childOrNull(localName: String, block: Konsumer.() -> T): T? =
            childOrNull(Names.of(localName), block)

    @KonsumerDsl
    @Deprecated("use childOrNull", ReplaceWith("childOrNull"))
    public fun  childOpt(localName: String, block: Konsumer.() -> T): T? =
            childOrNull(localName, block)

    /**
     * Expects the next element is of given [localName], and consumes it; does nothing and returns `null` if there is no such next element.
     * If there is, runs a child consumer which
     * is responsible for consuming of the contents of this element. This consumer cannot be used while the child consumer
     * haven't finished consuming of the tokens.
     *
     * The function fails if any non-whitespace text is encountered. Use [text] to
     * read any text nodes for elements with mixed contents.
     *
     * @throws KonsumerException if the next element is of different name, or there is no next element (the current element ends),
     * or on end-of-stream.
     */
    @KonsumerDsl
    public fun  childOrNull(names: Names, block: Konsumer.() -> T): T? {
        val childKonsumer: Konsumer = nextElement(names) ?: return null
        val result: T = childKonsumer.runProtected(block)
        childKonsumer.finish()
        return result
    }

    @KonsumerDsl
    @Deprecated("use childOrNull", ReplaceWith("childOrNull"))
    public fun  childOpt(names: Names, block: Konsumer.() -> T): T? =
            childOrNull(names, block)

    /**
     * Consumes all follow-up [XMLEvent.START_ELEMENT]s up to [maxCount] of an element with given [localName]. Runs a child consumer which
     * is responsible for consuming of the contents of this element. This consumer cannot be used while the child consumer
     * haven't finished consuming of the tokens. The consummation stops when another element is encountered or when the
     * current element ends.
     *
     * The function fails if any non-whitespace text is encountered. The function automatically
     * skips any whitespace encountered. If this is not desired, use [text] and [childOrNull] instead, to
     * read any text nodes for elements with mixed contents.
     *
     * Beware - this function will load all [T] items into memory and return that as a list. If the number of items is
     * potentially huge and they're processed in a stream fashion, try using [childrenSequence] to save a huge chunk of memory.
     *
     * @param minCount minimum elements to match, 0 or greater. If fewer elements are matched, [KonsumerException] is thrown. Defaults to 0.
     * @param maxCount maximum elements to match, 0 or greater. The function returns as soon as this amount is reached, therefore
     * it could leave some elements unconsumed. Defaults to [Int.MAX_VALUE].
     * @throws KonsumerException on end-of-stream, or if fewer elements than [minCount] has been encountered.
     * @return a list of values as returned by [block], one per every element encountered. Contains at most [maxCount] items.
     */
    @KonsumerDsl
    public fun  children(localName: String,
                                   minCount: Int = 0,
                                   maxCount: Int = Int.MAX_VALUE,
                                   block: Konsumer.() -> T): List =
            children(Names.of(localName), minCount, maxCount, block)

    /**
     * Consumes all follow-up [XMLEvent.START_ELEMENT]s up to [maxCount] of an element with any of given [names]. Runs a child consumer which
     * is responsible for consuming of the contents of this element. This consumer cannot be used while the child consumer
     * haven't finished consuming of the tokens. The consummation stops when another element is encountered or when the
     * current element ends.
     *
     * The function fails if any non-whitespace text is encountered. The function automatically
     * skips any whitespace encountered. If this is not desired, use [text] and [childOrNull] instead, to
     * read any text nodes for elements with mixed contents.
     *
     * Beware - this function will load all [T] items into memory and return that as a list. If the number of items is
     * potentially huge and they're processed in a stream fashion, try using [childrenSequence] to save a huge chunk of memory.
     *
     * @param minCount minimum elements to match, 0 or greater. If fewer elements are matched, [KonsumerException] is thrown. Defaults to 0.
     * @param maxCount maximum elements to match, 0 or greater. The function returns as soon as this amount is reached, therefore
     * it could leave some elements unconsumed. Defaults to [Int.MAX_VALUE].
     * @throws KonsumerException on end-of-stream, or if fewer elements than [minCount] has been encountered.
     * @return a list of values as returned by [block], one per every element encountered. Contains at most [maxCount] items.
     */
    @KonsumerDsl
    public fun  children(names: Names,
                            minCount: Int = 0,
                            maxCount: Int = Int.MAX_VALUE,
                            block: Konsumer.() -> T): List {
        if (maxCount == 0) return listOf()
        require(minCount >= 0) { "minCount must be 0 or greater but was $minCount" }
        require(maxCount >= minCount) { "maxCount must be $minCount or greater but was $maxCount" }

        val resultList: MutableList = mutableListOf()

        fun verifyMinCount() {
            if (resultList.size < minCount) {
                throw KonsumerException(location, elementName, "At least $minCount of element $names was expected, but only ${resultList.size} encountered")
            }
        }

        while (true) {
            val childKonsumer: Konsumer? = nextElement(names)
            if (childKonsumer == null) {
                verifyMinCount()
                return resultList
            }
            val result: T = childKonsumer.runProtected(block)
            childKonsumer.finish()
            resultList.add(result)
            if (resultList.size >= maxCount) {
                return resultList
            }
        }
    }

    /**
     * Returns a sequence that lazily polls elements with given [localName] from this konsumer, runs [block] and returns the
     * converted items. Intended to be used for stream-processing of XML contents. Uses way less memory than [children]
     * since it only holds one [T] item at any time.
     *
     * The sequence ends when there are no more elements with given [localName]. To upper-limit the number of elements returned,
     * use [Sequence.take]. The sequence can only be iterated once. The sequence does not need to be iterated entirely.
     * Iterating the sequence does not close this konsumer.
     *
     * The function fails if any non-whitespace text is encountered. Use [text] to
     * read any text nodes for elements with mixed contents.
     *
     * @param minCount minimum elements to match, 0 or greater. If fewer elements are matched, [KonsumerException] is thrown. Defaults to 0.
     * @throws KonsumerException on end-of-stream, or if fewer elements than [minCount] has been encountered.
     */
    @KonsumerDsl
    public fun  childrenSequence(localName: String, minCount: Int = 0, block: Konsumer.() -> T): Sequence =
            childrenSequence(Names.of(localName), minCount, block)

    /**
     * Returns a sequence that lazily polls elements with given [names] from this konsumer, runs [block] and returns the
     * converted items. Intended to be used for stream-processing of XML contents. Uses way less memory than [children]
     * since it only holds one [T] item at any time.
     *
     * The sequence ends when there are no more elements with one of given [names]. To upper-limit the number of elements returned,
     * use [Sequence.take]. The sequence can only be iterated once. The sequence does not need to be iterated entirely.
     * Iterating the sequence fully does not [finish] this konsumer.
     *
     * The function fails if any non-whitespace text is encountered. Use [text] to
     * read any text nodes for elements with mixed contents.
     *
     * @param minCount minimum elements to match, 0 or greater. If fewer elements are matched, [KonsumerException] is thrown. Defaults to 0.
     * @throws KonsumerException on end-of-stream, or if fewer elements than [minCount] has been encountered.
     */
    @KonsumerDsl
    public fun  childrenSequence(names: Names, minCount: Int = 0, block: Konsumer.() -> T): Sequence {

        var elementsEncountered = 0

        fun verifyMinCount() {
            if (elementsEncountered < minCount) {
                throw KonsumerException(location, elementName, "At least $minCount of element $names was expected, but only $elementsEncountered encountered")
            }
        }

        return generateSequence {
            val childKonsumer: Konsumer? = nextElement(names)
            if (childKonsumer == null) {
                verifyMinCount()
                null
            } else {
                val result: T = block(childKonsumer)
                childKonsumer.finish()
                elementsEncountered++
                result
            }
        }.constrainOnce()
    }

    /**
     * A lower-level API intended for "streaming" approach. The function checks if the next element has one of given [names]:
     * * If yes, then a child konsumer is returned, which consumes the contents of that element.
     *   Note: you must call [finish] after you're done with the child konsumer.
     * * If not, then `null` is returned and nothing happens.
     *
     * You should generally never use this method directly. If you wish to stream-process
     * the XML then it's safer to use [childrenSequence]; if you wish to read a particular
     * element then [child] is a better choice.
     *
     * The function fails if any non-whitespace text is encountered. Use [text] to
     * read any text nodes for elements with mixed contents.
     *
     * @param requireElement defaults to `false`. If `true`, the function will
     * fail if there is no next element or if the next element has a different name than one of [names].
     * If `true`, the function will therefore never return `null`.
     * @return if an element with matching name has been encountered, then a child konsumer is returned,
     * otherwise `null` is returned.
     */
    @KonsumerDsl
    public fun nextElement(names: Names,
                    requireElement: Boolean = false): Konsumer? {
        checkNotFinished()
        checkNoChildKonsumer()
        attributes.finish()

        while (reader.hasNext()) {
            val eventType: StaxEventType = reader.next()
            when (eventType) {
                StaxEventType.CData, StaxEventType.Characters -> {
                    if (!reader.stax.text.isNullOrBlank()) {
                        throw KonsumerException(location, elementName, "Expected element $names but there is unconsumed text: '${reader.stax.text?.trim()}'")
                    }
                }
                StaxEventType.EndDocument -> {
                    if (requireElement) {
                        throw KonsumerException(location, elementName, "Expected element $names but got END_DOCUMENT")
                    }
                    reader.pushBack()
                    return null
                }
                StaxEventType.EndElement -> {
                    if (requireElement) {
                        throw KonsumerException(location, this.elementName, "Expected child element $names but got END_ELEMENT")
                    }
                    reader.pushBack()
                    return null
                }
                StaxEventType.StartElement -> {
                    if (!names.accepts(reader.stax.elementName)) {
                        if (requireElement) {
                            throw KonsumerException(location, this.elementName, "Expected element $names but got '${reader.stax.elementName}'")
                        }
                        reader.pushBack()
                        return null
                    }
                    val konsumer = Konsumer(reader, reader.stax.elementName, settings)
                    currentChildKonsumer = konsumer
                    return konsumer
                }
                else -> throw AssertionError("unexpected $eventType")
            }
        }
        return null
    }

    /**
     * If true then we've parsed everything from this XML and there is no more content to parse.
     */
    public val isEndOfDocument: Boolean get() = !reader.hasNext()

    /**
     * Utility method for expecting an element with given [name] and text contents
     * only; the trimmed text contents are returned.
     * @param whitespace the way to process whitespaces, defaults to [Whitespace.collapse]
     * @throws KonsumerException if the element is absent, or the element contains child elements.
     */
    @KonsumerDsl
    public fun childText(name: String,
                  whitespace: Whitespace = Whitespace.collapse): String =
            childText(name, whitespace) { it }

    /**
     * Utility method for expecting an element with given [name] and text contents only; the trimmed text contents are
     * returned.
     * @param whitespace the way to process whitespaces, defaults to [Whitespace.collapse]
     * @throws KonsumerException if the element is present and contains child elements.
     */
    @KonsumerDsl
    public fun childTextOrNull(
            name: String,
            whitespace: Whitespace = Whitespace.collapse
    ): String? = childTextOrNull(name, whitespace) { it }

    @KonsumerDsl
    @Deprecated("use childTextOrNull", ReplaceWith("childTextOrNull"))
    public fun childTextOpt(
            name: String,
            whitespace: Whitespace = Whitespace.collapse
    ): String? = childTextOrNull(name, whitespace)

    /**
     * Utility method for expecting a zero-to-infinite elements with given [name] and text contents only; the trimmed text contents are
     * returned.
     * @throws KonsumerException if the [minCount] condition can not be satisfied, or if the elements contains child elements.
     */
    @KonsumerDsl
    public fun childrenText(name: String, minCount: Int = 0, maxCount: Int = Int.MAX_VALUE): List =
            childrenText(name, minCount, maxCount) { it }

    /**
     * Utility method for expecting one element with given [name] and text contents only; the trimmed text contents are
     * ran through given [converter] and returned. If the converter throws, the exception is wrapped in [KonsumerException]
     * with exact location and returned. This is perfect for performing conversion of the value to e.g. Int.
     * @param whitespace the way to process whitespaces, defaults to [Whitespace.collapse]
     * @throws KonsumerException if the element is absent, or the conversion fails, or the element contains child elements.
     */
    @KonsumerDsl
    public fun  childText(
            name: String,
            whitespace: Whitespace = Whitespace.collapse,
            converter: (String) -> T
    ): T = child(name) { text(whitespace, converter) }

    /**
     * Utility method for expecting 0..1 elements with given [name] and text contents only; the trimmed text contents are
     * ran through given [converter] and returned. If the converter throws, the exception is wrapped in [KonsumerException]
     * with exact location and returned. This is perfect for performing conversion of the value to e.g. Int.
     *
     * The function does nothing and returns `null` if there is no such next element.
     * @param whitespace the way to process whitespaces, defaults to [Whitespace.collapse]
     * @throws KonsumerException if the element is present, and either the conversion fails, or the element contains child elements.
     */
    @KonsumerDsl
    public fun  childTextOrNull(
            name: String,
            whitespace: Whitespace = Whitespace.collapse,
            converter: (String) -> T
    ): T? = childOrNull(name) { text(whitespace, converter) }

    @KonsumerDsl
    @Deprecated("Use childTextOrNull", ReplaceWith("childTextOrNull"))
    public fun  childTextOpt(
            name: String,
            whitespace: Whitespace = Whitespace.collapse,
            converter: (String) -> T
    ): T? = childTextOrNull(name, whitespace, converter)

    /**
     * Utility method for expecting a zero-to-infinite elements with given [name] and text contents only; the trimmed text contents are
     * ran through given [converter] and returned. If the converter throws, the exception is wrapped in [KonsumerException]
     * with exact location and returned. This is perfect for performing conversion of the value to e.g. Int.
     * @param whitespace the way to process whitespaces, defaults to [Whitespace.collapse]
     * @throws KonsumerException if the [minCount] condition can not be satisfied, or if the elements contains child elements,
     * or if the conversion fails for any of the children elements.
     */
    @KonsumerDsl
    public fun  childrenText(name: String,
                         minCount: Int = 0,
                         maxCount: Int = Int.MAX_VALUE,
                         whitespace: Whitespace = Whitespace.collapse,
                         converter: (String) -> T): List =
            children(name, minCount, maxCount) { text(whitespace, converter) }

    /**
     * Skims through all follow-up text nodes until [XMLEvent.END_ELEMENT] is encountered,
     * then returns all the text encountered in this element.
     *
     * By default, the function fails if an element is encountered. However, that
     * would prevent us from reading mixed contents. To support mixed contents,
     * set [failOnElement] to false. In this mode, the function will read all text nodes
     * up to the nearest element, or up until [XMLEvent.END_ELEMENT] is encountered.
     * If there are no text nodes left, the function will return an empty string.
     *
     * *Warning:* when setting [failOnElement] to false, you still need to manually
     * handle elements yourself, for example by calling [childOrNull] or [allChildrenAutoIgnore] or similar.
     * Alternatively, you can use [textRecursively] if you simply wish to parse the recursive text contents.
     *
     * @param whitespace the way to process whitespaces, defaults to [Whitespace.collapse]
     * @param failOnElement defaults to true. If true, the function fails if an element is encountered.
     * If false, the function will instead read all the text up to the nearest element, returning
     * the text found. This allows you
     * to consume the element encountered, e.g. by using [childOrNull]. Perfect for mixed contents such as `a beautiful day`.
     * @throws KonsumerException if an element is started or end-of-stream is encountered.
     */
    @KonsumerDsl
    public fun text(whitespace: Whitespace = Whitespace.collapse,
             failOnElement: Boolean = true): String {
        checkNotFinished()
        checkNoChildKonsumer()
        attributes.finish()
        val textContents = StringBuilder()
        while (reader.hasNext()) {
            val eventType: StaxEventType = reader.next()
            when (eventType) {
                StaxEventType.CData, StaxEventType.Characters -> textContents.append(reader.stax.text!!)
                StaxEventType.StartElement -> {
                    if (failOnElement) {
                        throw KonsumerException(location, elementName, "Expected text but got START_ELEMENT: ${reader.stax.elementName}")
                    }
                    reader.pushBack()
                    // don't process the whitespaces on the fly, but rather at the end of the processing: https://gitlab.com/mvysny/konsume-xml/-/issues/22
                    return whitespace.process(textContents.toString())
                }
                StaxEventType.EndElement, StaxEventType.EndDocument -> {
                    isFinished = true
                    return whitespace.process(textContents.toString())
                }
                else -> throw AssertionError("unexpected $eventType")
            }
        }
        isFinished = true
        return whitespace.process(textContents.toString())
    }

    /**
     * Exactly as [text] but runs the text through given [converter]. If the converter fails, the exception is automatically
     * wrapped in [KonsumerException] with appropriate location. Handy for converting values to e.g. integers:
     * ```kotlin
     * text { it.toInt() }
     * ```
     * @param whitespace the way to process whitespaces, defaults to [Whitespace.collapse]
     * @throws KonsumerException if the conversion fails, an element is started or end-of-stream is encountered.
     */
    @KonsumerDsl
    public fun  text(whitespace: Whitespace, converter: (String) -> T): T {
        val t: String = text(whitespace)
        try {
            return converter(t)
        } catch (e: Exception) {
            throw KonsumerException(location, elementName, "Failed to convert '$t': ${e.message}", e)
        }
    }

    /**
     * Exactly as [text] but runs the text through given [converter]. If the converter fails, the exception is automatically
     * wrapped in [KonsumerException] with appropriate location. Handy for converting values to e.g. integers:
     * ```kotlin
     * text { it.toInt() }
     * ```
     * Whitespaces are [Whitespace.collapse]d.
     * @throws KonsumerException if the conversion fails, an element is started or end-of-stream is encountered.
     */
    @KonsumerDsl
    public fun  text(converter: (String) -> T): T = text(Whitespace.collapse, converter)

    /**
     * Consumes nearest [XMLEvent.END_ELEMENT], to finalize this Konsumer, to allow the parent konsumer to continue.
     * Fails if there are unconsumed events such as [XMLEvent.CDATA], [XMLEvent.CHARACTERS], [XMLEvent.START_ELEMENT].
     *
     * A low-level API, only to be used with the [nextElement] function.
     *
     * Do not call for the root Konsumer - it will fail with `Expected END_ELEMENT but got END_DOCUMENT`.
     *
     * Multiple invocations of this method do nothing.
     */
    @KonsumerDsl
    public fun finish() {
        if (isFinished) return
        attributes.finish()
        while (reader.hasNext()) {
            val eventType: StaxEventType = reader.next()
            when (eventType) {
                StaxEventType.CData, StaxEventType.Characters -> {
                    if (!reader.stax.text.isNullOrBlank()) {
                        throw KonsumerException(location, elementName, "Expected END_ELEMENT but there is unconsumed text: '${reader.stax.text?.trim()}'")
                    }
                }
                StaxEventType.EndDocument -> throw KonsumerException(location, elementName, "Expected END_ELEMENT but got END_DOCUMENT")
                StaxEventType.StartElement -> throw KonsumerException(location, elementName, "Expected END_ELEMENT but got START_ELEMENT: '${reader.stax.elementName}'")
                StaxEventType.EndElement -> {
                    isFinished = true
                    return
                }
                else -> throw AssertionError("unexpected $eventType")
            }
        }
        throw KonsumerException(location, elementName, "Expected END_ELEMENT but got end of stream")
    }

    /**
     * Closes the underlying reader and makes it impossible to continue with XML parsing.
     *
     * Closing this class closes all underlying resources including the input stream. Konsumer only implements [Closeable]
     * for convenience purpose, so that you can parse the file contents easily, simply by writing:
     * ```kotlin
     * File("in.xml").konsumeXml().use { k ->
     *   k.child("root") {}
     * }
     * ```
     *
     * You must not close child consumers, since that would close the underlying [reader], making any further attempts to
     * read the XML fail.
     */
    override fun close() {
        reader.close()
    }

    /**
     * Skips over the rest of the contents of this element: skips all text nodes, child elements. Note that [finish] still needs
     * to be called on this konsumer.
     *
     * WARNING: This effectively disables validation for the contents of this element, since no child elements are
     * expressed as expected. Please see the README on how to skip contents while having support for validation.
     *
     * See the `README.md` file for examples on how to use this function most effectively.
     */
    @KonsumerDsl
    public fun skipContents() {
        checkNotFinished()
        checkNoChildKonsumer()
        attributes.finish()

        while (reader.hasNext()) {
            val eventType: StaxEventType = reader.next()
            when (eventType) {
                StaxEventType.CData, StaxEventType.Characters -> {
                    // skip text
                }
                StaxEventType.EndDocument, StaxEventType.EndElement -> {
                    reader.pushBack()  // so that finish() can be called
                    return
                }
                StaxEventType.StartElement -> {
                    val konsumer = Konsumer(reader, reader.stax.elementName, settings)
                    currentChildKonsumer = konsumer
                    konsumer.skipContents()
                    konsumer.finish()
                }
                else -> throw AssertionError("unexpected $eventType")
            }
        }
        throw KonsumerException(location, this.elementName, "Expected END_ELEMENT but got end-of-stream")
    }

    override fun toString(): String = "$location, in element <${elementName?.getFullName()}>"
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy