All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tencent.devops.process.yaml.v2.parsers.template.ParametersExpressionParse.kt Maven / Gradle / Ivy

package com.tencent.devops.process.yaml.v2.parsers.template

import com.fasterxml.jackson.databind.JsonNode
import com.tencent.devops.common.api.util.JsonUtil
import com.tencent.devops.common.expression.ExecutionContext
import com.tencent.devops.common.expression.ExpressionParser
import com.tencent.devops.common.expression.SubNameValueEvaluateInfo
import com.tencent.devops.common.expression.SubNameValueEvaluateResult
import com.tencent.devops.common.expression.SubNameValueResultType
import com.tencent.devops.common.expression.context.ArrayContextData
import com.tencent.devops.common.expression.context.BooleanContextData
import com.tencent.devops.common.expression.context.ContextValueNode
import com.tencent.devops.common.expression.context.DictionaryContextData
import com.tencent.devops.common.expression.context.ExpressionContextData
import com.tencent.devops.common.expression.context.NumberContextData
import com.tencent.devops.common.expression.context.PipelineContextData
import com.tencent.devops.common.expression.context.StringContextData
import com.tencent.devops.common.expression.expression.FunctionInfo
import com.tencent.devops.common.expression.expression.sdk.NamedValueInfo
import com.tencent.devops.common.expression.expression.specialFuctions.hashFiles.HashFilesFunction
import com.tencent.devops.process.yaml.v2.exception.YamlFormatException
import com.tencent.devops.process.yaml.v2.exception.YamlTemplateException
import com.tencent.devops.process.yaml.v2.parameter.Parameters
import com.tencent.devops.process.yaml.v2.parameter.ParametersType
import com.tencent.devops.process.yaml.v2.parsers.template.models.ExpressionBlock
import org.apache.commons.text.StringEscapeUtils
import org.apache.tools.ant.filters.StringInputStream
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader

/**
 * 模板参数表达式解析相关
 */
@Suppress("ALL")
object ParametersExpressionParse {
    /**
     * 为模板中的变量赋值
     * @param fromPath 来自哪个文件
     * @param path 读取的哪个模板文件
     * @param template 被读取的模板文件内容
     * @param templateParameters 被读取的模板文件自带的参数
     * @param parameters 引用模板文件时传入的参数
     */
    fun parseTemplateParameters(
        fromPath: String,
        path: String,
        template: String,
        templateParameters: MutableList?,
        parameters: Map?
    ): String {
        if (templateParameters.isNullOrEmpty()) {
            return template
        }

        // 模板替换 先替换调用模板传入的参数,再替换模板的默认参数
        templateParameters.forEachIndexed { index, param ->
            if (param.name.contains(".")) {
                throw error(
                    Constants.PARAMETER_FORMAT_ERROR.format(path, "parameter name ${param.name} not allow contains '.'")
                )
            }

            if (parameters == null) {
                return@forEachIndexed
            }

            val valueName = param.name
            val newValue = parameters[param.name]
            if (!parameters.keys.contains(valueName)) {
                return@forEachIndexed
            }

            if (!param.values.isNullOrEmpty() && !param.values.contains(newValue)) {
                throw error(
                    Constants.VALUE_NOT_IN_ENUM.format(
                        fromPath,
                        path,
                        valueName,
                        newValue,
                        param.values.joinToString(",")
                    )
                )
            } else {
                templateParameters[index] = param.copy(default = newValue)
            }
        }

        // 拼接表达式变量
        val expNameValues = mutableListOf().apply {
            add(NamedValueInfo("parameters", ContextValueNode()))
        }
        val expContext = ExecutionContext(expressionValues = DictionaryContextData())
        val parameterContext = DictionaryContextData()
        expContext.expressionValues.add("parameters", parameterContext)
        templateParameters.filter { it.default != null }.forEach { param ->
            when (param.type.toLowerCase()) {
                ParametersType.ARRAY.value -> {
                    if (param.default !is Iterable<*>) {
                        throw error(
                            Constants.PARAMETER_FORMAT_ERROR.format(
                                path, "parameter ${param.name} type is ${param.type} but value not"
                            )
                        )
                    }
                    val arr = fromJsonToArrayContext(path, param.name, param.default)
                    parameterContext.add(param.name, arr)
                }

                ParametersType.BOOLEAN.value, ParametersType.NUMBER.value -> {
                    parameterContext.add(param.name, nativeTypeToContext(param.default!!))
                }

                ParametersType.STRING.value -> {
                    val value = param.default!!.toString()
                    // 针对插入表达式单独处理
                    if (value.trim().startsWith("\${{") && value.trim().endsWith("}}")) {
                        parameterContext.add(param.name, ExpressionContextData(value.trim()))
                    } else {
                        parameterContext.add(param.name, nativeTypeToContext(param.default))
                    }
                }

                else -> throw error(
                    Constants.PARAMETER_FORMAT_ERROR.format(
                        path, "parameter ${param.name} type ${param.type} not support"
                    )
                )
            }
        }

        return parseParameterValue(path, template, expNameValues, expContext)
    }

    // 因为array的里面可能嵌套array所以先转成json再转成array
    fun fromJsonToArrayContext(path: String, parameterName: String, value: Iterable<*>): ArrayContextData {
        val jsonTree = try {
            JsonUtil.getObjectMapper().readTree(JsonUtil.toJson(value))
        } catch (e: Throwable) {
            throw error(
                Constants.PARAMETER_FORMAT_ERROR.format(
                    path, "array parameter $parameterName value [$value] can't to json."
                )
            )
        }
        if (!jsonTree.isArray) {
            throw error(
                Constants.PARAMETER_FORMAT_ERROR.format(
                    path, "array parameter $parameterName value  [$value] json type [${jsonTree.nodeType}] not array."
                )
            )
        }
        return initByJsonTree(jsonTree, null, null) as ArrayContextData
    }

    fun parseParameterValue(
        path: String,
        value: String,
        nameValues: List,
        context: ExecutionContext
    ): String {
        val strReader = InputStreamReader(StringInputStream(value))
        val bufferReader = BufferedReader(strReader)
        val newValue = StringBuilder()
        try {
            var line = bufferReader.readLine()
            while (line != null) {
                // 跳过空行和注释行,如果一行除空格外最左是 # 那一定是注释
                if (line.isBlank() || line.trimStart().startsWith("#")) {
                    newValue.append(line).append("\n")
                    line = bufferReader.readLine()
                    continue
                }

                val lineString = line.trim().replace("\\s".toRegex(), "")
                // if 表达式替换
                if (lineString.startsWith("if:") || lineString.startsWith("-if:")) {
                    val ifPrefix = line.substring(0 until line.indexOfFirst { it == ':' } + 1)

                    var condition = line.removePrefix(ifPrefix).trim().removeSurrounding("\"")
                    // if如果没有需要预先添加一个双括号,方便后面的替换
                    condition = "\${{$condition}}"

                    val blocks = findExpressions(condition)

                    val newLine = parseExpression(
                        line = condition,
                        blocks = blocks,
                        path = path,
                        needBrackets = false,
                        nameValues = nameValues,
                        context = context
                    )
                    newLine.removePrefix("\${{")
                    newLine.removeSuffix("}}")

                    newValue.append("$ifPrefix \"${newLine}\"").append("\n")
                    line = bufferReader.readLine()
                    continue
                }

                val condition = line
                val blocks = findExpressions(condition)
                if (blocks.isEmpty()) {
                    newValue.append(line).append("\n")
                    line = bufferReader.readLine()
                    continue
                }

                val newLine = parseExpression(
                    line = condition,
                    blocks = blocks,
                    path = path,
                    needBrackets = true,
                    nameValues = nameValues,
                    context = context
                )
                newValue.append(newLine).append("\n")
                line = bufferReader.readLine()
            }
        } finally {
            try {
                strReader.close()
                bufferReader.close()
            } catch (ignore: IOException) {
            }
        }

        return newValue.toString()
    }

    /**
     * 寻找语句中包含 ${{}}的表达式的位置,返回成对的位置坐标,并根据优先级排序
     * 优先级算法目前暂定为 从里到外,从左到右
     * @param levelMax 返回的最大层数,从深到浅。默认为2层
     * 例如: 替换顺序如数字所示 ${{ 4  ${{ 2 ${{ 1 }} }} ${{ 3 }} }}
     * @return [ 层数次序 [ 括号 ] ]  [[1], [2, 3], [4]]]]
     */
    fun findExpressions(condition: String, levelMax: Int = 2): List> {
        val stack = ArrayDeque()
        var index = 0
        val chars = condition.toCharArray()
        val levelMap = mutableMapOf>()
        while (index < chars.size) {
            if (index + 2 < chars.size && chars[index] == '$' && chars[index + 1] == '{' && chars[index + 2] == '{'
            ) {
                stack.addLast(index)
                index += 3
                continue
            }

            if (index + 1 < chars.size && chars[index] == '}' && chars[index + 1] == '}'
            ) {
                val start = stack.removeLastOrNull()
                if (start != null) {
                    // 栈里剩下几个前括号,这个完整括号的优先级就是多少
                    val level = stack.size + 1
                    if (levelMap.containsKey(level)) {
                        levelMap[level]!!.add(ExpressionBlock(start, index + 1))
                    } else {
                        levelMap[level] = mutableListOf(ExpressionBlock(start, index + 1))
                    }
                }
                index += 2
                continue
            }

            index++
        }

        if (levelMap.isEmpty()) {
            return listOf()
        }

        val result = mutableListOf>()
        var max = 0
        var listIndex = 0
        run end@{
            levelMap.keys.sortedDescending().forEach result@{ level ->
                val blocks = levelMap[level] ?: return@result
                blocks.sortBy { it.startIndex }
                blocks.forEach { block ->
                    if (result.size < listIndex + 1) {
                        result.add(mutableListOf(block))
                    } else {
                        result[listIndex].add(block)
                    }
                }
                listIndex++
                max++
                if (max == levelMax) {
                    return@end
                }
            }
        }
        return result
    }

    /**
     * 解析表达式,根据 findExpressions 寻找的括号优先级进行解析
     */
    fun parseExpression(
        line: String,
        blocks: List>,
        path: String,
        needBrackets: Boolean,
        nameValues: List,
        context: ExecutionContext
    ): String {
        var lineChars = line.toList()
        blocks.forEachIndexed { blockLevel, blocksInLevel ->
            blocksInLevel.forEachIndexed { blockI, block ->
                // 表达式因为含有 ${{ }} 所以起始向后推3位,末尾往前推两位
                val expression = lineChars.joinToString("").substring(block.startIndex + 3, block.endIndex - 1)

                val result = expressionEvaluate(
                    path = path,
                    expression = expression,
                    needBrackets = needBrackets,
                    nameValues = nameValues,
                    context = context
                )

                // 格式化返回值
                val (res, needFormatArr) = formatResult(
                    blockLevel = blockLevel,
                    blocks = blocks,
                    block = block,
                    lineChars = lineChars,
                    evaluateResult = result
                )

                // 去掉前后的可能的引号
                if (needFormatArr) {
                    if (block.startIndex - 1 >= 0 && lineChars[block.startIndex - 1] == '"') {
                        block.startIndex = block.startIndex - 1
                    }
                    if (block.endIndex + 1 < lineChars.size && lineChars[block.endIndex + 1] == '"') {
                        block.endIndex = block.endIndex + 1
                    }
                }

                // 将替换后的表达式嵌入原本的line
                val startSub = if (block.startIndex - 1 < 0) {
                    listOf()
                } else {
                    lineChars.slice(0 until block.startIndex)
                }
                val endSub = if (block.endIndex + 1 >= lineChars.size) {
                    listOf()
                } else {
                    lineChars.slice(block.endIndex + 1 until lineChars.size)
                }
                lineChars = startSub + res + endSub

                // 将替换后的字符查传递给后边的括号位数
                val diffNum = res.size - (block.endIndex - block.startIndex + 1)
                blocks.forEachIndexed { i, bl ->
                    bl.forEachIndexed level@{ j, b ->
                        if (i <= blockLevel && j <= blockI) {
                            return@level
                        }
                        if (blocks[i][j].startIndex > block.endIndex) {
                            blocks[i][j].startIndex += diffNum
                        }
                        if (blocks[i][j].endIndex > block.endIndex) {
                            blocks[i][j].endIndex += diffNum
                        }
                    }
                }
            }
        }

        return lineChars.joinToString("")
    }

    private val functionList = listOf(
        FunctionInfo(
            HashFilesFunction.name,
            1,
            Byte.MAX_VALUE.toInt(),
            HashFilesFunction()
        )
    )

    fun expressionEvaluate(
        path: String,
        expression: String,
        needBrackets: Boolean,
        nameValues: List,
        context: ExecutionContext
    ): SubNameValueEvaluateResult {
        val subInfo = SubNameValueEvaluateInfo()
        val (value, isComplete, type) = try {
            ExpressionParser.createSubNameValueEvaluateTree(
                expression, null, nameValues, functionList, subInfo
            )?.subNameValueEvaluate(null, context, null, subInfo, null)
                ?: throw YamlTemplateException("create evaluate tree is null")
        } catch (e: Throwable) {
            throw error(Constants.EXPRESSION_EVALUATE_ERROR.format(path, expression, e.message))
        }
        if (isComplete) {
            return SubNameValueEvaluateResult(value.trim(), true, type)
        }
        if (needBrackets) {
            return SubNameValueEvaluateResult("\${{ ${value.trim()} }}", false, type)
        }
        return SubNameValueEvaluateResult(value.trim(), false, type)
    }

    /**
     * 格式化表达式计算的返回值
     * @return <格式化结果, 是否需要格式化列表>
     */
    private fun formatResult(
        blockLevel: Int,
        blocks: List>,
        block: ExpressionBlock,
        lineChars: List,
        evaluateResult: SubNameValueEvaluateResult
    ): Pair, Boolean> {
        if (!evaluateResult.isComplete) {
            return Pair(evaluateResult.value.toList(), false)
        }

        // ScriptUtils.formatYaml会将所有的带上 "" 但换时数组不需要"" 所以为数组去掉可能的额外的""
        // 需要去掉额外""的情况只可能出现只替换了一次列表的情景,即作为参数 var_a: "{{ parameters.xxx }}"
        // 需要将给表达式的转义 \" 转回 "
        if (evaluateResult.type == SubNameValueResultType.ARRAY) {
            return Pair(evaluateResult.value.replace("\\\"", "\"").toList(), (blocks.size == 1))
        }

        // 对于还有下一层的表达式,其替换出来的string需要加上 '' 方便后续第二层使用
        // 例外: 当string前或后存在 . 时,说明是用来做索引,不加 ''
        var result = evaluateResult.value
        if ((blockLevel + 1 < blocks.size && evaluateResult.type == SubNameValueResultType.STRING) &&
            !(
                (block.startIndex - 1 >= 0 && lineChars[block.startIndex - 1] == '.') ||
                    (block.endIndex + 1 < lineChars.size && lineChars[block.endIndex + 1] == '.')
                )
        ) {
            result = "'$result'"
        }

        // 对于字符传可能用的非转义的 \n \s 改为转义后的
        result = StringEscapeUtils.escapeJava(result)

        return Pair(result.toList(), false)
    }

    private fun initByJsonTree(node: JsonNode, context: PipelineContextData?, nodeName: String?): PipelineContextData {
        if (node.isValueNode) {
            return context.addNode(node, nodeName)
        }

        var ctx: PipelineContextData? = context

        if (node.isObject) {
            val fields = node.fields()
            ctx = context ?: DictionaryContextData()
            if (!fields.hasNext()) {
                return ctx
            }
            val newContext = when {
                context == null -> {
                    ctx
                }

                ctx is ArrayContextData -> {
                    val c = DictionaryContextData()
                    ctx.add(c)
                    c
                }

                ctx is DictionaryContextData -> {
                    val c = DictionaryContextData()
                    ctx.add(nodeName!!, c)
                    c
                }

                else -> return ctx
            }
            while (fields.hasNext()) {
                val entry = fields.next()
                initByJsonTree(entry.value, newContext, entry.key)
            }
        }

        if (node.isArray) {
            val iter = node.iterator()
            ctx = context ?: ArrayContextData()
            if (!iter.hasNext()) {
                return ctx
            }
            val newContext = when {
                context == null -> {
                    ctx
                }

                ctx is ArrayContextData -> {
                    val c = ArrayContextData()
                    ctx.add(c)
                    c
                }

                ctx is DictionaryContextData -> {
                    val c = ArrayContextData()
                    ctx.add(nodeName!!, c)
                    c
                }

                else -> return ctx
            }
            while (iter.hasNext()) {
                val entry = iter.next()
                initByJsonTree(entry, newContext, null)
            }
        }

        return ctx ?: StringContextData(node.toString())
    }

    private fun PipelineContextData?.addNode(node: JsonNode, nodeName: String?): PipelineContextData {
        val value = when {
            node.isBoolean -> BooleanContextData(node.booleanValue())
            node.isNumber -> NumberContextData(node.doubleValue())
            node.isNull -> StringContextData("")
            node.isTextual -> StringContextData(node.textValue())
            else -> StringContextData(node.toString())
        }
        if (this == null) {
            return value
        }
        if (this is ArrayContextData) {
            this.add(value)
        }
        if (this is DictionaryContextData) {
            this.add(nodeName!!, value)
        }
        return this
    }

    private fun nativeTypeToContext(value: Any): PipelineContextData {
        return when (value) {
            is Char, is String -> StringContextData(value.toString())
            is Number -> NumberContextData(value.toDouble())
            is Boolean -> BooleanContextData(value)
            else -> StringContextData(value.toString())
        }
    }

    private fun error(content: String) = YamlFormatException(content)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy