Skip to content
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