Skip to content

Commit 88482db

Browse files
Add initial classes for Remote Config API (#477)
- Add initial classes for Remote Config API - Add unit tests
1 parent 47d4347 commit 88482db

File tree

7 files changed

+737
-0
lines changed

7 files changed

+737
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
/**
20+
* An interface for managing Firebase Remote Config templates.
21+
*/
22+
interface FirebaseRemoteConfigClient {
23+
24+
/**
25+
* Gets the current active version of the Remote Config template.
26+
*
27+
* @return A {@link RemoteConfigTemplate}.
28+
* @throws FirebaseRemoteConfigException If an error occurs while getting the template.
29+
*/
30+
RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException;
31+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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.checkArgument;
20+
import static com.google.common.base.Preconditions.checkNotNull;
21+
import static com.google.common.base.Preconditions.checkState;
22+
23+
import com.google.api.client.http.HttpRequestFactory;
24+
import com.google.api.client.http.HttpResponseInterceptor;
25+
import com.google.api.client.json.JsonFactory;
26+
import com.google.common.annotations.VisibleForTesting;
27+
import com.google.common.base.Strings;
28+
import com.google.common.collect.ImmutableMap;
29+
import com.google.firebase.FirebaseApp;
30+
import com.google.firebase.FirebaseException;
31+
import com.google.firebase.ImplFirebaseTrampolines;
32+
import com.google.firebase.IncomingHttpResponse;
33+
import com.google.firebase.internal.AbstractPlatformErrorHandler;
34+
import com.google.firebase.internal.ApiClientUtils;
35+
import com.google.firebase.internal.ErrorHandlingHttpClient;
36+
import com.google.firebase.internal.HttpRequestInfo;
37+
import com.google.firebase.internal.SdkUtils;
38+
import com.google.firebase.remoteconfig.internal.RemoteConfigServiceErrorResponse;
39+
40+
import java.io.IOException;
41+
import java.util.List;
42+
import java.util.Map;
43+
44+
/**
45+
* A helper class for interacting with Firebase Remote Config service.
46+
*/
47+
final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient {
48+
49+
private static final String REMOTE_CONFIG_URL = "https://firebaseremoteconfig.googleapis.com/v1/projects/%s/remoteConfig";
50+
51+
private static final Map<String, String> COMMON_HEADERS =
52+
ImmutableMap.of(
53+
"X-Firebase-Client", "fire-admin-java/" + SdkUtils.getVersion(),
54+
// There is a known issue in which the ETag is not properly returned in cases
55+
// where the request does not specify a compression type. Currently, it is
56+
// required to include the header `Accept-Encoding: gzip` or equivalent in all
57+
// requests. https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates
58+
"Accept-Encoding", "gzip"
59+
);
60+
61+
private final String remoteConfigUrl;
62+
private final HttpRequestFactory requestFactory;
63+
private final JsonFactory jsonFactory;
64+
private final ErrorHandlingHttpClient<FirebaseRemoteConfigException> httpClient;
65+
66+
private FirebaseRemoteConfigClientImpl(Builder builder) {
67+
checkArgument(!Strings.isNullOrEmpty(builder.projectId));
68+
this.remoteConfigUrl = String.format(REMOTE_CONFIG_URL, builder.projectId);
69+
this.requestFactory = checkNotNull(builder.requestFactory);
70+
this.jsonFactory = checkNotNull(builder.jsonFactory);
71+
HttpResponseInterceptor responseInterceptor = builder.responseInterceptor;
72+
RemoteConfigErrorHandler errorHandler = new RemoteConfigErrorHandler(this.jsonFactory);
73+
this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler)
74+
.setInterceptor(responseInterceptor);
75+
}
76+
77+
@VisibleForTesting
78+
String getRemoteConfigUrl() {
79+
return remoteConfigUrl;
80+
}
81+
82+
@VisibleForTesting
83+
HttpRequestFactory getRequestFactory() {
84+
return requestFactory;
85+
}
86+
87+
@VisibleForTesting
88+
JsonFactory getJsonFactory() {
89+
return jsonFactory;
90+
}
91+
92+
@Override
93+
public RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException {
94+
HttpRequestInfo request = HttpRequestInfo.buildGetRequest(remoteConfigUrl)
95+
.addAllHeaders(COMMON_HEADERS);
96+
IncomingHttpResponse response = httpClient.send(request);
97+
RemoteConfigTemplate parsed = httpClient.parse(response, RemoteConfigTemplate.class);
98+
parsed.setETag(getETag(response));
99+
return parsed;
100+
}
101+
102+
private String getETag(IncomingHttpResponse response) {
103+
List<String> etagList = (List<String>) response.getHeaders().get("etag");
104+
checkState(etagList != null && !etagList.isEmpty(),
105+
"ETag header is not available in the server response.");
106+
107+
String etag = etagList.get(0);
108+
checkState(!Strings.isNullOrEmpty(etag),
109+
"ETag header is not available in the server response.");
110+
111+
return etag;
112+
}
113+
114+
static FirebaseRemoteConfigClientImpl fromApp(FirebaseApp app) {
115+
String projectId = ImplFirebaseTrampolines.getProjectId(app);
116+
checkArgument(!Strings.isNullOrEmpty(projectId),
117+
"Project ID is required to access Remote Config service. Use a service "
118+
+ "account credential or set the project ID explicitly via FirebaseOptions. "
119+
+ "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT "
120+
+ "environment variable.");
121+
return FirebaseRemoteConfigClientImpl.builder()
122+
.setProjectId(projectId)
123+
.setRequestFactory(ApiClientUtils.newAuthorizedRequestFactory(app))
124+
.setJsonFactory(app.getOptions().getJsonFactory())
125+
.build();
126+
}
127+
128+
static Builder builder() {
129+
return new Builder();
130+
}
131+
132+
static final class Builder {
133+
134+
private String projectId;
135+
private HttpRequestFactory requestFactory;
136+
private JsonFactory jsonFactory;
137+
private HttpResponseInterceptor responseInterceptor;
138+
139+
private Builder() { }
140+
141+
Builder setProjectId(String projectId) {
142+
this.projectId = projectId;
143+
return this;
144+
}
145+
146+
Builder setRequestFactory(HttpRequestFactory requestFactory) {
147+
this.requestFactory = requestFactory;
148+
return this;
149+
}
150+
151+
Builder setJsonFactory(JsonFactory jsonFactory) {
152+
this.jsonFactory = jsonFactory;
153+
return this;
154+
}
155+
156+
Builder setResponseInterceptor(
157+
HttpResponseInterceptor responseInterceptor) {
158+
this.responseInterceptor = responseInterceptor;
159+
return this;
160+
}
161+
162+
FirebaseRemoteConfigClientImpl build() {
163+
return new FirebaseRemoteConfigClientImpl(this);
164+
}
165+
}
166+
167+
private static class RemoteConfigErrorHandler
168+
extends AbstractPlatformErrorHandler<FirebaseRemoteConfigException> {
169+
170+
private RemoteConfigErrorHandler(JsonFactory jsonFactory) {
171+
super(jsonFactory);
172+
}
173+
174+
@Override
175+
protected FirebaseRemoteConfigException createException(FirebaseException base) {
176+
String response = getResponse(base);
177+
RemoteConfigServiceErrorResponse parsed = safeParse(response);
178+
return FirebaseRemoteConfigException.withRemoteConfigErrorCode(
179+
base, parsed.getRemoteConfigErrorCode());
180+
}
181+
182+
private String getResponse(FirebaseException base) {
183+
if (base.getHttpResponse() == null) {
184+
return null;
185+
}
186+
187+
return base.getHttpResponse().getContent();
188+
}
189+
190+
private RemoteConfigServiceErrorResponse safeParse(String response) {
191+
if (!Strings.isNullOrEmpty(response)) {
192+
try {
193+
return jsonFactory.createJsonParser(response)
194+
.parseAndClose(RemoteConfigServiceErrorResponse.class);
195+
} catch (IOException ignore) {
196+
// Ignore any error that may occur while parsing the error response. The server
197+
// may have responded with a non-json payload.
198+
}
199+
}
200+
201+
return new RemoteConfigServiceErrorResponse();
202+
}
203+
}
204+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 com.google.firebase.ErrorCode;
20+
import com.google.firebase.FirebaseException;
21+
import com.google.firebase.IncomingHttpResponse;
22+
import com.google.firebase.internal.NonNull;
23+
import com.google.firebase.internal.Nullable;
24+
25+
/**
26+
* Generic exception related to Firebase Remote Config. Check the error code and message for more
27+
* details.
28+
*/
29+
public final class FirebaseRemoteConfigException extends FirebaseException {
30+
31+
private final RemoteConfigErrorCode errorCode;
32+
33+
public FirebaseRemoteConfigException(
34+
@NonNull ErrorCode errorCode,
35+
@NonNull String message,
36+
@Nullable Throwable cause,
37+
@Nullable IncomingHttpResponse response,
38+
@Nullable RemoteConfigErrorCode remoteConfigErrorCode) {
39+
super(errorCode, message, cause, response);
40+
this.errorCode = remoteConfigErrorCode;
41+
}
42+
43+
static FirebaseRemoteConfigException withRemoteConfigErrorCode(
44+
FirebaseException base, @Nullable RemoteConfigErrorCode errorCode) {
45+
return new FirebaseRemoteConfigException(
46+
base.getErrorCode(),
47+
base.getMessage(),
48+
base.getCause(),
49+
base.getHttpResponse(),
50+
errorCode);
51+
}
52+
53+
@Nullable
54+
public RemoteConfigErrorCode getRemoteConfigErrorCode() {
55+
return errorCode;
56+
}
57+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
/**
20+
* Error codes that can be raised by the Remote Config APIs.
21+
*/
22+
public enum RemoteConfigErrorCode {
23+
24+
/**
25+
* One or more arguments specified in the request were invalid.
26+
*/
27+
INVALID_ARGUMENT,
28+
29+
/**
30+
* Internal server error.
31+
*/
32+
INTERNAL,
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 com.google.api.client.util.Key;
20+
21+
public final class RemoteConfigTemplate {
22+
23+
@Key("etag")
24+
private String etag;
25+
26+
public String getETag() {
27+
return this.etag;
28+
}
29+
30+
void setETag(String etag) {
31+
this.etag = etag;
32+
}
33+
}

0 commit comments

Comments
 (0)