1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.room.vo
18 
19 import androidx.room.compiler.codegen.XTypeName
20 import androidx.room.compiler.processing.XFieldElement
21 import androidx.room.compiler.processing.XNullability
22 import androidx.room.compiler.processing.XType
23 import androidx.room.ext.capitalize
24 import androidx.room.ext.decapitalize
25 import androidx.room.migration.bundle.FieldBundle
26 import androidx.room.parser.Collate
27 import androidx.room.parser.SQLTypeAffinity
28 import androidx.room.solver.types.StatementValueBinder
29 import androidx.room.solver.types.StatementValueReader
30 import java.util.Locale
31 
32 // used in cache matching, must stay as a data class or implement equals
33 data class Property(
34     val element: XFieldElement,
35     val name: String,
36     val type: XType,
37     var affinity: SQLTypeAffinity?,
38     val collate: Collate? = null,
39     val columnName: String = name,
40     val defaultValue: String? = null,
41     // null here means that this property does not belong to parent, instead, it belongs to an
42     // embedded child of the main Pojo
43     val parent: EmbeddedProperty? = null,
44     // index might be removed when being merged into an Entity
45     var indexed: Boolean = false,
46     /** Whether the table column for this property should be NOT NULL */
47     val nonNull: Boolean = calcNonNull(type, parent)
48 ) : HasSchemaIdentity {
49     lateinit var getter: PropertyGetter
50     lateinit var setter: PropertySetter
51     // binds the property into a statement
52     var statementBinder: StatementValueBinder? = null
53 
54     // reads this property from a cursor column
55     var statementValueReader: StatementValueReader? = null
<lambda>null56     val typeName: XTypeName by lazy { type.asTypeName() }
57 
getIdKeynull58     override fun getIdKey(): String {
59         return buildString {
60             // we don't get the collate information from sqlite so ignoring it here.
61             append("$columnName-${affinity?.name ?: SQLTypeAffinity.TEXT.name}-$nonNull")
62             // defaultValue was newly added; it should affect the ID only when it is used.
63             if (defaultValue != null) {
64                 append("-defaultValue=$defaultValue")
65             }
66         }
67     }
68 
69     /** Used when reporting errors on duplicate names */
getPathnull70     fun getPath(): String {
71         return if (parent == null) {
72             name
73         } else {
74             "${parent.property.getPath()} > $name"
75         }
76     }
77 
<lambda>null78     private val pathWithDotNotation: String by lazy {
79         if (parent == null) {
80             name
81         } else {
82             "${parent.property.pathWithDotNotation}.$name"
83         }
84     }
85 
86     /**
87      * List of names that include variations. e.g. if it is mUser, user is added to the list or if
88      * it is isAdmin, admin is added to the list
89      */
<lambda>null90     val nameWithVariations by lazy {
91         val result = arrayListOf(name)
92         if (name.length > 1) {
93             if (name.startsWith('_')) {
94                 result.add(name.substring(1))
95             }
96             if (name.startsWith("m") && name[1].isUpperCase()) {
97                 result.add(name.substring(1).decapitalize(Locale.US))
98             }
99 
100             if (typeName == XTypeName.PRIMITIVE_BOOLEAN || typeName == XTypeName.BOXED_BOOLEAN) {
101                 if (name.length > 2 && name.startsWith("is") && name[2].isUpperCase()) {
102                     result.add(name.substring(2).decapitalize(Locale.US))
103                 }
104                 if (name.length > 3 && name.startsWith("has") && name[3].isUpperCase()) {
105                     result.add(name.substring(3).decapitalize(Locale.US))
106                 }
107             }
108         }
109         result
110     }
111 
<lambda>null112     val getterNameWithVariations by lazy {
113         nameWithVariations.map { "get${it.capitalize(Locale.US)}" } +
114             if (typeName == XTypeName.PRIMITIVE_BOOLEAN || typeName == XTypeName.BOXED_BOOLEAN) {
115                 nameWithVariations.flatMap {
116                     listOf("is${it.capitalize(Locale.US)}", "has${it.capitalize(Locale.US)}")
117                 }
118             } else {
119                 emptyList()
120             }
121     }
122 
<lambda>null123     val setterNameWithVariations by lazy {
124         nameWithVariations.map { "set${it.capitalize(Locale.US)}" }
125     }
126 
127     /** definition to be used in create query */
databaseDefinitionnull128     fun databaseDefinition(autoIncrementPKey: Boolean): String {
129         val columnSpec = StringBuilder("")
130         if (autoIncrementPKey) {
131             columnSpec.append(" PRIMARY KEY AUTOINCREMENT")
132         }
133         if (nonNull) {
134             columnSpec.append(" NOT NULL")
135         }
136         if (collate != null) {
137             columnSpec.append(" COLLATE ${collate.name}")
138         }
139         if (defaultValue != null) {
140             columnSpec.append(" DEFAULT $defaultValue")
141         }
142         return "`$columnName` ${(affinity ?: SQLTypeAffinity.TEXT).name}$columnSpec"
143     }
144 
toBundlenull145     fun toBundle(): FieldBundle =
146         FieldBundle(
147             pathWithDotNotation,
148             columnName,
149             affinity?.name ?: SQLTypeAffinity.TEXT.name,
150             nonNull,
151             defaultValue
152         )
153 
154     companion object {
155         fun calcNonNull(type: XType, parent: EmbeddedProperty?): Boolean {
156             return XNullability.NONNULL == type.nullability &&
157                 (parent == null || parent.isNonNullRecursively())
158         }
159     }
160 }
161