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