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