如何强制初始化子父关系?

时间:2018-04-20 16:31:18

标签: kotlin jackson

我有一个配置元素的层次结构,它们都实现了一个接口(简化):

interface Configuration {
    val elements: Long
    val data: Long?
    var parent: Configuration?
}

实现将由jackson-kotlin构建,读取这样的Yaml文件:

checks:
  elements: 10
  data: 1234
  ports:
    elements: 5

代码也可以这样做:

CheckConfig(elements = 10, data = 1234, portConfig = PortConfig(elements = 5))

我的问题是 - 当我构建PortConfig时,我还不知道它的父母。杰克逊可能,但我不知道如何利用这一点。

如果用户没有为此特定配置部分提供任何PortConfig属性,则data实现需要其父级回退。

我目前所做的事情完全是hacky:

inline fun <reified T: Configuration> T.updateChildLinks() {
    (this::class as KClass<T>).memberProperties.forEach {
        if (it.returnType.isSubtypeOf(Configuration::class.createType(nullable = true))) {
            it.isAccessible = true
            val config: Configuration = it.get(this) as Configuration
            config.parent = this
        }
    }
}

并在每个父级init区块中调用此方法。我可以做得更好(特别是,我不喜欢我必须记得在初始阶段调用辅助函数)?

已编辑添加:之前,我有一个仅代码解决方案,可以将data传递到链中:

CheckConfig(elements = 10, data = 1234, 
        portConfig = PortConfig(elements = 5, parentData = 1234))

然后dataparentData旁观,但我无法与杰克逊合作。

1 个答案:

答案 0 :(得分:0)

因此,为了将来的参考,这是我最终的结果:

诀窍是承认我的data class无法链接到它的父母,因为孩子将在他们的父母之前建造。

然而,我可以利用Jackson通过实现一个自定义反序列化来攻击原始的回退问题,该反序列化器跟踪父属性值并应用它(如果缺少的话)。

我的下面的实现使用Deque,即使对Kotlin感到有些尴尬。

唯一的缺点是反序列化程序在反序列化后显然只知道你的属性值(显然),因此不能将它传递给可能需要它的子值。因此,这会错误地为您123提供345

top:
  data: 123
  sub:
    subsub:
      important: false
    data: 345

但是如果你可以控制它,你可以使用下面的内容:

class PropInheritance : JsonDeserializer<Long>() {
    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Long {
        val currentContextName = p.parsingContext
                .pathAsPointer().toString()
                // cut the last "prop" variable name
                .replaceAfterLast("/", "")
        val prop = p.longValue
        val propStack = initStackIfNeeded(ctxt)
        propStack.push(currentContextName to prop)
        return prop
    }

    override fun getNullValue(ctxt: DeserializationContext): Long {
        val propStack = initStackIfNeeded(ctxt)

        // here, the prop name is missing the context is just the parent
        val currentContextName = ctxt.parser.parsingContext
                .pathAsPointer().toString()
        // find a prop on stack that has the same path
        while (propStack.isNotEmpty() 
                && !currentContextName.startsWith(propStack.peek().first)) {
            propStack.pop()
        }

        val prop = if (propStack.isNotEmpty()) 
                       propStack.peek().second
                   else 
                       ctxt.getAttribute("default") as Long

        // record current prop on the stack so potential children can find it
        propStack.push(currentContextName to prop)
        return prop
    }

    private fun initStackIfNeeded(ctxt: DeserializationContext)
            : Deque<Pair<String, Long>> {
        if (ctxt.getAttribute("stack") == null) {
            ctxt.setAttribute("stack", ArrayDeque<Pair<String, Long>>())
        }
        return ctxt.getAttribute("stack") as Deque<Pair<String, Long>>
    }
}

就我而言,我使用mix-in@JsonDeserialize(using = PropInheritance::class)应用于我的界面方法的getter:

interface HasProp {
    val prop: Long
}

interface HasPropMixin {
    @JsonDeserialize(using = PropInheritance::class)
    fun getProp(): Long
}

mapper = ObjectMapper(YAMLFactory()).registerModule(KotlinModule())
    .addMixin(HasProp::class.java, HasPropMixin::class.java)