我們先從 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
後方戴的那幾個參數outputClass
、 CHANNEL
、 exceptionClass
和 docBuilderFactoryClass
。L 代表字串型態,T 則代表類別。前面用 ClassName
的方式宣告在 KotlinPoet ,在它產生程式碼的同時,會幫我們把對應的類別自動 import ,所以我們不用擔心會有 import 錯誤的問題。