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

com.zeoflow.depot.writer.FieldReadWriteWriter.kt Maven / Gradle / Ivy

Go to download

The Depot persistence library provides an abstraction layer over SQLite to allow for more robust database access while using the full power of SQLite.

The newest version!
/*
 * Copyright (C) 2021 ZeoFlow SRL
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.zeoflow.depot.writer

import com.zeoflow.depot.ext.L
import com.zeoflow.depot.ext.T
import com.zeoflow.depot.ext.defaultValue
import com.zeoflow.depot.solver.CodeGenScope
import com.zeoflow.depot.vo.CallType
import com.zeoflow.depot.vo.Constructor
import com.zeoflow.depot.vo.EmbeddedField
import com.zeoflow.depot.vo.Field
import com.zeoflow.depot.vo.FieldWithIndex
import com.zeoflow.depot.vo.Pojo
import com.zeoflow.depot.vo.RelationCollector
import com.squareup.javapoet.TypeName
import java.util.Locale

/**
 * Handles writing a field into statement or reading it from statement.
 */
class FieldReadWriteWriter(fieldWithIndex: FieldWithIndex) {
    val field = fieldWithIndex.field
    val indexVar = fieldWithIndex.indexVar
    val alwaysExists = fieldWithIndex.alwaysExists

    companion object {
        /*
         * Get all parents including the ones which have grand children in this list but does not
         * have any direct children in the list.
         */
        fun getAllParents(fields: List): Set {
            val allParents = mutableSetOf()
            fun addAllParents(field: Field) {
                var parent = field.parent
                while (parent != null) {
                    if (allParents.add(parent)) {
                        parent = parent.parent
                    } else {
                        break
                    }
                }
            }
            fields.forEach(::addAllParents)
            return allParents
        }

        /**
         * Convert the fields with indices into a Node tree so that we can recursively process
         * them. This work is done here instead of parsing because the result may include arbitrary
         * fields.
         */
        private fun createNodeTree(
            rootVar: String,
            fieldsWithIndices: List,
            scope: CodeGenScope
        ): Node {
            val allParents = getAllParents(fieldsWithIndices.map { it.field })
            val rootNode = Node(rootVar, null)
            rootNode.directFields = fieldsWithIndices.filter { it.field.parent == null }
            val parentNodes = allParents.associate {
                Pair(
                    it,
                    Node(
                        varName = scope.getTmpVar("_tmp${it.field.name.capitalize(Locale.US)}"),
                        fieldParent = it
                    )
                )
            }
            parentNodes.values.forEach { node ->
                val fieldParent = node.fieldParent!!
                val grandParent = fieldParent.parent
                val grandParentNode = grandParent?.let {
                    parentNodes[it]
                } ?: rootNode
                node.directFields = fieldsWithIndices.filter { it.field.parent == fieldParent }
                node.parentNode = grandParentNode
                grandParentNode.subNodes.add(node)
            }
            return rootNode
        }

        fun bindToStatement(
            ownerVar: String,
            stmtParamVar: String,
            fieldsWithIndices: List,
            scope: CodeGenScope
        ) {
            fun visitNode(node: Node) {
                fun bindWithDescendants() {
                    node.directFields.forEach {
                        FieldReadWriteWriter(it).bindToStatement(
                            ownerVar = node.varName,
                            stmtParamVar = stmtParamVar,
                            scope = scope
                        )
                    }
                    node.subNodes.forEach(::visitNode)
                }

                val fieldParent = node.fieldParent
                if (fieldParent != null) {
                    fieldParent.getter.writeGet(
                        ownerVar = node.parentNode!!.varName,
                        outVar = node.varName,
                        builder = scope.builder()
                    )
                    scope.builder().apply {
                        beginControlFlow("if($L != null)", node.varName).apply {
                            bindWithDescendants()
                        }
                        nextControlFlow("else").apply {
                            node.allFields().forEach {
                                addStatement("$L.bindNull($L)", stmtParamVar, it.indexVar)
                            }
                        }
                        endControlFlow()
                    }
                } else {
                    bindWithDescendants()
                }
            }
            visitNode(createNodeTree(ownerVar, fieldsWithIndices, scope))
        }

        /**
         * Just constructs the given item, does NOT DECLARE. Declaration happens outside the
         * reading statement since we may never read if the cursor does not have necessary
         * columns.
         */
        private fun construct(
            outVar: String,
            constructor: Constructor?,
            typeName: TypeName,
            localVariableNames: Map,
            localEmbeddeds: List,
            localRelations: Map,
            scope: CodeGenScope
        ) {
            if (constructor == null) {
                // best hope code generation
                scope.builder().apply {
                    addStatement("$L = new $T()", outVar, typeName)
                }
                return
            }
            val variableNames = constructor.params.map { param ->
                when (param) {
                    is Constructor.Param.FieldParam -> localVariableNames.entries.firstOrNull {
                        it.value.field === param.field
                    }?.key
                    is Constructor.Param.EmbeddedParam -> localEmbeddeds.firstOrNull {
                        it.fieldParent == param.embedded
                    }?.varName
                    is Constructor.Param.RelationParam -> localRelations.entries.firstOrNull {
                        it.value === param.relation.field
                    }?.key
                }
            }
            val args = variableNames.joinToString(",") { it ?: "null" }
            constructor.writeConstructor(outVar, args, scope.builder())
        }

        /**
         * Reads the row into the given variable. It does not declare it but constructs it.
         */
        fun readFromCursor(
            outVar: String,
            outPojo: Pojo,
            cursorVar: String,
            fieldsWithIndices: List,
            scope: CodeGenScope,
            relationCollectors: List
        ) {
            fun visitNode(node: Node) {
                val fieldParent = node.fieldParent
                fun readNode() {
                    // read constructor parameters into local fields
                    val constructorFields = node.directFields.filter {
                        it.field.setter.callType == CallType.CONSTRUCTOR
                    }.associateBy { fwi ->
                        FieldReadWriteWriter(fwi).readIntoTmpVar(
                            cursorVar,
                            fwi.field.setter.type.typeName,
                            scope
                        )
                    }
                    // read decomposed fields (e.g. embedded)
                    node.subNodes.forEach(::visitNode)
                    // read relationship fields
                    val relationFields = relationCollectors.filter { (relation) ->
                        relation.field.parent === fieldParent
                    }.associate {
                        it.writeReadCollectionIntoTmpVar(
                            cursorVarName = cursorVar,
                            fieldsWithIndices = fieldsWithIndices,
                            scope = scope
                        )
                    }

                    // construct the object
                    if (fieldParent != null) {
                        construct(
                            outVar = node.varName,
                            constructor = fieldParent.pojo.constructor,
                            typeName = fieldParent.field.typeName,
                            localEmbeddeds = node.subNodes,
                            localRelations = relationFields,
                            localVariableNames = constructorFields,
                            scope = scope
                        )
                    } else {
                        construct(
                            outVar = node.varName,
                            constructor = outPojo.constructor,
                            typeName = outPojo.typeName,
                            localEmbeddeds = node.subNodes,
                            localRelations = relationFields,
                            localVariableNames = constructorFields,
                            scope = scope
                        )
                    }
                    // ready any field that was not part of the constructor
                    node.directFields.filterNot {
                        it.field.setter.callType == CallType.CONSTRUCTOR
                    }.forEach { fwi ->
                        FieldReadWriteWriter(fwi).readFromCursor(
                            ownerVar = node.varName,
                            cursorVar = cursorVar,
                            scope = scope
                        )
                    }
                    // assign sub nodes to fields if they were not part of the constructor.
                    node.subNodes.mapNotNull {
                        val setter = it.fieldParent?.setter
                        if (setter != null && setter.callType != CallType.CONSTRUCTOR) {
                            Pair(it.varName, setter)
                        } else {
                            null
                        }
                    }.forEach { (varName, setter) ->
                        setter.writeSet(
                            ownerVar = node.varName,
                            inVar = varName,
                            builder = scope.builder()
                        )
                    }
                    // assign relation fields that were not part of the constructor
                    relationFields.filterNot { (_, field) ->
                        field.setter.callType == CallType.CONSTRUCTOR
                    }.forEach { (varName, field) ->
                        field.setter.writeSet(
                            ownerVar = node.varName,
                            inVar = varName,
                            builder = scope.builder()
                        )
                    }
                }
                if (fieldParent == null) {
                    // root element
                    // always declared by the caller so we don't declare this
                    readNode()
                } else {
                    // always declare, we'll set below
                    scope.builder().addStatement(
                        "final $T $L", fieldParent.pojo.typeName,
                        node.varName
                    )
                    if (fieldParent.nonNull) {
                        readNode()
                    } else {
                        val myDescendants = node.allFields()
                        val allNullCheck = myDescendants.joinToString(" && ") {
                            if (it.alwaysExists) {
                                "$cursorVar.isNull(${it.indexVar})"
                            } else {
                                "( ${it.indexVar} == -1 || $cursorVar.isNull(${it.indexVar}))"
                            }
                        }
                        scope.builder().apply {
                            beginControlFlow("if (! ($L))", allNullCheck).apply {
                                readNode()
                            }
                            nextControlFlow(" else ").apply {
                                addStatement("$L = null", node.varName)
                            }
                            endControlFlow()
                        }
                    }
                }
            }
            visitNode(createNodeTree(outVar, fieldsWithIndices, scope))
        }
    }

    /**
     * @param ownerVar The entity / pojo that owns this field. It must own this field! (not the
     * container pojo)
     * @param stmtParamVar The statement variable
     * @param scope The code generation scope
     */
    private fun bindToStatement(ownerVar: String, stmtParamVar: String, scope: CodeGenScope) {
        field.statementBinder?.let { binder ->
            val varName = if (field.getter.callType == CallType.FIELD) {
                "$ownerVar.${field.name}"
            } else {
                "$ownerVar.${field.getter.name}()"
            }
            binder.bindToStmt(stmtParamVar, indexVar, varName, scope)
        }
    }

    /**
     * @param ownerVar The entity / pojo that owns this field. It must own this field (not the
     * container pojo)
     * @param cursorVar The cursor variable
     * @param scope The code generation scope
     */
    private fun readFromCursor(ownerVar: String, cursorVar: String, scope: CodeGenScope) {
        fun toRead() {
            field.cursorValueReader?.let { reader ->
                scope.builder().apply {
                    when (field.setter.callType) {
                        CallType.FIELD -> {
                            reader.readFromCursor(
                                "$ownerVar.${field.setter.name}", cursorVar,
                                indexVar, scope
                            )
                        }
                        CallType.METHOD -> {
                            val tmpField = scope.getTmpVar(
                                "_tmp${field.name.capitalize(Locale.US)}"
                            )
                            addStatement("final $T $L", field.setter.type.typeName, tmpField)
                            reader.readFromCursor(tmpField, cursorVar, indexVar, scope)
                            addStatement("$L.$L($L)", ownerVar, field.setter.name, tmpField)
                        }
                        CallType.CONSTRUCTOR -> {
                            // no-op
                        }
                    }
                }
            }
        }
        if (alwaysExists) {
            toRead()
        } else {
            scope.builder().apply {
                beginControlFlow("if ($L != -1)", indexVar).apply {
                    toRead()
                }
                endControlFlow()
            }
        }
    }

    /**
     * Reads the value into a temporary local variable.
     */
    fun readIntoTmpVar(
        cursorVar: String,
        typeName: TypeName,
        scope: CodeGenScope
    ): String {
        val tmpField = scope.getTmpVar("_tmp${field.name.capitalize(Locale.US)}")
        scope.builder().apply {
            addStatement("final $T $L", typeName, tmpField)
            if (alwaysExists) {
                field.cursorValueReader?.readFromCursor(tmpField, cursorVar, indexVar, scope)
            } else {
                beginControlFlow("if ($L == -1)", indexVar).apply {
                    addStatement("$L = $L", tmpField, typeName.defaultValue())
                }
                nextControlFlow("else").apply {
                    field.cursorValueReader?.readFromCursor(tmpField, cursorVar, indexVar, scope)
                }
                endControlFlow()
            }
        }
        return tmpField
    }

    /**
     * On demand node which is created based on the fields that were passed into this class.
     */
    private class Node(
        // root for me
        val varName: String,
        // set if I'm a FieldParent
        val fieldParent: EmbeddedField?
    ) {
        // whom do i belong
        var parentNode: Node? = null
        // these fields are my direct fields
        lateinit var directFields: List
        // these nodes are under me
        val subNodes = mutableListOf()

        fun allFields(): List {
            return directFields + subNodes.flatMap { it.allFields() }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy