Skip to content

Commit 498018e

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

File tree

7 files changed

+793
-0
lines changed

7 files changed

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