Skip to content

Commit c13d889

Browse files
committed
Add initial classes for Remote Config API
Add unit tests
1 parent 47d4347 commit c13d889

File tree

7 files changed

+835
-0
lines changed

7 files changed

+835
-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: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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+
22+
import com.google.api.client.http.HttpRequestFactory;
23+
import com.google.api.client.http.HttpResponseInterceptor;
24+
import com.google.api.client.json.JsonFactory;
25+
import com.google.common.annotations.VisibleForTesting;
26+
import com.google.common.base.Strings;
27+
import com.google.common.collect.ImmutableMap;
28+
import com.google.firebase.ErrorCode;
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.ArrayList;
42+
import java.util.List;
43+
import java.util.Map;
44+
45+
/**
46+
* A helper class for interacting with Firebase Remote Config service.
47+
*/
48+
final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient {
49+
50+
private static final String RC_URL = "https://firebaseremoteconfig.googleapis.com/v1/projects/%s/remoteConfig";
51+
52+
private static final Map<String, String> COMMON_HEADERS =
53+
ImmutableMap.of(
54+
"X-GOOG-API-FORMAT-VERSION", "2",
55+
"X-Firebase-Client", "fire-admin-java/" + SdkUtils.getVersion(),
56+
"Accept-Encoding", "gzip"
57+
);
58+
59+
private final String rcSendUrl;
60+
private final HttpRequestFactory requestFactory;
61+
private final HttpRequestFactory childRequestFactory;
62+
private final JsonFactory jsonFactory;
63+
private final ErrorHandlingHttpClient<FirebaseRemoteConfigException> httpClient;
64+
65+
private FirebaseRemoteConfigClientImpl(Builder builder) {
66+
checkArgument(!Strings.isNullOrEmpty(builder.projectId));
67+
this.rcSendUrl = String.format(RC_URL, builder.projectId);
68+
this.requestFactory = checkNotNull(builder.requestFactory);
69+
this.childRequestFactory = checkNotNull(builder.childRequestFactory);
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 getRcSendUrl() {
79+
return rcSendUrl;
80+
}
81+
82+
@VisibleForTesting
83+
HttpRequestFactory getRequestFactory() {
84+
return requestFactory;
85+
}
86+
87+
@VisibleForTesting
88+
HttpRequestFactory getChildRequestFactory() {
89+
return childRequestFactory;
90+
}
91+
92+
@VisibleForTesting
93+
JsonFactory getJsonFactory() {
94+
return jsonFactory;
95+
}
96+
97+
@Override
98+
public RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException {
99+
HttpRequestInfo request = HttpRequestInfo.buildGetRequest(rcSendUrl)
100+
.addAllHeaders(COMMON_HEADERS);
101+
IncomingHttpResponse response = httpClient.send(request);
102+
RemoteConfigTemplate parsed = httpClient.parse(response, RemoteConfigTemplate.class);
103+
104+
List<String> etagList = (List<String>) response.getHeaders().get("etag");
105+
106+
if (etagList == null || etagList.isEmpty()) {
107+
throw new FirebaseRemoteConfigException(
108+
ErrorCode.INTERNAL,
109+
"ETag header is not available in the server response.", null, null,
110+
RemoteConfigErrorCode.INTERNAL);
111+
}
112+
113+
String etag = etagList.get(0);
114+
115+
if (Strings.isNullOrEmpty(etag)) {
116+
throw new FirebaseRemoteConfigException(
117+
ErrorCode.INTERNAL,
118+
"ETag header is not available in the server response.", null, null,
119+
RemoteConfigErrorCode.INTERNAL);
120+
}
121+
122+
parsed.setETag(etag);
123+
return parsed;
124+
}
125+
126+
static FirebaseRemoteConfigClientImpl fromApp(FirebaseApp app) {
127+
String projectId = ImplFirebaseTrampolines.getProjectId(app);
128+
checkArgument(!Strings.isNullOrEmpty(projectId),
129+
"Project ID is required to access Remote Config service. Use a service "
130+
+ "account credential or set the project ID explicitly via FirebaseOptions. "
131+
+ "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT "
132+
+ "environment variable.");
133+
return FirebaseRemoteConfigClientImpl.builder()
134+
.setProjectId(projectId)
135+
.setRequestFactory(ApiClientUtils.newAuthorizedRequestFactory(app))
136+
.setChildRequestFactory(ApiClientUtils.newUnauthorizedRequestFactory(app))
137+
.setJsonFactory(app.getOptions().getJsonFactory())
138+
.build();
139+
}
140+
141+
static Builder builder() {
142+
return new Builder();
143+
}
144+
145+
static final class Builder {
146+
147+
private String projectId;
148+
private HttpRequestFactory requestFactory;
149+
private HttpRequestFactory childRequestFactory;
150+
private JsonFactory jsonFactory;
151+
private HttpResponseInterceptor responseInterceptor;
152+
153+
private Builder() { }
154+
155+
Builder setProjectId(String projectId) {
156+
this.projectId = projectId;
157+
return this;
158+
}
159+
160+
Builder setRequestFactory(HttpRequestFactory requestFactory) {
161+
this.requestFactory = requestFactory;
162+
return this;
163+
}
164+
165+
Builder setChildRequestFactory(
166+
HttpRequestFactory childRequestFactory) {
167+
this.childRequestFactory = childRequestFactory;
168+
return this;
169+
}
170+
171+
Builder setJsonFactory(JsonFactory jsonFactory) {
172+
this.jsonFactory = jsonFactory;
173+
return this;
174+
}
175+
176+
Builder setResponseInterceptor(
177+
HttpResponseInterceptor responseInterceptor) {
178+
this.responseInterceptor = responseInterceptor;
179+
return this;
180+
}
181+
182+
FirebaseRemoteConfigClientImpl build() {
183+
return new FirebaseRemoteConfigClientImpl(this);
184+
}
185+
}
186+
187+
private static class RemoteConfigErrorHandler
188+
extends AbstractPlatformErrorHandler<FirebaseRemoteConfigException> {
189+
190+
private RemoteConfigErrorHandler(JsonFactory jsonFactory) {
191+
super(jsonFactory);
192+
}
193+
194+
@Override
195+
protected FirebaseRemoteConfigException createException(FirebaseException base) {
196+
String response = getResponse(base);
197+
RemoteConfigServiceErrorResponse parsed = safeParse(response);
198+
return FirebaseRemoteConfigException.withRemoteConfigErrorCode(
199+
base, parsed.getRemoteConfigErrorCode());
200+
}
201+
202+
private String getResponse(FirebaseException base) {
203+
if (base.getHttpResponse() == null) {
204+
return null;
205+
}
206+
207+
return base.getHttpResponse().getContent();
208+
}
209+
210+
private RemoteConfigServiceErrorResponse safeParse(String response) {
211+
if (!Strings.isNullOrEmpty(response)) {
212+
try {
213+
return jsonFactory.createJsonParser(response)
214+
.parseAndClose(RemoteConfigServiceErrorResponse.class);
215+
} catch (IOException ignore) {
216+
// Ignore any error that may occur while parsing the error response. The server
217+
// may have responded with a non-json payload.
218+
}
219+
}
220+
221+
return new RemoteConfigServiceErrorResponse();
222+
}
223+
}
224+
}
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 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)