Skip to content

Implementation for Fetching and Caching Server Side Remote Config #1107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 23, 2025
64 changes: 64 additions & 0 deletions src/main/java/com/google/firebase/remoteconfig/AndCondition.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.remoteconfig;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.collect.ImmutableList;
import com.google.firebase.internal.NonNull;
import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.AndConditionResponse;
import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OneOfConditionResponse;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

final class AndCondition {
private final ImmutableList<OneOfCondition> conditions;

AndCondition(@NonNull List<OneOfCondition> conditions) {
checkNotNull(conditions, "List of conditions for AND operation must not be null.");
checkArgument(!conditions.isEmpty(),
"List of conditions for AND operation must not be empty.");
this.conditions = ImmutableList.copyOf(conditions);
}

AndCondition(AndConditionResponse andConditionResponse) {
List<OneOfConditionResponse> conditionList = andConditionResponse.getConditions();
checkNotNull(conditionList, "List of conditions for AND operation must not be null.");
checkArgument(!conditionList.isEmpty(),
"List of conditions for AND operation must not be empty");
this.conditions = conditionList.stream()
.map(OneOfCondition::new)
.collect(ImmutableList.toImmutableList());
}

@NonNull
List<OneOfCondition> getConditions() {
return new ArrayList<>(conditions);
}

AndConditionResponse toAndConditionResponse() {
return new AndConditionResponse()
.setConditions(this.conditions.stream()
.map(OneOfCondition::toOneOfConditionResponse)
.collect(Collectors.toList()));
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,73 @@ protected Template execute() throws FirebaseRemoteConfigException {
};
}

/**
* Alternative to {@link #getServerTemplate} where developers can initialize with a pre-cached
* template or config.
*/
public ServerTemplateImpl.Builder serverTemplateBuilder() {
return new ServerTemplateImpl.Builder(this.remoteConfigClient);
}

/**
* Initializes a template instance and loads the latest template data.
*
* @param defaultConfig Default parameter values to use if a getter references a parameter not
* found in the template.
* @return A {@link Template} instance with the latest template data.
*/
public ServerTemplate getServerTemplate(KeysAndValues defaultConfig)
throws FirebaseRemoteConfigException {
return getServerTemplateOp(defaultConfig).call();
}

/**
* Initializes a template instance without any defaults and loads the latest template data.
*
* @return A {@link Template} instance with the latest template data.
*/
public ServerTemplate getServerTemplate() throws FirebaseRemoteConfigException {
return getServerTemplate(null);
}

/**
* Initializes a template instance and asynchronously loads the latest template data.
*
* @param defaultConfig Default parameter values to use if a getter references a parameter not
* found in the template.
* @return A {@link Template} instance with the latest template data.
*/
public ApiFuture<ServerTemplate> getServerTemplateAsync(KeysAndValues defaultConfig) {
return getServerTemplateOp(defaultConfig).callAsync(app);
}

/**
* Initializes a template instance without any defaults and asynchronously loads the latest
* template data.
*
* @return A {@link Template} instance with the latest template data.
*/
public ApiFuture<ServerTemplate> getServerTemplateAsync() {
return getServerTemplateAsync(null);
}

private CallableOperation<ServerTemplate, FirebaseRemoteConfigException> getServerTemplateOp(
KeysAndValues defaultConfig) {
return new CallableOperation<ServerTemplate, FirebaseRemoteConfigException>() {
@Override
protected ServerTemplate execute() throws FirebaseRemoteConfigException {
String serverTemplateData = remoteConfigClient.getServerTemplate();
ServerTemplate template =
serverTemplateBuilder()
.defaultConfig(defaultConfig)
.cachedTemplate(serverTemplateData)
.build();

return template;
}
};
}

/**
* Gets the requested version of the of the Remote Config template.
*
Expand Down Expand Up @@ -413,3 +480,4 @@ public void destroy() {
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ Template publishTemplate(Template template, boolean validateOnly,

ListVersionsResponse listVersions(
ListVersionsOptions options) throws FirebaseRemoteConfigException;

String getServerTemplate() throws FirebaseRemoteConfigException;
}

Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import com.google.firebase.internal.NonNull;
import com.google.firebase.internal.SdkUtils;
import com.google.firebase.remoteconfig.internal.RemoteConfigServiceErrorResponse;
import com.google.firebase.remoteconfig.internal.ServerTemplateResponse;
import com.google.firebase.remoteconfig.internal.TemplateResponse;

import java.io.IOException;
Expand All @@ -51,6 +52,9 @@ final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient

private static final String REMOTE_CONFIG_URL = "https://firebaseremoteconfig.googleapis.com/v1/projects/%s/remoteConfig";

private static final String SERVER_REMOTE_CONFIG_URL =
"https://firebaseremoteconfig.googleapis.com/v1/projects/%s/namespaces/firebase-server/serverRemoteConfig";

private static final Map<String, String> COMMON_HEADERS =
ImmutableMap.of(
"X-Firebase-Client", "fire-admin-java/" + SdkUtils.getVersion(),
Expand All @@ -62,13 +66,15 @@ final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient
);

private final String remoteConfigUrl;
private final String serverRemoteConfigUrl;
private final HttpRequestFactory requestFactory;
private final JsonFactory jsonFactory;
private final ErrorHandlingHttpClient<FirebaseRemoteConfigException> httpClient;

private FirebaseRemoteConfigClientImpl(Builder builder) {
checkArgument(!Strings.isNullOrEmpty(builder.projectId));
this.remoteConfigUrl = String.format(REMOTE_CONFIG_URL, builder.projectId);
this.serverRemoteConfigUrl = String.format(SERVER_REMOTE_CONFIG_URL, builder.projectId);
this.requestFactory = checkNotNull(builder.requestFactory);
this.jsonFactory = checkNotNull(builder.jsonFactory);
HttpResponseInterceptor responseInterceptor = builder.responseInterceptor;
Expand All @@ -82,6 +88,11 @@ String getRemoteConfigUrl() {
return remoteConfigUrl;
}

@VisibleForTesting
String getServerRemoteConfigUrl() {
return serverRemoteConfigUrl;
}

@VisibleForTesting
HttpRequestFactory getRequestFactory() {
return requestFactory;
Expand All @@ -102,6 +113,18 @@ public Template getTemplate() throws FirebaseRemoteConfigException {
return template.setETag(getETag(response));
}

@Override
public String getServerTemplate() throws FirebaseRemoteConfigException {
HttpRequestInfo request =
HttpRequestInfo.buildGetRequest(serverRemoteConfigUrl).addAllHeaders(COMMON_HEADERS);
IncomingHttpResponse response = httpClient.send(request);
ServerTemplateResponse templateResponse = httpClient.parse(response,
ServerTemplateResponse.class);
ServerTemplateData serverTemplateData = new ServerTemplateData(templateResponse);
serverTemplateData.setETag(getETag(response));
return serverTemplateData.toJSON();
}

@Override
public Template getTemplateAtVersion(
@NonNull String versionNumber) throws FirebaseRemoteConfigException {
Expand Down Expand Up @@ -267,3 +290,4 @@ private RemoteConfigServiceErrorResponse safeParse(String response) {
}
}
}

136 changes: 136 additions & 0 deletions src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@

/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.remoteconfig;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.firebase.internal.NonNull;

import java.util.HashMap;
import java.util.Map;

/**
* Represents data stored in context passed to server-side Remote Config.
*/
public class KeysAndValues {
final ImmutableMap<String, String> keysAndValues;

private KeysAndValues(@NonNull Builder builder) {
keysAndValues = ImmutableMap.<String, String>builder().putAll(builder.keysAndValues).build();
}

/**
* Checks whether a key is present in the context.
*
* @param key The key for data stored in context.
* @return Boolean representing whether the key passed is present in context.
*/
public boolean containsKey(String key) {
return keysAndValues.containsKey(key);
}

/**
* Gets the value of the data stored in context.
*
* @param key The key for data stored in context.
* @return Value assigned to the key in context.
*/
public String get(String key) {
return keysAndValues.get(key);
}

/**
* Builder class for KeysAndValues using which values will be assigned to
* private variables.
*/
public static class Builder {
// Holds the converted pairs of custom keys and values.
private final Map<String, String> keysAndValues = new HashMap<>();

/**
* Adds a context data with string value.
*
* @param key Identifies the value in context.
* @param value Value assigned to the context.
* @return Reference to class itself so that more data can be added.
*/
@NonNull
public Builder put(@NonNull String key, @NonNull String value) {
checkArgument(!Strings.isNullOrEmpty(key), "Context key must not be null or empty.");
checkArgument(!Strings.isNullOrEmpty(value), "Context key must not be null or empty.");
keysAndValues.put(key, value);
return this;
}

/**
* Adds a context data with boolean value.
*
* @param key Identifies the value in context.
* @param value Value assigned to the context.
* @return Reference to class itself so that more data can be added.
*/
@NonNull
public Builder put(@NonNull String key, boolean value) {
checkArgument(!Strings.isNullOrEmpty(key), "Context key must not be null or empty.");
keysAndValues.put(key, Boolean.toString(value));
return this;
}

/**
* Adds a context data with double value.
*
* @param key Identifies the value in context.
* @param value Value assigned to the context.
* @return Reference to class itself so that more data can be added.
*/
@NonNull
public Builder put(@NonNull String key, double value) {
checkArgument(!Strings.isNullOrEmpty(key), "Context key must not be null or empty.");
keysAndValues.put(key, Double.toString(value));
return this;
}

/**
* Adds a context data with long value.
*
* @param key Identifies the value in context.
* @param value Value assigned to the context.
* @return Reference to class itself so that more data can be added.
*/
@NonNull
public Builder put(@NonNull String key, long value) {
checkArgument(!Strings.isNullOrEmpty(key), "Context key must not be null or empty.");
keysAndValues.put(key, Long.toString(value));
return this;
}

/**
* Creates an instance of KeysAndValues with the values assigned through
* builder.
*
* @return instance of KeysAndValues
*/
@NonNull
public KeysAndValues build() {
return new KeysAndValues(this);
}
}
}

Loading