From 071940e3e852fd03f24ca69029f96b6eb23caf60 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Fri, 4 Dec 2020 16:35:06 -0500 Subject: [PATCH 01/10] Add toJSON and fromJSON to Remote Config Template --- .../firebase/remoteconfig/Condition.java | 7 +- .../firebase/remoteconfig/Template.java | 68 ++++++++ .../remoteconfig/internal/TemplateInput.java | 75 +++++++++ .../firebase/remoteconfig/TemplateTest.java | 151 ++++++++++++++++++ src/test/resources/rcTemplateWithETag.json | 1 + 5 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/google/firebase/remoteconfig/internal/TemplateInput.java create mode 100644 src/test/resources/rcTemplateWithETag.json diff --git a/src/main/java/com/google/firebase/remoteconfig/Condition.java b/src/main/java/com/google/firebase/remoteconfig/Condition.java index c10498c36..fa05cd2d0 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Condition.java +++ b/src/main/java/com/google/firebase/remoteconfig/Condition.java @@ -69,7 +69,7 @@ public Condition(@NonNull String name, @NonNull String expression, @Nullable Tag checkNotNull(conditionResponse); this.name = conditionResponse.getName(); this.expression = conditionResponse.getExpression(); - if (conditionResponse.getTagColor() == null) { + if (Strings.isNullOrEmpty(conditionResponse.getTagColor())) { this.tagColor = TagColor.UNSPECIFIED; } else { this.tagColor = TagColor.valueOf(conditionResponse.getTagColor()); @@ -168,4 +168,9 @@ public boolean equals(Object o) { return Objects.equals(name, condition.name) && Objects.equals(expression, condition.expression) && tagColor == condition.tagColor; } + + @Override + public int hashCode() { + return Objects.hash(name, expression, tagColor); + } } diff --git a/src/main/java/com/google/firebase/remoteconfig/Template.java b/src/main/java/com/google/firebase/remoteconfig/Template.java index c6ffcb49a..90826475e 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Template.java +++ b/src/main/java/com/google/firebase/remoteconfig/Template.java @@ -16,11 +16,27 @@ 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.googleapis.util.Utils; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonParser; +import com.google.common.base.Strings; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.internal.NonNull; +import com.google.firebase.remoteconfig.internal.TemplateInput; import com.google.firebase.remoteconfig.internal.TemplateResponse; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.JsonSyntaxException; +import java.io.IOException; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -75,6 +91,29 @@ public Template() { } } + /** + * Creates and returns a new Remote Config template from a JSON string. + * + * @param json A non-null JSON string to populate a Remote Config template. + * @return A new {@link Template} instance. + * @throws IOException If the input JSON string is not parsable. + */ + public static Template fromJSON(@NonNull String json) throws IOException { + checkArgument(!Strings.isNullOrEmpty(json), "JSON String must not be null or empty."); + JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); + JsonParser parser = jsonFactory.createJsonParser(json); + TemplateInput templateInput = parser.parseAndClose(TemplateInput.class); + TemplateResponse templateResponse = new TemplateResponse() + .setParameters(templateInput.getParameters()) + .setParameterGroups(templateInput.getParameterGroups()) + .setConditions(templateInput.getConditions()); + if (templateInput.getVersion() != null) { + templateResponse.setVersion(new TemplateResponse.VersionResponse() + .setDescription(templateInput.getVersion().getDescription())); + } + return new Template(templateResponse).setETag(templateInput.getEtag()); + } + /** * Gets the ETag of the template. * @@ -177,6 +216,24 @@ public Template setVersion(Version version) { return this; } + /** + * Gets the JSON-serializable representation of this template. + * + * @return A JSON-serializable representation of this {@link Template} instance. + */ + public String toJSON() { + String jsonSerialization; + Gson gson = new GsonBuilder() + .registerTypeAdapter(ParameterValue.InAppDefault.class, new InAppDefaultAdapter()) + .create(); + try { + jsonSerialization = gson.toJson(this); + } catch (JsonSyntaxException e) { + throw new RuntimeException(e); + } + return jsonSerialization; + } + Template setETag(String etag) { this.etag = etag; return this; @@ -224,4 +281,15 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(etag, parameters, conditions, parameterGroups, version); } + + private static class InAppDefaultAdapter implements JsonSerializer { + + @Override + public JsonElement serialize(ParameterValue.InAppDefault src, Type typeOfSrc, + JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.addProperty("useInAppDefault", true); + return obj; + } + } } diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateInput.java b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateInput.java new file mode 100644 index 000000000..420d0b2e5 --- /dev/null +++ b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateInput.java @@ -0,0 +1,75 @@ +/* + * 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 com.google.api.client.util.Key; + +import java.util.List; +import java.util.Map; + +/** + * The Data Transfer Object for parsing Remote Config template from input json strings. + **/ +public final class TemplateInput { + + @Key("parameters") + private Map parameters; + + @Key("conditions") + private List conditions; + + @Key("parameterGroups") + private Map parameterGroups; + + @Key("version") + private VersionInput version; + + @Key("etag") + private String etag; + + public Map getParameters() { + return parameters; + } + + public List getConditions() { + return conditions; + } + + public Map getParameterGroups() { + return parameterGroups; + } + + public VersionInput getVersion() { + return version; + } + + public String getEtag() { + return etag; + } + + /** + * The Data Transfer Object for parsing Remote Config version from input json strings. + **/ + public static final class VersionInput { + @Key("description") + private String description; + + public String getDescription() { + return description; + } + } +} diff --git a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java index 57f5a2116..c77e5e037 100644 --- a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java @@ -24,7 +24,9 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.testing.TestUtils; +import java.io.IOException; import java.util.List; import java.util.Map; @@ -78,6 +80,9 @@ public class TemplateTest { private static final Template TEMPLATE_WITH_VERSION = new Template() .setVersion(Version.withDescription("promo version")); + private static final String TEMPLATE_STRING = TestUtils + .loadResource("rcTemplateWithETag.json"); + @Test public void testConstructor() { Template template = new Template(); @@ -153,4 +158,150 @@ public void testEquality() { assertNotEquals(templateThree, templateFive); assertNotEquals(templateFour, templateFive); } + + @Test(expected = IOException.class) + public void testFromJSONWithInvalidString() throws IOException { + Template.fromJSON("abc"); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromJSONWithEmptyString() throws IOException { + Template.fromJSON(""); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromJSONWithNullString() throws IOException { + Template.fromJSON(null); + } + + @Test + public void testFromJSON() throws IOException { + Template template = Template.fromJSON("{}"); + + assertNotNull(template.getParameters()); + assertNotNull(template.getConditions()); + assertNotNull(template.getParameterGroups()); + assertTrue(template.getParameters().isEmpty()); + assertTrue(template.getConditions().isEmpty()); + assertTrue(template.getParameterGroups().isEmpty()); + assertNull(template.getETag()); + + template = Template.fromJSON("{" + + " \"etag\": \"etag-001234\"," + + " \"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']\"" + + " }" + + " ]" + + "}"); + + assertNotNull(template.getParameters()); + assertNotNull(template.getConditions()); + assertNotNull(template.getParameterGroups()); + assertTrue(template.getParameters().isEmpty()); + assertEquals(2, template.getConditions().size()); + assertEquals("ios_en", template.getConditions().get(0).getName()); + assertEquals("device.os == 'ios' && device.country in ['us', 'uk']", + template.getConditions().get(0).getExpression()); + assertEquals(TagColor.INDIGO, template.getConditions().get(0).getTagColor()); + assertEquals("android_en", template.getConditions().get(1).getName()); + assertEquals("device.os == 'android' && device.country in ['us', 'uk']", + template.getConditions().get(1).getExpression()); + assertEquals(TagColor.UNSPECIFIED, template.getConditions().get(1).getTagColor()); + assertTrue(template.getParameterGroups().isEmpty()); + assertEquals("etag-001234", template.getETag()); + } + + @Test + public void testToJSON() { + // Empty template + String jsonString = new Template().toJSON(); + + assertEquals("{\"parameters\":{},\"conditions\":[]," + + "\"parameterGroups\":{}}", jsonString); + + // Template with parameter values + Template t = new Template(); + t.getParameters() + .put("with_value", new Parameter().setDefaultValue(ParameterValue.of("hello"))); + t.getParameters() + .put("with_inApp", new Parameter().setDefaultValue(ParameterValue.inAppDefault())); + jsonString = t.toJSON(); + + assertEquals("{\"parameters\":{\"with_value\":{\"defaultValue\":{\"value\":\"hello\"}," + + "\"conditionalValues\":{}},\"with_inApp\":{\"defaultValue\":" + + "{\"useInAppDefault\":true},\"conditionalValues\":{}}},\"conditions\":[]," + + "\"parameterGroups\":{}}", jsonString); + + // Template with etag + jsonString = new Template().setETag("etag-12345").toJSON(); + + assertEquals("{\"etag\":\"etag-12345\",\"parameters\":{},\"conditions\":[]," + + "\"parameterGroups\":{}}", jsonString); + + // Template with etag and conditions + jsonString = new Template() + .setETag("etag-0010201") + .setConditions(CONDITIONS).toJSON(); + + assertEquals("{\"etag\":\"etag-0010201\",\"parameters\":{}," + + "\"conditions\":[{\"name\":\"ios_en\",\"expression\":\"exp ios\"," + + "\"tagColor\":\"INDIGO\"},{\"name\":\"android_en\"," + + "\"expression\":\"exp android\"}]," + + "\"parameterGroups\":{}}", jsonString); + + // Complete template + jsonString = new Template() + .setETag("etag-0010201") + .setParameters(PARAMETERS) + .setConditions(CONDITIONS) + .setParameterGroups(PARAMETER_GROUPS) + .setVersion(Version.withDescription("promo version")) + .toJSON(); + + assertEquals(TEMPLATE_STRING, jsonString); + } + + @Test + public void testToJSONAndFromJSON() throws IOException { + String jsonString = new Template().toJSON(); + Template template = Template.fromJSON(jsonString); + + assertNotNull(template.getParameters()); + assertNotNull(template.getConditions()); + assertNotNull(template.getParameterGroups()); + assertTrue(template.getParameters().isEmpty()); + assertTrue(template.getConditions().isEmpty()); + assertTrue(template.getParameterGroups().isEmpty()); + assertNull(template.getETag()); + + Version expectedVersion = Version.withDescription("promo version"); + jsonString = new Template() + .setETag("etag-0010201") + .setParameters(PARAMETERS) + .setConditions(CONDITIONS) + .setParameterGroups(PARAMETER_GROUPS) + .setVersion(expectedVersion) + .toJSON(); + template = Template.fromJSON(jsonString); + + assertEquals("etag-0010201", template.getETag()); + assertEquals(PARAMETERS, template.getParameters()); + assertEquals(PARAMETER_GROUPS, template.getParameterGroups()); + assertEquals(expectedVersion, template.getVersion()); + // check conditions + assertEquals(2, template.getConditions().size()); + assertEquals("ios_en", template.getConditions().get(0).getName()); + assertEquals("exp ios", template.getConditions().get(0).getExpression()); + assertEquals(TagColor.INDIGO, template.getConditions().get(0).getTagColor()); + assertEquals("android_en", template.getConditions().get(1).getName()); + assertEquals("exp android", template.getConditions().get(1).getExpression()); + assertEquals(TagColor.UNSPECIFIED, template.getConditions().get(1).getTagColor()); + } } diff --git a/src/test/resources/rcTemplateWithETag.json b/src/test/resources/rcTemplateWithETag.json new file mode 100644 index 000000000..11ec4f62a --- /dev/null +++ b/src/test/resources/rcTemplateWithETag.json @@ -0,0 +1 @@ +{"etag":"etag-0010201","parameters":{"greeting_header":{"defaultValue":{"useInAppDefault":true},"description":"greeting header text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}},"greeting_text":{"defaultValue":{"useInAppDefault":true},"description":"greeting text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}}},"conditions":[{"name":"ios_en","expression":"exp ios","tagColor":"INDIGO"},{"name":"android_en","expression":"exp android"}],"parameterGroups":{"greetings_group":{"description":"description","parameters":{"greeting_header":{"defaultValue":{"useInAppDefault":true},"description":"greeting header text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}},"greeting_text":{"defaultValue":{"useInAppDefault":true},"description":"greeting text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}}}}},"version":{"updateTime":0,"legacy":false,"description":"promo version"}} \ No newline at end of file From da323994ac5c0ffc95bc64b2d7f1040e45a0f77b Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 8 Dec 2020 12:28:00 -0500 Subject: [PATCH 02/10] Update version update time serialization --- .../remoteconfig/RemoteConfigUtil.java | 53 +++++++++++++ .../firebase/remoteconfig/Template.java | 34 ++++++--- .../google/firebase/remoteconfig/Version.java | 8 +- .../remoteconfig/internal/TemplateInput.java | 75 ------------------- .../internal/TemplateResponse.java | 7 ++ .../firebase/remoteconfig/TemplateTest.java | 56 ++++++++++++++ src/test/resources/rcTemplateWithETag.json | 2 +- 7 files changed, 140 insertions(+), 95 deletions(-) delete mode 100644 src/main/java/com/google/firebase/remoteconfig/internal/TemplateInput.java diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java index 13a49076a..18c22a7ae 100644 --- a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java +++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java @@ -20,6 +20,7 @@ import com.google.common.base.Strings; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; @@ -30,10 +31,62 @@ static boolean isValidVersionNumber(String versionNumber) { return !Strings.isNullOrEmpty(versionNumber) && versionNumber.matches("^\\d+$"); } + static long convertToMilliseconds(String dateString) throws ParseException { + if (isUTCDateString(dateString)) { + return convertFromUtcDateFormat(dateString); + } + return convertFromUtcZuluFormat(dateString); + } + static String convertToUtcZuluFormat(long millis) { + // sample output date string: 2020-12-08T15:49:51.887878Z checkArgument(millis >= 0, "Milliseconds duration must not be negative"); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS000000'Z'"); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); return dateFormat.format(new Date(millis)); } + + static String convertToUtcDateFormat(long millis) { + // sample output date string: Tue, 08 Dec 2020 15:49:51 GMT + checkArgument(millis >= 0, "Milliseconds duration must not be negative"); + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return dateFormat.format(new Date(millis)); + } + + static long convertFromUtcZuluFormat(String dateString) throws ParseException { + // sample input date string: 2020-12-08T15:49:51.887878Z + checkArgument(!Strings.isNullOrEmpty(dateString), "Date string must not be null or empty"); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return dateFormat.parse(dateString).getTime(); + } + + static long convertFromUtcDateFormat(String dateString) throws ParseException { + // sample input date string: Tue, 08 Dec 2020 15:49:51 GMT + checkArgument(!Strings.isNullOrEmpty(dateString), "Date string must not be null or empty"); + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return dateFormat.parse(dateString).getTime(); + } + + static boolean isUTCDateString(String dateString) { + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); + try { + dateFormat.parse(dateString); + return true; + } catch (ParseException e) { + return false; + } + } + + static boolean isUTCDateStringInZuluFormat(String dateString) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS000000'Z'"); + try { + dateFormat.parse(dateString); + return true; + } catch (ParseException e) { + return false; + } + } } diff --git a/src/main/java/com/google/firebase/remoteconfig/Template.java b/src/main/java/com/google/firebase/remoteconfig/Template.java index 90826475e..e9c1a1639 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Template.java +++ b/src/main/java/com/google/firebase/remoteconfig/Template.java @@ -23,9 +23,7 @@ import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonParser; import com.google.common.base.Strings; -import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.internal.NonNull; -import com.google.firebase.remoteconfig.internal.TemplateInput; import com.google.firebase.remoteconfig.internal.TemplateResponse; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -89,6 +87,7 @@ public Template() { if (templateResponse.getVersion() != null) { this.version = new Version(templateResponse.getVersion()); } + this.etag = templateResponse.getEtag(); } /** @@ -102,16 +101,8 @@ public static Template fromJSON(@NonNull String json) throws IOException { checkArgument(!Strings.isNullOrEmpty(json), "JSON String must not be null or empty."); JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); JsonParser parser = jsonFactory.createJsonParser(json); - TemplateInput templateInput = parser.parseAndClose(TemplateInput.class); - TemplateResponse templateResponse = new TemplateResponse() - .setParameters(templateInput.getParameters()) - .setParameterGroups(templateInput.getParameterGroups()) - .setConditions(templateInput.getConditions()); - if (templateInput.getVersion() != null) { - templateResponse.setVersion(new TemplateResponse.VersionResponse() - .setDescription(templateInput.getVersion().getDescription())); - } - return new Template(templateResponse).setETag(templateInput.getEtag()); + TemplateResponse templateResponse = parser.parseAndClose(TemplateResponse.class); + return new Template(templateResponse); } /** @@ -225,6 +216,7 @@ public String toJSON() { String jsonSerialization; Gson gson = new GsonBuilder() .registerTypeAdapter(ParameterValue.InAppDefault.class, new InAppDefaultAdapter()) + .registerTypeAdapter(Version.class, new VersionAdapter()) .create(); try { jsonSerialization = gson.toJson(this); @@ -292,4 +284,22 @@ public JsonElement serialize(ParameterValue.InAppDefault src, Type typeOfSrc, return obj; } } + + private static class VersionAdapter implements JsonSerializer { + + @Override + public JsonElement serialize(Version src, Type typeOfSrc, + JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.addProperty("versionNumber", src.getVersionNumber()); + obj.addProperty("updateTime", RemoteConfigUtil.convertToUtcDateFormat(src.getUpdateTime())); + obj.addProperty("updateOrigin", src.getUpdateOrigin()); + obj.addProperty("updateType", src.getUpdateType()); + obj.add("updateUser", context.serialize(src.getUpdateUser())); + obj.addProperty("rollbackSource", src.getRollbackSource()); + obj.addProperty("legacy", src.isLegacy()); + obj.addProperty("description", src.getDescription()); + return obj; + } + } } diff --git a/src/main/java/com/google/firebase/remoteconfig/Version.java b/src/main/java/com/google/firebase/remoteconfig/Version.java index e3c182457..1e61f8516 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Version.java +++ b/src/main/java/com/google/firebase/remoteconfig/Version.java @@ -25,11 +25,7 @@ import com.google.firebase.remoteconfig.internal.TemplateResponse.VersionResponse; import java.text.ParseException; -import java.text.SimpleDateFormat; import java.util.Objects; -import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * Represents a Remote Config template version. @@ -72,10 +68,8 @@ private Version() { if (indexOfPeriod != -1) { updateTime = updateTime.substring(0, indexOfPeriod); } - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); try { - this.updateTime = dateFormat.parse(updateTime).getTime(); + this.updateTime = RemoteConfigUtil.convertToMilliseconds(updateTime); } catch (ParseException e) { throw new IllegalStateException("Unable to parse update time.", e); } diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateInput.java b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateInput.java deleted file mode 100644 index 420d0b2e5..000000000 --- a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateInput.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 com.google.api.client.util.Key; - -import java.util.List; -import java.util.Map; - -/** - * The Data Transfer Object for parsing Remote Config template from input json strings. - **/ -public final class TemplateInput { - - @Key("parameters") - private Map parameters; - - @Key("conditions") - private List conditions; - - @Key("parameterGroups") - private Map parameterGroups; - - @Key("version") - private VersionInput version; - - @Key("etag") - private String etag; - - public Map getParameters() { - return parameters; - } - - public List getConditions() { - return conditions; - } - - public Map getParameterGroups() { - return parameterGroups; - } - - public VersionInput getVersion() { - return version; - } - - public String getEtag() { - return etag; - } - - /** - * The Data Transfer Object for parsing Remote Config version from input json strings. - **/ - public static final class VersionInput { - @Key("description") - private String description; - - public String getDescription() { - return description; - } - } -} diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java index b89abf1eb..b62e8e2df 100644 --- a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java +++ b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java @@ -39,6 +39,9 @@ public final class TemplateResponse { @Key("version") private VersionResponse version; + @Key("etag") + private String etag; + public Map getParameters() { return parameters; } @@ -55,6 +58,10 @@ public VersionResponse getVersion() { return version; } + public String getEtag() { + return etag; + } + public TemplateResponse setParameters( Map parameters) { this.parameters = parameters; diff --git a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java index c77e5e037..f1412736e 100644 --- a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.firebase.remoteconfig.internal.TemplateResponse; import com.google.firebase.testing.TestUtils; import java.io.IOException; @@ -218,6 +219,39 @@ public void testFromJSON() throws IOException { assertEquals("etag-001234", template.getETag()); } + @Test + public void testFromJSONWithVersion() throws IOException { + final Version expectedVersion = new Version(new TemplateResponse.VersionResponse() + .setDescription("template version") + .setUpdateTime("2020-12-08T15:49:51.887878Z") + .setUpdateUser(new TemplateResponse.UserResponse().setEmail("user@user.com")) + .setLegacy(false) + .setUpdateType("INCREMENTAL_UPDATE") + .setRollbackSource("26") + .setVersionNumber("34") + .setUpdateOrigin("ADMIN_SDK_NODE") + ); + String jsonString = "{\"parameters\":{},\"conditions\":[],\"parameterGroups\":{}," + + "\"version\":{\"versionNumber\":\"34\"," + + "\"updateTime\":\"Tue, 08 Dec 2020 15:49:51 UTC\"," + + "\"updateOrigin\":\"ADMIN_SDK_NODE\",\"updateType\":\"INCREMENTAL_UPDATE\"," + + "\"updateUser\":{\"email\":\"user@user.com\"},\"rollbackSource\":\"26\"," + + "\"legacy\":false,\"description\":\"template version\"}}"; + Template template = Template.fromJSON(jsonString); + + assertNotNull(template.getParameters()); + assertNotNull(template.getConditions()); + assertNotNull(template.getParameterGroups()); + assertTrue(template.getParameters().isEmpty()); + assertTrue(template.getConditions().isEmpty()); + assertTrue(template.getParameterGroups().isEmpty()); + assertNull(template.getETag()); + // check version + assertEquals(expectedVersion, template.getVersion()); + // update time should be correctly converted to milliseconds + assertEquals(1607442591000L, template.getVersion().getUpdateTime()); + } + @Test public void testToJSON() { // Empty template @@ -268,6 +302,28 @@ public void testToJSON() { assertEquals(TEMPLATE_STRING, jsonString); } + @Test + public void testToJSONWithVersion() { + Version version = new Version(new TemplateResponse.VersionResponse() + .setDescription("template version") + .setUpdateTime("2020-12-08T15:49:51.887878Z") + .setUpdateUser(new TemplateResponse.UserResponse().setEmail("user@user.com")) + .setLegacy(false) + .setUpdateType("INCREMENTAL_UPDATE") + .setRollbackSource("26") + .setVersionNumber("34") + .setUpdateOrigin("ADMIN_SDK_NODE") + ); + String jsonString = new Template().setVersion(version).toJSON(); + + assertEquals("{\"parameters\":{},\"conditions\":[],\"parameterGroups\":{}," + + "\"version\":{\"versionNumber\":\"34\"," + + "\"updateTime\":\"Tue, 08 Dec 2020 15:49:51 UTC\"," + + "\"updateOrigin\":\"ADMIN_SDK_NODE\",\"updateType\":\"INCREMENTAL_UPDATE\"," + + "\"updateUser\":{\"email\":\"user@user.com\"},\"rollbackSource\":\"26\"," + + "\"legacy\":false,\"description\":\"template version\"}}", jsonString); + } + @Test public void testToJSONAndFromJSON() throws IOException { String jsonString = new Template().toJSON(); diff --git a/src/test/resources/rcTemplateWithETag.json b/src/test/resources/rcTemplateWithETag.json index 11ec4f62a..7b6f6ac0b 100644 --- a/src/test/resources/rcTemplateWithETag.json +++ b/src/test/resources/rcTemplateWithETag.json @@ -1 +1 @@ -{"etag":"etag-0010201","parameters":{"greeting_header":{"defaultValue":{"useInAppDefault":true},"description":"greeting header text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}},"greeting_text":{"defaultValue":{"useInAppDefault":true},"description":"greeting text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}}},"conditions":[{"name":"ios_en","expression":"exp ios","tagColor":"INDIGO"},{"name":"android_en","expression":"exp android"}],"parameterGroups":{"greetings_group":{"description":"description","parameters":{"greeting_header":{"defaultValue":{"useInAppDefault":true},"description":"greeting header text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}},"greeting_text":{"defaultValue":{"useInAppDefault":true},"description":"greeting text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}}}}},"version":{"updateTime":0,"legacy":false,"description":"promo version"}} \ No newline at end of file +{"etag":"etag-0010201","parameters":{"greeting_header":{"defaultValue":{"useInAppDefault":true},"description":"greeting header text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}},"greeting_text":{"defaultValue":{"useInAppDefault":true},"description":"greeting text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}}},"conditions":[{"name":"ios_en","expression":"exp ios","tagColor":"INDIGO"},{"name":"android_en","expression":"exp android"}],"parameterGroups":{"greetings_group":{"description":"description","parameters":{"greeting_header":{"defaultValue":{"useInAppDefault":true},"description":"greeting header text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}},"greeting_text":{"defaultValue":{"useInAppDefault":true},"description":"greeting text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}}}}},"version":{"updateTime":"Thu, 01 Jan 1970 00:00:00 UTC","legacy":false,"description":"promo version"}} \ No newline at end of file From b42051cd2b545f12d46d0577a245de34c8c21185 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 8 Dec 2020 16:46:57 -0500 Subject: [PATCH 03/10] Use Response types to serialize template --- .../FirebaseRemoteConfigClientImpl.java | 2 +- .../firebase/remoteconfig/Template.java | 56 +++---------------- .../google/firebase/remoteconfig/User.java | 7 +++ .../google/firebase/remoteconfig/Version.java | 14 ++++- .../internal/TemplateResponse.java | 5 ++ .../firebase/remoteconfig/TemplateTest.java | 35 ++++++------ src/test/resources/rcTemplateWithETag.json | 2 +- 7 files changed, 52 insertions(+), 69 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java index aff5439ac..7425673fb 100644 --- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java +++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java @@ -121,7 +121,7 @@ public Template publishTemplate(@NonNull Template template, boolean validateOnly boolean forcePublish) throws FirebaseRemoteConfigException { checkArgument(template != null, "Template must not be null."); HttpRequestInfo request = HttpRequestInfo.buildRequest("PUT", remoteConfigUrl, - new JsonHttpContent(jsonFactory, template.toTemplateResponse())) + new JsonHttpContent(jsonFactory, template.toTemplateResponse(false))) .addAllHeaders(COMMON_HEADERS) .addHeader("If-Match", forcePublish ? "*" : template.getETag()); if (validateOnly) { diff --git a/src/main/java/com/google/firebase/remoteconfig/Template.java b/src/main/java/com/google/firebase/remoteconfig/Template.java index e9c1a1639..b4a613e3a 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Template.java +++ b/src/main/java/com/google/firebase/remoteconfig/Template.java @@ -25,16 +25,8 @@ import com.google.common.base.Strings; import com.google.firebase.internal.NonNull; import com.google.firebase.remoteconfig.internal.TemplateResponse; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import com.google.gson.JsonSyntaxException; import java.io.IOException; -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -99,6 +91,7 @@ public Template() { */ public static Template fromJSON(@NonNull String json) throws IOException { 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 = Utils.getDefaultJsonFactory(); JsonParser parser = jsonFactory.createJsonParser(json); TemplateResponse templateResponse = parser.parseAndClose(TemplateResponse.class); @@ -213,17 +206,12 @@ public Template setVersion(Version version) { * @return A JSON-serializable representation of this {@link Template} instance. */ public String toJSON() { - String jsonSerialization; - Gson gson = new GsonBuilder() - .registerTypeAdapter(ParameterValue.InAppDefault.class, new InAppDefaultAdapter()) - .registerTypeAdapter(Version.class, new VersionAdapter()) - .create(); + JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); try { - jsonSerialization = gson.toJson(this); - } catch (JsonSyntaxException e) { + return jsonFactory.toString(this.toTemplateResponse(true)); + } catch (IOException e) { throw new RuntimeException(e); } - return jsonSerialization; } Template setETag(String etag) { @@ -231,7 +219,7 @@ Template setETag(String etag) { return this; } - TemplateResponse toTemplateResponse() { + TemplateResponse toTemplateResponse(boolean includeAll) { Map parameterResponses = new HashMap<>(); for (Map.Entry entry : this.parameters.entrySet()) { parameterResponses.put(entry.getKey(), entry.getValue().toParameterResponse()); @@ -245,12 +233,13 @@ TemplateResponse toTemplateResponse() { parameterGroupResponse.put(entry.getKey(), entry.getValue().toParameterGroupResponse()); } TemplateResponse.VersionResponse versionResponse = (this.version == null) ? null - : this.version.toVersionResponse(); + : this.version.toVersionResponse(includeAll); return new TemplateResponse() .setParameters(parameterResponses) .setConditions(conditionResponses) .setParameterGroups(parameterGroupResponse) - .setVersion(versionResponse); + .setVersion(versionResponse) + .setEtag(includeAll ? this.etag : null); } @Override @@ -273,33 +262,4 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(etag, parameters, conditions, parameterGroups, version); } - - private static class InAppDefaultAdapter implements JsonSerializer { - - @Override - public JsonElement serialize(ParameterValue.InAppDefault src, Type typeOfSrc, - JsonSerializationContext context) { - JsonObject obj = new JsonObject(); - obj.addProperty("useInAppDefault", true); - return obj; - } - } - - private static class VersionAdapter implements JsonSerializer { - - @Override - public JsonElement serialize(Version src, Type typeOfSrc, - JsonSerializationContext context) { - JsonObject obj = new JsonObject(); - obj.addProperty("versionNumber", src.getVersionNumber()); - obj.addProperty("updateTime", RemoteConfigUtil.convertToUtcDateFormat(src.getUpdateTime())); - obj.addProperty("updateOrigin", src.getUpdateOrigin()); - obj.addProperty("updateType", src.getUpdateType()); - obj.add("updateUser", context.serialize(src.getUpdateUser())); - obj.addProperty("rollbackSource", src.getRollbackSource()); - obj.addProperty("legacy", src.isLegacy()); - obj.addProperty("description", src.getDescription()); - return obj; - } - } } diff --git a/src/main/java/com/google/firebase/remoteconfig/User.java b/src/main/java/com/google/firebase/remoteconfig/User.java index ae21328b0..264fe8b63 100644 --- a/src/main/java/com/google/firebase/remoteconfig/User.java +++ b/src/main/java/com/google/firebase/remoteconfig/User.java @@ -88,4 +88,11 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(email, name, imageUrl); } + + UserResponse toUserResponse() { + return new UserResponse() + .setEmail(this.email) + .setImageUrl(this.imageUrl) + .setName(this.name); + } } diff --git a/src/main/java/com/google/firebase/remoteconfig/Version.java b/src/main/java/com/google/firebase/remoteconfig/Version.java index 1e61f8516..adf6e6a7e 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Version.java +++ b/src/main/java/com/google/firebase/remoteconfig/Version.java @@ -190,8 +190,20 @@ public Version setDescription(String description) { return this; } - VersionResponse toVersionResponse() { + VersionResponse toVersionResponse(boolean includeAll) { + if (!includeAll) { + return new VersionResponse() + .setDescription(this.description); + } return new VersionResponse() + .setUpdateTime(this.updateTime > 0L ? RemoteConfigUtil + .convertToUtcDateFormat(this.updateTime) : null) + .setLegacy(this.legacy) + .setRollbackSource(this.rollbackSource) + .setUpdateOrigin(this.updateOrigin) + .setUpdateType(this.updateType) + .setUpdateUser((this.updateUser == null) ? null : this.updateUser.toUserResponse()) + .setVersionNumber(this.versionNumber) .setDescription(this.description); } diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java index b62e8e2df..7a31f73ca 100644 --- a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java +++ b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java @@ -85,6 +85,11 @@ public TemplateResponse setVersion(VersionResponse version) { return this; } + public TemplateResponse setEtag(String etag) { + this.etag = etag; + return this; + } + /** * The Data Transfer Object for parsing Remote Config parameter responses from the * Remote Config service. diff --git a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java index f1412736e..c8472d94e 100644 --- a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java @@ -257,8 +257,8 @@ public void testToJSON() { // Empty template String jsonString = new Template().toJSON(); - assertEquals("{\"parameters\":{},\"conditions\":[]," - + "\"parameterGroups\":{}}", jsonString); + assertEquals("{\"conditions\":[]," + + "\"parameterGroups\":{},\"parameters\":{}}", jsonString); // Template with parameter values Template t = new Template(); @@ -268,27 +268,26 @@ public void testToJSON() { .put("with_inApp", new Parameter().setDefaultValue(ParameterValue.inAppDefault())); jsonString = t.toJSON(); - assertEquals("{\"parameters\":{\"with_value\":{\"defaultValue\":{\"value\":\"hello\"}," - + "\"conditionalValues\":{}},\"with_inApp\":{\"defaultValue\":" - + "{\"useInAppDefault\":true},\"conditionalValues\":{}}},\"conditions\":[]," - + "\"parameterGroups\":{}}", jsonString); + assertEquals("{\"conditions\":[],\"parameterGroups\":{}," + + "\"parameters\":{\"with_value\":{\"conditionalValues\":{}," + + "\"defaultValue\":{\"value\":\"hello\"}},\"with_inApp\":{\"conditionalValues\":{}," + + "\"defaultValue\":{\"useInAppDefault\":true}}}}", jsonString); // Template with etag jsonString = new Template().setETag("etag-12345").toJSON(); - assertEquals("{\"etag\":\"etag-12345\",\"parameters\":{},\"conditions\":[]," - + "\"parameterGroups\":{}}", jsonString); + assertEquals("{\"conditions\":[],\"etag\":\"etag-12345\",\"parameterGroups\":{}," + + "\"parameters\":{}}", jsonString); // Template with etag and conditions jsonString = new Template() .setETag("etag-0010201") .setConditions(CONDITIONS).toJSON(); - assertEquals("{\"etag\":\"etag-0010201\",\"parameters\":{}," - + "\"conditions\":[{\"name\":\"ios_en\",\"expression\":\"exp ios\"," - + "\"tagColor\":\"INDIGO\"},{\"name\":\"android_en\"," - + "\"expression\":\"exp android\"}]," - + "\"parameterGroups\":{}}", jsonString); + assertEquals("{\"conditions\":[{\"expression\":\"exp ios\",\"name\":\"ios_en\"," + + "\"tagColor\":\"INDIGO\"},{\"expression\":\"exp android\"," + + "\"name\":\"android_en\"}],\"etag\":\"etag-0010201\",\"parameterGroups\":{}," + + "\"parameters\":{}}", jsonString); // Complete template jsonString = new Template() @@ -316,12 +315,12 @@ public void testToJSONWithVersion() { ); String jsonString = new Template().setVersion(version).toJSON(); - assertEquals("{\"parameters\":{},\"conditions\":[],\"parameterGroups\":{}," - + "\"version\":{\"versionNumber\":\"34\"," + assertEquals("{\"conditions\":[],\"parameterGroups\":{},\"parameters\":{}," + + "\"version\":{\"description\":\"template version\",\"legacy\":false," + + "\"rollbackSource\":\"26\",\"updateOrigin\":\"ADMIN_SDK_NODE\"," + "\"updateTime\":\"Tue, 08 Dec 2020 15:49:51 UTC\"," - + "\"updateOrigin\":\"ADMIN_SDK_NODE\",\"updateType\":\"INCREMENTAL_UPDATE\"," - + "\"updateUser\":{\"email\":\"user@user.com\"},\"rollbackSource\":\"26\"," - + "\"legacy\":false,\"description\":\"template version\"}}", jsonString); + + "\"updateType\":\"INCREMENTAL_UPDATE\",\"updateUser\":{" + + "\"email\":\"user@user.com\"},\"versionNumber\":\"34\"}}", jsonString); } @Test diff --git a/src/test/resources/rcTemplateWithETag.json b/src/test/resources/rcTemplateWithETag.json index 7b6f6ac0b..e278ee25f 100644 --- a/src/test/resources/rcTemplateWithETag.json +++ b/src/test/resources/rcTemplateWithETag.json @@ -1 +1 @@ -{"etag":"etag-0010201","parameters":{"greeting_header":{"defaultValue":{"useInAppDefault":true},"description":"greeting header text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}},"greeting_text":{"defaultValue":{"useInAppDefault":true},"description":"greeting text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}}},"conditions":[{"name":"ios_en","expression":"exp ios","tagColor":"INDIGO"},{"name":"android_en","expression":"exp android"}],"parameterGroups":{"greetings_group":{"description":"description","parameters":{"greeting_header":{"defaultValue":{"useInAppDefault":true},"description":"greeting header text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}},"greeting_text":{"defaultValue":{"useInAppDefault":true},"description":"greeting text","conditionalValues":{"ios":{"value":"hello ios"},"android":{"value":"hello android"},"promo":{"useInAppDefault":true}}}}}},"version":{"updateTime":"Thu, 01 Jan 1970 00:00:00 UTC","legacy":false,"description":"promo version"}} \ No newline at end of file +{"conditions":[{"expression":"exp ios","name":"ios_en","tagColor":"INDIGO"},{"expression":"exp android","name":"android_en"}],"etag":"etag-0010201","parameterGroups":{"greetings_group":{"description":"description","parameters":{"greeting_text":{"conditionalValues":{"promo":{"useInAppDefault":true},"android":{"value":"hello android"},"ios":{"value":"hello ios"}},"defaultValue":{"useInAppDefault":true},"description":"greeting text"},"greeting_header":{"conditionalValues":{"promo":{"useInAppDefault":true},"android":{"value":"hello android"},"ios":{"value":"hello ios"}},"defaultValue":{"useInAppDefault":true},"description":"greeting header text"}}}},"parameters":{"greeting_text":{"conditionalValues":{"promo":{"useInAppDefault":true},"android":{"value":"hello android"},"ios":{"value":"hello ios"}},"defaultValue":{"useInAppDefault":true},"description":"greeting text"},"greeting_header":{"conditionalValues":{"promo":{"useInAppDefault":true},"android":{"value":"hello android"},"ios":{"value":"hello ios"}},"defaultValue":{"useInAppDefault":true},"description":"greeting header text"}},"version":{"description":"promo version","legacy":false}} \ No newline at end of file From 8d92f733542f8765b0da375cf9b689eb09181c2e Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 8 Dec 2020 17:06:53 -0500 Subject: [PATCH 04/10] Remove json string equality check to prevent failures with child reordering --- .../google/firebase/remoteconfig/TemplateTest.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java index c8472d94e..91568fb3c 100644 --- a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java @@ -288,17 +288,6 @@ public void testToJSON() { + "\"tagColor\":\"INDIGO\"},{\"expression\":\"exp android\"," + "\"name\":\"android_en\"}],\"etag\":\"etag-0010201\",\"parameterGroups\":{}," + "\"parameters\":{}}", jsonString); - - // Complete template - jsonString = new Template() - .setETag("etag-0010201") - .setParameters(PARAMETERS) - .setConditions(CONDITIONS) - .setParameterGroups(PARAMETER_GROUPS) - .setVersion(Version.withDescription("promo version")) - .toJSON(); - - assertEquals(TEMPLATE_STRING, jsonString); } @Test From ca4907df9f52747f170f920278d82466ec79a123 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 8 Dec 2020 17:17:00 -0500 Subject: [PATCH 05/10] Remove unused time validation function --- .../google/firebase/remoteconfig/RemoteConfigUtil.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java index 18c22a7ae..214dbe1aa 100644 --- a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java +++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java @@ -79,14 +79,4 @@ static boolean isUTCDateString(String dateString) { return false; } } - - static boolean isUTCDateStringInZuluFormat(String dateString) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS000000'Z'"); - try { - dateFormat.parse(dateString); - return true; - } catch (ParseException e) { - return false; - } - } } From 4bf780a2f57394ed00a52dd2951e373bc3046df8 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 8 Dec 2020 17:21:13 -0500 Subject: [PATCH 06/10] Improve Version update time unit tests --- .../google/firebase/remoteconfig/VersionTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/com/google/firebase/remoteconfig/VersionTest.java b/src/test/java/com/google/firebase/remoteconfig/VersionTest.java index c0a600445..515629660 100644 --- a/src/test/java/com/google/firebase/remoteconfig/VersionTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/VersionTest.java @@ -38,6 +38,20 @@ public void testConstructorWithInvalidUpdateTime() { .setUpdateTime("sunday,26th")); } + @Test + public void testConstructorWithValidZuluUpdateTime() { + Version version = new Version(new VersionResponse() + .setUpdateTime("2020-12-08T15:49:51.887878Z")); + assertEquals(1607442591000L, version.getUpdateTime()); + } + + @Test + public void testConstructorWithValidUTCUpdateTime() { + Version version = new Version(new VersionResponse() + .setUpdateTime("Tue, 08 Dec 2020 15:49:51 GMT")); + assertEquals(1607442591000L, version.getUpdateTime()); + } + @Test public void testWithDescription() { final Version version = Version.withDescription("version description text"); From 15bf2af6f2cc91af2c969fdda134726ba657dff0 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 9 Dec 2020 01:06:17 -0500 Subject: [PATCH 07/10] Clean up code base --- .../remoteconfig/RemoteConfigUtil.java | 23 +++++++++------- .../firebase/remoteconfig/Template.java | 18 ++++++++++--- .../google/firebase/remoteconfig/Version.java | 6 +++-- .../firebase/remoteconfig/TemplateTest.java | 26 +++++++++++++------ 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java index 214dbe1aa..91cd21ee9 100644 --- a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java +++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java @@ -27,6 +27,11 @@ final class RemoteConfigUtil { + private static final String ZULU_DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS000000'Z'"; + private static final String ZULU_DATE_NO_FRAC_SECS_PATTERN = "yyyy-MM-dd'T'HH:mm:ss"; + private static final String UTC_DATE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; + private static final String UTC_TIME_ZONE_ID = "UTC"; + static boolean isValidVersionNumber(String versionNumber) { return !Strings.isNullOrEmpty(versionNumber) && versionNumber.matches("^\\d+$"); } @@ -41,37 +46,37 @@ static long convertToMilliseconds(String dateString) throws ParseException { static String convertToUtcZuluFormat(long millis) { // sample output date string: 2020-12-08T15:49:51.887878Z checkArgument(millis >= 0, "Milliseconds duration must not be negative"); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS000000'Z'"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + SimpleDateFormat dateFormat = new SimpleDateFormat(ZULU_DATE_PATTERN); + dateFormat.setTimeZone(TimeZone.getTimeZone(UTC_TIME_ZONE_ID)); return dateFormat.format(new Date(millis)); } static String convertToUtcDateFormat(long millis) { // sample output date string: Tue, 08 Dec 2020 15:49:51 GMT checkArgument(millis >= 0, "Milliseconds duration must not be negative"); - SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + SimpleDateFormat dateFormat = new SimpleDateFormat(UTC_DATE_PATTERN); + dateFormat.setTimeZone(TimeZone.getTimeZone(UTC_TIME_ZONE_ID)); return dateFormat.format(new Date(millis)); } static long convertFromUtcZuluFormat(String dateString) throws ParseException { // sample input date string: 2020-12-08T15:49:51.887878Z checkArgument(!Strings.isNullOrEmpty(dateString), "Date string must not be null or empty"); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + SimpleDateFormat dateFormat = new SimpleDateFormat(ZULU_DATE_NO_FRAC_SECS_PATTERN); + dateFormat.setTimeZone(TimeZone.getTimeZone(UTC_TIME_ZONE_ID)); return dateFormat.parse(dateString).getTime(); } static long convertFromUtcDateFormat(String dateString) throws ParseException { // sample input date string: Tue, 08 Dec 2020 15:49:51 GMT checkArgument(!Strings.isNullOrEmpty(dateString), "Date string must not be null or empty"); - SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + SimpleDateFormat dateFormat = new SimpleDateFormat(UTC_DATE_PATTERN); + dateFormat.setTimeZone(TimeZone.getTimeZone(UTC_TIME_ZONE_ID)); return dateFormat.parse(dateString).getTime(); } static boolean isUTCDateString(String dateString) { - SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); + SimpleDateFormat dateFormat = new SimpleDateFormat(UTC_DATE_PATTERN); try { dateFormat.parse(dateString); return true; diff --git a/src/main/java/com/google/firebase/remoteconfig/Template.java b/src/main/java/com/google/firebase/remoteconfig/Template.java index b4a613e3a..bdeb04bf8 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Template.java +++ b/src/main/java/com/google/firebase/remoteconfig/Template.java @@ -46,11 +46,20 @@ public final class Template { /** * Creates a new {@link Template}. + * + * @param etag The ETag of this template. */ - public Template() { - parameters = new HashMap<>(); - conditions = new ArrayList<>(); - parameterGroups = new HashMap<>(); + public Template(String etag) { + this.parameters = new HashMap<>(); + this.conditions = new ArrayList<>(); + this.parameterGroups = new HashMap<>(); + this.etag = etag; + } + + Template() { + this.parameters = new HashMap<>(); + this.conditions = new ArrayList<>(); + this.parameterGroups = new HashMap<>(); } Template(@NonNull TemplateResponse templateResponse) { @@ -84,6 +93,7 @@ public Template() { /** * Creates and returns a new Remote Config template from a JSON string. + * Input JSON string must contain an {@code etag} property to create a valid template. * * @param json A non-null JSON string to populate a Remote Config template. * @return A new {@link Template} instance. diff --git a/src/main/java/com/google/firebase/remoteconfig/Version.java b/src/main/java/com/google/firebase/remoteconfig/Version.java index adf6e6a7e..d2f8bd347 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Version.java +++ b/src/main/java/com/google/firebase/remoteconfig/Version.java @@ -60,9 +60,11 @@ private Version() { this.versionNumber = versionResponse.getVersionNumber(); if (!Strings.isNullOrEmpty(versionResponse.getUpdateTime())) { - // Update Time is a timestamp in RFC3339 UTC "Zulu" format, accurate to nanoseconds. + // Update Time is a timestamp in RFC3339 UTC "Zulu" format, accurate to + // nanoseconds (up to 9 fractional seconds digits). // example: "2014-10-02T15:01:23.045123456Z" - // SimpleDateFormat cannot handle nanoseconds, therefore we strip nanoseconds from the string. + // SimpleDateFormat cannot handle fractional seconds, therefore we strip fractional seconds + // from the date string. String updateTime = versionResponse.getUpdateTime(); int indexOfPeriod = updateTime.indexOf("."); if (indexOfPeriod != -1) { diff --git a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java index 91568fb3c..3390bf6cd 100644 --- a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java @@ -97,9 +97,22 @@ public void testConstructor() { assertNull(template.getETag()); } + @Test + public void testConstructorWithETag() { + Template template = new Template("etag-01-324324"); + + assertNotNull(template.getParameters()); + assertNotNull(template.getConditions()); + assertNotNull(template.getParameterGroups()); + assertTrue(template.getParameters().isEmpty()); + assertTrue(template.getConditions().isEmpty()); + assertTrue(template.getParameterGroups().isEmpty()); + assertEquals("etag-01-324324", template.getETag()); + } + @Test(expected = NullPointerException.class) public void testConstructorWithNullTemplateResponse() { - new Template(null); + new Template((TemplateResponse) null); } @Test(expected = NullPointerException.class) @@ -139,8 +152,7 @@ public void testEquality() { assertEquals(TEMPLATE_WITH_CONDITIONS_PARAMETERS_GROUPS, templateThree); - final Template templateFour = new Template() - .setETag("etag-123456789097-20"); + final Template templateFour = new Template("etag-123456789097-20"); assertEquals(TEMPLATE_WITH_ETAG, templateFour); @@ -274,14 +286,13 @@ public void testToJSON() { + "\"defaultValue\":{\"useInAppDefault\":true}}}}", jsonString); // Template with etag - jsonString = new Template().setETag("etag-12345").toJSON(); + jsonString = new Template("etag-12345").toJSON(); assertEquals("{\"conditions\":[],\"etag\":\"etag-12345\",\"parameterGroups\":{}," + "\"parameters\":{}}", jsonString); // Template with etag and conditions - jsonString = new Template() - .setETag("etag-0010201") + jsonString = new Template("etag-0010201") .setConditions(CONDITIONS).toJSON(); assertEquals("{\"conditions\":[{\"expression\":\"exp ios\",\"name\":\"ios_en\"," @@ -326,8 +337,7 @@ public void testToJSONAndFromJSON() throws IOException { assertNull(template.getETag()); Version expectedVersion = Version.withDescription("promo version"); - jsonString = new Template() - .setETag("etag-0010201") + jsonString = new Template("etag-0010201") .setParameters(PARAMETERS) .setConditions(CONDITIONS) .setParameterGroups(PARAMETER_GROUPS) From 9bfc019df5e122699ac84ff2121a6333a17c3a46 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 9 Dec 2020 15:05:54 -0500 Subject: [PATCH 08/10] PR fixes --- .../remoteconfig/RemoteConfigUtil.java | 25 +++----- .../firebase/remoteconfig/Template.java | 26 +++++--- .../google/firebase/remoteconfig/Version.java | 24 ++++--- .../internal/TemplateResponse.java | 2 + .../firebase/remoteconfig/TemplateTest.java | 64 +++++++++---------- src/test/resources/rcTemplateWithETag.json | 1 - 6 files changed, 68 insertions(+), 74 deletions(-) delete mode 100644 src/test/resources/rcTemplateWithETag.json diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java index 91cd21ee9..bb84122a1 100644 --- a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java +++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java @@ -30,24 +30,25 @@ final class RemoteConfigUtil { private static final String ZULU_DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS000000'Z'"; private static final String ZULU_DATE_NO_FRAC_SECS_PATTERN = "yyyy-MM-dd'T'HH:mm:ss"; private static final String UTC_DATE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; - private static final String UTC_TIME_ZONE_ID = "UTC"; + private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC"); static boolean isValidVersionNumber(String versionNumber) { return !Strings.isNullOrEmpty(versionNumber) && versionNumber.matches("^\\d+$"); } static long convertToMilliseconds(String dateString) throws ParseException { - if (isUTCDateString(dateString)) { + try { + return convertFromUtcZuluFormat(dateString); + } catch (ParseException e) { return convertFromUtcDateFormat(dateString); } - return convertFromUtcZuluFormat(dateString); } static String convertToUtcZuluFormat(long millis) { // sample output date string: 2020-12-08T15:49:51.887878Z checkArgument(millis >= 0, "Milliseconds duration must not be negative"); SimpleDateFormat dateFormat = new SimpleDateFormat(ZULU_DATE_PATTERN); - dateFormat.setTimeZone(TimeZone.getTimeZone(UTC_TIME_ZONE_ID)); + dateFormat.setTimeZone(UTC_TIME_ZONE); return dateFormat.format(new Date(millis)); } @@ -55,7 +56,7 @@ static String convertToUtcDateFormat(long millis) { // sample output date string: Tue, 08 Dec 2020 15:49:51 GMT checkArgument(millis >= 0, "Milliseconds duration must not be negative"); SimpleDateFormat dateFormat = new SimpleDateFormat(UTC_DATE_PATTERN); - dateFormat.setTimeZone(TimeZone.getTimeZone(UTC_TIME_ZONE_ID)); + dateFormat.setTimeZone(UTC_TIME_ZONE); return dateFormat.format(new Date(millis)); } @@ -63,7 +64,7 @@ static long convertFromUtcZuluFormat(String dateString) throws ParseException { // sample input date string: 2020-12-08T15:49:51.887878Z checkArgument(!Strings.isNullOrEmpty(dateString), "Date string must not be null or empty"); SimpleDateFormat dateFormat = new SimpleDateFormat(ZULU_DATE_NO_FRAC_SECS_PATTERN); - dateFormat.setTimeZone(TimeZone.getTimeZone(UTC_TIME_ZONE_ID)); + dateFormat.setTimeZone(UTC_TIME_ZONE); return dateFormat.parse(dateString).getTime(); } @@ -71,17 +72,7 @@ static long convertFromUtcDateFormat(String dateString) throws ParseException { // sample input date string: Tue, 08 Dec 2020 15:49:51 GMT checkArgument(!Strings.isNullOrEmpty(dateString), "Date string must not be null or empty"); SimpleDateFormat dateFormat = new SimpleDateFormat(UTC_DATE_PATTERN); - dateFormat.setTimeZone(TimeZone.getTimeZone(UTC_TIME_ZONE_ID)); + dateFormat.setTimeZone(UTC_TIME_ZONE); return dateFormat.parse(dateString).getTime(); } - - static boolean isUTCDateString(String dateString) { - SimpleDateFormat dateFormat = new SimpleDateFormat(UTC_DATE_PATTERN); - try { - dateFormat.parse(dateString); - return true; - } catch (ParseException e) { - return false; - } - } } diff --git a/src/main/java/com/google/firebase/remoteconfig/Template.java b/src/main/java/com/google/firebase/remoteconfig/Template.java index bdeb04bf8..2b96ebfce 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Template.java +++ b/src/main/java/com/google/firebase/remoteconfig/Template.java @@ -21,8 +21,8 @@ import com.google.api.client.googleapis.util.Utils; import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonParser; import com.google.common.base.Strings; +import com.google.firebase.ErrorCode; import com.google.firebase.internal.NonNull; import com.google.firebase.remoteconfig.internal.TemplateResponse; @@ -97,15 +97,20 @@ public Template(String etag) { * * @param json A non-null JSON string to populate a Remote Config template. * @return A new {@link Template} instance. - * @throws IOException If the input JSON string is not parsable. + * @throws FirebaseRemoteConfigException If the input JSON string is not parsable. */ - public static Template fromJSON(@NonNull String json) throws IOException { + public static Template 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 = Utils.getDefaultJsonFactory(); - JsonParser parser = jsonFactory.createJsonParser(json); - TemplateResponse templateResponse = parser.parseAndClose(TemplateResponse.class); - return new Template(templateResponse); + try { + return new Template(jsonFactory + .createJsonParser(json) + .parseAndClose(TemplateResponse.class)); + } catch (IOException e) { + throw new FirebaseRemoteConfigException(ErrorCode.INVALID_ARGUMENT, + "Unable to parse JSON string."); + } } /** @@ -244,12 +249,15 @@ TemplateResponse toTemplateResponse(boolean includeAll) { } TemplateResponse.VersionResponse versionResponse = (this.version == null) ? null : this.version.toVersionResponse(includeAll); - return new TemplateResponse() + TemplateResponse templateResponse = new TemplateResponse() .setParameters(parameterResponses) .setConditions(conditionResponses) .setParameterGroups(parameterGroupResponse) - .setVersion(versionResponse) - .setEtag(includeAll ? this.etag : null); + .setVersion(versionResponse); + if (includeAll) { + return templateResponse.setEtag(this.etag); + } + return templateResponse; } @Override diff --git a/src/main/java/com/google/firebase/remoteconfig/Version.java b/src/main/java/com/google/firebase/remoteconfig/Version.java index d2f8bd347..2cec65072 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Version.java +++ b/src/main/java/com/google/firebase/remoteconfig/Version.java @@ -193,20 +193,18 @@ public Version setDescription(String description) { } VersionResponse toVersionResponse(boolean includeAll) { - if (!includeAll) { - return new VersionResponse() - .setDescription(this.description); + VersionResponse versionResponse = new VersionResponse().setDescription(this.description); + if (includeAll) { + versionResponse.setUpdateTime(this.updateTime > 0L + ? RemoteConfigUtil.convertToUtcDateFormat(this.updateTime) : null) + .setLegacy(this.legacy) + .setRollbackSource(this.rollbackSource) + .setUpdateOrigin(this.updateOrigin) + .setUpdateType(this.updateType) + .setUpdateUser((this.updateUser == null) ? null : this.updateUser.toUserResponse()) + .setVersionNumber(this.versionNumber); } - return new VersionResponse() - .setUpdateTime(this.updateTime > 0L ? RemoteConfigUtil - .convertToUtcDateFormat(this.updateTime) : null) - .setLegacy(this.legacy) - .setRollbackSource(this.rollbackSource) - .setUpdateOrigin(this.updateOrigin) - .setUpdateType(this.updateType) - .setUpdateUser((this.updateUser == null) ? null : this.updateUser.toUserResponse()) - .setVersionNumber(this.versionNumber) - .setDescription(this.description); + return versionResponse; } @Override diff --git a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java index 7a31f73ca..09e71ef84 100644 --- a/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java +++ b/src/main/java/com/google/firebase/remoteconfig/internal/TemplateResponse.java @@ -39,6 +39,8 @@ public final class TemplateResponse { @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; diff --git a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java index 3390bf6cd..01aa72455 100644 --- a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java @@ -25,9 +25,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firebase.remoteconfig.internal.TemplateResponse; -import com.google.firebase.testing.TestUtils; -import java.io.IOException; import java.util.List; import java.util.Map; @@ -81,9 +79,6 @@ public class TemplateTest { private static final Template TEMPLATE_WITH_VERSION = new Template() .setVersion(Version.withDescription("promo version")); - private static final String TEMPLATE_STRING = TestUtils - .loadResource("rcTemplateWithETag.json"); - @Test public void testConstructor() { Template template = new Template(); @@ -172,23 +167,23 @@ public void testEquality() { assertNotEquals(templateFour, templateFive); } - @Test(expected = IOException.class) - public void testFromJSONWithInvalidString() throws IOException { + @Test(expected = FirebaseRemoteConfigException.class) + public void testFromJSONWithInvalidString() throws FirebaseRemoteConfigException { Template.fromJSON("abc"); } @Test(expected = IllegalArgumentException.class) - public void testFromJSONWithEmptyString() throws IOException { + public void testFromJSONWithEmptyString() throws FirebaseRemoteConfigException { Template.fromJSON(""); } @Test(expected = IllegalArgumentException.class) - public void testFromJSONWithNullString() throws IOException { + public void testFromJSONWithNullString() throws FirebaseRemoteConfigException { Template.fromJSON(null); } @Test - public void testFromJSON() throws IOException { + public void testFromJSONWithEmptyTemplateString() throws FirebaseRemoteConfigException { Template template = Template.fromJSON("{}"); assertNotNull(template.getParameters()); @@ -198,8 +193,11 @@ public void testFromJSON() throws IOException { assertTrue(template.getConditions().isEmpty()); assertTrue(template.getParameterGroups().isEmpty()); assertNull(template.getETag()); + } - template = Template.fromJSON("{" + @Test + public void testFromJSONWithNonEmptyTemplateString() throws FirebaseRemoteConfigException { + Template template = Template.fromJSON("{" + " \"etag\": \"etag-001234\"," + " \"conditions\": [" + " {" @@ -232,7 +230,7 @@ public void testFromJSON() throws IOException { } @Test - public void testFromJSONWithVersion() throws IOException { + public void testFromJSONWithVersion() throws FirebaseRemoteConfigException { final Version expectedVersion = new Version(new TemplateResponse.VersionResponse() .setDescription("template version") .setUpdateTime("2020-12-08T15:49:51.887878Z") @@ -265,34 +263,39 @@ public void testFromJSONWithVersion() throws IOException { } @Test - public void testToJSON() { - // Empty template + public void testToJSONWithEmptyTemplate() { String jsonString = new Template().toJSON(); assertEquals("{\"conditions\":[]," + "\"parameterGroups\":{},\"parameters\":{}}", jsonString); + } - // Template with parameter values + @Test + public void testToJSONWithParameterValues() { Template t = new Template(); t.getParameters() .put("with_value", new Parameter().setDefaultValue(ParameterValue.of("hello"))); t.getParameters() .put("with_inApp", new Parameter().setDefaultValue(ParameterValue.inAppDefault())); - jsonString = t.toJSON(); + String jsonString = t.toJSON(); assertEquals("{\"conditions\":[],\"parameterGroups\":{}," + "\"parameters\":{\"with_value\":{\"conditionalValues\":{}," + "\"defaultValue\":{\"value\":\"hello\"}},\"with_inApp\":{\"conditionalValues\":{}," + "\"defaultValue\":{\"useInAppDefault\":true}}}}", jsonString); + } - // Template with etag - jsonString = new Template("etag-12345").toJSON(); + @Test + public void testToJSONWithEtag() { + String jsonString = new Template("etag-12345").toJSON(); assertEquals("{\"conditions\":[],\"etag\":\"etag-12345\",\"parameterGroups\":{}," + "\"parameters\":{}}", jsonString); + } - // Template with etag and conditions - jsonString = new Template("etag-0010201") + @Test + public void testToJSONWithEtagAndConditions() { + String jsonString = new Template("etag-0010201") .setConditions(CONDITIONS).toJSON(); assertEquals("{\"conditions\":[{\"expression\":\"exp ios\",\"name\":\"ios_en\"," @@ -324,26 +327,19 @@ public void testToJSONWithVersion() { } @Test - public void testToJSONAndFromJSON() throws IOException { - String jsonString = new Template().toJSON(); - Template template = Template.fromJSON(jsonString); + public void testToJSONAndFromJSON() throws FirebaseRemoteConfigException { + Template originalTemplate = new Template(); + Template otherTemplate = Template.fromJSON(originalTemplate.toJSON()); - assertNotNull(template.getParameters()); - assertNotNull(template.getConditions()); - assertNotNull(template.getParameterGroups()); - assertTrue(template.getParameters().isEmpty()); - assertTrue(template.getConditions().isEmpty()); - assertTrue(template.getParameterGroups().isEmpty()); - assertNull(template.getETag()); + assertEquals(originalTemplate, otherTemplate); Version expectedVersion = Version.withDescription("promo version"); - jsonString = new Template("etag-0010201") + originalTemplate = new Template("etag-0010201") .setParameters(PARAMETERS) .setConditions(CONDITIONS) .setParameterGroups(PARAMETER_GROUPS) - .setVersion(expectedVersion) - .toJSON(); - template = Template.fromJSON(jsonString); + .setVersion(expectedVersion); + Template template = Template.fromJSON(originalTemplate.toJSON()); assertEquals("etag-0010201", template.getETag()); assertEquals(PARAMETERS, template.getParameters()); diff --git a/src/test/resources/rcTemplateWithETag.json b/src/test/resources/rcTemplateWithETag.json deleted file mode 100644 index e278ee25f..000000000 --- a/src/test/resources/rcTemplateWithETag.json +++ /dev/null @@ -1 +0,0 @@ -{"conditions":[{"expression":"exp ios","name":"ios_en","tagColor":"INDIGO"},{"expression":"exp android","name":"android_en"}],"etag":"etag-0010201","parameterGroups":{"greetings_group":{"description":"description","parameters":{"greeting_text":{"conditionalValues":{"promo":{"useInAppDefault":true},"android":{"value":"hello android"},"ios":{"value":"hello ios"}},"defaultValue":{"useInAppDefault":true},"description":"greeting text"},"greeting_header":{"conditionalValues":{"promo":{"useInAppDefault":true},"android":{"value":"hello android"},"ios":{"value":"hello ios"}},"defaultValue":{"useInAppDefault":true},"description":"greeting header text"}}}},"parameters":{"greeting_text":{"conditionalValues":{"promo":{"useInAppDefault":true},"android":{"value":"hello android"},"ios":{"value":"hello ios"}},"defaultValue":{"useInAppDefault":true},"description":"greeting text"},"greeting_header":{"conditionalValues":{"promo":{"useInAppDefault":true},"android":{"value":"hello android"},"ios":{"value":"hello ios"}},"defaultValue":{"useInAppDefault":true},"description":"greeting header text"}},"version":{"description":"promo version","legacy":false}} \ No newline at end of file From 5aa86007b0e220df47789d7c07481aa183885c02 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 10 Dec 2020 13:39:02 -0500 Subject: [PATCH 09/10] Code cleanup --- .../remoteconfig/RemoteConfigUtil.java | 19 +++++++++++++++++-- .../firebase/remoteconfig/Template.java | 10 ++++------ .../google/firebase/remoteconfig/Version.java | 12 +----------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java index bb84122a1..e9ce24618 100644 --- a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java +++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java @@ -27,6 +27,12 @@ final class RemoteConfigUtil { + // SimpleDateFormat cannot handle fractional seconds in timestamps + // (example: "2014-10-02T15:01:23.045123456Z"). Therefore, we strip fractional seconds + // from the date string (example: "2014-10-02T15:01:23") when parsing Zulu timestamp strings. + // The backend API expects timestamps in Zulu format with fractional seconds. To generate correct + // timestamps in payloads we use ".SSS000000'Z'" suffix. + // Hence, two Zulu date patterns are used below. private static final String ZULU_DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS000000'Z'"; private static final String ZULU_DATE_NO_FRAC_SECS_PATTERN = "yyyy-MM-dd'T'HH:mm:ss"; private static final String UTC_DATE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; @@ -45,7 +51,7 @@ static long convertToMilliseconds(String dateString) throws ParseException { } static String convertToUtcZuluFormat(long millis) { - // sample output date string: 2020-12-08T15:49:51.887878Z + // sample output date string: 2020-11-12T22:12:02.000000000Z checkArgument(millis >= 0, "Milliseconds duration must not be negative"); SimpleDateFormat dateFormat = new SimpleDateFormat(ZULU_DATE_PATTERN); dateFormat.setTimeZone(UTC_TIME_ZONE); @@ -61,7 +67,16 @@ static String convertToUtcDateFormat(long millis) { } static long convertFromUtcZuluFormat(String dateString) throws ParseException { - // sample input date string: 2020-12-08T15:49:51.887878Z + // Input timestamp is in RFC3339 UTC "Zulu" format, accurate to + // nanoseconds (up to 9 fractional seconds digits). + // SimpleDateFormat cannot handle fractional seconds, therefore we strip fractional seconds + // from the input date string before parsing. + // example: input -> "2014-10-02T15:01:23.045123456Z" + // formatted -> "2014-10-02T15:01:23" + int indexOfPeriod = dateString.indexOf("."); + if (indexOfPeriod != -1) { + dateString = dateString.substring(0, indexOfPeriod); + } checkArgument(!Strings.isNullOrEmpty(dateString), "Date string must not be null or empty"); SimpleDateFormat dateFormat = new SimpleDateFormat(ZULU_DATE_NO_FRAC_SECS_PATTERN); dateFormat.setTimeZone(UTC_TIME_ZONE); diff --git a/src/main/java/com/google/firebase/remoteconfig/Template.java b/src/main/java/com/google/firebase/remoteconfig/Template.java index 2b96ebfce..24bd68a5b 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Template.java +++ b/src/main/java/com/google/firebase/remoteconfig/Template.java @@ -57,9 +57,7 @@ public Template(String etag) { } Template() { - this.parameters = new HashMap<>(); - this.conditions = new ArrayList<>(); - this.parameterGroups = new HashMap<>(); + this((String) null); } Template(@NonNull TemplateResponse templateResponse) { @@ -104,9 +102,9 @@ public static Template fromJSON(@NonNull String json) throws FirebaseRemoteConfi // using the default json factory as no rpc calls are made here JsonFactory jsonFactory = Utils.getDefaultJsonFactory(); try { - return new Template(jsonFactory - .createJsonParser(json) - .parseAndClose(TemplateResponse.class)); + TemplateResponse templateResponse = jsonFactory.createJsonParser(json) + .parseAndClose(TemplateResponse.class); + return new Template(templateResponse); } catch (IOException e) { throw new FirebaseRemoteConfigException(ErrorCode.INVALID_ARGUMENT, "Unable to parse JSON string."); diff --git a/src/main/java/com/google/firebase/remoteconfig/Version.java b/src/main/java/com/google/firebase/remoteconfig/Version.java index 2cec65072..5aa6c942d 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Version.java +++ b/src/main/java/com/google/firebase/remoteconfig/Version.java @@ -60,18 +60,8 @@ private Version() { this.versionNumber = versionResponse.getVersionNumber(); if (!Strings.isNullOrEmpty(versionResponse.getUpdateTime())) { - // Update Time is a timestamp in RFC3339 UTC "Zulu" format, accurate to - // nanoseconds (up to 9 fractional seconds digits). - // example: "2014-10-02T15:01:23.045123456Z" - // SimpleDateFormat cannot handle fractional seconds, therefore we strip fractional seconds - // from the date string. - String updateTime = versionResponse.getUpdateTime(); - int indexOfPeriod = updateTime.indexOf("."); - if (indexOfPeriod != -1) { - updateTime = updateTime.substring(0, indexOfPeriod); - } try { - this.updateTime = RemoteConfigUtil.convertToMilliseconds(updateTime); + this.updateTime = RemoteConfigUtil.convertToMilliseconds(versionResponse.getUpdateTime()); } catch (ParseException e) { throw new IllegalStateException("Unable to parse update time.", e); } From 2c9ca43e84a583dab5f4044156ef734819bb1fd1 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 10 Dec 2020 16:00:08 -0500 Subject: [PATCH 10/10] PR fixes --- .../firebase/remoteconfig/Condition.java | 4 +--- .../remoteconfig/RemoteConfigUtil.java | 2 +- .../FirebaseRemoteConfigClientImplTest.java | 1 - .../firebase/remoteconfig/TemplateTest.java | 19 ++++--------------- 4 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/google/firebase/remoteconfig/Condition.java b/src/main/java/com/google/firebase/remoteconfig/Condition.java index fa05cd2d0..6ccde59d9 100644 --- a/src/main/java/com/google/firebase/remoteconfig/Condition.java +++ b/src/main/java/com/google/firebase/remoteconfig/Condition.java @@ -69,9 +69,7 @@ public Condition(@NonNull String name, @NonNull String expression, @Nullable Tag checkNotNull(conditionResponse); this.name = conditionResponse.getName(); this.expression = conditionResponse.getExpression(); - if (Strings.isNullOrEmpty(conditionResponse.getTagColor())) { - this.tagColor = TagColor.UNSPECIFIED; - } else { + if (!Strings.isNullOrEmpty(conditionResponse.getTagColor())) { this.tagColor = TagColor.valueOf(conditionResponse.getTagColor()); } } diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java index e9ce24618..4011af8ff 100644 --- a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java +++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigUtil.java @@ -67,6 +67,7 @@ static String convertToUtcDateFormat(long millis) { } static long convertFromUtcZuluFormat(String dateString) throws ParseException { + checkArgument(!Strings.isNullOrEmpty(dateString), "Date string must not be null or empty"); // Input timestamp is in RFC3339 UTC "Zulu" format, accurate to // nanoseconds (up to 9 fractional seconds digits). // SimpleDateFormat cannot handle fractional seconds, therefore we strip fractional seconds @@ -77,7 +78,6 @@ static long convertFromUtcZuluFormat(String dateString) throws ParseException { if (indexOfPeriod != -1) { dateString = dateString.substring(0, indexOfPeriod); } - checkArgument(!Strings.isNullOrEmpty(dateString), "Date string must not be null or empty"); SimpleDateFormat dateFormat = new SimpleDateFormat(ZULU_DATE_NO_FRAC_SECS_PATTERN); dateFormat.setTimeZone(UTC_TIME_ZONE); return dateFormat.parse(dateString).getTime(); diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java index 690abcb99..9be41852d 100644 --- a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImplTest.java @@ -105,7 +105,6 @@ public class FirebaseRemoteConfigClientImplTest { .setTagColor(TagColor.INDIGO), new Condition("android_en", "device.os == 'android' && device.country in ['us', 'uk']") - .setTagColor(TagColor.UNSPECIFIED) ); private static final Version EXPECTED_VERSION = new Version(new TemplateResponse.VersionResponse() diff --git a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java index 01aa72455..ad4d48f87 100644 --- a/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java +++ b/src/test/java/com/google/firebase/remoteconfig/TemplateTest.java @@ -224,7 +224,7 @@ public void testFromJSONWithNonEmptyTemplateString() throws FirebaseRemoteConfig assertEquals("android_en", template.getConditions().get(1).getName()); assertEquals("device.os == 'android' && device.country in ['us', 'uk']", template.getConditions().get(1).getExpression()); - assertEquals(TagColor.UNSPECIFIED, template.getConditions().get(1).getTagColor()); + assertNull(template.getConditions().get(1).getTagColor()); assertTrue(template.getParameterGroups().isEmpty()); assertEquals("etag-001234", template.getETag()); } @@ -334,24 +334,13 @@ public void testToJSONAndFromJSON() throws FirebaseRemoteConfigException { assertEquals(originalTemplate, otherTemplate); Version expectedVersion = Version.withDescription("promo version"); - originalTemplate = new Template("etag-0010201") + Template expectedTemplate = new Template("etag-0010201") .setParameters(PARAMETERS) .setConditions(CONDITIONS) .setParameterGroups(PARAMETER_GROUPS) .setVersion(expectedVersion); - Template template = Template.fromJSON(originalTemplate.toJSON()); + Template actualTemplate = Template.fromJSON(expectedTemplate.toJSON()); - assertEquals("etag-0010201", template.getETag()); - assertEquals(PARAMETERS, template.getParameters()); - assertEquals(PARAMETER_GROUPS, template.getParameterGroups()); - assertEquals(expectedVersion, template.getVersion()); - // check conditions - assertEquals(2, template.getConditions().size()); - assertEquals("ios_en", template.getConditions().get(0).getName()); - assertEquals("exp ios", template.getConditions().get(0).getExpression()); - assertEquals(TagColor.INDIGO, template.getConditions().get(0).getTagColor()); - assertEquals("android_en", template.getConditions().get(1).getName()); - assertEquals("exp android", template.getConditions().get(1).getExpression()); - assertEquals(TagColor.UNSPECIFIED, template.getConditions().get(1).getTagColor()); + assertEquals(expectedTemplate, actualTemplate); } }