iT邦幫忙

2021 iThome 鐵人賽

DAY 21
0
Mobile Development

如何使用 Kotlin Annotation Processor 做出自己的 Custom Data Parser Library系列 第 21

Parser Generator (三)

Generator process.drawio.png

上篇提到了 parser generator 在產生程式碼的時候,可以用四個步驟去拆解裡面的資訊並產生程式碼,我們現在來看一下範例吧!

進入範例之前,我們也複習一下之前提我們預計要產生出來的 class 內部程式碼的結構大概是會長這個樣子,我們就叫他 Code 1.1 。本篇文章有很多程式碼都是要參考這個結構,對照著看才會比較有感覺。

Code 1.1

object RssItemParser {
	fun Element.getItem(): RssItem {
			// #1 Value Statement
			val titleTitle: String? = readString("title")
	    val titleItunesTitle: String? = readString("itunes:title")
	    val titleGoogleplayTitle: String? = readString("googleplay:title")
	    val authorAuthor: String? = readString("author")
	    val authorItunesAuthor: String? = readString("itunes:author")
	    val authorGoogleplayAuthor: String? = readString("googleplay:author")
	    val guidGuid: TestGuid? = getElementByTag("guid")?.getGuid()
	    val guidItunesGuid: TestGuid? = getElementByTag("itunes:guid")?.getGuid()
	    val guidGoogleplayGuid: TestGuid? = getElementByTag("googleplay:guid")?.getGuid()
	
			// #2 Use class constructor
      return RssItem(
	  		title = titleTitle ?: titleItunesTitle ?: titleGoogleplayTitle,
	  		author = authorAuthor ?: authorItunesAuthor ?: authorGoogleplayAuthor,
	  		guid = guidGuid ?: guidItunesGuid ?: guidGoogleplayGuid
	  	)
	}
}

接著就是呼叫四個步驟的程式碼:

private fun getClassFunSpec(
        rootElement: Element,
        outputClassName: String,
        objectBuilder: TypeSpec.Builder
    ): FunSpec {
        val outputClass = ClassName(rootElement.getPackage(), outputClassName)
        val rssTag = rootElement.getAnnotation(RssTag::class.java)
        val rssRawData = rootElement.getAnnotation(RssRawData::class.java)
        if (rssTag != null && rssRawData != null) {
            logger.error("@RssTag and @RssRawData should not be used on the same class!", rootElement)
        }

        val tagName = rssTag?.name?.takeIfNotEmpty() ?: rootElement.simpleName.toString()
        rootTagName = tagName
        logger.log("[KotlinParserGenerator][getActionFunSpec] $rootTagName")
        val funSpec = FunSpec.builder(tagName.getFuncName())
            .receiver(elementClassName)
            .returns(outputClass)
        val propertyToParseData = mutableMapOf<String, ParseData>()
        topLevelCandidateOrder =
            rssTag?.order ?: arrayOf(OrderType.RSS_STANDARD, OrderType.ITUNES, OrderType.GOOGLE)

        val annotations = preProcessAnnotations(rootElement)

        rootElement.enclosedElements.forEach { preProcessParseData(it, propertyToParseData, annotations) }

        propertyToParseData.forEach { generateValueStatementForConstructor(it, funSpec, objectBuilder) }

        funSpec.addStatement("\nreturn $outputClassName(")
        // Generate constructor statements
        var index = 0
        val lastIndex = propertyToParseData.size - 1
        propertyToParseData.forEach {
            generateConstructor(it, funSpec, index == lastIndex)
            index ++
        }
        funSpec.addStatement("${TAB})")
        return funSpec.build()
    }

上面這段程式碼包含了上述的四個步驟,分別可以對應到四個 function :

  1. Annotation Pre-processing: preProcessAnnotations
  2. Convert to ParseData: preProcessParseData
  3. Generate Value Statement: generateValueStatementForConstructor
  4. Generate Constructor: generateConstructor

Annotation Pre-processing

首先,我們可以透過先把 annotation 裡的 class 和 member 資訊整理好,放入 map 中以便之後快速查找,這邊用 map 的原因是之後要取用 annotation 裡面的資訊時,可以透過名稱直接拿到資訊。

protected fun preProcessAnnotations(rootElement: Element): Map<String, Any?> {
        val result = mutableMapOf<String, Any?>()
        rootElement.enclosedElements.forEach { child ->
            if (child.kind != ElementKind.METHOD
                || !child.simpleName.isGetterMethod()
                || !child.simpleName.contains(ANNOTATION_SIGN)
            ) return@forEach

            val nameFromMethod = child.simpleName.extractNameFromMethod()

            val rssValue: RssValue? = child.getAnnotation(RssValue::class.java)
            val rssTag: RssTag? = child.getAnnotation(RssTag::class.java)
            val rssAttribute: RssAttribute? = child.getAnnotation(RssAttribute::class.java)
            val rssRawData: RssRawData? = child.getAnnotation(RssRawData::class.java)
            val nonNullCount = listOf<Any?>(rssTag, rssAttribute, rssRawData).count { it != null }
            val attributeValueCount = listOf<Any?>(rssAttribute, rssValue).count { it != null }
            if (nonNullCount > 1 || attributeValueCount > 1) {
                logger.error(
                    "You can't annotate more than one annotation at a field or property!",
                    child
                )
            }

            result[nameFromMethod] = rssValue ?: rssTag ?: rssAttribute ?: rssRawData

            if (rssValue != null) {
                hasRssValueAnnotation = true
            }
        }
        return result
    }

在處理 annotation 的時候,我們有給它一個限制就是不能在同一個元素上面標註超過一個 annotation ,所以才會有 if (nonNullCount > 1 || attributeValueCount > 1) 的判斷。

Convert to ParseData

接著,我們就可以把這個 map 轉成我們自定義的 ParseData 類別來包住一些我們需要的資訊:

data class ParseData(
    val type: String?,
    val rawType: String?,
    val dataType: DataType,
    val listItemType: String?,
    val packageName: String?,
    val processorElement: Element,
    val tagCandidates: List<String> = listOf()
)
protected fun preProcessParseData(
    child: Element,
    parseDataMap: MutableMap<String, ParseData>,
    nameToAnnotation: Map<String, Any?>
) {
    if (child.kind!= ElementKind.METHOD
|| !child.simpleName.isGetterMethod()
        || child.simpleName.contains(ANNOTATION_SIGN)
    ) return

    val nameFromMethod = child.simpleName.extractNameFromMethod()
    val exeElement = child as? ExecutableElement ?: return
    val rawType = exeElement.returnType.toString()
    val type: String?
    val packageName: String?
    val dataType: DataType
    var listItemType: String? = null

    if (rawType.isListType()) {
        listItemType = rawType.extractListType()
        type = rawType
            .substringBeforeLast('<')
            .extractType()
        packageName = rawType.substringAfterLast('<')
            .substringBeforeLast('>')
            .substringBeforeLast('.')
        dataType = DataType.LIST
} else {
        type = rawType.extractType()
        val annotation = nameToAnnotation[nameFromMethod]
        dataType = when {
            annotation is RssValue -> DataType.VALUE
annotation is RssAttribute -> DataType.ATTRIBUTE
rawType.isPrimitive() -> DataType.PRIMITIVE
else -> DataType.OTHER
}
        packageName = rawType.substringBeforeLast('.')
    }

    parseDataMap[nameFromMethod] = ParseData(
        type = type,
        rawType = rawType,
        dataType = dataType,
        listItemType = listItemType,
        packageName = packageName,
        processorElement = child,
        tagCandidates = getTagCandidates(nameToAnnotation, nameFromMethod, child)
    )
}

可以看到這邊我們其實做了很多字串處理,為了要直接從 ElementrawType 中抽取出型別,型別抽取出來後把這些資料放到 ParseData 的 map 裡面。

Generate Value Statement

利用上個步驟產生的 ParseData map ,我們可以開始把 map 裡的資料轉換成要產生 Code 1.1 註解 #1 內的程式碼結構的... 程式碼。(恩,很饒舌)

下面的程式碼很長,但做的事情只有一件,就是把 ParseData 拆掉,按照 dataType 去產生 KotlinPoet 的 code statement 。

protected fun generateVariableStatement(
        propertyToParseData: Map.Entry<String, ParseData>,
        funSpec: FunSpec.Builder
    ) {
        val name = propertyToParseData.key
        val data = propertyToParseData.value
        val packageName = data.packageName ?: return
        val type = data.type ?: return

        when (data.dataType) {
            DataType.LIST -> {
                val itemType = data.listItemType ?: return

                if (itemType.isPrimitive()) {
                    val kClass = itemType.getKPrimitiveClass() ?: return

                    data.tagCandidates.forEach { tag ->
                        funSpec.addStatement(
                            "var ${name.getVariableName(tag)}: ArrayList<%T> = arrayListOf()",
                            kClass
                        )
                    }
                } else {
                    val itemClassName = ClassName(packageName, data.listItemType)
                    data.tagCandidates.forEach { tag ->
                        funSpec.addStatement(
                            "var ${name.getVariableName(tag)}: ArrayList<%T> = arrayListOf()",
                            itemClassName
                        )
                    }
                }
            }
            DataType.PRIMITIVE -> {
                val kClass = type.getKPrimitiveClass() ?: return

                data.tagCandidates.forEach { tag ->
                    funSpec.addStatement("var ${name.getVariableName(tag)}: %T? = null", kClass)
                }
            }
            DataType.ATTRIBUTE -> {
                val kClass = type.getKPrimitiveClass() ?: return

                data.tagCandidates.forEach { tag ->
                    val statement =
                        "var ${name.getVariableName(tag)}: %T? = getAttributeValue(null, \"$tag\")"
                            .appendTypeConversion(type)
                    if (type.isBooleanType()) {
                        funSpec.addStatement(statement, kClass, booleanConversionMemberName)
                    } else {
                        funSpec.addStatement(statement, kClass)
                    }
                }
            }
            DataType.VALUE -> {
                // Do nothing
            }
            else -> {
                val className = ClassName(packageName, type)
                data.tagCandidates.forEach { tag ->
                    funSpec.addStatement("var ${name.getVariableName(tag)}: %T? = null", className)
                }
            }
        }
    }

Generate Constructor

最後,我們可以利用前面已經產生的 Code 1.1 註解 #1 的區域,去產生 constructor ,也就是 Code 1.1 註解 #2 的區域,主要就是按照 dataType 去產生對應的 constructor 而已。

private fun generateValueStatementForConstructor(
        propertyToParseData: Map.Entry<String, ParseData>,
        funSpec: FunSpec.Builder,
        objectBuilder: TypeSpec.Builder
    ) {
        val name = propertyToParseData.key
        val data = propertyToParseData.value
        val packageName = data.packageName ?: return
        val type = data.type ?: return

        when (data.dataType) {
            DataType.LIST -> {
                generateValueListStatement(data, objectBuilder, packageName, funSpec, name)
            }
            DataType.PRIMITIVE -> {
                val kClass = type.getKPrimitiveClass() ?: return

                data.tagCandidates.forEach { tag ->
                    funSpec.addStatement("val ${name.getVariableName(tag)}: %T? =", kClass)
                    funSpec.addPrimitiveStatement("%M(\"$tag\")", type)
                }
            }
            DataType.ATTRIBUTE -> {
                generateValueAttributeStatement(type, data, name, funSpec)
            }
            DataType.VALUE -> {
                // Do nothing
            }
            else -> {
                val className = ClassName(packageName, type)
                data.tagCandidates.forEach { tag ->
                    val memberName = MemberName(type.getGeneratedClassPath(), tag.getFuncName())
                    funSpec.addStatement("val ${name.getVariableName(tag)}: %T? = %M(\"$tag\")?.%M()",
                        className, getElementByTagMemberName, memberName)
                }
            }
        }
    }

其他 Parser Generator 的產生方式

目前的範例都只有用純 Kotlin 的 parser generator 去做講解,其他像是 Android parser 和 reader generator 的程式碼產生方式也是大同小異,看到這裡你大概也有感受到,寫 annotation processor 和 code generator 其實是一個苦工,你要從這些 annotation processor 給你的這些資訊,先過濾出你要的,然後用 KotlinPoet 去產生對應的程式碼,而中間還會做一大堆的字串處理去拿到有用的資訊,真的是會很容易迷失在程式碼裡面。我自己寫完這邊的感受是,這個東西真的是寫一次就好了... 可以解決很多重複程式碼的問題,不用讓大家跟我一起 “一袋米要扛幾樓” 感受痛苦。

如果你也對其他的 parser generator 有興趣,可以參考一下原始碼:


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

尚未有邦友留言

立即登入留言