com.gitlab.mvysny.konsumexml.AttributeKonsumer.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of konsume-xml Show documentation
Show all versions of konsume-xml Show documentation
Konsume-XML: A simple functional XML parser with no annotations
The newest version!
package com.gitlab.mvysny.konsumexml
import com.gitlab.mvysny.konsumexml.stax.Location
import com.gitlab.mvysny.konsumexml.stax.StaxParser
import javax.xml.XMLConstants
import javax.xml.namespace.QName
/**
* Accesses attributes of current element.
*/
@KonsumerDsl
public interface AttributeKonsumer {
/**
* Returns the current location.
*/
public val location: Location
/**
* Returns the current element name.
*/
public val elementName: QName
/**
* Returns the [QName]s of all available attributes.
*/
public val names: List
/**
* Returns the [QName.getFullName] of all available attributes.
*
* Mostly used for debugging purposes, or producing an error message in case of parsing error.
*/
public val fullNames: List get() = names.map { it.getFullName() }
/**
* Returns the normalized attribute value of the attribute with given [name].
* If the attribute is missing, throws [KonsumerException].
* @return returns the value of the attribute.
* @throws IllegalStateException if the current stax event is not a START_ELEMENT or ATTRIBUTE,
* or if this konsumer has been [finish]d.
* @throws KonsumerException if the attribute is missing.
*/
public fun getValue(name: QName): String = getValue(name.localPart, name.namespaceURI)
/**
* Returns the normalized attribute value of the attribute with given [name].
* If the attribute is missing, returns `null`.
* @return returns the value of the attribute, returns `null` if not found
* @throws IllegalStateException if the current stax event is not a START_ELEMENT or ATTRIBUTE,
* or if this konsumer has been [finish]d.
*/
public fun getValueOrNull(name: QName): String? = getValueOrNull(name.localPart, name.namespaceURI)
@Deprecated("Use getValueOrNull", replaceWith = ReplaceWith("getValueOrNull"))
public fun getValueOpt(name: QName): String? = getValueOrNull(name)
/**
* Returns the normalized attribute value of the attribute with the [localName] and [namespace].
* If the attribute is missing, throws [KonsumerException].
* @return returns the value of the attribute.
* @throws IllegalStateException if the current stax event is not a START_ELEMENT or ATTRIBUTE,
* or if this konsumer has been [finish]d.
* @throws KonsumerException if the attribute is missing.
*/
public fun getValue(localName: String, namespace: String = XMLConstants.NULL_NS_URI): String {
val value: String = getValueOrNull(localName, namespace)
?: throw KonsumerException(location, elementName, "Required attribute '${QName(namespace, localName)}' is missing. Available attributes: $fullNames")
return value
}
/**
* Returns the normalized attribute value of the attribute with the [localName] and [namespace].
* If the attribute is missing, returns `null`.
* @return returns the value of the attribute, returns `null` if not found
* @throws IllegalStateException if the current stax event is not a START_ELEMENT or ATTRIBUTE,
* or if this konsumer has been [finish]ed.
*/
public fun getValueOrNull(localName: String, namespace: String = XMLConstants.NULL_NS_URI): String?
@Deprecated("Use getValueOrNull", replaceWith = ReplaceWith("getValueOrNull"))
public fun getValueOpt(localName: String, namespace: String = XMLConstants.NULL_NS_URI): String? =
getValueOrNull(localName, namespace)
/**
* Finalizes this attribute konsumer and cleans up its memory. Checks for any unconsumed attributes
* (if so configured).
*
* This konsumer should not be used anymore - any attempt to use this konsumer will fail with
* [IllegalStateException].
*/
public fun finish()
/**
* The same as [getValue].
*/
public operator fun get(name: QName): String = getValue(name)
/**
* The same as [getValue].
*/
public operator fun get(localName: String): String = getValue(localName)
}
/**
* Accesses attributes of the current element from given [stax] parser.
*/
public class StaxAttributeKonsumer(public val stax: StaxParser) : AttributeKonsumer {
override val location: Location
get() = stax.location
override val elementName: QName
get() = stax.elementName
private var isFinished = false
override val names: List
get() {
checkNotFinalized()
return stax.attributeNames
}
override fun getValueOrNull(localName: String, namespace: String): String? {
checkNotFinalized()
return stax.getAttributeValue(namespace, localName)
}
private fun checkNotFinalized() {
check(!isFinished) { "You can only read element attributes when the element has been freshly entered" }
}
override fun finish() {
isFinished = true
}
}
/**
* Returns the QNames of all attributes currently available.
*
* This method is only valid on a START_ELEMENT or ATTRIBUTE.
*/
public val StaxParser.attributeNames: List get() =
(0 until attributeCount).map { getAttributeName(it) }
/**
* Makes sure that all attributes are properly consumed. If not, fails in
* [finish] with a proper error message.
*/
public class AttributeKonsumerWatchdog(public val delegate: AttributeKonsumer, public val stax: StaxParser) : AttributeKonsumer {
/**
* Remembers all names of all attributes that were requested via one of the [getValue] methods. In [finish] we'll
* verify that all attributes has been requested and there are no unrequested (=not present in schema) attributes.
*
* Implementation detail: most elements do not have any attributes. Use lazy to defer construction of mutable set unless
* absolutely necessary.
*/
private val consumedAttributes: MutableSet by lazy(LazyThreadSafetyMode.NONE) { mutableSetOf() }
private var isFinished = false
override val location: Location
get() = stax.location
override val elementName: QName
get() = stax.elementName
override val names: List
get() = delegate.names
override fun getValue(name: QName): String {
onRequested(name)
return delegate.getValue(name)
}
override fun getValue(localName: String, namespace: String): String {
val qname = QName(namespace, localName)
onRequested(qname)
return delegate.getValue(qname)
}
private val QName.isIgnored: Boolean get() = namespaceURI == XMLConstants.XML_NS_URI || namespaceURI == XMLConstants.XMLNS_ATTRIBUTE_NS_URI
private fun onRequested(name: QName) {
if (!name.isIgnored) {
consumedAttributes.add(name)
}
}
override fun getValueOrNull(name: QName): String? {
onRequested(name)
return delegate.getValueOrNull(name)
}
override fun getValueOrNull(localName: String, namespace: String): String? {
val qname = QName(namespace, localName)
onRequested(qname)
return delegate.getValueOrNull(qname)
}
override fun finish() {
if (isFinished) return
(0 until stax.attributeCount).forEach { attributeIndex ->
val qname: QName = stax.getAttributeName(attributeIndex)
if (!qname.isIgnored && !consumedAttributes.contains(qname)) {
throw KonsumerException(location, elementName, "The attribute $qname has not been consumed")
}
}
delegate.finish()
isFinished = true
}
}
/**
* Used for root [Konsumer] which is supposed to consume the root element.
*/
public class NullElementKonsumer(public val stax: StaxParser) : AttributeKonsumer {
override val location: Location
get() = stax.location
override val elementName: QName
get() = QName("", "")
override val names: List
get() = listOf()
override fun getValueOrNull(localName: String, namespace: String): String? = throw IllegalStateException("Not in element")
override fun finish() {}
}
/**
* Retrieves a value of attribute with given [localName], passing it through [converter].
* If the conversion throws, the exception is wrapped in [KonsumerException]
* with exact location and rethrown.
*/
public fun AttributeKonsumer.getValue(
localName: String,
namespace: String = XMLConstants.NULL_NS_URI,
converter: (String) -> T
): T {
val value: String = getValue(localName, namespace)
return try {
converter(value)
} catch (e: Exception) {
throw KonsumerException(location, elementName, "Failed to convert the value '$value' of attribute '$localName': '${e.message}'", e)
}
}
/**
* Retrieves a value of attribute with given [localName], passing it through [converter]. If the conversion throws, the exception is wrapped in [KonsumerException]
* with exact location and rethrown.
* @throws KonsumerException if the conversion fails.
*/
public fun AttributeKonsumer.getValueOrNull(localName: String, namespace: String = XMLConstants.NULL_NS_URI, converter: (String) -> T): T? {
val value: String = getValueOrNull(localName, namespace) ?: return null
return try {
converter(value)
} catch (e: Exception) {
throw KonsumerException(location, elementName, "Failed to convert the value '$value' of attribute '$localName': '${e.message}'", e)
}
}