Repository: firebase/firebase-admin-java
Pull request #479 (2020-09-25 21:37:03)

Add parameters to Remote Config template
- Introduce `Response Types` and `Public Types`
- Implement `toResponseType()` and `toPublicType()`
- Add `parameters` to `RemoteConfigTemplate`
- Add unit tests

Related Issue: #446 


Patch:
diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java
index 20b5e3a..038a700 100644
--- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java
+++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java
@@ -36,6 +36,7 @@ import com.google.firebase.internal.ErrorHandlingHttpClient;
 import com.google.firebase.internal.HttpRequestInfo;
 import com.google.firebase.internal.SdkUtils;
 import com.google.firebase.remoteconfig.internal.RemoteConfigServiceErrorResponse;
+import com.google.firebase.remoteconfig.internal.TemplateResponse;
 
 import java.io.IOException;
 import java.util.List;
@@ -94,8 +95,9 @@ final class FirebaseRemoteConfigClientImpl implements FirebaseRemoteConfigClient
     HttpRequestInfo request = HttpRequestInfo.buildGetRequest(remoteConfigUrl)
             .addAllHeaders(COMMON_HEADERS);
     IncomingHttpResponse response = httpClient.send(request);
-    RemoteConfigTemplate parsed = httpClient.parse(response, RemoteConfigTemplate.class);
-    return parsed.setETag(getETag(response));
+    TemplateResponse templateResponse = httpClient.parse(response, TemplateResponse.class);
+    RemoteConfigTemplate template = templateResponse.toRemoteConfigTemplate();
+    return template.setETag(getETag(response));
   }
 
   private String getETag(IncomingHttpResponse response) {
diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigParameter.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigParameter.java
new file mode 100644
index 0000000..757c143
--- /dev/null
+++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigParameter.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2020 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.firebase.internal.NonNull;
+import com.google.firebase.internal.Nullable;
+import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterResponse;
+import com.google.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a Remote Config parameter that can be included in a {@link RemoteConfigTemplate}.
+ * At minimum, a default value or a conditional value must be present for the
+ * parameter to have any effect.
+ */
+public final class RemoteConfigParameter {
+
+  private RemoteConfigParameterValue defaultValue;
+  private String description;
+  private Map<String, RemoteConfigParameterValue> conditionalValues;
+
+  /**
+   * Creates a new {@link RemoteConfigParameter}.
+   */
+  public RemoteConfigParameter() {
+    conditionalValues = new HashMap<>();
+  }
+
+  /**
+   * Gets the default value of the parameter.
+   *
+   * @return A {@link RemoteConfigParameterValue} instance or null.
+   */
+  @Nullable
+  public RemoteConfigParameterValue getDefaultValue() {
+    return defaultValue;
+  }
+
+  /**
+   * Gets the description of the parameter.
+   *
+   * @return The {@link String} description of the parameter or null.
+   */
+  @Nullable
+  public String getDescription() {
+    return description;
+  }
+
+  /**
+   * Gets the conditional values of the parameter.
+   * The condition name of the highest priority (the one listed first in the
+   * {@link RemoteConfigTemplate}'s conditions list) determines the value of this parameter.
+   *
+   * @return A non-null map of conditional values.
+   */
+  @NonNull
+  public Map<String, RemoteConfigParameterValue> getConditionalValues() {
+    return conditionalValues;
+  }
+
+  /**
+   * Sets the default value of the parameter.
+   * This is the value to set the parameter to, when none of the named conditions
+   * evaluate to true.
+   *
+   * @param value An {@link RemoteConfigParameterValue} instance.
+   * @return This {@link RemoteConfigParameter}.
+   */
+  public RemoteConfigParameter setDefaultValue(@Nullable RemoteConfigParameterValue value) {
+    defaultValue = value;
+    return this;
+  }
+
+  /**
+   * Sets the description of the parameter.
+   * Should not be over 100 characters and may contain any Unicode characters.
+   *
+   * @param description The description of the parameter.
+   * @return This {@link RemoteConfigParameter}.
+   */
+  public RemoteConfigParameter setDescription(@Nullable String description) {
+    this.description = description;
+    return this;
+  }
+
+  /**
+   * Sets the conditional values of the parameter.
+   * The condition name of the highest priority (the one listed first in the
+   * {@link RemoteConfigTemplate}'s conditions list) determines the value of this parameter.
+   *
+   * @param conditionalValues A non-null map of conditional values.
+   * @return This {@link RemoteConfigParameter}.
+   */
+  public RemoteConfigParameter setConditionalValues(
+          @NonNull Map<String, RemoteConfigParameterValue> conditionalValues) {
+    checkNotNull(conditionalValues, "conditional values must not be null.");
+    this.conditionalValues = conditionalValues;
+    return this;
+  }
+
+  ParameterResponse toParameterResponse() {
+    Map<String, ParameterValueResponse> conditionalResponseValues = new HashMap<>();
+    for (Map.Entry<String, RemoteConfigParameterValue> entry : conditionalValues.entrySet()) {
+      conditionalResponseValues.put(entry.getKey(), entry.getValue().toParameterValueResponse());
+    }
+    ParameterValueResponse parameterValueResponse = (defaultValue == null) ? null : defaultValue
+            .toParameterValueResponse();
+    return new ParameterResponse(parameterValueResponse, description,
+            conditionalResponseValues);
+  }
+}
diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigParameterValue.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigParameterValue.java
new file mode 100644
index 0000000..6a51ffd
--- /dev/null
+++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigParameterValue.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2020 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.firebase.remoteconfig.internal.TemplateResponse.ParameterValueResponse;
+
+/**
+ * Represents a Remote Config parameter value that can be used in a {@link RemoteConfigTemplate}.
+ */
+public abstract class RemoteConfigParameterValue {
+
+  /**
+   * Creates a new {@link RemoteConfigParameterValue.Explicit} instance with the given value.
+   *
+   * @param value The value of the {@link RemoteConfigParameterValue.Explicit}.
+   * @return A {@link RemoteConfigParameterValue.Explicit} instance.
+   */
+  public static Explicit of(String value) {
+    return new Explicit(value);
+  }
+
+  /**
+   * Creates a new {@link RemoteConfigParameterValue.InAppDefault} instance.
+   *
+   * @return A {@link RemoteConfigParameterValue.InAppDefault} instance.
+   */
+  public static InAppDefault inAppDefault() {
+    return new InAppDefault();
+  }
+
+  abstract ParameterValueResponse toParameterValueResponse();
+
+  /**
+   * Represents an explicit Remote Config parameter value with a {@link String} value that the
+   * parameter is set to.
+   */
+  public static final class Explicit extends RemoteConfigParameterValue {
+
+    private final String value;
+
+    private Explicit(String value) {
+      this.value = value;
+    }
+
+    /**
+     * Gets the value of {@link RemoteConfigParameterValue.Explicit}.
+     *
+     * @return The {@link String} value.
+     */
+    public String getValue() {
+      return this.value;
+    }
+
+    @Override
+    ParameterValueResponse toParameterValueResponse() {
+      return ParameterValueResponse.ofValue(this.value);
+    }
+  }
+
+  /**
+   * Represents an in app default parameter value.
+   */
+  public static final class InAppDefault extends RemoteConfigParameterValue {
+
+    @Override
+    ParameterValueResponse toParameterValueResponse() {
+      return ParameterValueResponse.ofInAppDefaultValue();
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java
index 632fb77..143f569 100644
--- a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java
+++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java
@@ -16,19 +16,73 @@
 
 package com.google.firebase.remoteconfig;
 
-import com.google.api.client.util.Key;
+import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.firebase.internal.NonNull;
+import com.google.firebase.remoteconfig.internal.TemplateResponse;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a Remote Config template.
+ */
 public final class RemoteConfigTemplate {
 
-  @Key("etag")
   private String etag;
+  private Map<String, RemoteConfigParameter> parameters;
 
+  /**
+   * Creates a new {@link RemoteConfigTemplate}.
+   */
+  public RemoteConfigTemplate() {
+    parameters = new HashMap<>();
+  }
+
+  /**
+   * Gets the ETag of the template.
+   *
+   * @return The ETag of the template.
+   */
   public String getETag() {
     return this.etag;
   }
 
+  /**
+   * Gets the map of parameters of the template.
+   *
+   * @return A non-null map of parameter keys to their optional default values and optional
+   *     conditional values.
+   */
+  @NonNull
+  public Map<String, RemoteConfigParameter> getParameters() {
+    return this.parameters;
+  }
+
+  /**
+   * Sets the map of parameters of the template.
+   *
+   * @param parameters A non-null map of parameter keys to their optional default values and
+   *                   optional conditional values.
+   * @return This {@link RemoteConfigTemplate} instance.
+   */
+  public RemoteConfigTemplate setParameters(
+          @NonNull Map<String, RemoteConfigParameter> parameters) {
+    checkNotNull(parameters, "parameters must not be null.");
+    this.parameters = parameters;
+    return this;
+  }
+
   RemoteConfigTemplate setETag(String etag) {
     this.etag = etag;
     return this;
   }
+
+  TemplateResponse toTemplateResponse() {
+    Map<String, TemplateResponse.ParameterResponse> parameterResponses = new HashMap<>();
+    for (Map.Entry<String, RemoteConfigParameter> entry : parameters.entrySet()) {
+      parameterResponses.put(entry.getKey(), entry.getValue().toParameterResponse());
+    }
+    return new TemplateResponse(parameterResponses);
+  }
 }
diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java
new file mode 100644
index 0000000..c1ae433
--- /dev/null
+++ b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2020 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 static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.client.util.Key;
+import com.google.firebase.database.annotations.Nullable;
+import com.google.firebase.internal.NonNull;
+import com.google.firebase.remoteconfig.RemoteConfigParameter;
+import com.google.firebase.remoteconfig.RemoteConfigParameterValue;
+import com.google.firebase.remoteconfig.RemoteConfigTemplate;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The Data Transfer Object for parsing Remote Config template responses from the
+ * Remote Config service.
+ **/
+public final class TemplateResponse {
+
+  @Key("parameters")
+  private Map<String, ParameterResponse> parameters;
+
+  public TemplateResponse() {
+    parameters = Collections.emptyMap();
+  }
+
+  public TemplateResponse(@NonNull Map<String, ParameterResponse> parameters) {
+    checkNotNull(parameters, "parameters must not be null.");
+    this.parameters = parameters;
+  }
+
+  public RemoteConfigTemplate toRemoteConfigTemplate() {
+    Map<String, RemoteConfigParameter> parameterPublicTypes = new HashMap<>();
+    for (Map.Entry<String, ParameterResponse> entry : parameters.entrySet()) {
+      parameterPublicTypes.put(entry.getKey(), entry.getValue().toRemoteConfigParameter());
+    }
+    return new RemoteConfigTemplate().setParameters(parameterPublicTypes);
+  }
+
+  /**
+   * The Data Transfer Object for parsing Remote Config parameter responses from the
+   * Remote Config service.
+   **/
+  public static final class ParameterResponse {
+
+    @Key("defaultValue")
+    private ParameterValueResponse defaultValue;
+
+    @Key("description")
+    private String description;
+
+    @Key("conditionalValues")
+    private Map<String, ParameterValueResponse> conditionalValues;
+
+    public ParameterResponse() {
+      conditionalValues = Collections.emptyMap();
+    }
+
+    public ParameterResponse(@Nullable ParameterValueResponse defaultValue,
+                             @Nullable String description,
+                             @NonNull Map<String, ParameterValueResponse> conditionalValues) {
+      this.defaultValue = defaultValue;
+      this.description = description;
+      this.conditionalValues = checkNotNull(conditionalValues);
+    }
+
+    public RemoteConfigParameter toRemoteConfigParameter() {
+      Map<String, RemoteConfigParameterValue> conditionalPublicValues = new HashMap<>();
+      for (Map.Entry<String, ParameterValueResponse> entry : conditionalValues.entrySet()) {
+        conditionalPublicValues
+                .put(entry.getKey(), entry.getValue().toRemoteConfigParameterValue());
+      }
+      RemoteConfigParameterValue remoteConfigParameterValue =
+              (defaultValue == null) ? null : defaultValue.toRemoteConfigParameterValue();
+      return new RemoteConfigParameter()
+              .setDefaultValue(remoteConfigParameterValue)
+              .setDescription(description)
+              .setConditionalValues(conditionalPublicValues);
+    }
+  }
+
+  /**
+   * The Data Transfer Object for parsing Remote Config parameter value responses from the
+   * Remote Config service.
+   **/
+  public static final class ParameterValueResponse {
+
+    @Key("value")
+    private String value;
+
+    @Key("useInAppDefault")
+    private Boolean inAppDefaultValue;
+
+    public ParameterValueResponse() {
+    }
+
+    private ParameterValueResponse(String value, Boolean inAppDefaultValue) {
+      this.value = value;
+      this.inAppDefaultValue = inAppDefaultValue;
+    }
+
+    public static ParameterValueResponse ofValue(String value) {
+      return new ParameterValueResponse(value, null);
+    }
+
+    public static ParameterValueResponse ofInAppDefaultValue() {
+      return new ParameterValueResponse(null, true);
+    }
+
+    public RemoteConfigParameterValue toRemoteConfigParameterValue() {
+      if (this.inAppDefaultValue != null && this.inAppDefaultValue) {
+        return RemoteConfigParameterValue.inAppDefault();
+      }
+      return RemoteConfigParameterValue.of(this.value);
+    }
+  }
+}
diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/package-info.java b/src/main/java/com/google/firebase/remoteconfig/internal/package-info.java
new file mode 100644
index 0000000..50a2768
--- /dev/null
+++ b/src/main/java/com/google/firebase/remoteconfig/internal/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020 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.
+ */
+
+/**
+ * @hide
+ */
+package com.google.firebase.remoteconfig.internal;


Test patch:
diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java
index 8b6e94c..8ee5fb5 100644
--- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java
+++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java
@@ -63,7 +63,8 @@ public class FirebaseRemoteConfigClientImplTest {
           404, ErrorCode.NOT_FOUND,
           500, ErrorCode.INTERNAL);
 
-  private static final String MOCK_TEMPLATE_RESPONSE = "{\"conditions\": [], \"parameters\": {}}";
+  private static final String MOCK_TEMPLATE_RESPONSE = TestUtils
+          .loadResource("getRemoteConfig.json");
 
   private static final String TEST_ETAG = "etag-123456789012-1";
 
@@ -86,6 +87,37 @@ public class FirebaseRemoteConfigClientImplTest {
     RemoteConfigTemplate template = client.getTemplate();
 
     assertEquals(TEST_ETAG, template.getETag());
+    Map<String, RemoteConfigParameter> parameters = template.getParameters();
+    assertEquals(2, parameters.size());
+    assertTrue(parameters.containsKey("welcome_message_text"));
+    RemoteConfigParameter welcomeMessageParameter = parameters.get("welcome_message_text");
+    assertEquals("text for welcome message!", welcomeMessageParameter.getDescription());
+    RemoteConfigParameterValue.Explicit explicitDefaultValue =
+            (RemoteConfigParameterValue.Explicit) welcomeMessageParameter.getDefaultValue();
+    assertEquals("welcome to app", explicitDefaultValue.getValue());
+    Map<String, RemoteConfigParameterValue> conditionalValues = welcomeMessageParameter
+            .getConditionalValues();
+    assertEquals(1, conditionalValues.size());
+    assertTrue(conditionalValues.containsKey("ios_en"));
+    RemoteConfigParameterValue.Explicit value =
+            (RemoteConfigParameterValue.Explicit) conditionalValues.get("ios_en");
+    assertEquals("welcome to app en", value.getValue());
+    assertTrue(parameters.containsKey("header_text"));
+    RemoteConfigParameter headerParameter = parameters.get("header_text");
+    assertTrue(
+            headerParameter.getDefaultValue() instanceof RemoteConfigParameterValue.InAppDefault);
+    checkGetRequestHeader(interceptor.getLastRequest());
+  }
+
+  @Test
+  public void testGetTemplateWithEmptyTemplateResponse() throws Exception {
+    response.addHeader("etag", TEST_ETAG);
+    response.setContent("{}");
+
+    RemoteConfigTemplate template = client.getTemplate();
+
+    assertEquals(TEST_ETAG, template.getETag());
+    assertEquals(0, template.getParameters().size());
     checkGetRequestHeader(interceptor.getLastRequest());
   }
 
diff --git a/src/test/resources/getRemoteConfig.json b/src/test/resources/getRemoteConfig.json
new file mode 100644
index 0000000..86322f2
--- /dev/null
+++ b/src/test/resources/getRemoteConfig.json
@@ -0,0 +1,42 @@
+{
+  "conditions": [
+    {
+      "name": "ios_en",
+      "expression": "device.os == 'ios' && device.country in ['us', 'uk']",
+      "tagColor": "INDIGO"
+    },
+    {
+      "name": "android_en",
+      "expression": "device.os == 'android' && device.country in ['us', 'uk']",
+      "tagColor": "GREEN"
+    }
+  ],
+  "parameters": {
+    "welcome_message_text": {
+      "defaultValue": {
+        "value": "welcome to app"
+      },
+      "conditionalValues": {
+        "ios_en": {
+          "value": "welcome to app en"
+        }
+      },
+      "description": "text for welcome message!"
+    },
+    "header_text": {
+      "defaultValue": {
+        "useInAppDefault": true
+      }
+    }
+  },
+  "parameterGroups": {},
+  "version": {
+    "versionNumber": "17",
+    "updateOrigin": "ADMIN_SDK_NODE",
+    "updateType": "INCREMENTAL_UPDATE",
+    "updateUser": {
+      "email": "firebase-user@account.com"
+    },
+    "updateTime": "Wed, 30 Sep 2020 17:56:07 GMT"
+  }
+}