Skip to content
9 changes: 6 additions & 3 deletions src/main/java/com/google/firebase/remoteconfig/Condition.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 (conditionResponse.getTagColor() == null) {
this.tagColor = TagColor.UNSPECIFIED;
} else {
if (!Strings.isNullOrEmpty(conditionResponse.getTagColor())) {
this.tagColor = TagColor.valueOf(conditionResponse.getTagColor());
}
}
Expand Down Expand Up @@ -168,4 +166,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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,74 @@

import com.google.common.base.Strings;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

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";
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 {
try {
return convertFromUtcZuluFormat(dateString);
} catch (ParseException e) {
return convertFromUtcDateFormat(dateString);
}
}

static String convertToUtcZuluFormat(long millis) {
// sample output date string: 2020-11-12T22:12:02.000000000Z
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(UTC_TIME_ZONE);
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(UTC_DATE_PATTERN);
dateFormat.setTimeZone(UTC_TIME_ZONE);
return dateFormat.format(new Date(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
// 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);
}
SimpleDateFormat dateFormat = new SimpleDateFormat(ZULU_DATE_NO_FRAC_SECS_PATTERN);
dateFormat.setTimeZone(UTC_TIME_ZONE);
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(UTC_DATE_PATTERN);
dateFormat.setTimeZone(UTC_TIME_ZONE);
return dateFormat.parse(dateString).getTime();
}
}
68 changes: 61 additions & 7 deletions src/main/java/com/google/firebase/remoteconfig/Template.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@

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.common.base.Strings;
import com.google.firebase.ErrorCode;
import com.google.firebase.internal.NonNull;
import com.google.firebase.remoteconfig.internal.TemplateResponse;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand All @@ -40,11 +46,18 @@ 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((String) null);
}

Template(@NonNull TemplateResponse templateResponse) {
Expand Down Expand Up @@ -73,6 +86,29 @@ public Template() {
if (templateResponse.getVersion() != null) {
this.version = new Version(templateResponse.getVersion());
}
this.etag = templateResponse.getEtag();
}

/**
* 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.
* @throws FirebaseRemoteConfigException If the input JSON string is not parsable.
*/
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();
try {
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.");
}
}

/**
Expand Down Expand Up @@ -177,12 +213,26 @@ 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() {
JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
try {
return jsonFactory.toString(this.toTemplateResponse(true));
} catch (IOException e) {
throw new RuntimeException(e);
}
}

Template setETag(String etag) {
this.etag = etag;
return this;
}

TemplateResponse toTemplateResponse() {
TemplateResponse toTemplateResponse(boolean includeAll) {
Map<String, TemplateResponse.ParameterResponse> parameterResponses = new HashMap<>();
for (Map.Entry<String, Parameter> entry : this.parameters.entrySet()) {
parameterResponses.put(entry.getKey(), entry.getValue().toParameterResponse());
Expand All @@ -196,12 +246,16 @@ TemplateResponse toTemplateResponse() {
parameterGroupResponse.put(entry.getKey(), entry.getValue().toParameterGroupResponse());
}
TemplateResponse.VersionResponse versionResponse = (this.version == null) ? null
: this.version.toVersionResponse();
return new TemplateResponse()
: this.version.toVersionResponse(includeAll);
TemplateResponse templateResponse = new TemplateResponse()
.setParameters(parameterResponses)
.setConditions(conditionResponses)
.setParameterGroups(parameterGroupResponse)
.setVersion(versionResponse);
if (includeAll) {
return templateResponse.setEtag(this.etag);
}
return templateResponse;
}

@Override
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/google/firebase/remoteconfig/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
32 changes: 14 additions & 18 deletions src/main/java/com/google/firebase/remoteconfig/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -64,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.
// example: "2014-10-02T15:01:23.045123456Z"
// SimpleDateFormat cannot handle nanoseconds, therefore we strip nanoseconds from the string.
String updateTime = versionResponse.getUpdateTime();
int indexOfPeriod = updateTime.indexOf(".");
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(versionResponse.getUpdateTime());
} catch (ParseException e) {
throw new IllegalStateException("Unable to parse update time.", e);
}
Expand Down Expand Up @@ -196,9 +182,19 @@ public Version setDescription(String description) {
return this;
}

VersionResponse toVersionResponse() {
return new VersionResponse()
.setDescription(this.description);
VersionResponse toVersionResponse(boolean includeAll) {
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 versionResponse;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ 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;

public Map<String, ParameterResponse> getParameters() {
return parameters;
}
Expand All @@ -55,6 +60,10 @@ public VersionResponse getVersion() {
return version;
}

public String getEtag() {
return etag;
}

public TemplateResponse setParameters(
Map<String, ParameterResponse> parameters) {
this.parameters = parameters;
Expand All @@ -78,6 +87,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading