Created
May 6, 2022 00:23
-
-
Save ozodrukh/0331cbebbc05f90c55a86f2b15bedea5 to your computer and use it in GitHub Desktop.
Simple Plist(XML) Parser for Kotlin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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