iT邦幫忙

2021 iThome 鐵人賽

DAY 19
0

KotlinParserGenerator

我們先從 kotlin 的 parser 講起,這邊會順便帶到一些 KotlinPoet 的進階用法。我們目標是讀取 annotation 的資訊產生對應的 parser ,以下面這組的 annotation 為例:

@RssTag(name = "channel")
data class TestRssData(
    val title: String?,
    @RssTag
    val link: String?,
    val textInput: MyTextInput?,
    @RssTag(name = "item")
    val list: List<RssItem>,
    @RssTag(name = "category")
    val categories: List<TestCategory>,
    val skipDays: SkipDays?,
    val ttl: Long?,
    val image: TestImage?,
    val cloud: TestCloud?,
): Serializable

這個類別包含的子結構就不在這邊一一列出,原始碼可以參考這裡。在寫 generator 之前,我們要先規劃產生出來程式碼會長什麼樣子。以上面的例子,我們可以想像會有個 TestRssDataParser 提供一系列的 function 來從目標類別 ( TestRssData ) 爬出資訊, 而它包含的子結構,可以透過子結構本身再產生一個類別提供 function 去負責爬該類別的資訊。舉例來說,負責爬 TestRssData 資訊的是 TestRssDataParser ,而裡面會用到子結構 RssItem 的資訊,則是產生另一個 RssItemParser 去爬取,這個概念將可以套用在其他所有的 generator 上面。

讓我們一步步拆解 TestRssDataParser 來講解怎麼做的,首先我們先訂出最重要的 parse function ,輸入是 XML 字串,輸出是被標 annotation 的 data class ,在這邊是 TestRssData

object TestRssDataParser {
  fun parse(xml: String): TestRssData {
     val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
     val document = builder.parse(xml.byteInputStream())
     document.documentElement.normalize()
     val nodeList = document.getElementsByTagName("channel")
     var result: TestRssData? = null

     if (nodeList?.length == 1) {
    		val element = nodeList.item(0) as? Element
    		element?.let {
    			result = it.getChannel()
    		}
     }
     return result ?: throw IllegalStateException("No valid channel tag in the RSS feed.")
  }
}

上述的程式碼有沒有很熟悉啊?它就是前面在講 DOM parser 的時候的作法,一樣的邏輯搬過來,只是現在我們要用 annotation 給的資訊,接著用 KotlinPoet 把動態產生出來。

const val PARSER_FUNC_NAME = "parse"
const val CHANNEL = "channel"

private val outputClass = ClassName(element.getPackage(), element.simpleName.toString())
private val exceptionClass = ClassName("java.lang", "IllegalStateException")
private val docBuilderFactoryClass = ClassName("javax.xml.parsers", "DocumentBuilderFactory")

fun getParseFuncSpec(): FunSpec {
    return FunSpec.builder(PARSER_FUNC_NAME)
        .addParameter("xml", String::class)
        .addCode(
            """
            | val builder = %4T.newInstance().newDocumentBuilder()
            | val document = builder.parse(xml.byteInputStream())
            | document.documentElement.normalize()
            | val nodeList = document.getElementsByTagName("%2L")
            | var result: %1T? = null
            |
            | if (nodeList?.length == 1) {
            |${TAB}${TAB}val element = nodeList.item(0) as? Element
            |${TAB}${TAB}element?.let {
            |${TAB}${TAB}${TAB}result = it.getChannel()
            |${TAB}${TAB}}
            | }
            | return result ?: throw %3T("No valid channel tag in the RSS feed.")
            | """.trimMargin(),
            outputClass, CHANNEL, exceptionClass, docBuilderFactoryClass
        )
        .returns(outputClass)
        .build()
}

這個 function 就是產生上方 TestRssDataParser 內的 parse function ,除了設定好輸入輸出之外,還可以針對程式碼內部設定一些動態的 type 參數,也就是在 addCode 裡面的 %1T%2L%3T%4T 。1234 分別代表後方帶入的參數順序,對應到 addCode 後方戴的那幾個參數outputClassCHANNELexceptionClassdocBuilderFactoryClass 。L 代表字串型態,T 則代表類別。前面用 ClassName 的方式宣告在 KotlinPoet ,在它產生程式碼的同時,會幫我們把對應的類別自動 import ,所以我們不用擔心會有 import 錯誤的問題。


上一篇
Logger 與 Extension Generator for Kotlin
下一篇
Parser Generator (二)
系列文
如何使用 Kotlin Annotation Processor 做出自己的 Custom Data Parser Library30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言