Supporting ImmutableMap in Room.

This CL focuses on supporting the multimap of type ImmutableMap with the option to have an Object value, or a List / Set value collection type. The query result adapter for ImmutableMap wraps the MapQueryResultAdapter.

Bug: 187490856
Test: MultimapQueryTest.java
Change-Id: I7d79e7bde616d495ad8a5b05e7b5aeb3f030afaf
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
index c075fc2..1598905 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/dao/MusicDao.java
@@ -35,6 +35,8 @@
 import androidx.room.integration.testapp.vo.Song;
 import androidx.sqlite.db.SupportSQLiteQuery;
 
+import com.google.common.collect.ImmutableMap;
+
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -128,4 +130,16 @@
 
     @Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
     Flowable<Map<Artist, Set<Song>>> getAllArtistAndTheirSongsAsFlowableSet();
+
+    /* Guava ImmutableMap */
+    @Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
+    ImmutableMap<Artist, List<Song>> getAllArtistAndTheirSongsImmutableMap();
+
+    @RawQuery
+    ImmutableMap<Artist, List<Song>> getAllArtistAndTheirSongsRawQueryImmutableMap(
+            SupportSQLiteQuery query
+    );
+
+    @Query("SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song.mArtist")
+    LiveData<ImmutableMap<Artist, Set<Song>>> getAllArtistAndTheirSongsAsLiveDataImmutableMap();
 }
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultimapQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultimapQueryTest.java
index 30e46e4..e5785d9 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultimapQueryTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/MultimapQueryTest.java
@@ -41,6 +41,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
 
+import com.google.common.collect.ImmutableMap;
+
 import org.hamcrest.MatcherAssert;
 import org.junit.Before;
 import org.junit.Rule;
@@ -472,6 +474,49 @@
         }
     }
 
+    @Test
+    public void testJoinByArtistNameImmutableMap() {
+        mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+        mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+        ImmutableMap<Artist, List<Song>> artistToSongsMap =
+                mMusicDao.getAllArtistAndTheirSongsImmutableMap();
+        assertContentsOfResultMapWithList(artistToSongsMap);
+    }
+
+    @Test
+    public void testJoinByArtistNameRawQueryImmutableMap() {
+        mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+        mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+        ImmutableMap<Artist, List<Song>> artistToSongsMap =
+                mMusicDao.getAllArtistAndTheirSongsRawQueryImmutableMap(
+                        new SimpleSQLiteQuery(
+                                "SELECT * FROM Artist JOIN Song ON Artist.mArtistName = Song"
+                                        + ".mArtist"
+                        )
+                );
+        assertContentsOfResultMapWithList(artistToSongsMap);
+    }
+
+    @Test
+    public void testJoinByArtistNameImmutableMapWithSet()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        mMusicDao.addSongs(mRhcpSong1, mRhcpSong2, mAcdcSong1, mPinkFloydSong1);
+        mMusicDao.addArtists(mRhcp, mAcDc, mTheClash, mPinkFloyd);
+
+        LiveData<ImmutableMap<Artist, Set<Song>>> artistToSongsMapLiveData =
+                mMusicDao.getAllArtistAndTheirSongsAsLiveDataImmutableMap();
+        final TestLifecycleOwner testOwner = new TestLifecycleOwner(Lifecycle.State.CREATED);
+        final TestObserver<Map<Artist, Set<Song>>> observer = new MyTestObserver<>();
+        TestUtil.observeOnMainThread(artistToSongsMapLiveData, testOwner, observer);
+        MatcherAssert.assertThat(observer.hasValue(), is(false));
+        observer.reset();
+        testOwner.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+        assertThat(observer.get()).isNotNull();
+        assertContentsOfResultMapWithSet(observer.get());
+    }
+
     /**
      * Checks that the contents of the map are as expected.
      *
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 36df772..dddc4e3 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -56,6 +56,7 @@
 import androidx.room.solver.query.result.EntityRowAdapter
 import androidx.room.solver.query.result.GuavaOptionalQueryResultAdapter
 import androidx.room.solver.query.result.ImmutableListQueryResultAdapter
+import androidx.room.solver.query.result.ImmutableMapQueryResultAdapter
 import androidx.room.solver.query.result.ListQueryResultAdapter
 import androidx.room.solver.query.result.MapQueryResultAdapter
 import androidx.room.solver.query.result.OptionalQueryResultAdapter
@@ -95,6 +96,7 @@
 import androidx.room.vo.ShortcutQueryParameter
 import com.google.common.annotations.VisibleForTesting
 import com.google.common.collect.ImmutableList
+import com.google.common.collect.ImmutableMap
 import java.util.LinkedList
 
 @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
@@ -462,6 +464,24 @@
                 typeArg = typeArg,
                 rowAdapter = rowAdapter
             )
+        } else if (typeMirror.isTypeOf(ImmutableMap::class)) {
+            val keyTypeArg = typeMirror.typeArguments[0].extendsBoundOrSelf()
+            val valueTypeArg = typeMirror.typeArguments[1].extendsBoundOrSelf()
+
+            // Create a type mirror for a regular Map in order to use MapQueryResultAdapter. This
+            // avoids code duplication as Immutable Map can be initialized by creating an immutable
+            // copy of a regular map.
+            val mapType = context.processingEnv.getDeclaredType(
+                context.processingEnv.requireTypeElement(Map::class),
+                keyTypeArg,
+                valueTypeArg
+            )
+            val resultAdapter = findQueryResultAdapter(mapType, query = query) ?: return null
+            return ImmutableMapQueryResultAdapter(
+                keyTypeArg = keyTypeArg,
+                valueTypeArg = valueTypeArg,
+                resultAdapter = resultAdapter
+            )
         } else if (typeMirror.isTypeOf(java.util.Map::class)) {
             // TODO: Handle nested collection values in the map
             // TODO: Verify that hashCode() and equals() are declared by the keyTypeArg
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
new file mode 100644
index 0000000..c28b189
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ImmutableMapQueryResultAdapter.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.solver.query.result
+
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.solver.CodeGenScope
+import com.google.common.collect.ImmutableMap
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.ParameterizedTypeName
+
+class ImmutableMapQueryResultAdapter(
+    private val keyTypeArg: XType,
+    private val valueTypeArg: XType,
+    private val resultAdapter: QueryResultAdapter
+) : QueryResultAdapter(resultAdapter.rowAdapters) {
+    override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
+        scope.builder().apply {
+            val mapVarName = scope.getTmpVar("_mapResult")
+            resultAdapter.convert(mapVarName, cursorVarName, scope)
+            addStatement(
+                "final $T $L = $T.copyOf($L)",
+                ParameterizedTypeName.get(
+                    ClassName.get(ImmutableMap::class.java),
+                    keyTypeArg.typeName,
+                    valueTypeArg.typeName
+                ),
+                outVarName,
+                ClassName.get(ImmutableMap::class.java),
+                mapVarName
+            )
+        }
+    }
+}
\ No newline at end of file