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
17package androidx.room.solver
18
19import COMMON
20import androidx.paging.DataSource
21import androidx.paging.PositionalDataSource
22import androidx.room.Entity
23import androidx.room.ext.L
24import androidx.room.ext.LifecyclesTypeNames
25import androidx.room.ext.PagingTypeNames
26import androidx.room.ext.ReactiveStreamsTypeNames
27import androidx.room.ext.RoomTypeNames.STRING_UTIL
28import androidx.room.ext.RxJava2TypeNames
29import androidx.room.ext.T
30import androidx.room.ext.typeName
31import androidx.room.parser.SQLTypeAffinity
32import androidx.room.processor.Context
33import androidx.room.processor.ProcessorErrors
34import androidx.room.solver.binderprovider.DataSourceFactoryQueryResultBinderProvider
35import androidx.room.solver.binderprovider.DataSourceQueryResultBinderProvider
36import androidx.room.solver.binderprovider.FlowableQueryResultBinderProvider
37import androidx.room.solver.binderprovider.LiveDataQueryResultBinderProvider
38import androidx.room.solver.types.CompositeAdapter
39import androidx.room.solver.types.TypeConverter
40import androidx.room.testing.TestInvocation
41import androidx.room.testing.TestProcessor
42import com.google.auto.common.MoreTypes
43import com.google.common.truth.Truth
44import com.google.testing.compile.CompileTester
45import com.google.testing.compile.JavaFileObjects
46import com.google.testing.compile.JavaSourcesSubjectFactory
47import com.squareup.javapoet.TypeName
48import org.hamcrest.CoreMatchers.`is`
49import org.hamcrest.CoreMatchers.instanceOf
50import org.hamcrest.CoreMatchers.notNullValue
51import org.hamcrest.CoreMatchers.nullValue
52import org.hamcrest.MatcherAssert.assertThat
53import org.junit.Test
54import org.junit.runner.RunWith
55import org.junit.runners.JUnit4
56import simpleRun
57import testCodeGenScope
58import javax.annotation.processing.ProcessingEnvironment
59import javax.lang.model.type.TypeKind
60
61@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
62@RunWith(JUnit4::class)
63class TypeAdapterStoreTest {
64    companion object {
65        fun tmp(index: Int) = CodeGenScope._tmpVar(index)
66    }
67
68    @Test
69    fun testDirect() {
70        singleRun { invocation ->
71            val store = TypeAdapterStore.create(Context(invocation.processingEnv))
72            val primitiveType = invocation.processingEnv.typeUtils.getPrimitiveType(TypeKind.INT)
73            val adapter = store.findColumnTypeAdapter(primitiveType, null)
74            assertThat(adapter, notNullValue())
75        }.compilesWithoutError()
76    }
77
78    @Test
79    fun testJavaLangBoolean() {
80        singleRun { invocation ->
81            val store = TypeAdapterStore.create(Context(invocation.processingEnv))
82            val boolean = invocation
83                    .processingEnv
84                    .elementUtils
85                    .getTypeElement("java.lang.Boolean")
86                    .asType()
87            val adapter = store.findColumnTypeAdapter(boolean, null)
88            assertThat(adapter, notNullValue())
89            assertThat(adapter, instanceOf(CompositeAdapter::class.java))
90            val composite = adapter as CompositeAdapter
91            assertThat(composite.intoStatementConverter?.from?.typeName(),
92                    `is`(TypeName.BOOLEAN.box()))
93            assertThat(composite.columnTypeAdapter.out.typeName(),
94                    `is`(TypeName.INT.box()))
95        }.compilesWithoutError()
96    }
97
98    @Test
99    fun testVia1TypeAdapter() {
100        singleRun { invocation ->
101            val store = TypeAdapterStore.create(Context(invocation.processingEnv))
102            val booleanType = invocation.processingEnv.typeUtils
103                    .getPrimitiveType(TypeKind.BOOLEAN)
104            val adapter = store.findColumnTypeAdapter(booleanType, null)
105            assertThat(adapter, notNullValue())
106            assertThat(adapter, instanceOf(CompositeAdapter::class.java))
107            val bindScope = testCodeGenScope()
108            adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
109            assertThat(bindScope.generate().trim(), `is`("""
110                    final int ${tmp(0)};
111                    ${tmp(0)} = fooVar ? 1 : 0;
112                    stmt.bindLong(41, ${tmp(0)});
113                    """.trimIndent()))
114
115            val cursorScope = testCodeGenScope()
116            adapter.readFromCursor("res", "curs", "7", cursorScope)
117            assertThat(cursorScope.generate().trim(), `is`("""
118                    final int ${tmp(0)};
119                    ${tmp(0)} = curs.getInt(7);
120                    res = ${tmp(0)} != 0;
121                    """.trimIndent()))
122        }.compilesWithoutError()
123    }
124
125    @Test
126    fun testVia2TypeAdapters() {
127        singleRun { invocation ->
128            val store = TypeAdapterStore.create(Context(invocation.processingEnv),
129                    pointTypeConverters(invocation.processingEnv))
130            val pointType = invocation.processingEnv.elementUtils
131                    .getTypeElement("foo.bar.Point").asType()
132            val adapter = store.findColumnTypeAdapter(pointType, null)
133            assertThat(adapter, notNullValue())
134            assertThat(adapter, instanceOf(CompositeAdapter::class.java))
135
136            val bindScope = testCodeGenScope()
137            adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
138            assertThat(bindScope.generate().trim(), `is`("""
139                    final int ${tmp(0)};
140                    final boolean ${tmp(1)};
141                    ${tmp(1)} = foo.bar.Point.toBoolean(fooVar);
142                    ${tmp(0)} = ${tmp(1)} ? 1 : 0;
143                    stmt.bindLong(41, ${tmp(0)});
144                    """.trimIndent()))
145
146            val cursorScope = testCodeGenScope()
147            adapter.readFromCursor("res", "curs", "11", cursorScope).toString()
148            assertThat(cursorScope.generate().trim(), `is`("""
149                    final int ${tmp(0)};
150                    ${tmp(0)} = curs.getInt(11);
151                    final boolean ${tmp(1)};
152                    ${tmp(1)} = ${tmp(0)} != 0;
153                    res = foo.bar.Point.fromBoolean(${tmp(1)});
154                    """.trimIndent()))
155        }.compilesWithoutError()
156    }
157
158    @Test
159    fun testDate() {
160        singleRun { (processingEnv) ->
161            val store = TypeAdapterStore.create(Context(processingEnv),
162                    dateTypeConverters(processingEnv))
163            val tDate = processingEnv.elementUtils.getTypeElement("java.util.Date").asType()
164            val adapter = store.findCursorValueReader(tDate, SQLTypeAffinity.INTEGER)
165            assertThat(adapter, notNullValue())
166            assertThat(adapter?.typeMirror(), `is`(tDate))
167            val bindScope = testCodeGenScope()
168            adapter!!.readFromCursor("outDate", "curs", "0", bindScope)
169            assertThat(bindScope.generate().trim(), `is`("""
170                final java.lang.Long _tmp;
171                if (curs.isNull(0)) {
172                  _tmp = null;
173                } else {
174                  _tmp = curs.getLong(0);
175                }
176                // convert Long to Date;
177            """.trimIndent()))
178        }.compilesWithoutError()
179    }
180
181    @Test
182    fun testIntList() {
183        singleRun { invocation ->
184            val binders = createIntListToStringBinders(invocation)
185            val store = TypeAdapterStore.create(Context(invocation.processingEnv), binders[0],
186                    binders[1])
187
188            val adapter = store.findColumnTypeAdapter(binders[0].from, null)
189            assertThat(adapter, notNullValue())
190
191            val bindScope = testCodeGenScope()
192            adapter!!.bindToStmt("stmt", "41", "fooVar", bindScope)
193            assertThat(bindScope.generate().trim(), `is`("""
194                final java.lang.String ${tmp(0)};
195                ${tmp(0)} = androidx.room.util.StringUtil.joinIntoString(fooVar);
196                if (${tmp(0)} == null) {
197                  stmt.bindNull(41);
198                } else {
199                  stmt.bindString(41, ${tmp(0)});
200                }
201                """.trimIndent()))
202
203            val converter = store.findTypeConverter(binders[0].from,
204                    invocation.context.COMMON_TYPES.STRING)
205            assertThat(converter, notNullValue())
206            assertThat(store.reverse(converter!!), `is`(binders[1]))
207        }.compilesWithoutError()
208    }
209
210    @Test
211    fun testOneWayConversion() {
212        singleRun { invocation ->
213            val binders = createIntListToStringBinders(invocation)
214            val store = TypeAdapterStore.create(Context(invocation.processingEnv), binders[0])
215            val adapter = store.findColumnTypeAdapter(binders[0].from, null)
216            assertThat(adapter, nullValue())
217
218            val stmtBinder = store.findStatementValueBinder(binders[0].from, null)
219            assertThat(stmtBinder, notNullValue())
220
221            val converter = store.findTypeConverter(binders[0].from,
222                    invocation.context.COMMON_TYPES.STRING)
223            assertThat(converter, notNullValue())
224            assertThat(store.reverse(converter!!), nullValue())
225        }
226    }
227
228    @Test
229    fun testMissingRxRoom() {
230        simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE)) { invocation ->
231            val publisherElement = invocation.processingEnv.elementUtils
232                    .getTypeElement(ReactiveStreamsTypeNames.PUBLISHER.toString())
233            assertThat(publisherElement, notNullValue())
234            assertThat(FlowableQueryResultBinderProvider(invocation.context).matches(
235                    MoreTypes.asDeclared(publisherElement.asType())), `is`(true))
236        }.failsToCompile().withErrorContaining(ProcessorErrors.MISSING_ROOM_RXJAVA2_ARTIFACT)
237    }
238
239    @Test
240    fun testFindPublisher() {
241        simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE, COMMON.RX2_ROOM)) {
242            invocation ->
243            val publisher = invocation.processingEnv.elementUtils
244                    .getTypeElement(ReactiveStreamsTypeNames.PUBLISHER.toString())
245            assertThat(publisher, notNullValue())
246            assertThat(FlowableQueryResultBinderProvider(invocation.context).matches(
247                    MoreTypes.asDeclared(publisher.asType())), `is`(true))
248        }.compilesWithoutError()
249    }
250
251    @Test
252    fun testFindFlowable() {
253        simpleRun(jfos = *arrayOf(COMMON.PUBLISHER, COMMON.FLOWABLE, COMMON.RX2_ROOM)) {
254            invocation ->
255            val flowable = invocation.processingEnv.elementUtils
256                    .getTypeElement(RxJava2TypeNames.FLOWABLE.toString())
257            assertThat(flowable, notNullValue())
258            assertThat(FlowableQueryResultBinderProvider(invocation.context).matches(
259                    MoreTypes.asDeclared(flowable.asType())), `is`(true))
260        }.compilesWithoutError()
261    }
262
263    @Test
264    fun testFindLiveData() {
265        simpleRun(jfos = *arrayOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA)) {
266            invocation ->
267            val liveData = invocation.processingEnv.elementUtils
268                    .getTypeElement(LifecyclesTypeNames.LIVE_DATA.toString())
269            assertThat(liveData, notNullValue())
270            assertThat(LiveDataQueryResultBinderProvider(invocation.context).matches(
271                    MoreTypes.asDeclared(liveData.asType())), `is`(true))
272        }.compilesWithoutError()
273    }
274
275    @Test
276    fun findDataSource() {
277        simpleRun {
278            invocation ->
279            val dataSource = invocation.processingEnv.elementUtils
280                    .getTypeElement(DataSource::class.java.canonicalName)
281            assertThat(dataSource, notNullValue())
282            assertThat(DataSourceQueryResultBinderProvider(invocation.context).matches(
283                    MoreTypes.asDeclared(dataSource.asType())), `is`(true))
284        }.failsToCompile().withErrorContaining(ProcessorErrors.PAGING_SPECIFY_DATA_SOURCE_TYPE)
285    }
286
287    @Test
288    fun findPositionalDataSource() {
289        simpleRun {
290            invocation ->
291            val dataSource = invocation.processingEnv.elementUtils
292                    .getTypeElement(PositionalDataSource::class.java.canonicalName)
293            assertThat(dataSource, notNullValue())
294            assertThat(DataSourceQueryResultBinderProvider(invocation.context).matches(
295                    MoreTypes.asDeclared(dataSource.asType())), `is`(true))
296        }.compilesWithoutError()
297    }
298
299    @Test
300    fun findDataSourceFactory() {
301        simpleRun(jfos = *arrayOf(COMMON.DATA_SOURCE_FACTORY)) {
302            invocation ->
303            val pagedListProvider = invocation.processingEnv.elementUtils
304                    .getTypeElement(PagingTypeNames.DATA_SOURCE_FACTORY.toString())
305            assertThat(pagedListProvider, notNullValue())
306            assertThat(DataSourceFactoryQueryResultBinderProvider(invocation.context).matches(
307                    MoreTypes.asDeclared(pagedListProvider.asType())), `is`(true))
308        }.compilesWithoutError()
309    }
310
311    private fun createIntListToStringBinders(invocation: TestInvocation): List<TypeConverter> {
312        val intType = invocation.processingEnv.elementUtils
313                .getTypeElement(Integer::class.java.canonicalName)
314                .asType()
315        val listType = invocation.processingEnv.elementUtils
316                .getTypeElement(java.util.List::class.java.canonicalName)
317        val listOfInts = invocation.processingEnv.typeUtils.getDeclaredType(listType, intType)
318
319        val intListConverter = object : TypeConverter(listOfInts,
320                invocation.context.COMMON_TYPES.STRING) {
321            override fun convert(inputVarName: String, outputVarName: String,
322                                 scope: CodeGenScope) {
323                scope.builder().apply {
324                    addStatement("$L = $T.joinIntoString($L)", outputVarName, STRING_UTIL,
325                            inputVarName)
326                }
327            }
328        }
329
330        val stringToIntListConverter = object : TypeConverter(
331                invocation.context.COMMON_TYPES.STRING, listOfInts) {
332            override fun convert(inputVarName: String, outputVarName: String,
333                                 scope: CodeGenScope) {
334                scope.builder().apply {
335                    addStatement("$L = $T.splitToIntList($L)", outputVarName, STRING_UTIL,
336                            inputVarName)
337                }
338            }
339        }
340        return listOf(intListConverter, stringToIntListConverter)
341    }
342
343    fun singleRun(handler: (TestInvocation) -> Unit): CompileTester {
344        return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
345                .that(listOf(JavaFileObjects.forSourceString("foo.bar.DummyClass",
346                        """
347                        package foo.bar;
348                        import androidx.room.*;
349                        @Entity
350                        public class DummyClass {}
351                        """
352                ), JavaFileObjects.forSourceString("foo.bar.Point",
353                        """
354                        package foo.bar;
355                        import androidx.room.*;
356                        @Entity
357                        public class Point {
358                            public int x, y;
359                            public Point(int x, int y) {
360                                this.x = x;
361                                this.y = y;
362                            }
363                            public static Point fromBoolean(boolean val) {
364                                return val ? new Point(1, 1) : new Point(0, 0);
365                            }
366                            public static boolean toBoolean(Point point) {
367                                return point.x > 0;
368                            }
369                        }
370                        """
371                )))
372                .processedWith(TestProcessor.builder()
373                        .forAnnotations(Entity::class)
374                        .nextRunHandler { invocation ->
375                            handler(invocation)
376                            true
377                        }
378                        .build())
379    }
380
381    fun pointTypeConverters(env: ProcessingEnvironment): List<TypeConverter> {
382        val tPoint = env.elementUtils.getTypeElement("foo.bar.Point").asType()
383        val tBoolean = env.typeUtils.getPrimitiveType(TypeKind.BOOLEAN)
384        return listOf(
385                object : TypeConverter(tPoint, tBoolean) {
386                    override fun convert(inputVarName: String, outputVarName: String,
387                                         scope: CodeGenScope) {
388                        scope.builder().apply {
389                            addStatement("$L = $T.toBoolean($L)", outputVarName, from, inputVarName)
390                        }
391                    }
392                },
393                object : TypeConverter(tBoolean, tPoint) {
394                    override fun convert(inputVarName: String, outputVarName: String,
395                                         scope: CodeGenScope) {
396                        scope.builder().apply {
397                            addStatement("$L = $T.fromBoolean($L)", outputVarName, tPoint,
398                                    inputVarName)
399                        }
400                    }
401                }
402        )
403    }
404
405    fun dateTypeConverters(env: ProcessingEnvironment): List<TypeConverter> {
406        val tDate = env.elementUtils.getTypeElement("java.util.Date").asType()
407        val tLong = env.elementUtils.getTypeElement("java.lang.Long").asType()
408        return listOf(
409                object : TypeConverter(tDate, tLong) {
410                    override fun convert(inputVarName: String, outputVarName: String,
411                                         scope: CodeGenScope) {
412                        scope.builder().apply {
413                            addStatement("// convert Date to Long")
414                        }
415                    }
416                },
417                object : TypeConverter(tLong, tDate) {
418                    override fun convert(inputVarName: String, outputVarName: String,
419                                         scope: CodeGenScope) {
420                        scope.builder().apply {
421                            addStatement("// convert Long to Date")
422                        }
423                    }
424                }
425        )
426    }
427}
428