Skip to content

Commit be1d9a4

Browse files
committed
Add the entry point for Remote Config
1 parent 88482db commit be1d9a4

File tree

6 files changed

+380
-3
lines changed

6 files changed

+380
-3
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.remoteconfig;
18+
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
21+
import com.google.api.core.ApiFuture;
22+
import com.google.common.annotations.VisibleForTesting;
23+
import com.google.common.base.Supplier;
24+
import com.google.common.base.Suppliers;
25+
import com.google.firebase.FirebaseApp;
26+
import com.google.firebase.ImplFirebaseTrampolines;
27+
import com.google.firebase.internal.CallableOperation;
28+
import com.google.firebase.internal.FirebaseService;
29+
30+
/**
31+
* This class is the entry point for all server-side Firebase Remote Config actions.
32+
*
33+
* <p>You can get an instance of FirebaseRemoteConfig via {@link #getInstance(FirebaseApp)}, and
34+
* then use it to manage Remote Config templates.
35+
*/
36+
public final class FirebaseRemoteConfig {
37+
38+
private static final String SERVICE_ID = FirebaseRemoteConfig.class.getName();
39+
private final FirebaseApp app;
40+
private final Supplier<? extends FirebaseRemoteConfigClient> remoteConfigClient;
41+
42+
private FirebaseRemoteConfig(Builder builder) {
43+
this.app = checkNotNull(builder.firebaseApp);
44+
this.remoteConfigClient = Suppliers.memoize(builder.remoteConfigClient);
45+
}
46+
47+
/**
48+
* Gets the {@link FirebaseRemoteConfig} instance for the default {@link FirebaseApp}.
49+
*
50+
* @return The {@link FirebaseRemoteConfig} instance for the default {@link FirebaseApp}.
51+
*/
52+
public static FirebaseRemoteConfig getInstance() {
53+
return getInstance(FirebaseApp.getInstance());
54+
}
55+
56+
/**
57+
* Gets the {@link FirebaseRemoteConfig} instance for the specified {@link FirebaseApp}.
58+
*
59+
* @return The {@link FirebaseRemoteConfig} instance for the specified {@link FirebaseApp}.
60+
*/
61+
public static synchronized FirebaseRemoteConfig getInstance(FirebaseApp app) {
62+
FirebaseRemoteConfigService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID,
63+
FirebaseRemoteConfigService.class);
64+
if (service == null) {
65+
service = ImplFirebaseTrampolines.addService(app, new FirebaseRemoteConfigService(app));
66+
}
67+
return service.getInstance();
68+
}
69+
70+
/**
71+
* Gets the current active version of the Remote Config template.
72+
*
73+
* @return A {@link RemoteConfigTemplate}.
74+
* @throws FirebaseRemoteConfigException If an error occurs while getting the template.
75+
*/
76+
public RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException {
77+
return getTemplateOp().call();
78+
}
79+
80+
/**
81+
* Similar to {@link #getTemplate()} but performs the operation asynchronously.
82+
*
83+
* @return An {@code ApiFuture} that will complete with a {@link RemoteConfigTemplate} when
84+
* the template is available.
85+
*/
86+
public ApiFuture<RemoteConfigTemplate> getTemplateAsync() {
87+
return getTemplateOp().callAsync(app);
88+
}
89+
90+
private CallableOperation<RemoteConfigTemplate, FirebaseRemoteConfigException> getTemplateOp() {
91+
final FirebaseRemoteConfigClient remoteConfigClient = getRemoteConfigClient();
92+
return new CallableOperation<RemoteConfigTemplate, FirebaseRemoteConfigException>() {
93+
@Override
94+
protected RemoteConfigTemplate execute() throws FirebaseRemoteConfigException {
95+
return remoteConfigClient.getTemplate();
96+
}
97+
};
98+
}
99+
100+
@VisibleForTesting
101+
FirebaseRemoteConfigClient getRemoteConfigClient() {
102+
return remoteConfigClient.get();
103+
}
104+
105+
private static FirebaseRemoteConfig fromApp(final FirebaseApp app) {
106+
return FirebaseRemoteConfig.builder()
107+
.setFirebaseApp(app)
108+
.setRemoteConfigClient(new Supplier<FirebaseRemoteConfigClient>() {
109+
@Override
110+
public FirebaseRemoteConfigClient get() {
111+
return FirebaseRemoteConfigClientImpl.fromApp(app);
112+
}
113+
})
114+
.build();
115+
}
116+
117+
static Builder builder() {
118+
return new Builder();
119+
}
120+
121+
private static class FirebaseRemoteConfigService extends FirebaseService<FirebaseRemoteConfig> {
122+
123+
FirebaseRemoteConfigService(FirebaseApp app) {
124+
super(SERVICE_ID, FirebaseRemoteConfig.fromApp(app));
125+
}
126+
127+
@Override
128+
public void destroy() {
129+
// NOTE: We don't explicitly tear down anything here, but public methods of
130+
// FirebaseRemoteConfig will now fail because calls to getOptions() and getToken()
131+
// will hit FirebaseApp, which will throw once the app is deleted.
132+
}
133+
}
134+
135+
static class Builder {
136+
137+
private FirebaseApp firebaseApp;
138+
private Supplier<? extends FirebaseRemoteConfigClient> remoteConfigClient;
139+
140+
private Builder() { }
141+
142+
FirebaseRemoteConfig.Builder setFirebaseApp(FirebaseApp firebaseApp) {
143+
this.firebaseApp = firebaseApp;
144+
return this;
145+
}
146+
147+
FirebaseRemoteConfig.Builder setRemoteConfigClient(
148+
Supplier<? extends FirebaseRemoteConfigClient> remoteConfigClient) {
149+
this.remoteConfigClient = remoteConfigClient;
150+
return this;
151+
}
152+
153+
FirebaseRemoteConfig build() {
154+
return new FirebaseRemoteConfig(this);
155+
}
156+
}
157+
}

src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,7 @@ public RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException {
9595
.addAllHeaders(COMMON_HEADERS);
9696
IncomingHttpResponse response = httpClient.send(request);
9797
RemoteConfigTemplate parsed = httpClient.parse(response, RemoteConfigTemplate.class);
98-
parsed.setETag(getETag(response));
99-
return parsed;
98+
return parsed.setETag(getETag(response));
10099
}
101100

102101
private String getETag(IncomingHttpResponse response) {

src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.firebase.remoteconfig;
1818

19+
import com.google.common.annotations.VisibleForTesting;
1920
import com.google.firebase.ErrorCode;
2021
import com.google.firebase.FirebaseException;
2122
import com.google.firebase.IncomingHttpResponse;
@@ -30,6 +31,11 @@ public final class FirebaseRemoteConfigException extends FirebaseException {
3031

3132
private final RemoteConfigErrorCode errorCode;
3233

34+
@VisibleForTesting
35+
FirebaseRemoteConfigException(@NonNull ErrorCode code, @NonNull String message) {
36+
this(code, message, null, null, null);
37+
}
38+
3339
public FirebaseRemoteConfigException(
3440
@NonNull ErrorCode errorCode,
3541
@NonNull String message,

src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public String getETag() {
2727
return this.etag;
2828
}
2929

30-
void setETag(String etag) {
30+
RemoteConfigTemplate setETag(String etag) {
3131
this.etag = etag;
32+
return this;
3233
}
3334
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.remoteconfig;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertSame;
21+
import static org.junit.Assert.assertTrue;
22+
import static org.junit.Assert.fail;
23+
24+
import com.google.common.base.Supplier;
25+
import com.google.common.base.Suppliers;
26+
import com.google.firebase.ErrorCode;
27+
import com.google.firebase.FirebaseApp;
28+
import com.google.firebase.FirebaseOptions;
29+
import com.google.firebase.TestOnlyImplFirebaseTrampolines;
30+
import com.google.firebase.auth.MockGoogleCredentials;
31+
import java.util.concurrent.ExecutionException;
32+
import org.junit.After;
33+
import org.junit.Test;
34+
35+
public class FirebaseRemoteConfigTest {
36+
private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder()
37+
.setCredentials(new MockGoogleCredentials("test-token"))
38+
.setProjectId("test-project")
39+
.build();
40+
private static final FirebaseRemoteConfigException TEST_EXCEPTION =
41+
new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message");
42+
43+
@After
44+
public void tearDown() {
45+
TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
46+
}
47+
48+
@Test
49+
public void testGetInstance() {
50+
FirebaseApp.initializeApp(TEST_OPTIONS);
51+
52+
FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance();
53+
54+
assertSame(remoteConfig, FirebaseRemoteConfig.getInstance());
55+
}
56+
57+
@Test
58+
public void testGetInstanceByApp() {
59+
FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app");
60+
61+
FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance(app);
62+
63+
assertSame(remoteConfig, FirebaseRemoteConfig.getInstance(app));
64+
}
65+
66+
@Test
67+
public void testDefaultRemoteConfigClient() {
68+
FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app");
69+
FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance(app);
70+
71+
FirebaseRemoteConfigClient client = remoteConfig.getRemoteConfigClient();
72+
73+
assertTrue(client instanceof FirebaseRemoteConfigClientImpl);
74+
assertSame(client, remoteConfig.getRemoteConfigClient());
75+
String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig";
76+
assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getRemoteConfigUrl());
77+
}
78+
79+
@Test
80+
public void testPostDeleteApp() {
81+
FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app");
82+
83+
app.delete();
84+
85+
try {
86+
FirebaseRemoteConfig.getInstance(app);
87+
fail("No error thrown for deleted app");
88+
} catch (IllegalStateException expected) {
89+
// expected
90+
}
91+
}
92+
93+
@Test
94+
public void testRemoteConfigClientWithoutProjectId() {
95+
FirebaseOptions options = FirebaseOptions.builder()
96+
.setCredentials(new MockGoogleCredentials("test-token"))
97+
.build();
98+
FirebaseApp.initializeApp(options);
99+
FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance();
100+
101+
try {
102+
remoteConfig.getRemoteConfigClient();
103+
fail("No error thrown for missing project ID");
104+
} catch (IllegalArgumentException expected) {
105+
String message = "Project ID is required to access Remote Config service. Use a service "
106+
+ "account credential or set the project ID explicitly via FirebaseOptions. "
107+
+ "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT "
108+
+ "environment variable.";
109+
assertEquals(message, expected.getMessage());
110+
}
111+
}
112+
113+
private static final String TEST_ETAG = "etag-123456789012-1";
114+
115+
@Test
116+
public void testGetTemplate() throws FirebaseRemoteConfigException {
117+
MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
118+
new RemoteConfigTemplate().setETag(TEST_ETAG));
119+
FirebaseRemoteConfig remoteConfig = getRemoteConfig(Suppliers.ofInstance(client));
120+
121+
RemoteConfigTemplate template = remoteConfig.getTemplate();
122+
123+
assertEquals(TEST_ETAG, template.getETag());
124+
}
125+
126+
@Test
127+
public void testGetTemplateFailure() {
128+
MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION);
129+
FirebaseRemoteConfig remoteConfig = getRemoteConfig(Suppliers.ofInstance(client));
130+
131+
try {
132+
remoteConfig.getTemplate();
133+
} catch (FirebaseRemoteConfigException e) {
134+
assertSame(TEST_EXCEPTION, e);
135+
}
136+
}
137+
138+
@Test
139+
public void testGetTemplateAsync() throws Exception {
140+
MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
141+
new RemoteConfigTemplate().setETag(TEST_ETAG));
142+
FirebaseRemoteConfig remoteConfig = getRemoteConfig(Suppliers.ofInstance(client));
143+
144+
RemoteConfigTemplate template = remoteConfig.getTemplateAsync().get();
145+
146+
assertEquals(TEST_ETAG, template.getETag());
147+
}
148+
149+
@Test
150+
public void testGetTemplateAsyncFailure() throws InterruptedException {
151+
MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION);
152+
FirebaseRemoteConfig remoteConfig = getRemoteConfig(Suppliers.ofInstance(client));
153+
154+
try {
155+
remoteConfig.getTemplateAsync().get();
156+
} catch (ExecutionException e) {
157+
assertSame(TEST_EXCEPTION, e.getCause());
158+
}
159+
}
160+
161+
private FirebaseRemoteConfig getRemoteConfig(
162+
Supplier<? extends FirebaseRemoteConfigClient> supplier) {
163+
FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS);
164+
return FirebaseRemoteConfig.builder()
165+
.setFirebaseApp(app)
166+
.setRemoteConfigClient(supplier)
167+
.build();
168+
}
169+
}

0 commit comments

Comments
 (0)