Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement MultiDB Support #723

Merged
merged 8 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

Expand Down
86 changes: 72 additions & 14 deletions src/main/java/com/google/firebase/cloud/FirestoreClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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),
Expand All @@ -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 <a href="https://googlecloudplatform.github.io/google-cloud-java/google-cloud-clients/apidocs/com/google/cloud/firestore/Firestore.html">{@code Firestore}</a>
Expand All @@ -64,43 +68,97 @@ public static Firestore getFirestore() {
return getFirestore(FirebaseApp.getInstance());
}

/**
* Returns the default 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.
*
* @param app A non-null {@link FirebaseApp}.
* @return A non-null <a href="https://googlecloudplatform.github.io/google-cloud-java/google-cloud-clients/apidocs/com/google/cloud/firestore/Firestore.html">{@code Firestore}</a>
* instance.
*/
@NonNull
public static Firestore getFirestore(FirebaseApp app) {
return getFirestore(app, ImplFirebaseTrampolines.getFirestoreOptions(app).getDatabaseId());
}

/**
* 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.
*
* @param app A non-null {@link FirebaseApp}.
* @param app A non-null {@link FirebaseApp}.
* @param database - The name of database.
* @return A non-null <a href="https://googlecloudplatform.github.io/google-cloud-java/google-cloud-clients/apidocs/com/google/cloud/firestore/Firestore.html">{@code Firestore}</a>
* instance.
*/
@NonNull
public static Firestore getFirestore(FirebaseApp app) {
return getInstance(app).firestore;
public static Firestore getFirestore(FirebaseApp app, String database) {
return getInstance(app, database).firestore;
}

private static synchronized FirestoreClient getInstance(FirebaseApp app) {
/**
* 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all invocations given the same database parameter?

* becomes unusable, once the default app is deleted.
*
* @param database - The name of database.
* @return A non-null <a href="https://googlecloudplatform.github.io/google-cloud-java/google-cloud-clients/apidocs/com/google/cloud/firestore/Firestore.html">{@code Firestore}</a>
* instance.
*/
@NonNull
public static Firestore getFirestore(String database) {
return getFirestore(FirebaseApp.getInstance(), database);
}

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<FirestoreClient> {
private static class FirestoreClientService extends FirebaseService<FirestoreInstances> {

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<String, FirestoreClient> 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();
}
}
}
Expand Down
118 changes: 75 additions & 43 deletions src/test/java/com/google/firebase/cloud/FirestoreClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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")
Expand All @@ -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));
}
}