1 /* 2 * Copyright 2021 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.appsearch.ktx 18 19 import android.content.Context 20 import androidx.appsearch.annotation.Document 21 import androidx.appsearch.app.AppSearchSchema 22 import androidx.appsearch.app.AppSearchSession 23 import androidx.appsearch.app.EmbeddingVector 24 import androidx.appsearch.app.GenericDocument 25 import androidx.appsearch.app.PutDocumentsRequest 26 import androidx.appsearch.app.SearchSpec 27 import androidx.appsearch.app.SetSchemaRequest 28 import androidx.appsearch.localstorage.LocalStorage 29 import androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess 30 import androidx.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments 31 import androidx.test.core.app.ApplicationProvider 32 import com.google.common.truth.Truth.assertThat 33 import org.junit.After 34 import org.junit.Before 35 import org.junit.Test 36 37 public class AnnotationProcessorKtTest { 38 private companion object { 39 private const val DB_NAME = "" 40 } 41 42 private lateinit var session: AppSearchSession 43 44 @Before setUpnull45 public fun setUp() { 46 val context = ApplicationProvider.getApplicationContext<Context>() 47 session = 48 LocalStorage.createSearchSessionAsync( 49 LocalStorage.SearchContext.Builder(context, DB_NAME).build() 50 ) 51 .get() 52 53 // Cleanup whatever documents may still exist in these databases. This is needed in 54 // addition to tearDown in case a test exited without completing properly. 55 cleanup() 56 } 57 58 @After tearDownnull59 public fun tearDown() { 60 // Cleanup whatever documents may still exist in these databases. 61 cleanup() 62 } 63 cleanupnull64 private fun cleanup() { 65 session.setSchemaAsync(SetSchemaRequest.Builder().setForceOverride(true).build()).get() 66 } 67 68 @Document 69 internal data class Card( 70 @Document.Namespace val namespace: String, 71 @Document.Id val id: String, 72 @Document.CreationTimestampMillis val creationTimestampMillis: Long = 0L, 73 @Document.StringProperty( 74 indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES, 75 tokenizerType = AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN 76 ) 77 val string: String? = null, 78 ) 79 80 @Document 81 internal data class Gift( 82 @Document.Namespace val namespace: String, 83 @Document.Id val id: String, 84 @Document.CreationTimestampMillis val creationTimestampMillis: Long = 0L, 85 86 // Collections 87 @Document.LongProperty val collectLong: Collection<Long>, 88 @Document.LongProperty val collectInteger: Collection<Int>, 89 @Document.DoubleProperty val collectDouble: Collection<Double>, 90 @Document.DoubleProperty val collectFloat: Collection<Float>, 91 @Document.BooleanProperty val collectBoolean: Collection<Boolean>, 92 @Document.BytesProperty val collectByteArr: Collection<ByteArray>, 93 @Document.StringProperty val collectString: Collection<String>, 94 @Document.DocumentProperty val collectCard: Collection<Card>, 95 96 // Arrays 97 @Document.LongProperty val arrBoxLong: Array<Long>, 98 @Document.LongProperty val arrUnboxLong: LongArray, 99 @Document.LongProperty val arrBoxInteger: Array<Int>, 100 @Document.LongProperty val arrUnboxInt: IntArray, 101 @Document.DoubleProperty val arrBoxDouble: Array<Double>, 102 @Document.DoubleProperty val arrUnboxDouble: DoubleArray, 103 @Document.DoubleProperty val arrBoxFloat: Array<Float>, 104 @Document.DoubleProperty val arrUnboxFloat: FloatArray, 105 @Document.BooleanProperty val arrBoxBoolean: Array<Boolean>, 106 @Document.BooleanProperty val arrUnboxBoolean: BooleanArray, 107 @Document.BytesProperty val arrUnboxByteArr: Array<ByteArray>, 108 @Document.StringProperty val arrString: Array<String>, 109 @Document.DocumentProperty val arrCard: Array<Card>, 110 111 // Single values 112 @Document.StringProperty val string: String, 113 @Document.LongProperty val boxLong: Long, 114 @Document.LongProperty val unboxLong: Long = 0, 115 @Document.LongProperty val boxInteger: Int, 116 @Document.LongProperty val unboxInt: Int = 0, 117 @Document.DoubleProperty val boxDouble: Double, 118 @Document.DoubleProperty val unboxDouble: Double = 0.0, 119 @Document.DoubleProperty val boxFloat: Float, 120 @Document.DoubleProperty val unboxFloat: Float = 0f, 121 @Document.BooleanProperty val boxBoolean: Boolean, 122 @Document.BooleanProperty val unboxBoolean: Boolean = false, 123 @Document.BytesProperty val unboxByteArr: ByteArray, 124 @Document.DocumentProperty val card: Card 125 ) { equalsnull126 override fun equals(other: Any?): Boolean { 127 if (this === other) return true 128 if (javaClass != other?.javaClass) return false 129 130 other as Gift 131 132 if (namespace != other.namespace) return false 133 if (id != other.id) return false 134 if (collectLong != other.collectLong) return false 135 if (collectInteger != other.collectInteger) return false 136 if (collectDouble != other.collectDouble) return false 137 if (collectFloat != other.collectFloat) return false 138 if (collectBoolean != other.collectBoolean) return false 139 // It's complicated to do a deep comparison of this, so we skip it 140 // if (collectByteArr != other.collectByteArr) return false 141 if (collectString != other.collectString) return false 142 if (collectCard != other.collectCard) return false 143 if (!arrBoxLong.contentEquals(other.arrBoxLong)) return false 144 if (!arrUnboxLong.contentEquals(other.arrUnboxLong)) return false 145 if (!arrBoxInteger.contentEquals(other.arrBoxInteger)) return false 146 if (!arrUnboxInt.contentEquals(other.arrUnboxInt)) return false 147 if (!arrBoxDouble.contentEquals(other.arrBoxDouble)) return false 148 if (!arrUnboxDouble.contentEquals(other.arrUnboxDouble)) return false 149 if (!arrBoxFloat.contentEquals(other.arrBoxFloat)) return false 150 if (!arrUnboxFloat.contentEquals(other.arrUnboxFloat)) return false 151 if (!arrBoxBoolean.contentEquals(other.arrBoxBoolean)) return false 152 if (!arrUnboxBoolean.contentEquals(other.arrUnboxBoolean)) return false 153 if (!arrUnboxByteArr.contentDeepEquals(other.arrUnboxByteArr)) return false 154 if (!arrString.contentEquals(other.arrString)) return false 155 if (!arrCard.contentEquals(other.arrCard)) return false 156 if (string != other.string) return false 157 if (boxLong != other.boxLong) return false 158 if (unboxLong != other.unboxLong) return false 159 if (boxInteger != other.boxInteger) return false 160 if (unboxInt != other.unboxInt) return false 161 if (boxDouble != other.boxDouble) return false 162 if (unboxDouble != other.unboxDouble) return false 163 if (boxFloat != other.boxFloat) return false 164 if (unboxFloat != other.unboxFloat) return false 165 if (boxBoolean != other.boxBoolean) return false 166 if (unboxBoolean != other.unboxBoolean) return false 167 if (!unboxByteArr.contentEquals(other.unboxByteArr)) return false 168 if (card != other.card) return false 169 170 return true 171 } 172 } 173 174 @Document 175 internal data class NullabilityDefaults( 176 @Document.Namespace val namespace: String, 177 @Document.Id val id: String, 178 @Document.StringProperty val nonNullList: List<String>, 179 @Document.StringProperty val nullableList: List<String>?, 180 @Document.BooleanProperty val nonNullBoolean: Boolean, 181 @Document.BooleanProperty val nullableBoolean: Boolean? 182 ) {} 183 184 @Test testAnnotationProcessor_nullabilityDefaultsnull185 fun testAnnotationProcessor_nullabilityDefaults() { 186 // Create an empty GenericDocument 187 val genericDocument = 188 GenericDocument.Builder<GenericDocument.Builder<*>>("ns", "id", "schema").build() 189 val classDocument = genericDocument.toDocumentClass(NullabilityDefaults::class.java) 190 assertThat(classDocument.nullableBoolean).isNull() 191 assertThat(classDocument.nullableList).isNull() 192 assertThat(classDocument.nonNullBoolean).isNotNull() 193 assertThat(classDocument.nonNullList).isNotNull() 194 } 195 196 @Document 197 internal data class NullabilityLists( 198 @Document.Namespace val namespace: String, 199 @Document.Id val id: String, 200 @Document.StringProperty val nonNullStrings: List<String>, 201 @Document.StringProperty val nullableStrings: List<String>?, 202 @Document.BooleanProperty val nonNullBooleans: List<Boolean>, 203 @Document.BooleanProperty val nullableBooleans: List<Boolean>?, 204 @Document.LongProperty val nonNullLongs: List<Long>, 205 @Document.LongProperty val nullableLongs: List<Long>?, 206 @Document.DocumentProperty val nonNullCustomTypes: List<Gift>, 207 @Document.DocumentProperty val nullableCustomTypes: List<Gift>?, 208 @Document.EmbeddingProperty val nonNullEmbeddings: List<EmbeddingVector>, 209 @Document.EmbeddingProperty val nullableEmbeddings: List<EmbeddingVector>? 210 ) {} 211 212 @Test testAnnotationProcessor_nullableListDefaultsnull213 fun testAnnotationProcessor_nullableListDefaults() { 214 // Create an empty GenericDocument 215 val genericDocument = 216 GenericDocument.Builder<GenericDocument.Builder<*>>("ns", "id", "schema").build() 217 val classDocument = genericDocument.toDocumentClass(NullabilityLists::class.java) 218 assertThat(classDocument.nullableBooleans).isNull() 219 assertThat(classDocument.nullableLongs).isNull() 220 assertThat(classDocument.nullableStrings).isNull() 221 assertThat(classDocument.nullableCustomTypes).isNull() 222 assertThat(classDocument.nullableEmbeddings).isNull() 223 assertThat(classDocument.nonNullBooleans).isNotNull() 224 assertThat(classDocument.nonNullLongs).isNotNull() 225 assertThat(classDocument.nonNullStrings).isNotNull() 226 assertThat(classDocument.nonNullCustomTypes).isNotNull() 227 assertThat(classDocument.nonNullEmbeddings).isNotNull() 228 } 229 230 @Test testAnnotationProcessornull231 fun testAnnotationProcessor() { 232 session 233 .setSchemaAsync( 234 SetSchemaRequest.Builder() 235 .addDocumentClasses(Card::class.java, Gift::class.java) 236 .build() 237 ) 238 .get() 239 240 // Create a Gift object and assign values. 241 val inputDocument = createPopulatedGift() 242 243 // Index the Gift document and query it. 244 checkIsBatchResultSuccess( 245 session.putAsync(PutDocumentsRequest.Builder().addDocuments(inputDocument).build()) 246 ) 247 val searchResults = session.search("", SearchSpec.Builder().build()) 248 val documents = convertSearchResultsToDocuments(searchResults) 249 assertThat(documents).hasSize(1) 250 251 // Convert GenericDocument to Gift and check values. 252 val outputDocument = documents[0].toDocumentClass(Gift::class.java) 253 assertThat(outputDocument).isEqualTo(inputDocument) 254 } 255 256 @Test testGenericDocumentConversionnull257 fun testGenericDocumentConversion() { 258 val inGift = createPopulatedGift() 259 val genericDocument1 = GenericDocument.fromDocumentClass(inGift) 260 val genericDocument2 = GenericDocument.fromDocumentClass(inGift) 261 val outGift = genericDocument2.toDocumentClass(Gift::class.java) 262 assertThat(inGift).isNotSameInstanceAs(outGift) 263 assertThat(inGift).isEqualTo(outGift) 264 assertThat(genericDocument1).isNotSameInstanceAs(genericDocument2) 265 assertThat(genericDocument1).isEqualTo(genericDocument2) 266 } 267 createPopulatedGiftnull268 private fun createPopulatedGift(): Gift { 269 val card1 = Card("card.namespace", "card.id1") 270 val card2 = Card("card.namespace", "card.id2") 271 return Gift( 272 namespace = "gift.namespace", 273 id = "gift.id", 274 arrBoxBoolean = arrayOf(true, false), 275 arrBoxDouble = arrayOf(0.0, 1.0), 276 arrBoxFloat = arrayOf(2.0f, 3.0f), 277 arrBoxInteger = arrayOf(4, 5), 278 arrBoxLong = arrayOf(6L, 7L), 279 arrString = arrayOf("cat", "dog"), 280 arrUnboxBoolean = booleanArrayOf(false, true), 281 arrUnboxByteArr = arrayOf(byteArrayOf(0, 1), byteArrayOf(2, 3)), 282 arrUnboxDouble = doubleArrayOf(1.0, 0.0), 283 arrUnboxFloat = floatArrayOf(3.0f, 2.0f), 284 arrUnboxInt = intArrayOf(5, 4), 285 arrUnboxLong = longArrayOf(7, 6), 286 arrCard = arrayOf(card2, card2), 287 collectLong = listOf(6L, 7L), 288 collectInteger = listOf(4, 5), 289 collectBoolean = listOf(false, true), 290 collectString = listOf("cat", "dog"), 291 collectDouble = listOf(0.0, 1.0), 292 collectFloat = listOf(2.0f, 3.0f), 293 collectByteArr = listOf(byteArrayOf(0, 1), byteArrayOf(2, 3)), 294 collectCard = listOf(card2, card2), 295 string = "String", 296 boxLong = 1L, 297 unboxLong = 2L, 298 boxInteger = 3, 299 unboxInt = 4, 300 boxDouble = 5.0, 301 unboxDouble = 6.0, 302 boxFloat = 7.0f, 303 unboxFloat = 8.0f, 304 boxBoolean = true, 305 unboxBoolean = false, 306 unboxByteArr = byteArrayOf(1, 2, 3), 307 card = card1 308 ) 309 } 310 } 311