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