Skip to content

Commit 1eec932

Browse files
authored
Implement MultiDB Support (#723)
* Implement MultiDB Support * Fix Checkstyle * Revert accidental commit * Remove usage of Java 8 features (lambdas) * Revert "Remove usage of Java 8 features (lambdas)" This reverts commit 24813d0. * Target Java 8 * Fix API description
1 parent b0aff52 commit 1eec932

File tree

2 files changed

+148
-58
lines changed

2 files changed

+148
-58
lines changed

Diff for: src/main/java/com/google/firebase/cloud/FirestoreClient.java

+73-15
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
import com.google.firebase.ImplFirebaseTrampolines;
1212
import com.google.firebase.internal.FirebaseService;
1313
import com.google.firebase.internal.NonNull;
14+
import java.util.Collections;
15+
import java.util.HashMap;
16+
import java.util.Map;
1417
import org.slf4j.Logger;
1518
import org.slf4j.LoggerFactory;
1619

@@ -32,7 +35,7 @@ public class FirestoreClient {
3235

3336
private final Firestore firestore;
3437

35-
private FirestoreClient(FirebaseApp app) {
38+
private FirestoreClient(FirebaseApp app, String databaseId) {
3639
checkNotNull(app, "FirebaseApp must not be null");
3740
String projectId = ImplFirebaseTrampolines.getProjectId(app);
3841
checkArgument(!Strings.isNullOrEmpty(projectId),
@@ -47,13 +50,14 @@ private FirestoreClient(FirebaseApp app) {
4750
.setCredentialsProvider(
4851
FixedCredentialsProvider.create(ImplFirebaseTrampolines.getCredentials(app)))
4952
.setProjectId(projectId)
53+
.setDatabaseId(databaseId)
5054
.build()
5155
.getService();
5256
}
5357

5458
/**
55-
* Returns the Firestore instance associated with the default Firebase app. Returns the same
56-
* instance for all invocations. The Firestore instance and all references obtained from it
59+
* Returns the default Firestore instance associated with the default Firebase app. Returns the
60+
* same instance for all invocations. The Firestore instance and all references obtained from it
5761
* becomes unusable, once the default app is deleted.
5862
*
5963
* @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>
@@ -65,42 +69,96 @@ public static Firestore getFirestore() {
6569
}
6670

6771
/**
68-
* Returns the Firestore instance associated with the specified Firebase app. For a given app,
69-
* always returns the same instance. The Firestore instance and all references obtained from it
70-
* becomes unusable, once the specified app is deleted.
72+
* Returns the default Firestore instance associated with the specified Firebase app. For a given
73+
* app, invocation always returns the same instance. The Firestore instance and all references
74+
* obtained from it becomes unusable, once the specified app is deleted.
7175
*
7276
* @param app A non-null {@link FirebaseApp}.
7377
* @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>
7478
* instance.
7579
*/
7680
@NonNull
7781
public static Firestore getFirestore(FirebaseApp app) {
78-
return getInstance(app).firestore;
82+
return getFirestore(app, ImplFirebaseTrampolines.getFirestoreOptions(app).getDatabaseId());
83+
}
84+
85+
/**
86+
* Returns the Firestore instance associated with the specified Firebase app. Returns the same
87+
* instance for all invocations given the same app and database parameter. The Firestore instance
88+
* and all references obtained from it becomes unusable, once the specified app is deleted.
89+
*
90+
* @param app A non-null {@link FirebaseApp}.
91+
* @param database - The name of database.
92+
* @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>
93+
* instance.
94+
*/
95+
@NonNull
96+
static Firestore getFirestore(FirebaseApp app, String database) {
97+
return getInstance(app, database).firestore;
98+
}
99+
100+
/**
101+
* Returns the Firestore instance associated with the default Firebase app. Returns the same
102+
* instance for all invocations given the same database parameter. The Firestore instance and all
103+
* references obtained from it becomes unusable, once the default app is deleted.
104+
*
105+
* @param database - The name of database.
106+
* @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>
107+
* instance.
108+
*/
109+
@NonNull
110+
static Firestore getFirestore(String database) {
111+
return getFirestore(FirebaseApp.getInstance(), database);
79112
}
80113

81-
private static synchronized FirestoreClient getInstance(FirebaseApp app) {
114+
private static synchronized FirestoreClient getInstance(FirebaseApp app, String database) {
82115
FirestoreClientService service = ImplFirebaseTrampolines.getService(app,
83116
SERVICE_ID, FirestoreClientService.class);
84117
if (service == null) {
85118
service = ImplFirebaseTrampolines.addService(app, new FirestoreClientService(app));
86119
}
87-
return service.getInstance();
120+
return service.getInstance().get(database);
88121
}
89122

90123
private static final String SERVICE_ID = FirestoreClient.class.getName();
91124

92-
private static class FirestoreClientService extends FirebaseService<FirestoreClient> {
125+
private static class FirestoreClientService extends FirebaseService<FirestoreInstances> {
93126

94127
FirestoreClientService(FirebaseApp app) {
95-
super(SERVICE_ID, new FirestoreClient(app));
128+
super(SERVICE_ID, new FirestoreInstances(app));
96129
}
97130

98131
@Override
99132
public void destroy() {
100-
try {
101-
instance.firestore.close();
102-
} catch (Exception e) {
103-
logger.warn("Error while closing the Firestore instance", e);
133+
instance.destroy();
134+
}
135+
}
136+
137+
private static class FirestoreInstances {
138+
139+
private final FirebaseApp app;
140+
141+
private final Map<String, FirestoreClient> clients =
142+
Collections.synchronizedMap(new HashMap<>());
143+
144+
private FirestoreInstances(FirebaseApp app) {
145+
this.app = app;
146+
}
147+
148+
FirestoreClient get(String databaseId) {
149+
return clients.computeIfAbsent(databaseId, id -> new FirestoreClient(app, id));
150+
}
151+
152+
void destroy() {
153+
synchronized (clients) {
154+
for (FirestoreClient client : clients.values()) {
155+
try {
156+
client.firestore.close();
157+
} catch (Exception e) {
158+
logger.warn("Error while closing the Firestore instance", e);
159+
}
160+
}
161+
clients.clear();
104162
}
105163
}
106164
}

Diff for: src/test/java/com/google/firebase/cloud/FirestoreClientTest.java

+75-43
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import static org.junit.Assert.assertEquals;
44
import static org.junit.Assert.assertNotNull;
5+
import static org.junit.Assert.assertNotSame;
56
import static org.junit.Assert.assertSame;
6-
import static org.junit.Assert.assertTrue;
7-
import static org.junit.Assert.fail;
7+
import static org.junit.Assert.assertThrows;
88

99
import com.google.auth.oauth2.GoogleCredentials;
1010
import com.google.cloud.firestore.DocumentReference;
@@ -26,6 +26,7 @@ public class FirestoreClientTest {
2626
// Setting credentials is not required (they get overridden by Admin SDK), but without
2727
// this Firestore logs an ugly warning during tests.
2828
.setCredentials(new MockGoogleCredentials("test-token"))
29+
.setDatabaseId("differedDefaultDatabaseId")
2930
.build();
3031

3132
@After
@@ -35,47 +36,75 @@ public void tearDown() {
3536

3637
@Test
3738
public void testExplicitProjectId() throws IOException {
39+
final String databaseId = "databaseIdInTestExplicitProjectId";
3840
FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
3941
.setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream()))
4042
.setProjectId("explicit-project-id")
4143
.setFirestoreOptions(FIRESTORE_OPTIONS)
4244
.build());
43-
Firestore firestore = FirestoreClient.getFirestore(app);
44-
assertEquals("explicit-project-id", firestore.getOptions().getProjectId());
45+
Firestore firestore1 = FirestoreClient.getFirestore(app);
46+
assertEquals("explicit-project-id", firestore1.getOptions().getProjectId());
47+
assertEquals(FIRESTORE_OPTIONS.getDatabaseId(), firestore1.getOptions().getDatabaseId());
4548

46-
firestore = FirestoreClient.getFirestore();
47-
assertEquals("explicit-project-id", firestore.getOptions().getProjectId());
49+
assertSame(firestore1, FirestoreClient.getFirestore());
50+
51+
Firestore firestore2 = FirestoreClient.getFirestore(app, databaseId);
52+
assertEquals("explicit-project-id", firestore2.getOptions().getProjectId());
53+
assertEquals(databaseId, firestore2.getOptions().getDatabaseId());
54+
55+
assertSame(firestore2, FirestoreClient.getFirestore(databaseId));
56+
57+
assertNotSame(firestore1, firestore2);
4858
}
4959

5060
@Test
5161
public void testServiceAccountProjectId() throws IOException {
62+
final String databaseId = "databaseIdInTestServiceAccountProjectId";
5263
FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
5364
.setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream()))
5465
.setFirestoreOptions(FIRESTORE_OPTIONS)
5566
.build());
56-
Firestore firestore = FirestoreClient.getFirestore(app);
57-
assertEquals("mock-project-id", firestore.getOptions().getProjectId());
67+
Firestore firestore1 = FirestoreClient.getFirestore(app);
68+
assertEquals("mock-project-id", firestore1.getOptions().getProjectId());
69+
assertEquals(FIRESTORE_OPTIONS.getDatabaseId(), firestore1.getOptions().getDatabaseId());
70+
71+
assertSame(firestore1, FirestoreClient.getFirestore());
5872

59-
firestore = FirestoreClient.getFirestore();
60-
assertEquals("mock-project-id", firestore.getOptions().getProjectId());
73+
Firestore firestore2 = FirestoreClient.getFirestore(app, databaseId);
74+
assertEquals("mock-project-id", firestore2.getOptions().getProjectId());
75+
assertEquals(databaseId, firestore2.getOptions().getDatabaseId());
76+
77+
assertSame(firestore2, FirestoreClient.getFirestore(databaseId));
78+
79+
assertNotSame(firestore1, firestore2);
6180
}
6281

6382
@Test
6483
public void testFirestoreOptions() throws IOException {
84+
final String databaseId = "databaseIdInTestFirestoreOptions";
6585
FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
6686
.setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream()))
6787
.setProjectId("explicit-project-id")
6888
.setFirestoreOptions(FIRESTORE_OPTIONS)
6989
.build());
70-
Firestore firestore = FirestoreClient.getFirestore(app);
71-
assertEquals("explicit-project-id", firestore.getOptions().getProjectId());
90+
Firestore firestore1 = FirestoreClient.getFirestore(app);
91+
assertEquals("explicit-project-id", firestore1.getOptions().getProjectId());
92+
assertEquals(FIRESTORE_OPTIONS.getDatabaseId(), firestore1.getOptions().getDatabaseId());
93+
94+
assertSame(firestore1, FirestoreClient.getFirestore());
95+
96+
Firestore firestore2 = FirestoreClient.getFirestore(app, databaseId);
97+
assertEquals("explicit-project-id", firestore2.getOptions().getProjectId());
98+
assertEquals(databaseId, firestore2.getOptions().getDatabaseId());
7299

73-
firestore = FirestoreClient.getFirestore();
74-
assertEquals("explicit-project-id", firestore.getOptions().getProjectId());
100+
assertSame(firestore2, FirestoreClient.getFirestore(databaseId));
101+
102+
assertNotSame(firestore1, firestore2);
75103
}
76104

77105
@Test
78106
public void testFirestoreOptionsOverride() throws IOException {
107+
final String databaseId = "databaseIdInTestFirestoreOptions";
79108
FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
80109
.setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream()))
81110
.setProjectId("explicit-project-id")
@@ -84,48 +113,51 @@ public void testFirestoreOptionsOverride() throws IOException {
84113
.setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream()))
85114
.build())
86115
.build());
87-
Firestore firestore = FirestoreClient.getFirestore(app);
88-
assertEquals("explicit-project-id", firestore.getOptions().getProjectId());
116+
Firestore firestore1 = FirestoreClient.getFirestore(app);
117+
assertEquals("explicit-project-id", firestore1.getOptions().getProjectId());
89118
assertSame(ImplFirebaseTrampolines.getCredentials(app),
90-
firestore.getOptions().getCredentialsProvider().getCredentials());
119+
firestore1.getOptions().getCredentialsProvider().getCredentials());
120+
assertEquals("(default)", firestore1.getOptions().getDatabaseId());
121+
122+
assertSame(firestore1, FirestoreClient.getFirestore());
91123

92-
firestore = FirestoreClient.getFirestore();
93-
assertEquals("explicit-project-id", firestore.getOptions().getProjectId());
124+
Firestore firestore2 = FirestoreClient.getFirestore(app, databaseId);
125+
assertEquals("explicit-project-id", firestore2.getOptions().getProjectId());
94126
assertSame(ImplFirebaseTrampolines.getCredentials(app),
95-
firestore.getOptions().getCredentialsProvider().getCredentials());
127+
firestore2.getOptions().getCredentialsProvider().getCredentials());
128+
assertEquals(databaseId, firestore2.getOptions().getDatabaseId());
129+
130+
assertSame(firestore2, FirestoreClient.getFirestore(databaseId));
131+
132+
assertNotSame(firestore1, firestore2);
96133
}
97134

98135
@Test
99136
public void testAppDelete() throws IOException {
137+
final String databaseId = "databaseIdInTestAppDelete";
100138
FirebaseApp app = FirebaseApp.initializeApp(FirebaseOptions.builder()
101139
.setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream()))
102140
.setProjectId("mock-project-id")
103141
.setFirestoreOptions(FIRESTORE_OPTIONS)
104142
.build());
105143

106-
Firestore firestore = FirestoreClient.getFirestore(app);
107-
assertNotNull(firestore);
108-
DocumentReference document = firestore.collection("collection").document("doc");
144+
Firestore firestore1 = FirestoreClient.getFirestore(app);
145+
assertNotNull(firestore1);
146+
assertSame(firestore1, FirestoreClient.getFirestore());
147+
148+
Firestore firestore2 = FirestoreClient.getFirestore(app, databaseId);
149+
assertNotNull(firestore2);
150+
assertSame(firestore2, FirestoreClient.getFirestore(databaseId));
151+
152+
assertNotSame(firestore1, firestore2);
153+
154+
DocumentReference document = firestore1.collection("collection").document("doc");
109155
app.delete();
110-
try {
111-
FirestoreClient.getFirestore(app);
112-
fail("No error thrown for deleted app");
113-
} catch (IllegalStateException expected) {
114-
// ignore
115-
}
116-
117-
try {
118-
document.get();
119-
fail("No error thrown for deleted app");
120-
} catch (IllegalStateException expected) {
121-
// ignore
122-
}
123-
124-
try {
125-
FirestoreClient.getFirestore();
126-
fail("No error thrown for deleted app");
127-
} catch (IllegalStateException expected) {
128-
// ignore
129-
}
156+
157+
assertThrows(IllegalStateException.class, () -> FirestoreClient.getFirestore(app));
158+
assertThrows(IllegalStateException.class, () -> document.get());
159+
assertThrows(IllegalStateException.class, () -> FirestoreClient.getFirestore());
160+
assertThrows(IllegalStateException.class, () -> FirestoreClient.getFirestore(app, databaseId));
161+
assertThrows(IllegalStateException.class, () -> FirestoreClient.getFirestore(databaseId));
130162
}
131163
}

0 commit comments

Comments
 (0)