diff --git a/src/main/java/com/google/firebase/cloud/FirestoreClient.java b/src/main/java/com/google/firebase/cloud/FirestoreClient.java index fddf3320d..101201781 100644 --- a/src/main/java/com/google/firebase/cloud/FirestoreClient.java +++ b/src/main/java/com/google/firebase/cloud/FirestoreClient.java @@ -11,6 +11,9 @@ import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,7 +35,7 @@ public class FirestoreClient { private final Firestore firestore; - private FirestoreClient(FirebaseApp app) { + private FirestoreClient(FirebaseApp app, String databaseId) { checkNotNull(app, "FirebaseApp must not be null"); String projectId = ImplFirebaseTrampolines.getProjectId(app); checkArgument(!Strings.isNullOrEmpty(projectId), @@ -47,13 +50,14 @@ private FirestoreClient(FirebaseApp app) { .setCredentialsProvider( FixedCredentialsProvider.create(ImplFirebaseTrampolines.getCredentials(app))) .setProjectId(projectId) + .setDatabaseId(databaseId) .build() .getService(); } /** - * Returns the Firestore instance associated with the default Firebase app. Returns the same - * instance for all invocations. The Firestore instance and all references obtained from it + * Returns the default Firestore instance associated with the default Firebase app. Returns the + * same instance for all invocations. The Firestore instance and all references obtained from it * becomes unusable, once the default app is deleted. * * @return A non-null {@code Firestore} @@ -65,9 +69,9 @@ public static Firestore getFirestore() { } /** - * Returns the Firestore instance associated with the specified Firebase app. For a given app, - * always returns the same instance. The Firestore instance and all references obtained from it - * becomes unusable, once the specified app is deleted. + * Returns the default Firestore instance associated with the specified Firebase app. For a given + * app, invocation always returns the same instance. The Firestore instance and all references + * obtained from it becomes unusable, once the specified app is deleted. * * @param app A non-null {@link FirebaseApp}. * @return A non-null {@code Firestore} @@ -75,32 +79,86 @@ public static Firestore getFirestore() { */ @NonNull public static Firestore getFirestore(FirebaseApp app) { - return getInstance(app).firestore; + return getFirestore(app, ImplFirebaseTrampolines.getFirestoreOptions(app).getDatabaseId()); + } + + /** + * Returns the Firestore instance associated with the specified Firebase app. Returns the same + * instance for all invocations given the same app and database parameter. The Firestore instance + * and all references obtained from it becomes unusable, once the specified app is deleted. + * + * @param app A non-null {@link FirebaseApp}. + * @param database - The name of database. + * @return A non-null {@code Firestore} + * instance. + */ + @NonNull + static Firestore getFirestore(FirebaseApp app, String database) { + return getInstance(app, database).firestore; + } + + /** + * Returns the Firestore instance associated with the default Firebase app. Returns the same + * instance for all invocations given the same database parameter. The Firestore instance and all + * references obtained from it becomes unusable, once the default app is deleted. + * + * @param database - The name of database. + * @return A non-null {@code Firestore} + * instance. + */ + @NonNull + static Firestore getFirestore(String database) { + return getFirestore(FirebaseApp.getInstance(), database); } - private static synchronized FirestoreClient getInstance(FirebaseApp app) { + private static synchronized FirestoreClient getInstance(FirebaseApp app, String database) { FirestoreClientService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID, FirestoreClientService.class); if (service == null) { service = ImplFirebaseTrampolines.addService(app, new FirestoreClientService(app)); } - return service.getInstance(); + return service.getInstance().get(database); } private static final String SERVICE_ID = FirestoreClient.class.getName(); - private static class FirestoreClientService extends FirebaseService { + private static class FirestoreClientService extends FirebaseService { FirestoreClientService(FirebaseApp app) { - super(SERVICE_ID, new FirestoreClient(app)); + super(SERVICE_ID, new FirestoreInstances(app)); } @Override public void destroy() { - try { - instance.firestore.close(); - } catch (Exception e) { - logger.warn("Error while closing the Firestore instance", e); + instance.destroy(); + } + } + + private static class FirestoreInstances { + + private final FirebaseApp app; + + private final Map clients = + Collections.synchronizedMap(new HashMap<>()); + + private FirestoreInstances(FirebaseApp app) { + this.app = app; + } + + FirestoreClient get(String databaseId) { + return clients.computeIfAbsent(databaseId, id -> new FirestoreClient(app, id)); + } + + void destroy() { + synchronized (clients) { + for (FirestoreClient client : clients.values()) { + try { + client.firestore.close(); + } catch (Exception e) { + logger.warn("Error while closing the Firestore instance", e); + } + } + clients.clear(); } } } diff --git a/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java b/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java index 018d0e45b..a5f617ce6 100644 --- a/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java +++ b/src/test/java/com/google/firebase/cloud/FirestoreClientTest.java @@ -2,9 +2,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.firestore.DocumentReference; @@ -26,6 +26,7 @@ public class FirestoreClientTest { // Setting credentials is not required (they get overridden by Admin SDK), but without // this Firestore logs an ugly warning during tests. .setCredentials(new MockGoogleCredentials("test-token")) + .setDatabaseId("differedDefaultDatabaseId") .build(); @After @@ -35,47 +36,75 @@ public void tearDown() { @Test public void testExplicitProjectId() throws IOException { + final String databaseId = "databaseIdInTestExplicitProjectId"; FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("explicit-project-id") .setFirestoreOptions(FIRESTORE_OPTIONS) .build()); - Firestore firestore = FirestoreClient.getFirestore(app); - assertEquals("explicit-project-id", firestore.getOptions().getProjectId()); + Firestore firestore1 = FirestoreClient.getFirestore(app); + assertEquals("explicit-project-id", firestore1.getOptions().getProjectId()); + assertEquals(FIRESTORE_OPTIONS.getDatabaseId(), firestore1.getOptions().getDatabaseId()); - firestore = FirestoreClient.getFirestore(); - assertEquals("explicit-project-id", firestore.getOptions().getProjectId()); + assertSame(firestore1, FirestoreClient.getFirestore()); + + Firestore firestore2 = FirestoreClient.getFirestore(app, databaseId); + assertEquals("explicit-project-id", firestore2.getOptions().getProjectId()); + assertEquals(databaseId, firestore2.getOptions().getDatabaseId()); + + assertSame(firestore2, FirestoreClient.getFirestore(databaseId)); + + assertNotSame(firestore1, firestore2); } @Test public void testServiceAccountProjectId() throws IOException { + final String databaseId = "databaseIdInTestServiceAccountProjectId"; FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setFirestoreOptions(FIRESTORE_OPTIONS) .build()); - Firestore firestore = FirestoreClient.getFirestore(app); - assertEquals("mock-project-id", firestore.getOptions().getProjectId()); + Firestore firestore1 = FirestoreClient.getFirestore(app); + assertEquals("mock-project-id", firestore1.getOptions().getProjectId()); + assertEquals(FIRESTORE_OPTIONS.getDatabaseId(), firestore1.getOptions().getDatabaseId()); + + assertSame(firestore1, FirestoreClient.getFirestore()); - firestore = FirestoreClient.getFirestore(); - assertEquals("mock-project-id", firestore.getOptions().getProjectId()); + Firestore firestore2 = FirestoreClient.getFirestore(app, databaseId); + assertEquals("mock-project-id", firestore2.getOptions().getProjectId()); + assertEquals(databaseId, firestore2.getOptions().getDatabaseId()); + + assertSame(firestore2, FirestoreClient.getFirestore(databaseId)); + + assertNotSame(firestore1, firestore2); } @Test public void testFirestoreOptions() throws IOException { + final String databaseId = "databaseIdInTestFirestoreOptions"; FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("explicit-project-id") .setFirestoreOptions(FIRESTORE_OPTIONS) .build()); - Firestore firestore = FirestoreClient.getFirestore(app); - assertEquals("explicit-project-id", firestore.getOptions().getProjectId()); + Firestore firestore1 = FirestoreClient.getFirestore(app); + assertEquals("explicit-project-id", firestore1.getOptions().getProjectId()); + assertEquals(FIRESTORE_OPTIONS.getDatabaseId(), firestore1.getOptions().getDatabaseId()); + + assertSame(firestore1, FirestoreClient.getFirestore()); + + Firestore firestore2 = FirestoreClient.getFirestore(app, databaseId); + assertEquals("explicit-project-id", firestore2.getOptions().getProjectId()); + assertEquals(databaseId, firestore2.getOptions().getDatabaseId()); - firestore = FirestoreClient.getFirestore(); - assertEquals("explicit-project-id", firestore.getOptions().getProjectId()); + assertSame(firestore2, FirestoreClient.getFirestore(databaseId)); + + assertNotSame(firestore1, firestore2); } @Test public void testFirestoreOptionsOverride() throws IOException { + final String databaseId = "databaseIdInTestFirestoreOptions"; FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("explicit-project-id") @@ -84,48 +113,51 @@ public void testFirestoreOptionsOverride() throws IOException { .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .build()) .build()); - Firestore firestore = FirestoreClient.getFirestore(app); - assertEquals("explicit-project-id", firestore.getOptions().getProjectId()); + Firestore firestore1 = FirestoreClient.getFirestore(app); + assertEquals("explicit-project-id", firestore1.getOptions().getProjectId()); assertSame(ImplFirebaseTrampolines.getCredentials(app), - firestore.getOptions().getCredentialsProvider().getCredentials()); + firestore1.getOptions().getCredentialsProvider().getCredentials()); + assertEquals("(default)", firestore1.getOptions().getDatabaseId()); + + assertSame(firestore1, FirestoreClient.getFirestore()); - firestore = FirestoreClient.getFirestore(); - assertEquals("explicit-project-id", firestore.getOptions().getProjectId()); + Firestore firestore2 = FirestoreClient.getFirestore(app, databaseId); + assertEquals("explicit-project-id", firestore2.getOptions().getProjectId()); assertSame(ImplFirebaseTrampolines.getCredentials(app), - firestore.getOptions().getCredentialsProvider().getCredentials()); + firestore2.getOptions().getCredentialsProvider().getCredentials()); + assertEquals(databaseId, firestore2.getOptions().getDatabaseId()); + + assertSame(firestore2, FirestoreClient.getFirestore(databaseId)); + + assertNotSame(firestore1, firestore2); } @Test public void testAppDelete() throws IOException { + final String databaseId = "databaseIdInTestAppDelete"; FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setProjectId("mock-project-id") .setFirestoreOptions(FIRESTORE_OPTIONS) .build()); - Firestore firestore = FirestoreClient.getFirestore(app); - assertNotNull(firestore); - DocumentReference document = firestore.collection("collection").document("doc"); + Firestore firestore1 = FirestoreClient.getFirestore(app); + assertNotNull(firestore1); + assertSame(firestore1, FirestoreClient.getFirestore()); + + Firestore firestore2 = FirestoreClient.getFirestore(app, databaseId); + assertNotNull(firestore2); + assertSame(firestore2, FirestoreClient.getFirestore(databaseId)); + + assertNotSame(firestore1, firestore2); + + DocumentReference document = firestore1.collection("collection").document("doc"); app.delete(); - try { - FirestoreClient.getFirestore(app); - fail("No error thrown for deleted app"); - } catch (IllegalStateException expected) { - // ignore - } - - try { - document.get(); - fail("No error thrown for deleted app"); - } catch (IllegalStateException expected) { - // ignore - } - - try { - FirestoreClient.getFirestore(); - fail("No error thrown for deleted app"); - } catch (IllegalStateException expected) { - // ignore - } + + assertThrows(IllegalStateException.class, () -> FirestoreClient.getFirestore(app)); + assertThrows(IllegalStateException.class, () -> document.get()); + assertThrows(IllegalStateException.class, () -> FirestoreClient.getFirestore()); + assertThrows(IllegalStateException.class, () -> FirestoreClient.getFirestore(app, databaseId)); + assertThrows(IllegalStateException.class, () -> FirestoreClient.getFirestore(databaseId)); } }