diff --git a/src/main/java/com/google/firebase/remoteconfig/AndCondition.java b/src/main/java/com/google/firebase/remoteconfig/AndCondition.java new file mode 100644 index 000000000..3be10560a --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/AndCondition.java @@ -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 conditions; + + AndCondition(@NonNull List 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 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 getConditions() { + return new ArrayList<>(conditions); + } + + AndConditionResponse toAndConditionResponse() { + return new AndConditionResponse() + .setConditions(this.conditions.stream() + .map(OneOfCondition::toOneOfConditionResponse) + .collect(Collectors.toList())); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java index 41a0afbe4..e3edce4e4 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java @@ -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 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 getServerTemplateAsync() { + return getServerTemplateAsync(null); + } + + private CallableOperation getServerTemplateOp( + KeysAndValues defaultConfig) { + return new CallableOperation() { + @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. * @@ -413,3 +480,4 @@ public void destroy() { } } } + diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java index 9fdb596d6..2143d07d1 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClient.java @@ -40,4 +40,7 @@ Template publishTemplate(Template template, boolean validateOnly, ListVersionsResponse listVersions( ListVersionsOptions options) throws FirebaseRemoteConfigException; + + String getServerTemplate() throws FirebaseRemoteConfigException; } + diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java index 7425673fb..d84abae84 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java @@ -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; @@ -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 COMMON_HEADERS = ImmutableMap.of( "X-Firebase-Client", "fire-admin-java/" + SdkUtils.getVersion(), @@ -62,6 +66,7 @@ final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient ); private final String remoteConfigUrl; + private final String serverRemoteConfigUrl; private final HttpRequestFactory requestFactory; private final JsonFactory jsonFactory; private final ErrorHandlingHttpClient httpClient; @@ -69,6 +74,7 @@ final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient 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; @@ -82,6 +88,11 @@ String getRemoteConfigUrl() { return remoteConfigUrl; } + @VisibleForTesting + String getServerRemoteConfigUrl() { + return serverRemoteConfigUrl; + } + @VisibleForTesting HttpRequestFactory getRequestFactory() { return requestFactory; @@ -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 { @@ -267,3 +290,4 @@ private RemoteConfigServiceErrorResponse safeParse(String response) { } } } + diff --git a/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java b/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java new file mode 100644 index 000000000..97064d96a --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/KeysAndValues.java @@ -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 keysAndValues; + + private KeysAndValues(@NonNull Builder builder) { + keysAndValues = ImmutableMap.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 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); + } + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java b/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java new file mode 100644 index 000000000..255cee565 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/OneOfCondition.java @@ -0,0 +1,102 @@ +/* + * 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.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.firebase.internal.NonNull; +import com.google.firebase.internal.Nullable; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OneOfConditionResponse; + +class OneOfCondition { + private OrCondition orCondition; + private AndCondition andCondition; + private String trueValue; + private String falseValue; + + OneOfCondition(OneOfConditionResponse oneOfconditionResponse) { + if (oneOfconditionResponse.getOrCondition() != null) { + this.orCondition = new OrCondition(oneOfconditionResponse.getOrCondition()); + } + if (oneOfconditionResponse.getAndCondition() != null) { + this.andCondition = new AndCondition(oneOfconditionResponse.getAndCondition()); + } + } + + @VisibleForTesting + OneOfCondition() { + this.orCondition = null; + this.andCondition = null; + this.trueValue = null; + this.falseValue = null; + } + + @Nullable + OrCondition getOrCondition() { + return orCondition; + } + + @Nullable + AndCondition getAndCondition() { + return andCondition; + } + + @Nullable + String isTrue() { + return trueValue; + } + + @Nullable + String isFalse() { + return falseValue; + } + + OneOfCondition setOrCondition(@NonNull OrCondition orCondition) { + checkNotNull(orCondition, "`Or` condition cannot be set to null."); + this.orCondition = orCondition; + return this; + } + + OneOfCondition setAndCondition(@NonNull AndCondition andCondition) { + checkNotNull(andCondition, "`And` condition cannot be set to null."); + this.andCondition = andCondition; + return this; + } + + OneOfCondition setTrue() { + this.trueValue = "true"; + return this; + } + + OneOfCondition setFalse() { + this.falseValue = "false"; + return this; + } + + OneOfConditionResponse toOneOfConditionResponse() { + OneOfConditionResponse oneOfConditionResponse = new OneOfConditionResponse(); + if (this.andCondition != null) { + oneOfConditionResponse.setAndCondition(this.andCondition.toAndConditionResponse()); + } + if (this.orCondition != null) { + oneOfConditionResponse.setOrCondition(this.orCondition.toOrConditionResponse()); + } + return oneOfConditionResponse; + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/OrCondition.java b/src/main/java/com/google/firebase/remoteconfig/OrCondition.java new file mode 100644 index 000000000..4d258f23b --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/OrCondition.java @@ -0,0 +1,61 @@ + +/* + * 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.OneOfConditionResponse; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.OrConditionResponse; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +final class OrCondition { + private final ImmutableList conditions; + + public OrCondition(@NonNull List conditions) { + checkNotNull(conditions, "List of conditions for OR operation must not be null."); + checkArgument(!conditions.isEmpty(), "List of conditions for OR operation must not be empty."); + this.conditions = ImmutableList.copyOf(conditions); + } + + OrCondition(OrConditionResponse orConditionResponse) { + List conditionList = orConditionResponse.getConditions(); + checkNotNull(conditionList, "List of conditions for AND operation cannot be null."); + checkArgument(!conditionList.isEmpty(), "List of conditions for AND operation cannot be empty"); + this.conditions = conditionList.stream() + .map(OneOfCondition::new) + .collect(ImmutableList.toImmutableList()); + } + + @NonNull + List getConditions() { + return new ArrayList<>(conditions); + } + + OrConditionResponse toOrConditionResponse() { + return new OrConditionResponse() + .setConditions(this.conditions.stream() + .map(OneOfCondition::toOneOfConditionResponse) + .collect(Collectors.toList())); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerCondition.java b/src/main/java/com/google/firebase/remoteconfig/ServerCondition.java new file mode 100644 index 000000000..f16aeffc8 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerCondition.java @@ -0,0 +1,90 @@ +/* + * 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.base.Strings; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse.ServerConditionResponse; + +import java.util.Objects; + +final class ServerCondition { + + private String name; + private OneOfCondition serverCondition; + + ServerCondition(@NonNull String name, @NonNull OneOfCondition condition) { + checkArgument(!Strings.isNullOrEmpty(name), "condition name must not be null or empty"); + this.name = name; + this.serverCondition = condition; + } + + ServerCondition(@NonNull ServerConditionResponse serverConditionResponse) { + checkNotNull(serverConditionResponse); + this.name = serverConditionResponse.getName(); + this.serverCondition = new OneOfCondition(serverConditionResponse.getServerCondition()); + } + + @NonNull + String getName() { + return name; + } + + @NonNull + OneOfCondition getCondition() { + return serverCondition; + } + + ServerCondition setName(@NonNull String name) { + checkArgument(!Strings.isNullOrEmpty(name), "condition name must not be null or empty"); + this.name = name; + return this; + } + + ServerCondition setServerCondition(@NonNull OneOfCondition condition) { + checkNotNull(condition, "condition must not be null or empty"); + this.serverCondition = condition; + return this; + } + + ServerConditionResponse toServerConditionResponse() { + return new ServerConditionResponse().setName(this.name) + .setServerCondition(this.serverCondition.toOneOfConditionResponse()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServerCondition condition = (ServerCondition) o; + return Objects.equals(name, condition.name) + && Objects.equals(serverCondition, condition.serverCondition); + } + + @Override + public int hashCode() { + return Objects.hash(name, serverCondition); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java b/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java new file mode 100644 index 000000000..9e063009f --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerConfig.java @@ -0,0 +1,103 @@ +/* + * 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.firebase.internal.NonNull; + +import java.util.Map; + +/** + * Represents the configuration produced by evaluating a server template. + */ +public final class ServerConfig { + private final Map configValues; + + ServerConfig(Map configValues) { + this.configValues = configValues; + } + + /** + * Gets the value for the given key as a string. Convenience method for calling + * serverConfig.getValue(key).asString(). + * + * @param key The name of the parameter. + * @return config value for the given key as string. + */ + @NonNull + public String getString(@NonNull String key) { + return this.getValue(key).asString(); + } + + /** + * Gets the value for the given key as a boolean.Convenience method for calling + * serverConfig.getValue(key).asBoolean(). + * + * @param key The name of the parameter. + * @return config value for the given key as boolean. + */ + @NonNull + public boolean getBoolean(@NonNull String key) { + return this.getValue(key).asBoolean(); + } + + /** + * Gets the value for the given key as long.Convenience method for calling + * serverConfig.getValue(key).asLong(). + * + * @param key The name of the parameter. + * @return config value for the given key as long. + */ + @NonNull + public long getLong(@NonNull String key) { + return this.getValue(key).asLong(); + } + + /** + * Gets the value for the given key as double.Convenience method for calling + * serverConfig.getValue(key).asDouble(). + * + * @param key The name of the parameter. + * @return config value for the given key as double. + */ + @NonNull + public double getDouble(@NonNull String key) { + return this.getValue(key).asDouble(); + } + + /** + * Gets the {@link ValueSource} for the given key. + * + * @param key The name of the parameter. + * @return config value source for the given key. + */ + @NonNull + public ValueSource getValueSource(@NonNull String key) { + return this.getValue(key).getSource(); + } + + private Value getValue(String key) { + checkArgument(!Strings.isNullOrEmpty(key), "Server config key cannot be null or empty."); + if (configValues.containsKey(key)) { + return configValues.get(key); + } + return new Value(ValueSource.STATIC); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java new file mode 100644 index 000000000..f16992744 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplate.java @@ -0,0 +1,37 @@ +/* + * 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 com.google.api.core.ApiFuture; + +public interface ServerTemplate { + public interface Builder { + + Builder defaultConfig(KeysAndValues config); + + Builder cachedTemplate(String templateJson); + + ServerTemplate build(); + } + /** + * Fetches and caches the current active version of the project. + */ + ApiFuture load() throws FirebaseRemoteConfigException; + + String toJson(); +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java new file mode 100644 index 000000000..34674855f --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateData.java @@ -0,0 +1,216 @@ + +/* + * 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.api.client.json.JsonFactory; +import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; +import com.google.firebase.internal.ApiClientUtils; +import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.ServerTemplateResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +final class ServerTemplateData { + + private String etag; + private Map parameters; + private List serverConditions; + private Map parameterGroups; + private Version version; + + + ServerTemplateData(String etag) { + this.parameters = new HashMap<>(); + this.serverConditions = new ArrayList<>(); + this.parameterGroups = new HashMap<>(); + this.etag = etag; + } + + ServerTemplateData() { + this((String) null); + } + + ServerTemplateData(@NonNull ServerTemplateResponse serverTemplateResponse) { + checkNotNull(serverTemplateResponse); + this.parameters = new HashMap<>(); + this.serverConditions = new ArrayList<>(); + this.parameterGroups = new HashMap<>(); + if (serverTemplateResponse.getParameters() != null) { + for (Map.Entry entry : + serverTemplateResponse.getParameters().entrySet()) { + this.parameters.put(entry.getKey(), new Parameter(entry.getValue())); + } + } + if (serverTemplateResponse.getServerConditions() != null) { + for (ServerTemplateResponse.ServerConditionResponse conditionResponse : + serverTemplateResponse.getServerConditions()) { + this.serverConditions.add(new ServerCondition(conditionResponse)); + } + } + if (serverTemplateResponse.getParameterGroups() != null) { + for (Map.Entry entry : + serverTemplateResponse.getParameterGroups().entrySet()) { + this.parameterGroups.put(entry.getKey(), new ParameterGroup(entry.getValue())); + } + } + if (serverTemplateResponse.getVersion() != null) { + this.version = new Version(serverTemplateResponse.getVersion()); + } + this.etag = serverTemplateResponse.getEtag(); + } + + + static ServerTemplateData fromJSON(@NonNull String json) + throws FirebaseRemoteConfigException { + checkArgument(!Strings.isNullOrEmpty(json), "JSON String must not be null or empty."); + // using the default json factory as no rpc calls are made here + JsonFactory jsonFactory = ApiClientUtils.getDefaultJsonFactory(); + try { + ServerTemplateResponse serverTemplateResponse = + jsonFactory.createJsonParser(json).parseAndClose(ServerTemplateResponse.class); + return new ServerTemplateData(serverTemplateResponse); + } catch (IOException e) { + throw new FirebaseRemoteConfigException( + ErrorCode.INVALID_ARGUMENT, "Unable to parse JSON string."); + } + } + + + String getETag() { + return this.etag; + } + + + @NonNull + public Map getParameters() { + return this.parameters; + } + + @NonNull + List getServerConditions() { + return serverConditions; + } + + @NonNull + Map getParameterGroups() { + return parameterGroups; + } + + Version getVersion() { + return version; + } + + ServerTemplateData setParameters(@NonNull Map parameters) { + checkNotNull(parameters, "parameters must not be null."); + this.parameters = parameters; + return this; + } + + + ServerTemplateData setServerConditions(@NonNull List conditions) { + checkNotNull(conditions, "conditions must not be null."); + this.serverConditions = conditions; + return this; + } + + ServerTemplateData setParameterGroups( + @NonNull Map parameterGroups) { + checkNotNull(parameterGroups, "parameter groups must not be null."); + this.parameterGroups = parameterGroups; + return this; + } + + ServerTemplateData setVersion(Version version) { + this.version = version; + return this; + } + + String toJSON() { + JsonFactory jsonFactory = ApiClientUtils.getDefaultJsonFactory(); + try { + return jsonFactory.toString(this.toServerTemplateResponse(true)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + ServerTemplateData setETag(String etag) { + this.etag = etag; + return this; + } + + ServerTemplateResponse toServerTemplateResponse(boolean includeAll) { + Map parameterResponses = new HashMap<>(); + for (Map.Entry entry : this.parameters.entrySet()) { + parameterResponses.put(entry.getKey(), entry.getValue().toParameterResponse()); + } + List serverConditionResponses = + new ArrayList<>(); + for (ServerCondition condition : this.serverConditions) { + serverConditionResponses.add(condition.toServerConditionResponse()); + } + Map parameterGroupResponse = new HashMap<>(); + for (Map.Entry entry : this.parameterGroups.entrySet()) { + parameterGroupResponse.put(entry.getKey(), entry.getValue().toParameterGroupResponse()); + } + TemplateResponse.VersionResponse versionResponse = + (this.version == null) ? null : this.version.toVersionResponse(includeAll); + ServerTemplateResponse serverTemplateResponse = + new ServerTemplateResponse() + .setParameters(parameterResponses) + .setServerConditions(serverConditionResponses) + .setParameterGroups(parameterGroupResponse) + .setVersion(versionResponse); + if (includeAll) { + return serverTemplateResponse.setEtag(this.etag); + } + return serverTemplateResponse; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServerTemplateData template = (ServerTemplateData) o; + return Objects.equals(etag, template.etag) + && Objects.equals(parameters, template.parameters) + && Objects.equals(serverConditions, template.serverConditions) + && Objects.equals(parameterGroups, template.parameterGroups) + && Objects.equals(version, template.version); + } + + @Override + public int hashCode() { + return Objects.hash(etag, parameters, serverConditions, parameterGroups, version); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java new file mode 100644 index 000000000..81c71e898 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ServerTemplateImpl.java @@ -0,0 +1,95 @@ + +/* + * 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 com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.concurrent.atomic.AtomicReference; + +public final class ServerTemplateImpl implements ServerTemplate { + + private final KeysAndValues defaultConfig; + private FirebaseRemoteConfigClient client; + private ServerTemplateData cache; + private final AtomicReference cachedTemplate; // Added field for cached template + + public static class Builder implements ServerTemplate.Builder { + private KeysAndValues defaultConfig; + private String cachedTemplate; + private FirebaseRemoteConfigClient client; + + Builder(FirebaseRemoteConfigClient remoteConfigClient) { + this.client = remoteConfigClient; + } + + @Override + public Builder defaultConfig(KeysAndValues config) { + this.defaultConfig = config; + return this; + } + + @Override + public Builder cachedTemplate(String templateJson) { + this.cachedTemplate = templateJson; + return this; + } + + @Override + public ServerTemplate build() { + return new ServerTemplateImpl(this); + } + } + + private ServerTemplateImpl(Builder builder) { + this.defaultConfig = builder.defaultConfig; + this.cachedTemplate = new AtomicReference<>(builder.cachedTemplate); + this.client = builder.client; + try { + this.cache = ServerTemplateData.fromJSON(this.cachedTemplate.get()); + } catch (FirebaseRemoteConfigException e) { + e.printStackTrace(); + } + } + + @Override + public ApiFuture load() throws FirebaseRemoteConfigException { + String serverTemplate = client.getServerTemplate(); + this.cachedTemplate.set(serverTemplate); + this.cache = ServerTemplateData.fromJSON(serverTemplate); + return ApiFutures.immediateFuture(null); + } + + // Add getters or other methods as needed + public KeysAndValues getDefaultConfig() { + return defaultConfig; + } + + public String getCachedTemplate() { + return cachedTemplate.get(); + } + + @Override + public String toJson() { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + return gson.toJson(this.cache); + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/Value.java b/src/main/java/com/google/firebase/remoteconfig/Value.java new file mode 100644 index 000000000..7f04aefeb --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/Value.java @@ -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.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.internal.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wraps a parameter value with metadata and type-safe getters. Type-safe + * getters insulate application logic from remote changes to parameter names and + * types. + */ +class Value { + private static final Logger logger = LoggerFactory.getLogger(Value.class); + private static final boolean DEFAULT_VALUE_FOR_BOOLEAN = false; + private static final String DEFAULT_VALUE_FOR_STRING = ""; + private static final long DEFAULT_VALUE_FOR_LONG = 0; + private static final double DEFAULT_VALUE_FOR_DOUBLE = 0; + private static final ImmutableList BOOLEAN_TRUTHY_VALUES = ImmutableList.of("1", "true", + "t", "yes", "y", "on"); + + private final ValueSource source; + private final String value; + + /** + * Creates a new {@link Value} object. + * + * @param source Indicates the source of a value. + * @param value Indicates a parameter value. + */ + Value(@NonNull ValueSource source, String value) { + checkNotNull(source, "Value source cannot be null."); + this.source = source; + this.value = value; + } + + /** + * Creates a new {@link Value} object with default value. + * + * @param source Indicates the source of a value. + */ + Value(@NonNull ValueSource source) { + this(source, DEFAULT_VALUE_FOR_STRING); + } + + /** + * Gets the value as a string. + * + * @return value as string + */ + @NonNull + String asString() { + return this.value; + } + + /** + * Gets the value as a boolean.The following values (case + * insensitive) are interpreted as true: "1", "true", "t", "yes", "y", "on". + * Other values are interpreted as false. + * + * @return value as boolean + */ + @NonNull + boolean asBoolean() { + if (source == ValueSource.STATIC) { + return DEFAULT_VALUE_FOR_BOOLEAN; + } + return BOOLEAN_TRUTHY_VALUES.contains(value.toLowerCase()); + } + + /** + * Gets the value as long. Comparable to calling Number(value) || 0. + * + * @return value as long + */ + @NonNull + long asLong() { + if (source == ValueSource.STATIC) { + return DEFAULT_VALUE_FOR_LONG; + } + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + logger.warn("Unable to convert %s to long type.", value); + return DEFAULT_VALUE_FOR_LONG; + } + } + + /** + * Gets the value as double. Comparable to calling Number(value) || 0. + * + * @return value as double + */ + @NonNull + double asDouble() { + if (source == ValueSource.STATIC) { + return DEFAULT_VALUE_FOR_DOUBLE; + } + try { + return Double.parseDouble(this.value); + } catch (NumberFormatException e) { + logger.warn("Unable to convert %s to double type.", value); + return DEFAULT_VALUE_FOR_DOUBLE; + } + } + + /** + * Gets the {@link ValueSource} for the given key. + * + * @return source. + */ + @NonNull + ValueSource getSource() { + return source; + } +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/ValueSource.java b/src/main/java/com/google/firebase/remoteconfig/ValueSource.java new file mode 100644 index 000000000..c870e8514 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/ValueSource.java @@ -0,0 +1,31 @@ + +/* + * 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; + +/** + * Indicates the source of a value. + * "static" indicates the value was defined by a static constant. + * "default" indicates the value was defined by default config. + * "remote" indicates the value was defined by config produced by evaluating a template. + */ +public enum ValueSource { + STATIC, + REMOTE, + DEFAULT +} + diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java new file mode 100644 index 000000000..87f624a78 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/internal/ServerTemplateResponse.java @@ -0,0 +1,321 @@ +/* + * 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.internal; + +import com.google.api.client.util.Key; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterGroupResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterResponse; +import com.google.firebase.remoteconfig.internal.TemplateResponse.VersionResponse; + +import java.util.List; +import java.util.Map; + +/** + * The Data Transfer Object for parsing Remote Config template responses from the Remote Config + * service. + */ +public final class ServerTemplateResponse { + @Key("parameters") + private Map parameters; + + @Key("conditions") + private List serverConditions; + + @Key("parameterGroups") + private Map parameterGroups; + + @Key("version") + private VersionResponse version; + + // For local JSON serialization and deserialization purposes only. + // ETag in response type is never set by the HTTP response. + @Key("etag") + private String etag; + + public Map getParameters() { + return parameters; + } + + public List getServerConditions() { + return serverConditions; + } + + public Map getParameterGroups() { + return parameterGroups; + } + + public VersionResponse getVersion() { + return version; + } + + public String getEtag() { + return etag; + } + + public ServerTemplateResponse setParameters(Map parameters) { + this.parameters = parameters; + return this; + } + + public ServerTemplateResponse setServerConditions( + List serverConditions) { + this.serverConditions = serverConditions; + return this; + } + + public ServerTemplateResponse setParameterGroups( + Map parameterGroups) { + this.parameterGroups = parameterGroups; + return this; + } + + public ServerTemplateResponse setVersion(VersionResponse version) { + this.version = version; + return this; + } + + public ServerTemplateResponse setEtag(String etag) { + this.etag = etag; + return this; + } + + /** + * The Data Transfer Object for parsing Remote Config condition responses from the Remote Config + * service. + */ + public static final class ServerConditionResponse { + + @Key("name") + private String name; + + @Key("condition") + private OneOfConditionResponse condition; + + public String getName() { + return name; + } + + public OneOfConditionResponse getServerCondition() { + return condition; + } + + public ServerConditionResponse setName(String name) { + this.name = name; + return this; + } + + public ServerConditionResponse setServerCondition(OneOfConditionResponse condition) { + this.condition = condition; + return this; + } + } + + public static final class OneOfConditionResponse { + @Key("orCondition") + private OrConditionResponse orCondition; + + @Key("andCondition") + private AndConditionResponse andCondition; + + @Key("customSignal") + private CustomSignalConditionResponse customSignalCondition; + + @Key("percent") + private PercentConditionResponse percentCondition; + + public OrConditionResponse getOrCondition() { + return orCondition; + } + + public AndConditionResponse getAndCondition() { + return andCondition; + } + + public PercentConditionResponse getPercentCondition() { + return percentCondition; + } + + public CustomSignalConditionResponse getCustomSignalCondition() { + return customSignalCondition; + } + + public OneOfConditionResponse setOrCondition(OrConditionResponse orCondition) { + this.orCondition = orCondition; + return this; + } + + public OneOfConditionResponse setAndCondition(AndConditionResponse andCondition) { + this.andCondition = andCondition; + return this; + } + + public OneOfConditionResponse setCustomSignalCondition( + CustomSignalConditionResponse customSignalCondition) { + this.customSignalCondition = customSignalCondition; + return this; + } + + public OneOfConditionResponse setPercentCondition(PercentConditionResponse percentCondition) { + this.percentCondition = percentCondition; + return this; + } + } + + public static final class OrConditionResponse { + @Key("conditions") + private List conditions; + + public List getConditions() { + return conditions; + } + + public OrConditionResponse setConditions(List conditions) { + this.conditions = conditions; + return this; + } + } + + public static final class AndConditionResponse { + @Key("conditions") + private List conditions; + + public List getConditions() { + return conditions; + } + + public AndConditionResponse setConditions(List conditions) { + this.conditions = conditions; + return this; + } + } + + public static final class CustomSignalConditionResponse { + @Key("customSignalOperator") + private String operator; + + @Key("customSignalKey") + private String key; + + @Key("targetCustomSignalValues") + private List targetValues; + + public String getOperator() { + return operator; + } + + public String getKey() { + return key; + } + + public List getTargetValues() { + return targetValues; + } + + public CustomSignalConditionResponse setOperator(String operator) { + this.operator = operator; + return this; + } + + public CustomSignalConditionResponse setKey(String key) { + this.key = key; + return this; + } + + public CustomSignalConditionResponse setTargetValues(List targetValues) { + this.targetValues = targetValues; + return this; + } + } + + public static final class PercentConditionResponse { + @Key("microPercent") + private int microPercent; + + @Key("microPercentRange") + private MicroPercentRangeResponse microPercentRange; + + @Key("percentOperator") + private String percentOperator; + + @Key("seed") + private String seed; + + public int getMicroPercent() { + return microPercent; + } + + public MicroPercentRangeResponse getMicroPercentRange() { + return microPercentRange; + } + + public String getPercentOperator() { + return percentOperator; + } + + public String getSeed() { + return seed; + } + + public PercentConditionResponse setMicroPercent(int microPercent) { + this.microPercent = microPercent; + return this; + } + + public PercentConditionResponse setMicroPercentRange( + MicroPercentRangeResponse microPercentRange) { + this.microPercentRange = microPercentRange; + return this; + } + + public PercentConditionResponse setPercentOperator(String percentOperator) { + this.percentOperator = percentOperator; + return this; + } + + public PercentConditionResponse setSeed(String seed) { + this.seed = seed; + return this; + } + } + + public static final class MicroPercentRangeResponse { + @Key("microPercentLowerBound") + private int microPercentLowerBound; + + @Key("microPercentUpperBound") + private int microPercentUpperBound; + + public int getMicroPercentLowerBound() { + return microPercentLowerBound; + } + + public int getMicroPercentUpperBound() { + return microPercentUpperBound; + } + + public MicroPercentRangeResponse setMicroPercentLowerBound(int microPercentLowerBound) { + this.microPercentLowerBound = microPercentLowerBound; + return this; + } + + public MicroPercentRangeResponse setMicroPercentUpperBound(int microPercentUpperBound) { + this.microPercentUpperBound = microPercentUpperBound; + return this; + } + } +} + diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index edc52a19d..aea2dd1eb 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -42,6 +42,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.OutgoingHttpRequest; + import com.google.firebase.auth.MockGoogleCredentials; import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.SdkUtils; @@ -1239,3 +1240,4 @@ private void checkExceptionFromHttpResponse( assertTrue(request.getUrl().startsWith("https://firebaseremoteconfig.googleapis.com")); } } + diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java index d3e7fbff2..66821abb8 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java @@ -35,12 +35,22 @@ import org.junit.After; import org.junit.Test; +/** Unit tests + * for {@link FirebaseRemoteConfig}. + * */ public class FirebaseRemoteConfigTest { private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder() .setCredentials(new MockGoogleCredentials("test-token")) .setProjectId("test-project") .build(); + private static final String TEST_SERVER_TEMPLATE = + "{\n" + + " \"etag\": \"etag-123456789012-1\",\n" + + " \"parameters\": {},\n" + + " \"serverConditions\": [],\n" + + " \"parameterGroups\": {}\n" + + "}"; private static final FirebaseRemoteConfigException TEST_EXCEPTION = new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message"); @@ -80,6 +90,20 @@ public void testDefaultRemoteConfigClient() { assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getRemoteConfigUrl()); } + @Test + public void testDefaultServerRemoteConfigClient() { + FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); + FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance(app); + + FirebaseRemoteConfigClient client = remoteConfig.getRemoteConfigClient(); + + assertTrue(client instanceof FirebaseRemoteConfigClientImpl); + assertSame(client, remoteConfig.getRemoteConfigClient()); + String expectedUrl = + "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig"; + assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getServerRemoteConfigUrl()); + } + @Test public void testAppDelete() { FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app"); @@ -597,4 +621,54 @@ private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS); return new FirebaseRemoteConfig(app, client); } + + // Get Server template tests + + @Test + public void testGetServerTemplate() throws FirebaseRemoteConfigException { + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + ServerTemplate template = remoteConfig.getServerTemplate(); + String templateData = template.toJson(); + assertEquals(TEST_SERVER_TEMPLATE, templateData); + } + + @Test + public void testGetServerTemplateFailure() { + MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + try { + remoteConfig.getServerTemplate(); + } catch (FirebaseRemoteConfigException e) { + assertSame(TEST_EXCEPTION, e); + } + } + + @Test + public void testGetServerTemplateAsync() throws Exception { + MockRemoteConfigClient client = + MockRemoteConfigClient.fromServerTemplate( + new ServerTemplateData().setETag(TEST_ETAG).toJSON()); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + ServerTemplate template = remoteConfig.getServerTemplateAsync().get(); + String templateData = template.toJson(); + assertEquals(TEST_SERVER_TEMPLATE, templateData); + } + + @Test + public void testGetServerTemplateAsyncFailure() throws InterruptedException { + MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION); + FirebaseRemoteConfig remoteConfig = getRemoteConfig(client); + + try { + remoteConfig.getServerTemplateAsync().get(); + } catch (ExecutionException e) { + assertSame(TEST_EXCEPTION, e.getCause()); + } + } } diff --git a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java index 9ca58508d..1e7c0d470 100644 --- a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java +++ b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java @@ -21,28 +21,35 @@ public class MockRemoteConfigClient implements FirebaseRemoteConfigClient{ private final Template resultTemplate; + private final String resultServerTemplate; private final FirebaseRemoteConfigException exception; private final ListVersionsResponse listVersionsResponse; private MockRemoteConfigClient(Template resultTemplate, + String resultServerTemplate, ListVersionsResponse listVersionsResponse, FirebaseRemoteConfigException exception) { this.resultTemplate = resultTemplate; + this.resultServerTemplate = resultServerTemplate; this.listVersionsResponse = listVersionsResponse; this.exception = exception; } static MockRemoteConfigClient fromTemplate(Template resultTemplate) { - return new MockRemoteConfigClient(resultTemplate, null, null); + return new MockRemoteConfigClient(resultTemplate,null, null, null); + } + + static MockRemoteConfigClient fromServerTemplate(String resultServerTemplate) { + return new MockRemoteConfigClient(null, resultServerTemplate,null, null); } static MockRemoteConfigClient fromListVersionsResponse( ListVersionsResponse listVersionsResponse) { - return new MockRemoteConfigClient(null, listVersionsResponse, null); + return new MockRemoteConfigClient(null,null, listVersionsResponse, null); } static MockRemoteConfigClient fromException(FirebaseRemoteConfigException exception) { - return new MockRemoteConfigClient(null, null, exception); + return new MockRemoteConfigClient(null,null, null, exception); } @Override @@ -53,6 +60,14 @@ public Template getTemplate() throws FirebaseRemoteConfigException { return resultTemplate; } + @Override + public String getServerTemplate() throws FirebaseRemoteConfigException { + if (exception != null) { + throw exception; + } + return resultServerTemplate; + } + @Override public Template getTemplateAtVersion(String versionNumber) throws FirebaseRemoteConfigException { if (exception != null) { @@ -87,3 +102,4 @@ public ListVersionsResponse listVersions( return listVersionsResponse; } } +