Skip to content

Instantly share code, notes, and snippets.

@ozodrukh
Created May 6, 2022 00:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ozodrukh/0331cbebbc05f90c55a86f2b15bedea5 to your computer and use it in GitHub Desktop.
Save ozodrukh/0331cbebbc05f90c55a86f2b15bedea5 to your computer and use it in GitHub Desktop.
Simple Plist(XML) Parser for Kotlin
enum class DictTokenType {
plist,
dict,
key,
string,
date,
integer,
data,
array,
bool_true,
bool_false,
unknown;
companion object {
private val stringTokens = DictTokenType.values().map {
it.toString()
}
operator fun contains(token: String?): Boolean {
return token in stringTokens
}
}
}
import javax.xml.parsers.SAXParserFactory
fun main(args: Array<String>) {
println("Hello World!")
// Try adding program arguments via Run/Debug configuration.
// Learn more about running applications: https://www.jetbrains.com/help/idea/running-applications.html.
println("Program arguments: ${args.joinToString()}")
val fileUrl = getResource("Library.xml")
fileUrl.openStream().use {
val saxParser = SAXParserFactory.newInstance().newSAXParser()
val plistHandler = PropertyListHandler()
saxParser.parse(it, plistHandler)
println(plistHandler.parsedDocument)
}
}
private fun getResource(name: String) =
object {}.javaClass.getResource(name)
class PropertyListHandler : DefaultHandler() {
class ElementBuilder(val token: DictTokenType) {
private val valueBuilder = StringBuilder()
fun addCharactersChunk(ch: CharArray?, start: Int, length: Int) {
valueBuilder.append(ch, start, length)
}
fun asKey(): String {
if (token == key) {
return valueBuilder.toString()
} else {
throw IllegalStateException("only <key> token can be taken")
}
}
fun asValue(): PropertyValue {
val value = valueBuilder.toString()
return when(token) {
string -> PropertyValue.StringProp(value)
date -> PropertyValue.DateProp(value)
integer -> PropertyValue.IntegerProp(value.toLong())
else -> throw java.lang.IllegalArgumentException("unsupported token <$token>")
}
}
}
private var depthStack = Stack<Pair<String?, PropertyValue.CollectionProperty>>()
private val document = ArrayList<PropertyValue.CollectionProperty>()
private var currentElement: ElementBuilder? = null
private var currentKey: ElementBuilder? = null
val parsedDocument: List<PropertyValue.CollectionProperty>
get() = document
override fun startElement(uri: String?, localName: String?, qName: String?, attributes: Attributes?) {
super.startElement(uri, localName, qName, attributes)
val token = getTokenType(qName)
when(token) {
dict -> {
depthStack.push(currentKey?.asKey() to PropertyValue.Dictionary())
currentKey = null
}
array -> {
depthStack.push(currentKey?.asKey() to PropertyValue.Array())
currentKey = null
}
key -> {
if (currentKey != null) {
throw IllegalStateException("previous key(${currentKey?.token}) not finished")
}
currentKey = ElementBuilder(token)
}
string, date, integer -> {
if (currentElement != null) {
throw IllegalStateException("previous element(${currentElement?.token}) not finished")
}
currentElement = ElementBuilder(token)
}
bool_true, bool_false -> {
currentKey?.apply {
pushValueToCurrentParent(asKey(), PropertyValue.BoolProp(token == bool_true))
} ?: throw IllegalStateException("key not found")
currentKey = null
}
data -> currentKey = null
plist -> Unit
else -> throw IllegalStateException("unknown token $token")
}
}
override fun characters(ch: CharArray?, start: Int, length: Int) {
super.characters(ch, start, length)
if (currentElement == null) {
currentKey?.addCharactersChunk(ch, start, length)
} else {
currentElement?.addCharactersChunk(ch, start, length)
}
}
override fun endElement(uri: String?, localName: String?, qName: String?) {
super.endElement(uri, localName, qName)
val token = getTokenType(qName)
when(token) {
dict, array -> {
val (k, v) = depthStack.pop()
pushValueToCurrentParent(k, v)
}
key -> Unit // next parse key
string, date, integer -> {
currentElement?.apply {
if (!pushValueToCurrentParent(currentKey?.asKey(), asValue())) {
throw IllegalStateException("couldn't add $token(${asValue()})")
}
} ?: throw IllegalStateException("ElementBuilder not initiated for token=$token")
currentElement = null
currentKey = null
}
data, plist, bool_true, bool_false -> Unit
else -> throw IllegalStateException("unknown token $token")
}
}
private fun getTokenType(qName: String?): DictTokenType {
return if ("true".equals(qName)) {
bool_true
} else if ("false".equals(qName)) {
bool_false
} else if (qName != null && qName in DictTokenType) {
DictTokenType.valueOf(qName)
} else {
unknown
}
}
internal fun pushValueToCurrentParent(key: String?, propValue: PropertyValue): Boolean {
if (depthStack.isEmpty()){
if (propValue is PropertyValue.CollectionProperty) {
return document.add(propValue)
} else {
throw IllegalStateException("trying to added entry, expected collection property")
}
} else {
val (_, currentParent) = depthStack.peek()
if (currentParent is PropertyValue.Dictionary) {
if (key == null) {
throw IllegalStateException("dictionary can't contain value without key")
}
currentParent.dict[key] = propValue
return true
} else if (currentParent is PropertyValue.Array) {
if (propValue is PropertyValue.Dictionary) {
return currentParent.entries.add(propValue)
} else {
throw IllegalStateException("expected Dictionary value, received $propValue")
}
}
}
return false
}
}
class PropertyListHandlerTest {
@Test
fun `test dictionary parsed`() {
val handler = PropertyListHandler()
handler.createDict()
handler.handleTag("key", "Major Version")
handler.handleTag("integer", "1")
handler.handleTag("key", "Minor Version")
handler.handleTag("integer", "1")
handler.endDict()
DefaultAsserter.assertTrue(null, handler.parsedDocument.get(0) is PropertyValue.Dictionary)
val dict = handler.parsedDocument.get(0) as PropertyValue.Dictionary
DefaultAsserter.assertTrue(null, dict.dict["Major Version"] == PropertyValue.IntegerProp(1))
DefaultAsserter.assertTrue(null, dict.dict["Minor Version"] == PropertyValue.IntegerProp(1))
}
fun PropertyListHandler.createDict() {
startElement(null, null, "dict", null)
}
fun PropertyListHandler.endDict() {
endElement(null, null, "dict")
}
fun PropertyListHandler.handleTag(tagName: String, value: String) {
startElement(null, null, tagName, null)
characters(value.toCharArray(), 0, value.length)
endElement(null, null, tagName)
}
}
sealed class PropertyValue() {
class EmptyValue(): PropertyValue()
abstract class CollectionProperty(): PropertyValue()
data class Dictionary(val dict: MutableMap<String, PropertyValue> = hashMapOf()): CollectionProperty()
data class Array(val entries: MutableList<Dictionary> = arrayListOf()): CollectionProperty()
data class StringProp(val value: String): PropertyValue()
data class IntegerProp(val value: Long) : PropertyValue()
data class BoolProp(val value: Boolean) : PropertyValue()
data class DateProp(val value: String) : PropertyValue()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment